From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001
From: Daniel Baumann
Date: Sun, 28 Apr 2024 16:29:10 +0200
Subject: Adding upstream version 86.0.1.
Signed-off-by: Daniel Baumann
---
toolkit/mozapps/extensions/.eslintrc.js | 36 +
toolkit/mozapps/extensions/AbuseReporter.jsm | 672 +++
toolkit/mozapps/extensions/AddonContentPolicy.cpp | 483 ++
toolkit/mozapps/extensions/AddonContentPolicy.h | 21 +
toolkit/mozapps/extensions/AddonManager.jsm | 4918 ++++++++++++++++++++
.../extensions/AddonManagerStartup-inlines.h | 226 +
toolkit/mozapps/extensions/AddonManagerStartup.cpp | 876 ++++
toolkit/mozapps/extensions/AddonManagerStartup.h | 59 +
toolkit/mozapps/extensions/AddonManagerWebAPI.cpp | 157 +
toolkit/mozapps/extensions/AddonManagerWebAPI.h | 32 +
toolkit/mozapps/extensions/Blocklist.jsm | 1898 ++++++++
.../mozapps/extensions/LightweightThemeManager.jsm | 36 +
toolkit/mozapps/extensions/addonManager.js | 409 ++
toolkit/mozapps/extensions/amContentHandler.jsm | 81 +
.../mozapps/extensions/amIAddonManagerStartup.idl | 83 +
toolkit/mozapps/extensions/amIWebInstallPrompt.idl | 32 +
toolkit/mozapps/extensions/amInstallTrigger.jsm | 205 +
toolkit/mozapps/extensions/amWebAPI.jsm | 307 ++
toolkit/mozapps/extensions/components.conf | 44 +
.../extensions/content/OpenH264-license.txt | 59 +
toolkit/mozapps/extensions/content/aboutaddons.css | 739 +++
.../mozapps/extensions/content/aboutaddons.html | 427 ++
toolkit/mozapps/extensions/content/aboutaddons.js | 4811 +++++++++++++++++++
.../extensions/content/aboutaddonsCommon.js | 289 ++
.../extensions/content/abuse-report-frame.html | 144 +
.../extensions/content/abuse-report-panel.css | 252 +
.../extensions/content/abuse-report-panel.js | 865 ++++
.../mozapps/extensions/content/abuse-reports.js | 310 ++
toolkit/mozapps/extensions/content/blocklist.js | 119 +
toolkit/mozapps/extensions/content/blocklist.xhtml | 51 +
.../mozapps/extensions/content/default-theme.svg | 17 +
.../content/drag-drop-addon-installer.js | 81 +
toolkit/mozapps/extensions/content/extensions.js | 300 ++
.../mozapps/extensions/content/extensions.xhtml | 23 +
.../extensions/content/firefox-alpenglow.svg | 4 +
.../extensions/content/firefox-compact-dark.svg | 17 +
.../extensions/content/firefox-compact-light.svg | 17 +
toolkit/mozapps/extensions/content/message-bar.css | 144 +
toolkit/mozapps/extensions/content/message-bar.js | 148 +
toolkit/mozapps/extensions/content/named-deck.js | 380 ++
toolkit/mozapps/extensions/content/panel-item.css | 68 +
toolkit/mozapps/extensions/content/panel-list.css | 58 +
toolkit/mozapps/extensions/content/rating-star.css | 41 +
toolkit/mozapps/extensions/content/shortcuts.css | 139 +
toolkit/mozapps/extensions/content/shortcuts.js | 680 +++
toolkit/mozapps/extensions/default-theme/icon.svg | 14 +
.../mozapps/extensions/default-theme/manifest.json | 42 +
toolkit/mozapps/extensions/docs/AddonManager.rst | 4 +
toolkit/mozapps/extensions/docs/SystemAddons.rst | 275 ++
toolkit/mozapps/extensions/docs/index.rst | 15 +
toolkit/mozapps/extensions/extensions.manifest | 9 +
toolkit/mozapps/extensions/gen_built_in_addons.py | 101 +
.../extensions/internal/AddonRepository.jsm | 1126 +++++
.../mozapps/extensions/internal/AddonSettings.jsm | 147 +
.../mozapps/extensions/internal/AddonTestUtils.jsm | 1986 ++++++++
.../extensions/internal/AddonUpdateChecker.jsm | 616 +++
toolkit/mozapps/extensions/internal/Content.js | 31 +
.../mozapps/extensions/internal/GMPProvider.jsm | 891 ++++
.../mozapps/extensions/internal/PluginProvider.jsm | 535 +++
.../extensions/internal/ProductAddonChecker.jsm | 397 ++
.../mozapps/extensions/internal/XPIDatabase.jsm | 3500 ++++++++++++++
toolkit/mozapps/extensions/internal/XPIInstall.jsm | 4724 +++++++++++++++++++
.../mozapps/extensions/internal/XPIProvider.jsm | 3316 +++++++++++++
toolkit/mozapps/extensions/internal/moz.build | 26 +
toolkit/mozapps/extensions/jar.mn | 38 +
toolkit/mozapps/extensions/moz.build | 101 +
.../mozapps/extensions/test/browser/.eslintrc.js | 14 +
.../extensions/test/browser/addon_prefs.xhtml | 6 +
.../addons/browser_dragdrop1/META-INF/manifest.mf | 8 +
.../addons/browser_dragdrop1/META-INF/mozilla.rsa | Bin 0 -> 4210 bytes
.../addons/browser_dragdrop1/META-INF/mozilla.sf | 5 +
.../browser/addons/browser_dragdrop1/manifest.json | 12 +
.../addons/browser_dragdrop2/META-INF/manifest.mf | 8 +
.../addons/browser_dragdrop2/META-INF/mozilla.rsa | Bin 0 -> 4210 bytes
.../addons/browser_dragdrop2/META-INF/mozilla.sf | 5 +
.../browser/addons/browser_dragdrop2/manifest.json | 12 +
.../browser_dragdrop_incompat/META-INF/manifest.mf | 8 +
.../browser_dragdrop_incompat/META-INF/mozilla.rsa | Bin 0 -> 4218 bytes
.../browser_dragdrop_incompat/META-INF/mozilla.sf | 5 +
.../addons/browser_dragdrop_incompat/manifest.json | 13 +
.../addons/browser_installssl/META-INF/manifest.mf | 8 +
.../addons/browser_installssl/META-INF/mozilla.rsa | Bin 0 -> 4213 bytes
.../addons/browser_installssl/META-INF/mozilla.sf | 5 +
.../addons/browser_installssl/manifest.json | 12 +
.../browser/addons/browser_theme/manifest.json | 22 +
.../addons/options_signed/META-INF/manifest.mf | 12 +
.../addons/options_signed/META-INF/mozilla.rsa | Bin 0 -> 4197 bytes
.../addons/options_signed/META-INF/mozilla.sf | 4 +
.../browser/addons/options_signed/manifest.json | 11 +
.../browser/addons/options_signed/options.html | 9 +
.../mozapps/extensions/test/browser/browser.ini | 115 +
.../test/browser/browser_about_debugging_link.js | 129 +
.../test/browser/browser_addon_list_reordering.js | 197 +
.../extensions/test/browser/browser_bug523784.js | 153 +
.../extensions/test/browser/browser_bug572561.js | 96 +
.../browser/browser_checkAddonCompatibility.js | 36 +
.../extensions/test/browser/browser_dragdrop.js | 270 ++
.../browser/browser_file_xpi_no_process_switch.js | 122 +
.../test/browser/browser_globalwarnings.js | 168 +
.../extensions/test/browser/browser_gmpProvider.js | 461 ++
.../test/browser/browser_history_navigation.js | 599 +++
.../test/browser/browser_html_abuse_report.js | 937 ++++
.../browser/browser_html_abuse_report_dialog.js | 178 +
.../browser/browser_html_detail_permissions.js | 450 ++
.../test/browser/browser_html_detail_view.js | 1242 +++++
.../test/browser/browser_html_discover_view.js | 855 ++++
.../browser/browser_html_discover_view_clientid.js | 232 +
.../browser/browser_html_discover_view_prefs.js | 83 +
.../test/browser/browser_html_list_view.js | 990 ++++
.../browser_html_list_view_recommendations.js | 308 ++
.../test/browser/browser_html_message_bar.js | 185 +
.../test/browser/browser_html_named_deck.js | 250 +
.../test/browser/browser_html_options_ui.js | 539 +++
.../test/browser/browser_html_options_ui_in_tab.js | 136 +
.../test/browser/browser_html_pending_updates.js | 312 ++
.../test/browser/browser_html_recent_updates.js | 180 +
.../test/browser/browser_html_recommendations.js | 165 +
.../browser/browser_html_scroll_restoration.js | 226 +
.../test/browser/browser_html_updates.js | 829 ++++
.../test/browser/browser_html_warning_messages.js | 355 ++
.../extensions/test/browser/browser_installssl.js | 378 ++
.../test/browser/browser_installtrigger_install.js | 84 +
.../test/browser/browser_interaction_telemetry.js | 531 +++
.../test/browser/browser_manage_shortcuts.js | 309 ++
.../browser/browser_manage_shortcuts_hidden.js | 199 +
.../browser/browser_manage_shortcuts_remove.js | 173 +
.../browser/browser_menu_button_accessibility.js | 81 +
.../test/browser/browser_page_accessibility.js | 15 +
.../browser/browser_page_options_install_addon.js | 128 +
.../test/browser/browser_page_options_updates.js | 166 +
.../test/browser/browser_panel_item_accesskey.js | 100 +
.../browser/browser_panel_list_accessibility.js | 67 +
.../test/browser/browser_permission_prompt.js | 178 +
.../extensions/test/browser/browser_reinstall.js | 278 ++
.../test/browser/browser_search_bar_focus.js | 42 +
.../browser/browser_shortcuts_duplicate_check.js | 151 +
.../test/browser/browser_sidebar_categories.js | 163 +
.../browser/browser_sidebar_hidden_categories.js | 214 +
.../browser/browser_sidebar_restore_category.js | 76 +
.../test/browser/browser_task_next_test.js | 17 +
.../extensions/test/browser/browser_updateid.js | 87 +
.../extensions/test/browser/browser_updatessl.js | 392 ++
.../extensions/test/browser/browser_updatessl.json | 17 +
.../test/browser/browser_updatessl.json^headers^ | 1 +
.../extensions/test/browser/browser_webapi.js | 143 +
.../test/browser/browser_webapi_abuse_report.js | 370 ++
.../test/browser/browser_webapi_access.js | 146 +
.../test/browser/browser_webapi_addon_listener.js | 124 +
.../test/browser/browser_webapi_enable.js | 63 +
.../test/browser/browser_webapi_install.js | 542 +++
.../browser/browser_webapi_install_disabled.js | 58 +
.../test/browser/browser_webapi_theme.js | 80 +
.../test/browser/browser_webapi_uninstall.js | 72 +
.../extensions/test/browser/browser_webext_icon.js | 84 +
.../test/browser/browser_webext_incognito.js | 645 +++
.../test/browser/discovery/api_response.json | 769 +++
.../test/browser/discovery/api_response_empty.json | 1 +
.../test/browser/discovery/small-1x1.png | Bin 0 -> 82 bytes
toolkit/mozapps/extensions/test/browser/head.js | 1809 +++++++
.../extensions/test/browser/head_abuse_report.js | 541 +++
toolkit/mozapps/extensions/test/browser/moz.build | 31 +
.../extensions/test/browser/plugin_test.html | 7 +
.../mozapps/extensions/test/browser/redirect.sjs | 5 +
.../test/browser/webapi_addon_listener.html | 30 +
.../test/browser/webapi_checkavailable.html | 13 +
.../test/browser/webapi_checkchromeframe.xhtml | 6 +
.../test/browser/webapi_checkframed.html | 7 +
.../test/browser/webapi_checknavigatedwindow.html | 29 +
toolkit/mozapps/extensions/test/create_xpi.py | 21 +
.../mozapps/extensions/test/mochitest/chrome.ini | 2 +
.../extensions/test/mochitest/file_empty.html | 2 +
.../extensions/test/mochitest/mochitest.ini | 5 +
.../extensions/test/mochitest/test_bug887098.html | 53 +
.../test/mochitest/test_default_theme.html | 33 +
toolkit/mozapps/extensions/test/moz.build | 20 +
.../mozapps/extensions/test/xpcshell/.eslintrc.js | 24 +
.../xpcshell/data/blocklistchange/addon_change.xml | 31 +
.../data/blocklistchange/addon_update1.json | 102 +
.../data/blocklistchange/addon_update2.json | 102 +
.../data/blocklistchange/addon_update3.json | 102 +
.../xpcshell/data/blocklistchange/app_update.xml | 62 +
.../data/blocklistchange/blocklist_update1.xml | 3 +
.../data/blocklistchange/blocklist_update2.xml | 26 +
.../data/blocklistchange/manual_update.xml | 27 +
.../test/xpcshell/data/bug455906_block.xml | 18 +
.../test/xpcshell/data/bug455906_empty.xml | 7 +
.../test/xpcshell/data/bug455906_start.xml | 30 +
.../test/xpcshell/data/bug455906_warn.xml | 33 +
.../extensions/test/xpcshell/data/corrupt.xpi | 1 +
.../extensions/test/xpcshell/data/corruptfile.xpi | Bin 0 -> 633 bytes
.../extensions/test/xpcshell/data/empty.xpi | Bin 0 -> 197 bytes
.../xpcshell/data/mlbf-blocked1-unblocked2.bin | Bin 0 -> 32 bytes
.../test/xpcshell/data/pluginInfoURL_block.xml | 45 +
.../test/xpcshell/data/productaddons/bad.txt | 1 +
.../test/xpcshell/data/productaddons/bad.xml | 3 +
.../test/xpcshell/data/productaddons/bad2.xml | 3 +
.../test/xpcshell/data/productaddons/empty.xml | 5 +
.../test/xpcshell/data/productaddons/good.xml | 11 +
.../test/xpcshell/data/productaddons/missing.xml | 3 +
.../test/xpcshell/data/productaddons/unsigned.xpi | Bin 0 -> 452 bytes
.../data/signing_checks/langpack_signed.xpi | Bin 0 -> 4452 bytes
.../data/signing_checks/langpack_unsigned.xpi | Bin 0 -> 413 bytes
.../test/xpcshell/data/signing_checks/long.xpi | Bin 0 -> 4761 bytes
.../xpcshell/data/signing_checks/privileged.xpi | Bin 0 -> 4659 bytes
.../test/xpcshell/data/signing_checks/signed1.xpi | Bin 0 -> 4702 bytes
.../test/xpcshell/data/signing_checks/signed2.xpi | Bin 0 -> 4697 bytes
.../test/xpcshell/data/signing_checks/unsigned.xpi | Bin 0 -> 528 bytes
.../xpcshell/data/test_AddonRepository_cache.json | 134 +
.../xpcshell/data/test_AddonRepository_empty.json | 7 +
.../xpcshell/data/test_AddonRepository_fail.json | 1 +
.../data/test_AddonRepository_getAddonsByIDs.json | 117 +
.../test/xpcshell/data/test_backgroundupdate.json | 46 +
.../data/test_blocklist_metadata_filters_1.xml | 21 +
.../test/xpcshell/data/test_blocklist_prefs_1.xml | 28 +
.../test/xpcshell/data/test_bug393285.xml | 30 +
.../data/test_bug449027_app-extensions.json | 336 ++
.../xpcshell/data/test_bug449027_app-plugins.json | 336 ++
.../test/xpcshell/data/test_bug449027_app.xml | 333 ++
.../data/test_bug449027_toolkit-extensions.json | 154 +
.../data/test_bug449027_toolkit-plugins.json | 155 +
.../test/xpcshell/data/test_bug449027_toolkit.xml | 208 +
.../test/xpcshell/data/test_bug468528.xml | 15 +
.../test/xpcshell/data/test_bug514327_1.xml | 17 +
.../test/xpcshell/data/test_bug514327_2.xml | 10 +
.../test/xpcshell/data/test_bug514327_3_empty.xml | 4 +
.../xpcshell/data/test_bug514327_3_outdated_1.xml | 13 +
.../xpcshell/data/test_bug514327_3_outdated_2.xml | 13 +
.../test/xpcshell/data/test_bug655254.json | 17 +
.../test/xpcshell/data/test_corrupt.json | 30 +
.../xpcshell/data/test_delay_updates_complete.json | 11 +
.../data/test_delay_updates_complete_legacy.json | 18 +
.../xpcshell/data/test_delay_updates_defer.json | 11 +
.../data/test_delay_updates_defer_legacy.json | 18 +
.../xpcshell/data/test_delay_updates_ignore.json | 11 +
.../data/test_delay_updates_ignore_legacy.json | 18 +
.../test/xpcshell/data/test_gfxBlacklist.json | 352 ++
.../xpcshell/data/test_gfxBlacklist_AllOS.json | 837 ++++
.../xpcshell/data/test_gfxBlacklist_OSVersion.json | 28 +
.../test/xpcshell/data/test_install_addons.json | 31 +
.../test/xpcshell/data/test_install_compat.json | 27 +
.../test/xpcshell/data/test_no_update.json | 7 +
.../data/test_overrideblocklist/ancient.xml | 8 +
.../xpcshell/data/test_overrideblocklist/new.xml | 8 +
.../xpcshell/data/test_overrideblocklist/old.xml | 8 +
.../test/xpcshell/data/test_pluginBlocklistCtp.xml | 26 +
.../xpcshell/data/test_pluginBlocklistCtpUndo.xml | 10 +
.../test/xpcshell/data/test_softblocked1.xml | 9 +
.../extensions/test/xpcshell/data/test_update.json | 137 +
.../test/xpcshell/data/test_update_addons.json | 14 +
.../test/xpcshell/data/test_update_compat.json | 28 +
.../test/xpcshell/data/test_updatecheck.json | 269 ++
.../extensions/test/xpcshell/data/unsigned.xpi | Bin 0 -> 463 bytes
.../test/xpcshell/data/webext-implicit-id.xpi | Bin 0 -> 4182 bytes
.../extensions/test/xpcshell/head_addons.js | 1350 ++++++
.../extensions/test/xpcshell/head_compat.js | 49 +
.../extensions/test/xpcshell/head_sideload.js | 81 +
.../extensions/test/xpcshell/head_system_addons.js | 469 ++
.../extensions/test/xpcshell/head_unpack.js | 3 +
.../extensions/test/xpcshell/rs-blocklist/head.js | 51 +
.../rs-blocklist/test_blocklist_addonBlockURL.js | 56 +
.../rs-blocklist/test_blocklist_appversion.js | 293 ++
.../rs-blocklist/test_blocklist_clients.js | 233 +
.../xpcshell/rs-blocklist/test_blocklist_gfx.js | 113 +
.../test_blocklist_metadata_filters.js | 116 +
.../xpcshell/rs-blocklist/test_blocklist_mlbf.js | 219 +
.../rs-blocklist/test_blocklist_mlbf_dump.js | 97 +
.../rs-blocklist/test_blocklist_mlbf_fetch.js | 207 +
.../rs-blocklist/test_blocklist_mlbf_stashes.js | 252 +
.../rs-blocklist/test_blocklist_mlbf_telemetry.js | 184 +
.../rs-blocklist/test_blocklist_mlbf_update.js | 75 +
.../xpcshell/rs-blocklist/test_blocklist_osabi.js | 286 ++
.../xpcshell/rs-blocklist/test_blocklist_prefs.js | 106 +
.../rs-blocklist/test_blocklist_regexp_split.js | 229 +
.../rs-blocklist/test_blocklist_severities.js | 504 ++
.../test_blocklist_targetapp_filter.js | 395 ++
.../rs-blocklist/test_blocklist_telemetry.js | 124 +
.../xpcshell/rs-blocklist/test_blocklistchange.js | 1317 ++++++
.../rs-blocklist/test_blocklistchange_v2.js | 21 +
.../rs-blocklist/test_gfxBlacklist_Device.js | 74 +
.../rs-blocklist/test_gfxBlacklist_DriverNew.js | 68 +
.../test_gfxBlacklist_Equal_DriverNew.js | 106 +
.../test_gfxBlacklist_Equal_DriverOld.js | 69 +
.../rs-blocklist/test_gfxBlacklist_Equal_OK.js | 69 +
.../test_gfxBlacklist_GTE_DriverOld.js | 69 +
.../rs-blocklist/test_gfxBlacklist_GTE_OK.js | 69 +
.../test_gfxBlacklist_No_Comparison.js | 70 +
.../xpcshell/rs-blocklist/test_gfxBlacklist_OK.js | 70 +
.../xpcshell/rs-blocklist/test_gfxBlacklist_OS.js | 68 +
.../test_gfxBlacklist_OSVersion_match.js | 71 +
...fxBlacklist_OSVersion_mismatch_DriverVersion.js | 71 +
...st_gfxBlacklist_OSVersion_mismatch_OSVersion.js | 72 +
.../rs-blocklist/test_gfxBlacklist_Vendor.js | 69 +
.../rs-blocklist/test_gfxBlacklist_Version.js | 165 +
.../rs-blocklist/test_gfxBlacklist_prefs.js | 125 +
.../test/xpcshell/rs-blocklist/test_softblocked.js | 61 +
.../test/xpcshell/rs-blocklist/xpcshell.ini | 66 +
.../extensions/test/xpcshell/test_AbuseReporter.js | 903 ++++
.../test/xpcshell/test_AddonRepository.js | 316 ++
.../test/xpcshell/test_AddonRepository_cache.js | 711 +++
.../xpcshell/test_AddonRepository_langpacks.js | 135 +
.../test/xpcshell/test_AddonRepository_paging.js | 91 +
.../test/xpcshell/test_ProductAddonChecker.js | 292 ++
.../extensions/test/xpcshell/test_XPIStates.js | 132 +
.../extensions/test/xpcshell/test_XPIcancel.js | 70 +
.../extensions/test/xpcshell/test_addonStartup.js | 97 +
.../test_addon_manager_telemetry_events.js | 788 ++++
.../test/xpcshell/test_amo_stats_telemetry.js | 102 +
.../extensions/test/xpcshell/test_aom_startup.js | 187 +
.../extensions/test/xpcshell/test_bad_json.js | 39 +
.../extensions/test/xpcshell/test_badschema.js | 237 +
.../extensions/test/xpcshell/test_bug587088.js | 195 +
.../test/xpcshell/test_builtin_location.js | 153 +
.../extensions/test/xpcshell/test_cacheflush.js | 86 +
.../extensions/test/xpcshell/test_childprocess.js | 20 +
.../extensions/test/xpcshell/test_cookies.js | 102 +
.../extensions/test/xpcshell/test_corrupt.js | 210 +
.../test/xpcshell/test_crash_annotation_quoting.js | 25 +
.../extensions/test/xpcshell/test_db_path.js | 69 +
.../xpcshell/test_delay_update_webextension.js | 392 ++
.../extensions/test/xpcshell/test_dependencies.js | 140 +
.../test/xpcshell/test_dictionary_webextension.js | 198 +
.../extensions/test/xpcshell/test_distribution.js | 115 +
.../test/xpcshell/test_duplicateplugins.js | 160 +
.../test/xpcshell/test_embedderDisabled.js | 128 +
.../mozapps/extensions/test/xpcshell/test_error.js | 78 +
.../test/xpcshell/test_ext_management.js | 213 +
.../extensions/test/xpcshell/test_filepointer.js | 327 ++
.../extensions/test/xpcshell/test_general.js | 59 +
.../test/xpcshell/test_getInstallSourceFromHost.js | 42 +
.../extensions/test/xpcshell/test_gmpProvider.js | 507 ++
.../extensions/test/xpcshell/test_harness.js | 13 +
.../extensions/test/xpcshell/test_hidden.js | 58 +
.../extensions/test/xpcshell/test_install.js | 1050 +++++
.../test/xpcshell/test_install_cancel.js | 92 +
.../extensions/test/xpcshell/test_install_icons.js | 62 +
.../extensions/test/xpcshell/test_isDebuggable.js | 21 +
.../extensions/test/xpcshell/test_isReady.js | 71 +
.../extensions/test/xpcshell/test_locale.js | 103 +
.../test/xpcshell/test_moved_extension_metadata.js | 173 +
.../extensions/test/xpcshell/test_no_addons.js | 81 +
.../test/xpcshell/test_nodisable_hidden.js | 99 +
.../xpcshell/test_onPropertyChanged_appDisabled.js | 55 +
.../extensions/test/xpcshell/test_permissions.js | 199 +
.../test/xpcshell/test_permissions_prefs.js | 103 +
.../extensions/test/xpcshell/test_pluginchange.js | 242 +
.../test/xpcshell/test_pref_properties.js | 221 +
.../test/xpcshell/test_provider_markSafe.js | 43 +
.../test/xpcshell/test_provider_shutdown.js | 100 +
.../test_provider_unsafe_access_shutdown.js | 65 +
.../test_provider_unsafe_access_startup.js | 59 +
.../extensions/test/xpcshell/test_proxies.js | 225 +
.../test/xpcshell/test_recommendations.js | 336 ++
.../test/xpcshell/test_registerchrome.js | 88 +
.../extensions/test/xpcshell/test_registry.js | 166 +
.../test/xpcshell/test_reinstall_disabled_addon.js | 213 +
.../extensions/test/xpcshell/test_reload.js | 188 +
.../extensions/test/xpcshell/test_safemode.js | 90 +
.../extensions/test/xpcshell/test_schema_change.js | 160 +
.../mozapps/extensions/test/xpcshell/test_seen.js | 283 ++
.../extensions/test/xpcshell/test_shutdown.js | 133 +
.../test/xpcshell/test_shutdown_barriers.js | 71 +
.../test/xpcshell/test_shutdown_early.js | 61 +
.../test/xpcshell/test_sideload_scopes.js | 180 +
.../extensions/test/xpcshell/test_sideloads.js | 117 +
.../test/xpcshell/test_sideloads_after_rebuild.js | 137 +
.../extensions/test/xpcshell/test_signed_inject.js | 429 ++
.../test/xpcshell/test_signed_install.js | 280 ++
.../test/xpcshell/test_signed_langpack.js | 60 +
.../extensions/test/xpcshell/test_signed_long.js | 23 +
.../test/xpcshell/test_signed_updatepref.js | 131 +
.../extensions/test/xpcshell/test_signed_verify.js | 110 +
.../extensions/test/xpcshell/test_startup.js | 648 +++
.../test/xpcshell/test_startup_enable.js | 47 +
.../extensions/test/xpcshell/test_startup_scan.js | 125 +
.../test/xpcshell/test_strictcompatibility.js | 152 +
.../extensions/test/xpcshell/test_syncGUID.js | 117 +
.../test/xpcshell/test_system_allowed.js | 54 +
.../test/xpcshell/test_system_delay_update.js | 485 ++
.../test/xpcshell/test_system_profile_location.js | 208 +
.../test/xpcshell/test_system_repository.js | 68 +
.../extensions/test/xpcshell/test_system_reset.js | 535 +++
.../test/xpcshell/test_system_update_blank.js | 117 +
.../xpcshell/test_system_update_checkSizeHash.js | 181 +
.../test/xpcshell/test_system_update_custom.js | 431 ++
.../test/xpcshell/test_system_update_empty.js | 141 +
.../test_system_update_enterprisepolicy.js | 84 +
.../test/xpcshell/test_system_update_fail.js | 180 +
.../test/xpcshell/test_system_update_newset.js | 165 +
.../xpcshell/test_system_update_overlapping.js | 180 +
.../xpcshell/test_system_update_uninstall_check.js | 56 +
.../test/xpcshell/test_system_update_upgrades.js | 165 +
.../test/xpcshell/test_system_upgrades.js | 415 ++
.../test/xpcshell/test_systemaddomstartupprefs.js | 55 +
.../extensions/test/xpcshell/test_temporary.js | 774 +++
.../test/xpcshell/test_trash_directory.js | 39 +
.../mozapps/extensions/test/xpcshell/test_types.js | 67 +
.../extensions/test/xpcshell/test_undouninstall.js | 584 +++
.../extensions/test/xpcshell/test_update.js | 852 ++++
.../extensions/test/xpcshell/test_updateCancel.js | 139 +
.../test/xpcshell/test_update_compatmode.js | 112 +
.../test/xpcshell/test_update_ignorecompat.js | 100 +
.../xpcshell/test_update_noSystemAddonUpdate.js | 42 +
.../test/xpcshell/test_update_strictcompat.js | 216 +
.../extensions/test/xpcshell/test_update_theme.js | 121 +
.../test/xpcshell/test_update_webextensions.js | 206 +
.../extensions/test/xpcshell/test_updatecheck.js | 146 +
.../test/xpcshell/test_updatecheck_errors.js | 52 +
.../test/xpcshell/test_updatecheck_json.js | 379 ++
.../extensions/test/xpcshell/test_updateid.js | 80 +
.../extensions/test/xpcshell/test_upgrade.js | 208 +
.../test/xpcshell/test_upgrade_incompatible.js | 76 +
.../extensions/test/xpcshell/test_webextension.js | 513 ++
.../test/xpcshell/test_webextension_events.js | 95 +
.../test/xpcshell/test_webextension_icons.js | 212 +
.../test/xpcshell/test_webextension_install.js | 665 +++
.../test_webextension_install_syntax_error.js | 42 +
.../test/xpcshell/test_webextension_langpack.js | 573 +++
.../test/xpcshell/test_webextension_paths.js | 47 +
.../test/xpcshell/test_webextension_theme.js | 365 ++
.../extensions/test/xpcshell/xpcshell-unpack.ini | 13 +
.../mozapps/extensions/test/xpcshell/xpcshell.ini | 194 +
.../mozapps/extensions/test/xpinstall/.eslintrc.js | 5 +
.../extensions/test/xpinstall/amosigned.xpi | Bin 0 -> 4287 bytes
.../extensions/test/xpinstall/authRedirect.sjs | 21 +
.../mozapps/extensions/test/xpinstall/browser.ini | 90 +
.../test/xpinstall/browser_amosigned_trigger.js | 83 +
.../xpinstall/browser_amosigned_trigger_iframe.js | 74 +
.../test/xpinstall/browser_amosigned_url.js | 47 +
.../extensions/test/xpinstall/browser_auth.js | 65 +
.../extensions/test/xpinstall/browser_auth2.js | 70 +
.../extensions/test/xpinstall/browser_auth3.js | 69 +
.../extensions/test/xpinstall/browser_auth4.js | 68 +
.../extensions/test/xpinstall/browser_badargs.js | 46 +
.../extensions/test/xpinstall/browser_badargs2.js | 52 +
.../extensions/test/xpinstall/browser_badhash.js | 43 +
.../test/xpinstall/browser_badhashtype.js | 43 +
.../xpinstall/browser_block_fullscreen_prompt.js | 116 +
.../extensions/test/xpinstall/browser_bug540558.js | 28 +
.../extensions/test/xpinstall/browser_bug611242.js | 24 +
.../extensions/test/xpinstall/browser_bug638292.js | 44 +
.../extensions/test/xpinstall/browser_bug645699.js | 52 +
.../xpinstall/browser_bug645699_postDownload.js | 52 +
.../extensions/test/xpinstall/browser_bug672485.js | 60 +
.../test/xpinstall/browser_containers.js | 113 +
.../extensions/test/xpinstall/browser_cookies.js | 39 +
.../extensions/test/xpinstall/browser_cookies2.js | 61 +
.../extensions/test/xpinstall/browser_cookies3.js | 65 +
.../extensions/test/xpinstall/browser_cookies4.js | 65 +
.../extensions/test/xpinstall/browser_corrupt.js | 50 +
.../extensions/test/xpinstall/browser_datauri.js | 75 +
.../test/xpinstall/browser_doorhanger_installs.js | 1370 ++++++
.../extensions/test/xpinstall/browser_empty.js | 36 +
.../extensions/test/xpinstall/browser_enabled.js | 30 +
.../extensions/test/xpinstall/browser_enabled2.js | 38 +
.../extensions/test/xpinstall/browser_enabled3.js | 60 +
.../extensions/test/xpinstall/browser_hash.js | 44 +
.../extensions/test/xpinstall/browser_hash2.js | 44 +
.../extensions/test/xpinstall/browser_httphash.js | 52 +
.../extensions/test/xpinstall/browser_httphash2.js | 49 +
.../extensions/test/xpinstall/browser_httphash3.js | 49 +
.../extensions/test/xpinstall/browser_httphash4.js | 46 +
.../extensions/test/xpinstall/browser_httphash5.js | 50 +
.../extensions/test/xpinstall/browser_httphash6.js | 104 +
.../test/xpinstall/browser_installchrome.js | 33 +
.../extensions/test/xpinstall/browser_localfile.js | 42 +
.../test/xpinstall/browser_localfile2.js | 51 +
.../test/xpinstall/browser_localfile3.js | 42 +
.../test/xpinstall/browser_localfile4.js | 52 +
.../xpinstall/browser_localfile4_postDownload.js | 51 +
.../extensions/test/xpinstall/browser_newwindow.js | 74 +
.../extensions/test/xpinstall/browser_offline.js | 68 +
.../test/xpinstall/browser_privatebrowsing.js | 126 +
.../extensions/test/xpinstall/browser_relative.js | 64 +
.../test/xpinstall/browser_signed_trigger.js | 44 +
.../test/xpinstall/browser_signed_url.js | 29 +
.../test/xpinstall/browser_softwareupdate.js | 33 +
.../test/xpinstall/browser_trigger_redirect.js | 42 +
.../test/xpinstall/browser_unsigned_trigger.js | 65 +
.../xpinstall/browser_unsigned_trigger_iframe.js | 74 +
.../xpinstall/browser_unsigned_trigger_xorigin.js | 55 +
.../test/xpinstall/browser_unsigned_url.js | 30 +
.../extensions/test/xpinstall/browser_whitelist.js | 66 +
.../extensions/test/xpinstall/bug540558.html | 24 +
.../extensions/test/xpinstall/bug638292.html | 17 +
.../extensions/test/xpinstall/bug645699.html | 32 +
.../extensions/test/xpinstall/cookieRedirect.sjs | 24 +
.../mozapps/extensions/test/xpinstall/corrupt.xpi | 1 +
.../mozapps/extensions/test/xpinstall/empty.xpi | Bin 0 -> 197 bytes
.../mozapps/extensions/test/xpinstall/enabled.html | 25 +
.../extensions/test/xpinstall/hashRedirect.sjs | 15 +
toolkit/mozapps/extensions/test/xpinstall/head.js | 566 +++
.../extensions/test/xpinstall/incompatible.xpi | Bin 0 -> 428 bytes
.../extensions/test/xpinstall/installchrome.html | 23 +
.../extensions/test/xpinstall/installtrigger.html | 57 +
.../test/xpinstall/installtrigger_frame.html | 30 +
.../extensions/test/xpinstall/navigate.html | 25 +
.../extensions/test/xpinstall/recommended.xpi | Bin 0 -> 7884 bytes
.../mozapps/extensions/test/xpinstall/redirect.sjs | 45 +
.../extensions/test/xpinstall/restartless.xpi | Bin 0 -> 4447 bytes
.../extensions/test/xpinstall/slowinstall.sjs | 99 +
.../test/xpinstall/startsoftwareupdate.html | 21 +
.../extensions/test/xpinstall/triggerredirect.html | 37 +
.../mozapps/extensions/test/xpinstall/unsigned.xpi | Bin 0 -> 286 bytes
503 files changed, 102904 insertions(+)
create mode 100644 toolkit/mozapps/extensions/.eslintrc.js
create mode 100644 toolkit/mozapps/extensions/AbuseReporter.jsm
create mode 100644 toolkit/mozapps/extensions/AddonContentPolicy.cpp
create mode 100644 toolkit/mozapps/extensions/AddonContentPolicy.h
create mode 100644 toolkit/mozapps/extensions/AddonManager.jsm
create mode 100644 toolkit/mozapps/extensions/AddonManagerStartup-inlines.h
create mode 100644 toolkit/mozapps/extensions/AddonManagerStartup.cpp
create mode 100644 toolkit/mozapps/extensions/AddonManagerStartup.h
create mode 100644 toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
create mode 100644 toolkit/mozapps/extensions/AddonManagerWebAPI.h
create mode 100644 toolkit/mozapps/extensions/Blocklist.jsm
create mode 100644 toolkit/mozapps/extensions/LightweightThemeManager.jsm
create mode 100644 toolkit/mozapps/extensions/addonManager.js
create mode 100644 toolkit/mozapps/extensions/amContentHandler.jsm
create mode 100644 toolkit/mozapps/extensions/amIAddonManagerStartup.idl
create mode 100644 toolkit/mozapps/extensions/amIWebInstallPrompt.idl
create mode 100644 toolkit/mozapps/extensions/amInstallTrigger.jsm
create mode 100644 toolkit/mozapps/extensions/amWebAPI.jsm
create mode 100644 toolkit/mozapps/extensions/components.conf
create mode 100644 toolkit/mozapps/extensions/content/OpenH264-license.txt
create mode 100644 toolkit/mozapps/extensions/content/aboutaddons.css
create mode 100644 toolkit/mozapps/extensions/content/aboutaddons.html
create mode 100644 toolkit/mozapps/extensions/content/aboutaddons.js
create mode 100644 toolkit/mozapps/extensions/content/aboutaddonsCommon.js
create mode 100644 toolkit/mozapps/extensions/content/abuse-report-frame.html
create mode 100644 toolkit/mozapps/extensions/content/abuse-report-panel.css
create mode 100644 toolkit/mozapps/extensions/content/abuse-report-panel.js
create mode 100644 toolkit/mozapps/extensions/content/abuse-reports.js
create mode 100644 toolkit/mozapps/extensions/content/blocklist.js
create mode 100644 toolkit/mozapps/extensions/content/blocklist.xhtml
create mode 100644 toolkit/mozapps/extensions/content/default-theme.svg
create mode 100644 toolkit/mozapps/extensions/content/drag-drop-addon-installer.js
create mode 100644 toolkit/mozapps/extensions/content/extensions.js
create mode 100644 toolkit/mozapps/extensions/content/extensions.xhtml
create mode 100644 toolkit/mozapps/extensions/content/firefox-alpenglow.svg
create mode 100644 toolkit/mozapps/extensions/content/firefox-compact-dark.svg
create mode 100644 toolkit/mozapps/extensions/content/firefox-compact-light.svg
create mode 100644 toolkit/mozapps/extensions/content/message-bar.css
create mode 100644 toolkit/mozapps/extensions/content/message-bar.js
create mode 100644 toolkit/mozapps/extensions/content/named-deck.js
create mode 100644 toolkit/mozapps/extensions/content/panel-item.css
create mode 100644 toolkit/mozapps/extensions/content/panel-list.css
create mode 100644 toolkit/mozapps/extensions/content/rating-star.css
create mode 100644 toolkit/mozapps/extensions/content/shortcuts.css
create mode 100644 toolkit/mozapps/extensions/content/shortcuts.js
create mode 100644 toolkit/mozapps/extensions/default-theme/icon.svg
create mode 100644 toolkit/mozapps/extensions/default-theme/manifest.json
create mode 100644 toolkit/mozapps/extensions/docs/AddonManager.rst
create mode 100644 toolkit/mozapps/extensions/docs/SystemAddons.rst
create mode 100644 toolkit/mozapps/extensions/docs/index.rst
create mode 100644 toolkit/mozapps/extensions/extensions.manifest
create mode 100644 toolkit/mozapps/extensions/gen_built_in_addons.py
create mode 100644 toolkit/mozapps/extensions/internal/AddonRepository.jsm
create mode 100644 toolkit/mozapps/extensions/internal/AddonSettings.jsm
create mode 100644 toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
create mode 100644 toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
create mode 100644 toolkit/mozapps/extensions/internal/Content.js
create mode 100644 toolkit/mozapps/extensions/internal/GMPProvider.jsm
create mode 100644 toolkit/mozapps/extensions/internal/PluginProvider.jsm
create mode 100644 toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm
create mode 100644 toolkit/mozapps/extensions/internal/XPIDatabase.jsm
create mode 100644 toolkit/mozapps/extensions/internal/XPIInstall.jsm
create mode 100644 toolkit/mozapps/extensions/internal/XPIProvider.jsm
create mode 100644 toolkit/mozapps/extensions/internal/moz.build
create mode 100644 toolkit/mozapps/extensions/jar.mn
create mode 100644 toolkit/mozapps/extensions/moz.build
create mode 100644 toolkit/mozapps/extensions/test/browser/.eslintrc.js
create mode 100644 toolkit/mozapps/extensions/test/browser/addon_prefs.xhtml
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/manifest.mf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.rsa
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/META-INF/mozilla.sf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/manifest.json
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/manifest.mf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.rsa
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/META-INF/mozilla.sf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/manifest.json
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/manifest.mf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.rsa
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/META-INF/mozilla.sf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop_incompat/manifest.json
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/manifest.mf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.rsa
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_installssl/META-INF/mozilla.sf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_installssl/manifest.json
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/browser_theme/manifest.json
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/manifest.mf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.rsa
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/options_signed/META-INF/mozilla.sf
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json
create mode 100644 toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html
create mode 100644 toolkit/mozapps/extensions/test/browser/browser.ini
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_about_debugging_link.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_addon_list_reordering.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_bug523784.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_bug572561.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_dragdrop.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_file_xpi_no_process_switch.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_history_navigation.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_abuse_report.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_abuse_report_dialog.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_detail_permissions.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_detail_view.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_discover_view.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_discover_view_clientid.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_discover_view_prefs.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_list_view.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_list_view_recommendations.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_message_bar.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_named_deck.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_options_ui.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_updates.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_installssl.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_panel_item_accesskey.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_panel_list_accessibility.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_reinstall.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_search_bar_focus.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_task_next_test.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_updateid.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_updatessl.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_updatessl.json
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webext_icon.js
create mode 100644 toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js
create mode 100644 toolkit/mozapps/extensions/test/browser/discovery/api_response.json
create mode 100644 toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json
create mode 100644 toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png
create mode 100644 toolkit/mozapps/extensions/test/browser/head.js
create mode 100644 toolkit/mozapps/extensions/test/browser/head_abuse_report.js
create mode 100644 toolkit/mozapps/extensions/test/browser/moz.build
create mode 100644 toolkit/mozapps/extensions/test/browser/plugin_test.html
create mode 100644 toolkit/mozapps/extensions/test/browser/redirect.sjs
create mode 100644 toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
create mode 100644 toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
create mode 100644 toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml
create mode 100644 toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
create mode 100644 toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
create mode 100644 toolkit/mozapps/extensions/test/create_xpi.py
create mode 100644 toolkit/mozapps/extensions/test/mochitest/chrome.ini
create mode 100644 toolkit/mozapps/extensions/test/mochitest/file_empty.html
create mode 100644 toolkit/mozapps/extensions/test/mochitest/mochitest.ini
create mode 100644 toolkit/mozapps/extensions/test/mochitest/test_bug887098.html
create mode 100644 toolkit/mozapps/extensions/test/mochitest/test_default_theme.html
create mode 100644 toolkit/mozapps/extensions/test/moz.build
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_change.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update1.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update2.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update3.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/app_update.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update1.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update2.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/manual_update.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/mlbf-blocked1-unblocked2.bin
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_signed.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/langpack_unsigned.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/privileged.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed1.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed2.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_fail.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-extensions.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app-plugins.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-extensions.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit-plugins.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete_legacy.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer_legacy.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore_legacy.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.json
create mode 100755 toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_install_addons.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_install_compat.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_update.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_update_addons.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_update_compat.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/head_addons.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/head_compat.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/head_sideload.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/head_system_addons.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/head_unpack.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/head.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_addonBlockURL.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_appversion.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_clients.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_gfx.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_metadata_filters.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_dump.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_fetch.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_stashes.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_telemetry.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_mlbf_update.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_osabi.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_prefs.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_regexp_split.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_severities.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_targetapp_filter.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklist_telemetry.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_blocklistchange_v2.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Device.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_DriverNew.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverNew.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_DriverOld.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Equal_OK.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_DriverOld.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_GTE_OK.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_No_Comparison.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OK.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OS.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_match.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Vendor.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_Version.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_gfxBlacklist_prefs.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/test_softblocked.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/rs-blocklist/xpcshell.ini
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AbuseReporter.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_langpacks.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_paging.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_addonStartup.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_addon_manager_telemetry_events.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_amo_stats_telemetry.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_aom_startup.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_badschema.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_builtin_location.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_cookies.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_crash_annotation_quoting.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_db_path.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_dictionary_webextension.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_distribution.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_duplicateplugins.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_embedderDisabled.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_error.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_general.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_getInstallSourceFromHost.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_harness.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_hidden.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_install.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_isReady.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_locale.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_moved_extension_metadata.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_permissions.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_pluginchange.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_proxies.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_recommendations.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_registerchrome.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_registry.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_reinstall_disabled_addon.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_reload.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_safemode.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_seen.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_shutdown_barriers.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_shutdown_early.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_sideload_scopes.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_sideloads.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_sideloads_after_rebuild.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_signed_langpack.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_startup.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_startup_enable.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_startup_scan.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_allowed.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_profile_location.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_repository.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_blank.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_checkSizeHash.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_custom.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_empty.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_enterprisepolicy.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_fail.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_newset.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_overlapping.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_uninstall_check.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_update_upgrades.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_system_upgrades.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_systemaddomstartupprefs.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_trash_directory.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_types.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_update.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_update_noSystemAddonUpdate.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_update_theme.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_errors.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_updatecheck_json.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_updateid.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_upgrade_incompatible.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension_events.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension_install_syntax_error.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension_langpack.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_webextension_theme.js
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini
create mode 100644 toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/.eslintrc.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser.ini
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_auth.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_block_fullscreen_prompt.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_bug645699_postDownload.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_containers.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_empty.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_enabled2.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_enabled3.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_hash.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_localfile4_postDownload.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_newwindow.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_offline.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_privatebrowsing.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_relative.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_signed_trigger.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/browser_whitelist.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/bug540558.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/bug638292.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/bug645699.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/empty.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/enabled.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/head.js
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/installchrome.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/navigate.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/recommended.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html
create mode 100644 toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
(limited to 'toolkit/mozapps/extensions')
diff --git a/toolkit/mozapps/extensions/.eslintrc.js b/toolkit/mozapps/extensions/.eslintrc.js
new file mode 100644
index 0000000000..2e1909b8fb
--- /dev/null
+++ b/toolkit/mozapps/extensions/.eslintrc.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+ rules: {
+ // Warn about cyclomatic complexity in functions.
+ // XXX Bug 1326071 - This should be reduced down - probably to 20 or to
+ // be removed & synced with the mozilla/recommended value.
+ complexity: ["error", { max: 68 }],
+
+ "no-unused-vars": [
+ "error",
+ {
+ args: "none",
+ vars: "all",
+ },
+ ],
+ },
+ overrides: [
+ {
+ files: "test/xpcshell/head*.js",
+ rules: {
+ "no-unused-vars": [
+ "error",
+ {
+ args: "none",
+ vars: "local",
+ },
+ ],
+ },
+ },
+ ],
+};
diff --git a/toolkit/mozapps/extensions/AbuseReporter.jsm b/toolkit/mozapps/extensions/AbuseReporter.jsm
new file mode 100644
index 0000000000..d68e50cf7f
--- /dev/null
+++ b/toolkit/mozapps/extensions/AbuseReporter.jsm
@@ -0,0 +1,672 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["AbuseReporter", "AbuseReportError"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+Cu.importGlobalProperties(["fetch"]);
+
+const PREF_ABUSE_REPORT_URL = "extensions.abuseReport.url";
+const PREF_AMO_DETAILS_API_URL = "extensions.abuseReport.amoDetailsURL";
+
+// Name associated with the report dialog window.
+const DIALOG_WINDOW_NAME = "addons-abuse-report-dialog";
+
+// Maximum length of the string properties sent to the API endpoint.
+const MAX_STRING_LENGTH = 255;
+
+// Minimum time between report submissions (in ms).
+const MIN_MS_BETWEEN_SUBMITS = 30000;
+
+// The addon types currently supported by the integrated abuse report panel.
+const SUPPORTED_ADDON_TYPES = ["extension", "theme"];
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.jsm",
+ AMTelemetry: "resource://gre/modules/AddonManager.jsm",
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ ClientID: "resource://gre/modules/ClientID.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "ABUSE_REPORT_URL",
+ PREF_ABUSE_REPORT_URL
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "AMO_DETAILS_API_URL",
+ PREF_AMO_DETAILS_API_URL
+);
+
+const PRIVATE_REPORT_PROPS = Symbol("privateReportProps");
+
+const ERROR_TYPES = Object.freeze([
+ "ERROR_ABORTED_SUBMIT",
+ "ERROR_ADDON_NOTFOUND",
+ "ERROR_CLIENT",
+ "ERROR_NETWORK",
+ "ERROR_UNKNOWN",
+ "ERROR_RECENT_SUBMIT",
+ "ERROR_SERVER",
+ "ERROR_AMODETAILS_NOTFOUND",
+ "ERROR_AMODETAILS_FAILURE",
+]);
+
+class AbuseReportError extends Error {
+ constructor(errorType, errorInfo = undefined) {
+ if (!ERROR_TYPES.includes(errorType)) {
+ throw new Error(`Unknown AbuseReportError type "${errorType}"`);
+ }
+
+ let message = errorInfo ? `${errorType} - ${errorInfo}` : errorType;
+
+ super(message);
+ this.name = "AbuseReportError";
+ this.errorType = errorType;
+ this.errorInfo = errorInfo;
+ }
+}
+
+/**
+ * Create an error info string from a fetch response object.
+ *
+ * @param {Response} response
+ * A fetch response object to convert into an errorInfo string.
+ *
+ * @returns {Promise}
+ * The errorInfo string to be included in an AbuseReportError.
+ */
+async function responseToErrorInfo(response) {
+ return JSON.stringify({
+ status: response.status,
+ responseText: await response.text().catch(err => ""),
+ });
+}
+
+/**
+ * A singleton object used to create new AbuseReport instances for a given addonId
+ * and enforce a minium amount of time between two report submissions .
+ */
+const AbuseReporter = {
+ _lastReportTimestamp: null,
+
+ // Error types.
+ updateLastReportTimestamp() {
+ this._lastReportTimestamp = Date.now();
+ },
+
+ getTimeFromLastReport() {
+ const currentTimestamp = Date.now();
+ if (this._lastReportTimestamp > currentTimestamp) {
+ // Reset the last report timestamp if it is in the future.
+ this._lastReportTimestamp = null;
+ }
+
+ if (!this._lastReportTimestamp) {
+ return Infinity;
+ }
+
+ return currentTimestamp - this._lastReportTimestamp;
+ },
+
+ /**
+ * Create an AbuseReport instance, given the addonId and a reportEntryPoint.
+ *
+ * @param {string} addonId
+ * The id of the addon to create the report instance for.
+ * @param {object} options
+ * @param {string} options.reportEntryPoint
+ * An identifier that represent the entry point for the report flow.
+ *
+ * @returns {Promise}
+ * Returns a promise that resolves to an instance of the AbuseReport
+ * class, which represent an ongoing report.
+ */
+ async createAbuseReport(addonId, { reportEntryPoint } = {}) {
+ let addon = await AddonManager.getAddonByID(addonId);
+
+ if (!addon) {
+ // The addon isn't installed, query the details from the AMO API endpoint.
+ addon = await this.queryAMOAddonDetails(addonId, reportEntryPoint);
+ }
+
+ if (!addon) {
+ AMTelemetry.recordReportEvent({
+ addonId,
+ errorType: "ERROR_ADDON_NOTFOUND",
+ reportEntryPoint,
+ });
+ throw new AbuseReportError("ERROR_ADDON_NOTFOUND");
+ }
+
+ const reportData = await this.getReportData(addon);
+
+ return new AbuseReport({
+ addon,
+ reportData,
+ reportEntryPoint,
+ });
+ },
+
+ /**
+ * Retrieves the addon details from the AMO API endpoint, used to create
+ * abuse reports on non-installed addon-ons.
+ *
+ * For the addon details that may be translated (e.g. addon name, description etc.)
+ * the function will try to retrieve the string localized in the same locale used
+ * by Gecko (and fallback to "en-US" if that locale is unavailable).
+ *
+ * The addon creator properties are set to the first author available.
+ *
+ * @param {string} addonId
+ * The id of the addon to retrieve the details available on AMO.
+ * @param {string} reportEntryPoint
+ * The entry point for the report flow (to be included in the telemetry
+ * recorded in case of failures).
+ *
+ * @returns {Promise}
+ * Returns a promise that resolves to an AMOAddonDetails object,
+ * which has the subset of the AddonWrapper properties which are
+ * needed by the abuse report panel or the report data sent to
+ * the abuse report API endpoint), or null if it fails to
+ * retrieve the details from AMO.
+ *
+ * @typedef {object} AMOAddonDetails
+ * @prop {string} id
+ * @prop {string} name
+ * @prop {string} version
+ * @prop {string} description
+ * @prop {string} type
+ * @prop {string} iconURL
+ * @prop {string} homepageURL
+ * @prop {string} supportURL
+ * @prop {AMOAddonCreator} creator
+ * @prop {boolean} isRecommended
+ * @prop {number} signedState=AddonManager.SIGNEDSTATE_UNKNOWN
+ * @prop {object} installTelemetryInfo={ source: "not_installed" }
+ *
+ * @typedef {object} AMOAddonCreator
+ * @prop {string} name
+ * @prop {string} url
+ */
+ async queryAMOAddonDetails(addonId, reportEntryPoint) {
+ let details;
+ try {
+ // This should be the API endpoint documented at:
+ // https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#detail
+ details = await fetch(`${AMO_DETAILS_API_URL}/${addonId}`, {
+ credentials: "omit",
+ referrerPolicy: "no-referrer",
+ headers: { "Content-Type": "application/json" },
+ }).then(async response => {
+ if (response.status === 200) {
+ return response.json();
+ }
+
+ let errorInfo = await responseToErrorInfo(response).catch(
+ err => undefined
+ );
+
+ if (response.status === 404) {
+ // Record a different telemetry event for 404 errors.
+ throw new AbuseReportError("ERROR_AMODETAILS_NOTFOUND", errorInfo);
+ }
+
+ throw new AbuseReportError("ERROR_AMODETAILS_FAILURE", errorInfo);
+ });
+ } catch (err) {
+ // Log the original error in the browser console.
+ Cu.reportError(err);
+
+ AMTelemetry.recordReportEvent({
+ addonId,
+ errorType: err.errorType || "ERROR_AMODETAILS_FAILURE",
+ reportEntryPoint,
+ });
+
+ return null;
+ }
+
+ const locale = Services.locale.appLocaleAsBCP47;
+
+ // Get a string value from a translated value
+ // (https://addons-server.readthedocs.io/en/latest/topics/api/overview.html#api-overview-translations)
+ const getTranslatedValue = value => {
+ if (typeof value === "string") {
+ return value;
+ }
+ return value && (value[locale] || value["en-US"]);
+ };
+
+ const getAuthorField = fieldName =>
+ details.authors && details.authors[0] && details.authors[0][fieldName];
+
+ // Normalize type "statictheme" (which is the type used on the AMO API side)
+ // into "theme" (because it is the type we use and expect on the Firefox side
+ // for this addon type).
+ const addonType = details.type === "statictheme" ? "theme" : details.type;
+
+ return {
+ id: addonId,
+ name: getTranslatedValue(details.name),
+ version: details.current_version.version,
+ description: getTranslatedValue(details.summary),
+ type: addonType,
+ iconURL: details.icon_url,
+ homepageURL: getTranslatedValue(details.homepage),
+ supportURL: getTranslatedValue(details.support_url),
+ // Set the addon creator to the first author in the AMO details.
+ creator: {
+ name: getAuthorField("name"),
+ url: getAuthorField("url"),
+ },
+ isRecommended: details.is_recommended,
+ // Set signed state to unknown because it isn't installed.
+ signedState: AddonManager.SIGNEDSTATE_UNKNOWN,
+ // Set the installTelemetryInfo.source to "not_installed".
+ installTelemetryInfo: { source: "not_installed" },
+ };
+ },
+
+ /**
+ * Helper function that retrieves from an addon object all the data to send
+ * as part of the submission request, besides the `reason`, `message` which are
+ * going to be received from the submit method of the report object returned
+ * by `createAbuseReport`.
+ * (See https://addons-server.readthedocs.io/en/latest/topics/api/abuse.html)
+ *
+ * @param {AddonWrapper} addon
+ * The addon object to collect the detail from.
+ *
+ * @return {object}
+ * An object that contains the collected details.
+ */
+ async getReportData(addon) {
+ const truncateString = text =>
+ typeof text == "string" ? text.slice(0, MAX_STRING_LENGTH) : text;
+
+ // Normalize addon_install_source and addon_install_method values
+ // as expected by the server API endpoint. Returns null if the
+ // value is not a string.
+ const normalizeValue = text =>
+ typeof text == "string"
+ ? text.toLowerCase().replace(/[- :]/g, "_")
+ : null;
+
+ const installInfo = addon.installTelemetryInfo || {};
+
+ const data = {
+ addon: addon.id,
+ addon_version: addon.version,
+ addon_name: truncateString(addon.name),
+ addon_summary: truncateString(addon.description),
+ addon_install_origin:
+ addon.sourceURI && truncateString(addon.sourceURI.spec),
+ install_date: addon.installDate && addon.installDate.toISOString(),
+ addon_install_source: normalizeValue(installInfo.source),
+ addon_install_source_url:
+ installInfo.sourceURL && truncateString(installInfo.sourceURL),
+ addon_install_method: normalizeValue(installInfo.method),
+ };
+
+ switch (addon.signedState) {
+ case AddonManager.SIGNEDSTATE_BROKEN:
+ data.addon_signature = "broken";
+ break;
+ case AddonManager.SIGNEDSTATE_UNKNOWN:
+ data.addon_signature = "unknown";
+ break;
+ case AddonManager.SIGNEDSTATE_MISSING:
+ data.addon_signature = "missing";
+ break;
+ case AddonManager.SIGNEDSTATE_PRELIMINARY:
+ data.addon_signature = "preliminary";
+ break;
+ case AddonManager.SIGNEDSTATE_SIGNED:
+ data.addon_signature = "signed";
+ break;
+ case AddonManager.SIGNEDSTATE_SYSTEM:
+ data.addon_signature = "system";
+ break;
+ case AddonManager.SIGNEDSTATE_PRIVILEGED:
+ data.addon_signature = "privileged";
+ break;
+ default:
+ data.addon_signature = `unknown: ${addon.signedState}`;
+ }
+
+ // Set "curated" as addon_signature on recommended addons
+ // (addon.isRecommended internally checks that the addon is also
+ // signed correctly).
+ if (addon.isRecommended) {
+ data.addon_signature = "curated";
+ }
+
+ data.client_id = await ClientID.getClientIdHash();
+
+ data.app = Services.appinfo.name.toLowerCase();
+ data.appversion = Services.appinfo.version;
+ data.lang = Services.locale.appLocaleAsBCP47;
+ data.operating_system = AppConstants.platform;
+ data.operating_system_version = Services.sysinfo.getProperty("version");
+
+ return data;
+ },
+
+ /**
+ * Helper function that returns a reference to a report dialog window
+ * already opened (if any).
+ *
+ * @returns {Window?}
+ */
+ getOpenDialog() {
+ return Services.ww.getWindowByName(DIALOG_WINDOW_NAME, null);
+ },
+
+ /**
+ * Helper function that opens an abuse report form in a new dialog window.
+ *
+ * @param {string} addonId
+ * The addonId being reported.
+ * @param {string} reportEntryPoint
+ * The entry point from which the user has triggered the abuse report
+ * flow.
+ * @param {XULElement} browser
+ * The browser element (if any) that is opening the report window.
+ *
+ * @return {Promise}
+ * Returns an AbuseReportDialog object, rejects if it fails to open
+ * the dialog.
+ *
+ * @typedef {object} AbuseReportDialog
+ * An object that represents the abuse report dialog.
+ * @prop {function} close
+ * A method that closes the report dialog (used by the caller
+ * to close the dialog when the user chooses to close the window
+ * that started the abuse report flow).
+ * @prop {Promise {
+ dialogInit.deferredReport = { resolve, reject };
+ }).then(
+ ({ userCancelled }) => {
+ closeDialog();
+ return userCancelled ? undefined : report;
+ },
+ err => {
+ Cu.reportError(
+ `Unexpected abuse report panel error: ${err} :: ${err.stack}`
+ );
+ closeDialog();
+ return Promise.reject({
+ message: "Unexpected abuse report panel error",
+ });
+ }
+ );
+
+ const promiseReportPanel = new Promise((resolve, reject) => {
+ dialogInit.deferredReportPanel = { resolve, reject };
+ });
+
+ dialogInit.promiseReport = promiseReport;
+ dialogInit.promiseReportPanel = promiseReportPanel;
+
+ win = Services.ww.openWindow(
+ chromeWin,
+ "chrome://mozapps/content/extensions/abuse-report-frame.html",
+ DIALOG_WINDOW_NAME,
+ // Set the dialog window options (including a reasonable initial
+ // window height size, eventually adjusted by the panel once it
+ // has been rendered its content).
+ "dialog,centerscreen,height=700",
+ params
+ );
+
+ return {
+ close: closeDialog,
+ promiseReport,
+
+ // Properties used in tests
+ promiseReportPanel,
+ window: win,
+ };
+ },
+};
+
+/**
+ * Represents an ongoing abuse report. Instances of this class are created
+ * by the `AbuseReporter.createAbuseReport` method.
+ *
+ * This object is used by the reporting UI panel and message bars to:
+ *
+ * - get an errorType in case of a report creation error (e.g. because of a
+ * previously submitted report)
+ * - get the addon details used inside the reporting panel
+ * - submit the abuse report (and re-submit if a previous submission failed
+ * and the user choose to retry to submit it again)
+ * - abort an ongoing submission
+ *
+ * @param {object} options
+ * @param {AddonWrapper|null} options.addon
+ * AddonWrapper instance for the extension/theme being reported.
+ * (May be null if the extension has not been found).
+ * @param {object|null} options.reportData
+ * An object which contains addon and environment details to send as part of a submission
+ * (may be null if the report has a createErrorType).
+ * @param {string} options.reportEntryPoint
+ * A string that identify how the report has been triggered.
+ */
+class AbuseReport {
+ constructor({ addon, createErrorType, reportData, reportEntryPoint }) {
+ this[PRIVATE_REPORT_PROPS] = {
+ aborted: false,
+ abortController: new AbortController(),
+ addon,
+ reportData,
+ reportEntryPoint,
+ // message and reason are initially null, and then set by the panel
+ // using the related set method.
+ message: null,
+ reason: null,
+ };
+ }
+
+ recordTelemetry(errorType) {
+ const { addon, reportEntryPoint } = this;
+ AMTelemetry.recordReportEvent({
+ addonId: addon.id,
+ addonType: addon.type,
+ errorType,
+ reportEntryPoint,
+ });
+ }
+
+ /**
+ * Submit the current report, given a reason and a message.
+ *
+ * @returns {Promise}
+ * Resolves once the report has been successfully submitted.
+ * It rejects with an AbuseReportError if the report couldn't be
+ * submitted for a known reason (or another Error type otherwise).
+ */
+ async submit() {
+ const {
+ aborted,
+ abortController,
+ message,
+ reason,
+ reportData,
+ reportEntryPoint,
+ } = this[PRIVATE_REPORT_PROPS];
+
+ // Record telemetry event and throw an AbuseReportError.
+ const rejectReportError = async (errorType, { response } = {}) => {
+ this.recordTelemetry(errorType);
+
+ // Leave errorInfo empty if there is no response or fails to
+ // be converted into an error info object.
+ const errorInfo = response
+ ? await responseToErrorInfo(response).catch(err => undefined)
+ : undefined;
+
+ throw new AbuseReportError(errorType, errorInfo);
+ };
+
+ if (aborted) {
+ // Report aborted before being actually submitted.
+ return rejectReportError("ERROR_ABORTED_SUBMIT");
+ }
+
+ // Prevent submit of a new abuse report in less than MIN_MS_BETWEEN_SUBMITS.
+ let msFromLastReport = AbuseReporter.getTimeFromLastReport();
+ if (msFromLastReport < MIN_MS_BETWEEN_SUBMITS) {
+ return rejectReportError("ERROR_RECENT_SUBMIT");
+ }
+
+ let response;
+ try {
+ response = await fetch(ABUSE_REPORT_URL, {
+ signal: abortController.signal,
+ method: "POST",
+ credentials: "omit",
+ referrerPolicy: "no-referrer",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ ...reportData,
+ report_entry_point: reportEntryPoint,
+ message,
+ reason,
+ }),
+ });
+ } catch (err) {
+ if (err.name === "AbortError") {
+ return rejectReportError("ERROR_ABORTED_SUBMIT");
+ }
+ Cu.reportError(err);
+ return rejectReportError("ERROR_NETWORK");
+ }
+
+ if (response.ok && response.status >= 200 && response.status < 400) {
+ // Ensure that the response is also a valid json format.
+ try {
+ await response.json();
+ } catch (err) {
+ this.recordTelemetry("ERROR_UNKNOWN");
+ throw err;
+ }
+ AbuseReporter.updateLastReportTimestamp();
+ this.recordTelemetry();
+ return undefined;
+ }
+
+ if (response.status >= 400 && response.status < 500) {
+ return rejectReportError("ERROR_CLIENT", { response });
+ }
+
+ if (response.status >= 500 && response.status < 600) {
+ return rejectReportError("ERROR_SERVER", { response });
+ }
+
+ // We got an unexpected HTTP status code.
+ return rejectReportError("ERROR_UNKNOWN", { response });
+ }
+
+ /**
+ * Abort the report submission.
+ */
+ abort() {
+ const { abortController } = this[PRIVATE_REPORT_PROPS];
+ abortController.abort();
+ this[PRIVATE_REPORT_PROPS].aborted = true;
+ }
+
+ get addon() {
+ return this[PRIVATE_REPORT_PROPS].addon;
+ }
+
+ get reportEntryPoint() {
+ return this[PRIVATE_REPORT_PROPS].reportEntryPoint;
+ }
+
+ /**
+ * Set the open message (called from the panel when the user submit the report)
+ *
+ * @parm {string} message
+ * An optional string which contains a description for the reported issue.
+ */
+ setMessage(message) {
+ this[PRIVATE_REPORT_PROPS].message = message;
+ }
+
+ /**
+ * Set the report reason (called from the panel when the user submit the report)
+ *
+ * @parm {string} reason
+ * String identifier for the report reason.
+ */
+ setReason(reason) {
+ this[PRIVATE_REPORT_PROPS].reason = reason;
+ }
+}
diff --git a/toolkit/mozapps/extensions/AddonContentPolicy.cpp b/toolkit/mozapps/extensions/AddonContentPolicy.cpp
new file mode 100644
index 0000000000..98b87ad128
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonContentPolicy.cpp
@@ -0,0 +1,483 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AddonContentPolicy.h"
+
+#include "mozilla/dom/nsCSPContext.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentTypeParser.h"
+#include "nsContentUtils.h"
+#include "nsIConsoleService.h"
+#include "nsIContentSecurityPolicy.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIScriptError.h"
+#include "nsIStringBundle.h"
+#include "nsIUUIDGenerator.h"
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla;
+
+/* Enforces content policies for WebExtension scopes. Currently:
+ *
+ * - Prevents loading scripts with a non-default JavaScript version.
+ * - Checks custom content security policies for sufficiently stringent
+ * script-src and object-src directives.
+ */
+
+#define VERSIONED_JS_BLOCKED_MESSAGE \
+ u"Versioned JavaScript is a non-standard, deprecated extension, and is " \
+ u"not supported in WebExtension code. For alternatives, please see: " \
+ u"https://developer.mozilla.org/Add-ons/WebExtensions/Tips"
+
+AddonContentPolicy::AddonContentPolicy() = default;
+
+AddonContentPolicy::~AddonContentPolicy() = default;
+
+NS_IMPL_ISUPPORTS(AddonContentPolicy, nsIContentPolicy, nsIAddonContentPolicy)
+
+static nsresult GetWindowIDFromContext(nsISupports* aContext,
+ uint64_t* aResult) {
+ NS_ENSURE_TRUE(aContext, NS_ERROR_FAILURE);
+
+ nsCOMPtr content = do_QueryInterface(aContext);
+ NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
+
+ nsCOMPtr window = content->OwnerDoc()->GetInnerWindow();
+ NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
+
+ *aResult = window->WindowID();
+ return NS_OK;
+}
+
+static nsresult LogMessage(const nsAString& aMessage,
+ const nsAString& aSourceName,
+ const nsAString& aSourceSample,
+ nsISupports* aContext) {
+ nsCOMPtr error = do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
+ NS_ENSURE_TRUE(error, NS_ERROR_OUT_OF_MEMORY);
+
+ uint64_t windowID = 0;
+ GetWindowIDFromContext(aContext, &windowID);
+
+ nsresult rv = error->InitWithSanitizedSource(
+ aMessage, aSourceName, aSourceSample, 0, 0, nsIScriptError::errorFlag,
+ "JavaScript", windowID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr console =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY);
+
+ console->LogMessage(error);
+ return NS_OK;
+}
+
+// Content policy enforcement:
+
+NS_IMETHODIMP
+AddonContentPolicy::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
+ const nsACString& aMimeTypeGuess,
+ int16_t* aShouldLoad) {
+ if (!aContentLocation || !aLoadInfo) {
+ NS_SetRequestBlockingReason(
+ aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_WEBEXT);
+ *aShouldLoad = REJECT_REQUEST;
+ return NS_ERROR_FAILURE;
+ }
+
+ ExtContentPolicyType contentType = aLoadInfo->GetExternalContentPolicyType();
+
+ *aShouldLoad = nsIContentPolicy::ACCEPT;
+ nsCOMPtr loadingPrincipal = aLoadInfo->GetLoadingPrincipal();
+ if (!loadingPrincipal) {
+ return NS_OK;
+ }
+
+ // Only apply this policy to requests from documents loaded from
+ // moz-extension URLs, or to resources being loaded from moz-extension URLs.
+ if (!(aContentLocation->SchemeIs("moz-extension") ||
+ loadingPrincipal->SchemeIs("moz-extension"))) {
+ return NS_OK;
+ }
+
+ if (contentType == ExtContentPolicy::TYPE_SCRIPT) {
+ NS_ConvertUTF8toUTF16 typeString(aMimeTypeGuess);
+ nsContentTypeParser mimeParser(typeString);
+
+ // Reject attempts to load JavaScript scripts with a non-default version.
+ nsAutoString mimeType, version;
+ if (NS_SUCCEEDED(mimeParser.GetType(mimeType)) &&
+ nsContentUtils::IsJavascriptMIMEType(mimeType) &&
+ NS_SUCCEEDED(mimeParser.GetParameter("version", version))) {
+ NS_SetRequestBlockingReason(
+ aLoadInfo, nsILoadInfo::BLOCKING_REASON_CONTENT_POLICY_WEBEXT);
+ *aShouldLoad = nsIContentPolicy::REJECT_REQUEST;
+
+ nsCString sourceName;
+ loadingPrincipal->GetExposableSpec(sourceName);
+ NS_ConvertUTF8toUTF16 nameString(sourceName);
+
+ nsCOMPtr context = aLoadInfo->GetLoadingContext();
+ LogMessage(nsLiteralString(VERSIONED_JS_BLOCKED_MESSAGE), nameString,
+ typeString, context);
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AddonContentPolicy::ShouldProcess(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo,
+ const nsACString& aMimeTypeGuess,
+ int16_t* aShouldProcess) {
+ *aShouldProcess = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+}
+
+// CSP Validation:
+
+static const char* allowedSchemes[] = {"blob", "filesystem", nullptr};
+
+static const char* allowedHostSchemes[] = {"http", "https", "moz-extension",
+ nullptr};
+
+/**
+ * Validates a CSP directive to ensure that it is sufficiently stringent.
+ * In particular, ensures that:
+ *
+ * - No remote sources are allowed other than from https: schemes
+ *
+ * - No remote sources specify host wildcards for generic domains
+ * (*.blogspot.com, *.com, *)
+ *
+ * - All remote sources and local extension sources specify a host
+ *
+ * - No scheme sources are allowed other than blob:, filesystem:,
+ * moz-extension:, and https:
+ *
+ * - No keyword sources are allowed other than 'none', 'self', 'unsafe-eval',
+ * and hash sources.
+ *
+ * Manifest V3 limits CSP for extension_pages, the script-src, object-src, and
+ * worker-src directives may only be the following:
+ * - self
+ * - none
+ * - Any localhost source, (http://localhost, http://127.0.0.1, or any port
+ * on those domains)
+ */
+class CSPValidator final : public nsCSPSrcVisitor {
+ public:
+ CSPValidator(nsAString& aURL, CSPDirective aDirective,
+ bool aDirectiveRequired = true, uint32_t aPermittedPolicy = 0)
+ : mURL(aURL),
+ mDirective(CSP_CSPDirectiveToString(aDirective)),
+ mPermittedPolicy(aPermittedPolicy),
+ mFoundSelf(false) {
+ // Start with the default error message for a missing directive, since no
+ // visitors will be called if the directive isn't present.
+ mError.SetIsVoid(true);
+ if (aDirectiveRequired) {
+ FormatError("csp.error.missing-directive");
+ }
+ }
+
+ // Visitors
+
+ bool visitSchemeSrc(const nsCSPSchemeSrc& src) override {
+ nsAutoString scheme;
+ src.getScheme(scheme);
+
+ if (SchemeInList(scheme, allowedHostSchemes)) {
+ FormatError("csp.error.missing-host", scheme);
+ return false;
+ }
+ if (!SchemeInList(scheme, allowedSchemes)) {
+ FormatError("csp.error.illegal-protocol", scheme);
+ return false;
+ }
+ return true;
+ };
+
+ bool visitHostSrc(const nsCSPHostSrc& src) override {
+ nsAutoString scheme, host;
+
+ src.getScheme(scheme);
+ src.getHost(host);
+
+ if (scheme.LowerCaseEqualsLiteral("http")) {
+ // Allow localhost on http
+ if (mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_LOCALHOST &&
+ HostIsLocal(host)) {
+ return true;
+ }
+ FormatError("csp.error.illegal-protocol", scheme);
+ return false;
+ }
+ if (scheme.LowerCaseEqualsLiteral("https")) {
+ if (mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_LOCALHOST &&
+ HostIsLocal(host)) {
+ return true;
+ }
+ if (!(mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_REMOTE)) {
+ FormatError("csp.error.illegal-protocol", scheme);
+ return false;
+ }
+ if (!HostIsAllowed(host)) {
+ FormatError("csp.error.illegal-host-wildcard", scheme);
+ return false;
+ }
+ } else if (scheme.LowerCaseEqualsLiteral("moz-extension")) {
+ // The CSP parser silently converts 'self' keywords to the origin
+ // URL, so we need to reconstruct the URL to see if it was present.
+ if (!mFoundSelf) {
+ nsAutoString url(u"moz-extension://");
+ url.Append(host);
+
+ mFoundSelf = url.Equals(mURL);
+ }
+
+ if (host.IsEmpty() || host.EqualsLiteral("*")) {
+ FormatError("csp.error.missing-host", scheme);
+ return false;
+ }
+ } else if (!SchemeInList(scheme, allowedSchemes)) {
+ FormatError("csp.error.illegal-protocol", scheme);
+ return false;
+ }
+
+ return true;
+ };
+
+ bool visitKeywordSrc(const nsCSPKeywordSrc& src) override {
+ switch (src.getKeyword()) {
+ case CSP_NONE:
+ case CSP_SELF:
+ return true;
+ case CSP_UNSAFE_EVAL:
+ if (mPermittedPolicy & nsIAddonContentPolicy::CSP_ALLOW_EVAL) {
+ return true;
+ }
+ // fall through and produce an illegal-keyword error.
+ [[fallthrough]];
+ default:
+ FormatError(
+ "csp.error.illegal-keyword",
+ nsDependentString(CSP_EnumToUTF16Keyword(src.getKeyword())));
+ return false;
+ }
+ };
+
+ bool visitNonceSrc(const nsCSPNonceSrc& src) override {
+ FormatError("csp.error.illegal-keyword", u"'nonce-*'"_ns);
+ return false;
+ };
+
+ bool visitHashSrc(const nsCSPHashSrc& src) override { return true; };
+
+ // Accessors
+
+ inline nsAString& GetError() { return mError; };
+
+ inline bool FoundSelf() { return mFoundSelf; };
+
+ // Formatters
+
+ template
+ inline void FormatError(const char* aName, const T... aParams) {
+ AutoTArray params = {mDirective,
+ aParams...};
+ FormatErrorParams(aName, params);
+ };
+
+ private:
+ // Validators
+ bool HostIsLocal(nsAString& host) {
+ return host.EqualsLiteral("localhost") || host.EqualsLiteral("127.0.0.1");
+ }
+
+ bool HostIsAllowed(nsAString& host) {
+ if (host.First() == '*') {
+ if (host.EqualsLiteral("*") || host[1] != '.') {
+ return false;
+ }
+
+ host.Cut(0, 2);
+
+ nsCOMPtr tldService =
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+
+ if (!tldService) {
+ return false;
+ }
+
+ NS_ConvertUTF16toUTF8 cHost(host);
+ nsAutoCString publicSuffix;
+
+ nsresult rv = tldService->GetPublicSuffixFromHost(cHost, publicSuffix);
+
+ return NS_SUCCEEDED(rv) && !cHost.Equals(publicSuffix);
+ }
+
+ return true;
+ };
+
+ bool SchemeInList(nsAString& scheme, const char** schemes) {
+ for (; *schemes; schemes++) {
+ if (scheme.LowerCaseEqualsASCII(*schemes)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // Formatters
+
+ already_AddRefed GetStringBundle() {
+ nsCOMPtr sbs =
+ mozilla::services::GetStringBundleService();
+ NS_ENSURE_TRUE(sbs, nullptr);
+
+ nsCOMPtr stringBundle;
+ sbs->CreateBundle("chrome://global/locale/extensions.properties",
+ getter_AddRefs(stringBundle));
+
+ return stringBundle.forget();
+ };
+
+ void FormatErrorParams(const char* aName, const nsTArray& aParams) {
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr stringBundle = GetStringBundle();
+
+ if (stringBundle) {
+ rv = stringBundle->FormatStringFromName(aName, aParams, mError);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mError.AssignLiteral("An unexpected error occurred");
+ }
+ };
+
+ // Data members
+
+ nsAutoString mURL;
+ NS_ConvertASCIItoUTF16 mDirective;
+ nsString mError;
+
+ uint32_t mPermittedPolicy;
+ bool mFoundSelf;
+};
+
+/**
+ * Validates a custom content security policy string for use by an add-on.
+ * In particular, ensures that:
+ *
+ * - Both object-src and script-src directives are present, and meet
+ * the policies required by the CSPValidator class
+ *
+ * - The script-src directive includes the source 'self'
+ */
+NS_IMETHODIMP
+AddonContentPolicy::ValidateAddonCSP(const nsAString& aPolicyString,
+ uint32_t aPermittedPolicy,
+ nsAString& aResult) {
+ nsresult rv;
+
+ // Validate against a randomly-generated extension origin.
+ // There is no add-on-specific behavior in the CSP code, beyond the ability
+ // for add-ons to specify a custom policy, but the parser requires a valid
+ // origin in order to operate correctly.
+ nsAutoString url(u"moz-extension://");
+ {
+ nsCOMPtr uuidgen = services::GetUUIDGenerator();
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char idString[NSID_LENGTH];
+ id.ToProvidedString(idString);
+
+ MOZ_RELEASE_ASSERT(idString[0] == '{' && idString[NSID_LENGTH - 2] == '}',
+ "UUID generator did not return a valid UUID");
+
+ url.AppendASCII(idString + 1, NSID_LENGTH - 3);
+ }
+
+ RefPtr principal =
+ BasePrincipal::CreateContentPrincipal(NS_ConvertUTF16toUTF8(url));
+
+ nsCOMPtr selfURI;
+ principal->GetURI(getter_AddRefs(selfURI));
+ RefPtr csp = new nsCSPContext();
+ rv = csp->SetRequestContextWithPrincipal(principal, selfURI, u""_ns, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ csp->AppendPolicy(aPolicyString, false, false);
+
+ const nsCSPPolicy* policy = csp->GetPolicy(0);
+ if (!policy) {
+ CSPValidator validator(url, nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE,
+ true, aPermittedPolicy);
+ aResult.Assign(validator.GetError());
+ return NS_OK;
+ }
+
+ bool haveValidDefaultSrc = false;
+ bool hasValidScriptSrc = false;
+ {
+ CSPDirective directive = nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE;
+ CSPValidator validator(url, directive);
+
+ haveValidDefaultSrc = policy->visitDirectiveSrcs(directive, &validator);
+ }
+
+ aResult.SetIsVoid(true);
+ {
+ CSPDirective directive = nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE;
+ CSPValidator validator(url, directive, !haveValidDefaultSrc,
+ aPermittedPolicy);
+
+ if (!policy->visitDirectiveSrcs(directive, &validator)) {
+ aResult.Assign(validator.GetError());
+ } else if (!validator.FoundSelf()) {
+ validator.FormatError("csp.error.missing-source", u"'self'"_ns);
+ aResult.Assign(validator.GetError());
+ }
+ hasValidScriptSrc = true;
+ }
+
+ if (aResult.IsVoid()) {
+ CSPDirective directive = nsIContentSecurityPolicy::OBJECT_SRC_DIRECTIVE;
+ CSPValidator validator(url, directive, !haveValidDefaultSrc,
+ aPermittedPolicy);
+
+ if (!policy->visitDirectiveSrcs(directive, &validator)) {
+ aResult.Assign(validator.GetError());
+ }
+ }
+
+ if (aResult.IsVoid()) {
+ CSPDirective directive = nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE;
+ CSPValidator validator(url, directive,
+ !haveValidDefaultSrc && !hasValidScriptSrc,
+ aPermittedPolicy);
+
+ if (!policy->visitDirectiveSrcs(directive, &validator)) {
+ aResult.Assign(validator.GetError());
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/mozapps/extensions/AddonContentPolicy.h b/toolkit/mozapps/extensions/AddonContentPolicy.h
new file mode 100644
index 0000000000..db4c29db05
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonContentPolicy.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIContentPolicy.h"
+#include "nsIAddonPolicyService.h"
+
+class AddonContentPolicy : public nsIContentPolicy,
+ public nsIAddonContentPolicy {
+ protected:
+ virtual ~AddonContentPolicy();
+
+ public:
+ AddonContentPolicy();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTPOLICY
+ NS_DECL_NSIADDONCONTENTPOLICY
+};
diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm
new file mode 100644
index 0000000000..726819a254
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -0,0 +1,4918 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Cannot use Services.appinfo here, or else xpcshell-tests will blow up, as
+// most tests later register different nsIAppInfo implementations, which
+// wouldn't be reflected in Services.appinfo anymore, as the lazy getter
+// underlying it would have been initialized if we used it here.
+if ("@mozilla.org/xre/app-info;1" in Cc) {
+ // eslint-disable-next-line mozilla/use-services
+ let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+ if (runtime.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ // Refuse to run in child processes.
+ throw new Error("You cannot use the AddonManager in child processes!");
+ }
+}
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+const MOZ_COMPATIBILITY_NIGHTLY = ![
+ "aurora",
+ "beta",
+ "release",
+ "esr",
+].includes(AppConstants.MOZ_UPDATE_CHANNEL);
+
+const PREF_AMO_ABUSEREPORT = "extensions.abuseReport.amWebAPI.enabled";
+const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
+const PREF_EM_UPDATE_ENABLED = "extensions.update.enabled";
+const PREF_EM_LAST_APP_VERSION = "extensions.lastAppVersion";
+const PREF_EM_LAST_PLATFORM_VERSION = "extensions.lastPlatformVersion";
+const PREF_EM_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
+const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+const PREF_SYS_ADDON_UPDATE_ENABLED = "extensions.systemAddon.update.enabled";
+
+const PREF_MIN_WEBEXT_PLATFORM_VERSION =
+ "extensions.webExtensionsMinPlatformVersion";
+const PREF_WEBAPI_TESTING = "extensions.webapi.testing";
+const PREF_WEBEXT_PERM_PROMPTS = "extensions.webextPermissionPrompts";
+const PREF_EM_POSTDOWNLOAD_THIRD_PARTY =
+ "extensions.postDownloadThirdPartyPrompt";
+
+const UPDATE_REQUEST_VERSION = 2;
+
+const BRANCH_REGEXP = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
+const PREF_EM_CHECK_COMPATIBILITY_BASE = "extensions.checkCompatibility";
+var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY
+ ? PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly"
+ : undefined;
+
+const VALID_TYPES_REGEXP = /^[\w\-]+$/;
+
+const WEBAPI_INSTALL_HOSTS = ["addons.mozilla.org"];
+const WEBAPI_TEST_INSTALL_HOSTS = [
+ "addons.allizom.org",
+ "addons-dev.allizom.org",
+ "example.com",
+];
+
+const AMO_ATTRIBUTION_ALLOWED_SOURCES = ["amo", "disco"];
+const AMO_ATTRIBUTION_DATA_KEYS = [
+ "utm_campaign",
+ "utm_content",
+ "utm_medium",
+ "utm_source",
+];
+const AMO_ATTRIBUTION_DATA_MAX_LENGTH = 40;
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+// This global is overridden by xpcshell tests, and therefore cannot be
+// a const.
+var { AsyncShutdown } = ChromeUtils.import(
+ "resource://gre/modules/AsyncShutdown.jsm"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["Element"]);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
+ AbuseReporter: "resource://gre/modules/AbuseReporter.jsm",
+ Extension: "resource://gre/modules/Extension.jsm",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "WEBEXT_PERMISSION_PROMPTS",
+ PREF_WEBEXT_PERM_PROMPTS,
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "WEBEXT_POSTDOWNLOAD_THIRD_PARTY",
+ PREF_EM_POSTDOWNLOAD_THIRD_PARTY,
+ false
+);
+
+// Initialize the WebExtension process script service as early as possible,
+// since it needs to be able to track things like new frameLoader globals that
+// are created before other framework code has been initialized.
+Services.ppmm.loadProcessScript(
+ "resource://gre/modules/extensionProcessScriptLoader.js",
+ true
+);
+
+const INTEGER = /^[1-9]\d*$/;
+
+var EXPORTED_SYMBOLS = ["AddonManager", "AddonManagerPrivate", "AMTelemetry"];
+
+const CATEGORY_PROVIDER_MODULE = "addon-provider-module";
+
+// A list of providers to load by default
+const DEFAULT_PROVIDERS = ["resource://gre/modules/addons/XPIProvider.jsm"];
+
+const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+// Configure a logger at the parent 'addons' level to format
+// messages for all the modules under addons.*
+const PARENT_LOGGER_ID = "addons";
+var parentLogger = Log.repository.getLogger(PARENT_LOGGER_ID);
+parentLogger.level = Log.Level.Warn;
+var formatter = new Log.BasicFormatter();
+// Set parent logger (and its children) to append to
+// the Javascript section of the Browser Console
+parentLogger.addAppender(new Log.ConsoleAppender(formatter));
+
+// Create a new logger (child of 'addons' logger)
+// for use by the Addons Manager
+const LOGGER_ID = "addons.manager";
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+// Provide the ability to enable/disable logging
+// messages at runtime.
+// If the "extensions.logging.enabled" preference is
+// missing or 'false', messages at the WARNING and higher
+// severity should be logged to the JS console and standard error.
+// If "extensions.logging.enabled" is set to 'true', messages
+// at DEBUG and higher should go to JS console and standard error.
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+const UNNAMED_PROVIDER = "";
+function providerName(aProvider) {
+ return aProvider.name || UNNAMED_PROVIDER;
+}
+
+/**
+ * Preference listener which listens for a change in the
+ * "extensions.logging.enabled" preference and changes the logging level of the
+ * parent 'addons' level logger accordingly.
+ */
+var PrefObserver = {
+ init() {
+ Services.prefs.addObserver(PREF_LOGGING_ENABLED, this);
+ Services.obs.addObserver(this, "xpcom-shutdown");
+ this.observe(null, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, PREF_LOGGING_ENABLED);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "xpcom-shutdown") {
+ Services.prefs.removeObserver(PREF_LOGGING_ENABLED, this);
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ } else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) {
+ let debugLogEnabled = Services.prefs.getBoolPref(
+ PREF_LOGGING_ENABLED,
+ false
+ );
+ if (debugLogEnabled) {
+ parentLogger.level = Log.Level.Debug;
+ } else {
+ parentLogger.level = Log.Level.Warn;
+ }
+ }
+ },
+};
+
+PrefObserver.init();
+
+/**
+ * Calls a callback method consuming any thrown exception. Any parameters after
+ * the callback parameter will be passed to the callback.
+ *
+ * @param aCallback
+ * The callback method to call
+ */
+function safeCall(aCallback, ...aArgs) {
+ try {
+ aCallback.apply(null, aArgs);
+ } catch (e) {
+ logger.warn("Exception calling callback", e);
+ }
+}
+
+/**
+ * Report an exception thrown by a provider API method.
+ */
+function reportProviderError(aProvider, aMethod, aError) {
+ let method = `provider ${providerName(aProvider)}.${aMethod}`;
+ AddonManagerPrivate.recordException("AMI", method, aError);
+ logger.error("Exception calling " + method, aError);
+}
+
+/**
+ * Calls a method on a provider if it exists and consumes any thrown exception.
+ * Any parameters after the aDefault parameter are passed to the provider's method.
+ *
+ * @param aProvider
+ * The provider to call
+ * @param aMethod
+ * The method name to call
+ * @param aDefault
+ * A default return value if the provider does not implement the named
+ * method or throws an error.
+ * @return the return value from the provider, or aDefault if the provider does not
+ * implement method or throws an error
+ */
+function callProvider(aProvider, aMethod, aDefault, ...aArgs) {
+ if (!(aMethod in aProvider)) {
+ return aDefault;
+ }
+
+ try {
+ return aProvider[aMethod].apply(aProvider, aArgs);
+ } catch (e) {
+ reportProviderError(aProvider, aMethod, e);
+ return aDefault;
+ }
+}
+
+/**
+ * Calls a method on a provider if it exists and consumes any thrown exception.
+ * Parameters after aMethod are passed to aProvider.aMethod().
+ * If the provider does not implement the method, or the method throws, calls
+ * the callback with 'undefined'.
+ *
+ * @param aProvider
+ * The provider to call
+ * @param aMethod
+ * The method name to call
+ */
+async function promiseCallProvider(aProvider, aMethod, ...aArgs) {
+ if (!(aMethod in aProvider)) {
+ return undefined;
+ }
+ try {
+ return aProvider[aMethod].apply(aProvider, aArgs);
+ } catch (e) {
+ reportProviderError(aProvider, aMethod, e);
+ return undefined;
+ }
+}
+
+/**
+ * Gets the currently selected locale for display.
+ * @return the selected locale or "en-US" if none is selected
+ */
+function getLocale() {
+ return Services.locale.requestedLocale || "en-US";
+}
+
+const WEB_EXPOSED_ADDON_PROPERTIES = [
+ "id",
+ "version",
+ "type",
+ "name",
+ "description",
+ "isActive",
+];
+
+function webAPIForAddon(addon) {
+ if (!addon) {
+ return null;
+ }
+
+ // These web-exposed Addon properties (see AddonManager.webidl)
+ // just come directly from an Addon object.
+ let result = {};
+ for (let prop of WEB_EXPOSED_ADDON_PROPERTIES) {
+ result[prop] = addon[prop];
+ }
+
+ // These properties are computed.
+ result.isEnabled = !addon.userDisabled;
+ result.canUninstall = Boolean(
+ addon.permissions & AddonManager.PERM_CAN_UNINSTALL
+ );
+
+ return result;
+}
+
+/**
+ * Listens for a browser changing origin and cancels the installs that were
+ * started by it.
+ */
+function BrowserListener(aBrowser, aInstallingPrincipal, aInstall) {
+ this.browser = aBrowser;
+ this.messageManager = this.browser.messageManager;
+ this.principal = aInstallingPrincipal;
+ this.install = aInstall;
+
+ aBrowser.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+ Services.obs.addObserver(this, "message-manager-close", true);
+
+ aInstall.addListener(this);
+
+ this.registered = true;
+}
+
+BrowserListener.prototype = {
+ browser: null,
+ install: null,
+ registered: false,
+
+ unregister() {
+ if (!this.registered) {
+ return;
+ }
+ this.registered = false;
+
+ Services.obs.removeObserver(this, "message-manager-close");
+ // The browser may have already been detached
+ if (this.browser.removeProgressListener) {
+ this.browser.removeProgressListener(this);
+ }
+
+ this.install.removeListener(this);
+ this.install = null;
+ },
+
+ cancelInstall() {
+ try {
+ this.install.cancel();
+ } catch (e) {
+ // install may have already failed or been cancelled, ignore these
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (subject != this.messageManager) {
+ return;
+ }
+
+ // The browser's message manager has closed and so the browser is
+ // going away, cancel the install
+ this.cancelInstall();
+ },
+
+ onLocationChange(webProgress, request, location) {
+ if (
+ this.browser.contentPrincipal &&
+ this.principal.subsumes(this.browser.contentPrincipal)
+ ) {
+ return;
+ }
+
+ // The browser has navigated to a new origin so cancel the install
+ this.cancelInstall();
+ },
+
+ onDownloadCancelled(install) {
+ this.unregister();
+ },
+
+ onDownloadFailed(install) {
+ this.unregister();
+ },
+
+ onInstallFailed(install) {
+ this.unregister();
+ },
+
+ onInstallEnded(install) {
+ this.unregister();
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISupportsWeakReference",
+ "nsIWebProgressListener",
+ "nsIObserver",
+ ]),
+};
+
+/**
+ * This represents an author of an add-on (e.g. creator or developer)
+ *
+ * @param aName
+ * The name of the author
+ * @param aURL
+ * The URL of the author's profile page
+ */
+function AddonAuthor(aName, aURL) {
+ this.name = aName;
+ this.url = aURL;
+}
+
+AddonAuthor.prototype = {
+ name: null,
+ url: null,
+
+ // Returns the author's name, defaulting to the empty string
+ toString() {
+ return this.name || "";
+ },
+};
+
+/**
+ * This represents an screenshot for an add-on
+ *
+ * @param aURL
+ * The URL to the full version of the screenshot
+ * @param aWidth
+ * The width in pixels of the screenshot
+ * @param aHeight
+ * The height in pixels of the screenshot
+ * @param aThumbnailURL
+ * The URL to the thumbnail version of the screenshot
+ * @param aThumbnailWidth
+ * The width in pixels of the thumbnail version of the screenshot
+ * @param aThumbnailHeight
+ * The height in pixels of the thumbnail version of the screenshot
+ * @param aCaption
+ * The caption of the screenshot
+ */
+function AddonScreenshot(
+ aURL,
+ aWidth,
+ aHeight,
+ aThumbnailURL,
+ aThumbnailWidth,
+ aThumbnailHeight,
+ aCaption
+) {
+ this.url = aURL;
+ if (aWidth) {
+ this.width = aWidth;
+ }
+ if (aHeight) {
+ this.height = aHeight;
+ }
+ if (aThumbnailURL) {
+ this.thumbnailURL = aThumbnailURL;
+ }
+ if (aThumbnailWidth) {
+ this.thumbnailWidth = aThumbnailWidth;
+ }
+ if (aThumbnailHeight) {
+ this.thumbnailHeight = aThumbnailHeight;
+ }
+ if (aCaption) {
+ this.caption = aCaption;
+ }
+}
+
+AddonScreenshot.prototype = {
+ url: null,
+ width: null,
+ height: null,
+ thumbnailURL: null,
+ thumbnailWidth: null,
+ thumbnailHeight: null,
+ caption: null,
+
+ // Returns the screenshot URL, defaulting to the empty string
+ toString() {
+ return this.url || "";
+ },
+};
+
+/**
+ * A type of add-on, used by the UI to determine how to display different types
+ * of add-ons.
+ *
+ * @param aID
+ * The add-on type ID
+ * @param aLocaleURI
+ * The URI of a localized properties file to get the displayable name
+ * for the type from
+ * @param aLocaleKey
+ * The key for the string in the properties file.
+ * @param aViewType
+ * The optional type of view to use in the UI
+ * @param aUIPriority
+ * The priority is used by the UI to list the types in order. Lower
+ * values push the type higher in the list.
+ * @param aFlags
+ * An option set of flags that customize the display of the add-on in
+ * the UI.
+ */
+function AddonType(
+ aID,
+ aLocaleURI,
+ aLocaleKey,
+ aViewType,
+ aUIPriority,
+ aFlags
+) {
+ if (!aID) {
+ throw Components.Exception(
+ "An AddonType must have an ID",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aViewType && aUIPriority === undefined) {
+ throw Components.Exception(
+ "An AddonType with a defined view must have a set UI priority",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!aLocaleKey) {
+ throw Components.Exception(
+ "An AddonType must have a displayable name",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.id = aID;
+ this.uiPriority = aUIPriority;
+ this.viewType = aViewType;
+ this.flags = aFlags;
+
+ if (aLocaleURI) {
+ XPCOMUtils.defineLazyGetter(this, "name", () => {
+ let bundle = Services.strings.createBundle(aLocaleURI);
+ return bundle.GetStringFromName(aLocaleKey);
+ });
+ } else {
+ this.name = aLocaleKey;
+ }
+}
+
+var gStarted = false;
+var gStartupComplete = false;
+var gCheckCompatibility = true;
+var gStrictCompatibility = true;
+var gCheckUpdateSecurityDefault = true;
+var gCheckUpdateSecurity = gCheckUpdateSecurityDefault;
+var gUpdateEnabled = true;
+var gAutoUpdateDefault = true;
+var gWebExtensionsMinPlatformVersion = "";
+var gFinalShutdownBarrier = null;
+var gBeforeShutdownBarrier = null;
+var gRepoShutdownState = "";
+var gShutdownInProgress = false;
+var gBrowserUpdated = null;
+
+var AMTelemetry;
+
+/**
+ * This is the real manager, kept here rather than in AddonManager to keep its
+ * contents hidden from API users.
+ * @class
+ * @lends AddonManager
+ */
+var AddonManagerInternal = {
+ managerListeners: new Set(),
+ installListeners: new Set(),
+ addonListeners: new Set(),
+ typeListeners: new Set(),
+ pendingProviders: new Set(),
+ providers: new Set(),
+ providerShutdowns: new Map(),
+ types: {},
+ startupChanges: {},
+ // Store telemetry details per addon provider
+ telemetryDetails: {},
+ upgradeListeners: new Map(),
+ externalExtensionLoaders: new Map(),
+
+ recordTimestamp(name, value) {
+ this.TelemetryTimestamps.add(name, value);
+ },
+
+ /**
+ * Start up a provider, and register its shutdown hook if it has one
+ *
+ * @param {string} aProvider - An add-on provider.
+ * @param {boolean} aAppChanged - Whether or not the app version has changed since last session.
+ * @param {string} aOldAppVersion - Previous application version, if changed.
+ * @param {string} aOldPlatformVersion - Previous platform version, if changed.
+ *
+ * @private
+ */
+ _startProvider(aProvider, aAppChanged, aOldAppVersion, aOldPlatformVersion) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ logger.debug(`Starting provider: ${providerName(aProvider)}`);
+ callProvider(
+ aProvider,
+ "startup",
+ null,
+ aAppChanged,
+ aOldAppVersion,
+ aOldPlatformVersion
+ );
+ if ("shutdown" in aProvider) {
+ let name = providerName(aProvider);
+ let AMProviderShutdown = () => {
+ // If the provider has been unregistered, it will have been removed from
+ // this.providers. If it hasn't been unregistered, then this is a normal
+ // shutdown - and we move it to this.pendingProviders in case we're
+ // running in a test that will start AddonManager again.
+ if (this.providers.has(aProvider)) {
+ this.providers.delete(aProvider);
+ this.pendingProviders.add(aProvider);
+ }
+
+ return new Promise((resolve, reject) => {
+ logger.debug("Calling shutdown blocker for " + name);
+ resolve(aProvider.shutdown());
+ }).catch(err => {
+ logger.warn("Failure during shutdown of " + name, err);
+ AddonManagerPrivate.recordException(
+ "AMI",
+ "Async shutdown of " + name,
+ err
+ );
+ });
+ };
+ logger.debug("Registering shutdown blocker for " + name);
+ this.providerShutdowns.set(aProvider, AMProviderShutdown);
+ AddonManagerPrivate.finalShutdown.addBlocker(name, AMProviderShutdown);
+ }
+
+ this.pendingProviders.delete(aProvider);
+ this.providers.add(aProvider);
+ logger.debug(`Provider finished startup: ${providerName(aProvider)}`);
+ },
+
+ _getProviderByName(aName) {
+ for (let provider of this.providers) {
+ if (providerName(provider) == aName) {
+ return provider;
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * Initializes the AddonManager, loading any known providers and initializing
+ * them.
+ */
+ startup() {
+ try {
+ if (gStarted) {
+ return;
+ }
+
+ this.recordTimestamp("AMI_startup_begin");
+
+ // Enable the addonsManager telemetry event category.
+ AMTelemetry.init();
+
+ // clear this for xpcshell test restarts
+ for (let provider in this.telemetryDetails) {
+ delete this.telemetryDetails[provider];
+ }
+
+ let appChanged = undefined;
+
+ let oldAppVersion = null;
+ try {
+ oldAppVersion = Services.prefs.getCharPref(PREF_EM_LAST_APP_VERSION);
+ appChanged = Services.appinfo.version != oldAppVersion;
+ } catch (e) {}
+
+ gBrowserUpdated = appChanged;
+
+ let oldPlatformVersion = Services.prefs.getCharPref(
+ PREF_EM_LAST_PLATFORM_VERSION,
+ ""
+ );
+
+ if (appChanged !== false) {
+ logger.debug("Application has been upgraded");
+ Services.prefs.setCharPref(
+ PREF_EM_LAST_APP_VERSION,
+ Services.appinfo.version
+ );
+ Services.prefs.setCharPref(
+ PREF_EM_LAST_PLATFORM_VERSION,
+ Services.appinfo.platformVersion
+ );
+ Services.prefs.setIntPref(
+ PREF_BLOCKLIST_PINGCOUNTVERSION,
+ appChanged === undefined ? 0 : -1
+ );
+ }
+
+ if (!MOZ_COMPATIBILITY_NIGHTLY) {
+ PREF_EM_CHECK_COMPATIBILITY =
+ PREF_EM_CHECK_COMPATIBILITY_BASE +
+ "." +
+ Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
+ }
+
+ gCheckCompatibility = Services.prefs.getBoolPref(
+ PREF_EM_CHECK_COMPATIBILITY,
+ gCheckCompatibility
+ );
+ Services.prefs.addObserver(PREF_EM_CHECK_COMPATIBILITY, this);
+
+ gStrictCompatibility = Services.prefs.getBoolPref(
+ PREF_EM_STRICT_COMPATIBILITY,
+ gStrictCompatibility
+ );
+ Services.prefs.addObserver(PREF_EM_STRICT_COMPATIBILITY, this);
+
+ let defaultBranch = Services.prefs.getDefaultBranch("");
+ gCheckUpdateSecurityDefault = defaultBranch.getBoolPref(
+ PREF_EM_CHECK_UPDATE_SECURITY,
+ gCheckUpdateSecurityDefault
+ );
+
+ gCheckUpdateSecurity = Services.prefs.getBoolPref(
+ PREF_EM_CHECK_UPDATE_SECURITY,
+ gCheckUpdateSecurity
+ );
+ Services.prefs.addObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
+
+ gUpdateEnabled = Services.prefs.getBoolPref(
+ PREF_EM_UPDATE_ENABLED,
+ gUpdateEnabled
+ );
+ Services.prefs.addObserver(PREF_EM_UPDATE_ENABLED, this);
+
+ gAutoUpdateDefault = Services.prefs.getBoolPref(
+ PREF_EM_AUTOUPDATE_DEFAULT,
+ gAutoUpdateDefault
+ );
+ Services.prefs.addObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
+
+ gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
+ PREF_MIN_WEBEXT_PLATFORM_VERSION,
+ gWebExtensionsMinPlatformVersion
+ );
+ Services.prefs.addObserver(PREF_MIN_WEBEXT_PLATFORM_VERSION, this);
+
+ // Ensure all default providers have had a chance to register themselves
+ for (let url of DEFAULT_PROVIDERS) {
+ try {
+ let scope = {};
+ ChromeUtils.import(url, scope);
+ // Sanity check - make sure the provider exports a symbol that
+ // has a 'startup' method
+ let syms = Object.keys(scope);
+ if (syms.length < 1 || typeof scope[syms[0]].startup != "function") {
+ logger.warn("Provider " + url + " has no startup()");
+ AddonManagerPrivate.recordException(
+ "AMI",
+ "provider " + url,
+ "no startup()"
+ );
+ }
+ logger.debug(
+ "Loaded provider scope for " +
+ url +
+ ": " +
+ Object.keys(scope).toSource()
+ );
+ } catch (e) {
+ AddonManagerPrivate.recordException(
+ "AMI",
+ "provider " + url + " load failed",
+ e
+ );
+ logger.error('Exception loading default provider "' + url + '"', e);
+ }
+ }
+
+ // Load any providers registered in the category manager
+ for (let { entry, value: url } of Services.catMan.enumerateCategory(
+ CATEGORY_PROVIDER_MODULE
+ )) {
+ try {
+ ChromeUtils.import(url, {});
+ logger.debug(`Loaded provider scope for ${url}`);
+ } catch (e) {
+ AddonManagerPrivate.recordException(
+ "AMI",
+ "provider " + url + " load failed",
+ e
+ );
+ logger.error(
+ "Exception loading provider " +
+ entry +
+ ' from category "' +
+ url +
+ '"',
+ e
+ );
+ }
+ }
+
+ // Register our shutdown handler with the AsyncShutdown manager
+ gBeforeShutdownBarrier = new AsyncShutdown.Barrier(
+ "AddonManager: Waiting to start provider shutdown."
+ );
+ gFinalShutdownBarrier = new AsyncShutdown.Barrier(
+ "AddonManager: Waiting for providers to shut down."
+ );
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ "AddonManager: shutting down.",
+ this.shutdownManager.bind(this),
+ { fetchState: this.shutdownState.bind(this) }
+ );
+
+ // Once we start calling providers we must allow all normal methods to work.
+ gStarted = true;
+
+ for (let provider of this.pendingProviders) {
+ this._startProvider(
+ provider,
+ appChanged,
+ oldAppVersion,
+ oldPlatformVersion
+ );
+ }
+
+ // If this is a new profile just pretend that there were no changes
+ if (appChanged === undefined) {
+ for (let type in this.startupChanges) {
+ delete this.startupChanges[type];
+ }
+ }
+
+ gStartupComplete = true;
+ this.recordTimestamp("AMI_startup_end");
+ } catch (e) {
+ logger.error("startup failed", e);
+ AddonManagerPrivate.recordException("AMI", "startup failed", e);
+ }
+
+ logger.debug("Completed startup sequence");
+ this.callManagerListeners("onStartup");
+ },
+
+ /**
+ * Registers a new AddonProvider.
+ *
+ * @param {string} aProvider -The provider to register
+ * @param {string[]} [aTypes] - An optional array of add-on types
+ */
+ registerProvider(aProvider, aTypes) {
+ if (!aProvider || typeof aProvider != "object") {
+ throw Components.Exception(
+ "aProvider must be specified",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aTypes && !Array.isArray(aTypes)) {
+ throw Components.Exception(
+ "aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.pendingProviders.add(aProvider);
+
+ if (aTypes) {
+ for (let type of aTypes) {
+ if (!(type.id in this.types)) {
+ if (!VALID_TYPES_REGEXP.test(type.id)) {
+ logger.warn("Ignoring invalid type " + type.id);
+ return;
+ }
+
+ this.types[type.id] = {
+ type,
+ providers: [aProvider],
+ };
+
+ let typeListeners = new Set(this.typeListeners);
+ for (let listener of typeListeners) {
+ safeCall(() => listener.onTypeAdded(type));
+ }
+ } else {
+ this.types[type.id].providers.push(aProvider);
+ }
+ }
+ }
+
+ // If we're registering after startup call this provider's startup.
+ if (gStarted) {
+ this._startProvider(aProvider);
+ }
+ },
+
+ /**
+ * Unregisters an AddonProvider.
+ *
+ * @param aProvider
+ * The provider to unregister
+ * @return Whatever the provider's 'shutdown' method returns (if anything).
+ * For providers that have async shutdown methods returning Promises,
+ * the caller should wait for that Promise to resolve.
+ */
+ unregisterProvider(aProvider) {
+ if (!aProvider || typeof aProvider != "object") {
+ throw Components.Exception(
+ "aProvider must be specified",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.providers.delete(aProvider);
+ // The test harness will unregister XPIProvider *after* shutdown, which is
+ // after the provider will have been moved from providers to
+ // pendingProviders.
+ this.pendingProviders.delete(aProvider);
+
+ for (let type in this.types) {
+ this.types[type].providers = this.types[type].providers.filter(
+ p => p != aProvider
+ );
+ if (!this.types[type].providers.length) {
+ let oldType = this.types[type].type;
+ delete this.types[type];
+
+ let typeListeners = new Set(this.typeListeners);
+ for (let listener of typeListeners) {
+ safeCall(() => listener.onTypeRemoved(oldType));
+ }
+ }
+ }
+
+ // If we're unregistering after startup but before shutting down,
+ // remove the blocker for this provider's shutdown and call it.
+ // If we're already shutting down, just let gFinalShutdownBarrier
+ // call it to avoid races.
+ if (gStarted && !gShutdownInProgress) {
+ logger.debug(
+ "Unregistering shutdown blocker for " + providerName(aProvider)
+ );
+ let shutter = this.providerShutdowns.get(aProvider);
+ if (shutter) {
+ this.providerShutdowns.delete(aProvider);
+ gFinalShutdownBarrier.client.removeBlocker(shutter);
+ return shutter();
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * Mark a provider as safe to access via AddonManager APIs, before its
+ * startup has completed.
+ *
+ * Normally a provider isn't marked as safe until after its (synchronous)
+ * startup() method has returned. Until a provider has been marked safe,
+ * it won't be used by any of the AddonManager APIs. markProviderSafe()
+ * allows a provider to mark itself as safe during its startup; this can be
+ * useful if the provider wants to perform tasks that block startup, which
+ * happen after its required initialization tasks and therefore when the
+ * provider is in a safe state.
+ *
+ * @param aProvider Provider object to mark safe
+ */
+ markProviderSafe(aProvider) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aProvider || typeof aProvider != "object") {
+ throw Components.Exception(
+ "aProvider must be specified",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!this.pendingProviders.has(aProvider)) {
+ return;
+ }
+
+ this.pendingProviders.delete(aProvider);
+ this.providers.add(aProvider);
+ },
+
+ /**
+ * Calls a method on all registered providers if it exists and consumes any
+ * thrown exception. Return values are ignored. Any parameters after the
+ * method parameter are passed to the provider's method.
+ * WARNING: Do not use for asynchronous calls; callProviders() does not
+ * invoke callbacks if provider methods throw synchronous exceptions.
+ *
+ * @param aMethod
+ * The method name to call
+ * @see callProvider
+ */
+ callProviders(aMethod, ...aArgs) {
+ if (!aMethod || typeof aMethod != "string") {
+ throw Components.Exception(
+ "aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ try {
+ if (aMethod in provider) {
+ provider[aMethod].apply(provider, aArgs);
+ }
+ } catch (e) {
+ reportProviderError(provider, aMethod, e);
+ }
+ }
+ },
+
+ /**
+ * Report the current state of asynchronous shutdown
+ */
+ shutdownState() {
+ let state = [];
+ for (let barrier of [gBeforeShutdownBarrier, gFinalShutdownBarrier]) {
+ if (barrier) {
+ state.push({ name: barrier.client.name, state: barrier.state });
+ }
+ }
+ state.push({
+ name: "AddonRepository: async shutdown",
+ state: gRepoShutdownState,
+ });
+ return state;
+ },
+
+ /**
+ * Shuts down the addon manager and all registered providers, this must clean
+ * up everything in order for automated tests to fake restarts.
+ * @return Promise{null} that resolves when all providers and dependent modules
+ * have finished shutting down
+ */
+ async shutdownManager() {
+ logger.debug("before shutdown");
+ try {
+ await gBeforeShutdownBarrier.wait();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ logger.debug("shutdown");
+ this.callManagerListeners("onShutdown");
+
+ gRepoShutdownState = "pending";
+ gShutdownInProgress = true;
+ // Clean up listeners
+ Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this);
+ Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this);
+ Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
+ Services.prefs.removeObserver(PREF_EM_UPDATE_ENABLED, this);
+ Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
+
+ let savedError = null;
+ // Only shut down providers if they've been started.
+ if (gStarted) {
+ try {
+ await gFinalShutdownBarrier.wait();
+ } catch (err) {
+ savedError = err;
+ logger.error("Failure during wait for shutdown barrier", err);
+ AddonManagerPrivate.recordException(
+ "AMI",
+ "Async shutdown of AddonManager providers",
+ err
+ );
+ }
+ }
+
+ // Shut down AddonRepository after providers (if any).
+ try {
+ gRepoShutdownState = "in progress";
+ await AddonRepository.shutdown();
+ gRepoShutdownState = "done";
+ } catch (err) {
+ savedError = err;
+ logger.error("Failure during AddonRepository shutdown", err);
+ AddonManagerPrivate.recordException(
+ "AMI",
+ "Async shutdown of AddonRepository",
+ err
+ );
+ }
+
+ logger.debug("Async provider shutdown done");
+ this.managerListeners.clear();
+ this.installListeners.clear();
+ this.addonListeners.clear();
+ this.typeListeners.clear();
+ this.providerShutdowns.clear();
+ for (let type in this.startupChanges) {
+ delete this.startupChanges[type];
+ }
+ gStarted = false;
+ gStartupComplete = false;
+ gFinalShutdownBarrier = null;
+ gBeforeShutdownBarrier = null;
+ gShutdownInProgress = false;
+ if (savedError) {
+ throw savedError;
+ }
+ },
+
+ /**
+ * Notified when a preference we're interested in has changed.
+ *
+ * @see nsIObserver
+ */
+ observe(aSubject, aTopic, aData) {
+ switch (aData) {
+ case PREF_EM_CHECK_COMPATIBILITY: {
+ let oldValue = gCheckCompatibility;
+ gCheckCompatibility = Services.prefs.getBoolPref(
+ PREF_EM_CHECK_COMPATIBILITY,
+ true
+ );
+
+ this.callManagerListeners("onCompatibilityModeChanged");
+
+ if (gCheckCompatibility != oldValue) {
+ this.updateAddonAppDisabledStates();
+ }
+
+ break;
+ }
+ case PREF_EM_STRICT_COMPATIBILITY: {
+ let oldValue = gStrictCompatibility;
+ gStrictCompatibility = Services.prefs.getBoolPref(
+ PREF_EM_STRICT_COMPATIBILITY,
+ true
+ );
+
+ this.callManagerListeners("onCompatibilityModeChanged");
+
+ if (gStrictCompatibility != oldValue) {
+ this.updateAddonAppDisabledStates();
+ }
+
+ break;
+ }
+ case PREF_EM_CHECK_UPDATE_SECURITY: {
+ let oldValue = gCheckUpdateSecurity;
+ gCheckUpdateSecurity = Services.prefs.getBoolPref(
+ PREF_EM_CHECK_UPDATE_SECURITY,
+ true
+ );
+
+ this.callManagerListeners("onCheckUpdateSecurityChanged");
+
+ if (gCheckUpdateSecurity != oldValue) {
+ this.updateAddonAppDisabledStates();
+ }
+
+ break;
+ }
+ case PREF_EM_UPDATE_ENABLED: {
+ gUpdateEnabled = Services.prefs.getBoolPref(
+ PREF_EM_UPDATE_ENABLED,
+ true
+ );
+
+ this.callManagerListeners("onUpdateModeChanged");
+ break;
+ }
+ case PREF_EM_AUTOUPDATE_DEFAULT: {
+ gAutoUpdateDefault = Services.prefs.getBoolPref(
+ PREF_EM_AUTOUPDATE_DEFAULT,
+ true
+ );
+
+ this.callManagerListeners("onUpdateModeChanged");
+ break;
+ }
+ case PREF_MIN_WEBEXT_PLATFORM_VERSION: {
+ gWebExtensionsMinPlatformVersion = Services.prefs.getCharPref(
+ PREF_MIN_WEBEXT_PLATFORM_VERSION
+ );
+ break;
+ }
+ }
+ },
+
+ /**
+ * Replaces %...% strings in an addon url (update and updateInfo) with
+ * appropriate values.
+ *
+ * @param aAddon
+ * The Addon representing the add-on
+ * @param aUri
+ * The string representation of the URI to escape
+ * @param aAppVersion
+ * The optional application version to use for %APP_VERSION%
+ * @return The appropriately escaped URI.
+ */
+ escapeAddonURI(aAddon, aUri, aAppVersion) {
+ if (!aAddon || typeof aAddon != "object") {
+ throw Components.Exception(
+ "aAddon must be an Addon object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!aUri || typeof aUri != "string") {
+ throw Components.Exception(
+ "aUri must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aAppVersion && typeof aAppVersion != "string") {
+ throw Components.Exception(
+ "aAppVersion must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ var addonStatus =
+ aAddon.userDisabled || aAddon.softDisabled
+ ? "userDisabled"
+ : "userEnabled";
+
+ if (!aAddon.isCompatible) {
+ addonStatus += ",incompatible";
+ }
+
+ let { blocklistState } = aAddon;
+ if (blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ addonStatus += ",blocklisted";
+ }
+ if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
+ addonStatus += ",softblocked";
+ }
+
+ let params = new Map(
+ Object.entries({
+ ITEM_ID: aAddon.id,
+ ITEM_VERSION: aAddon.version,
+ ITEM_STATUS: addonStatus,
+ APP_ID: Services.appinfo.ID,
+ APP_VERSION: aAppVersion ? aAppVersion : Services.appinfo.version,
+ REQ_VERSION: UPDATE_REQUEST_VERSION,
+ APP_OS: Services.appinfo.OS,
+ APP_ABI: Services.appinfo.XPCOMABI,
+ APP_LOCALE: getLocale(),
+ CURRENT_APP_VERSION: Services.appinfo.version,
+ })
+ );
+
+ let uri = aUri.replace(/%([A-Z_]+)%/g, (m0, m1) => params.get(m1) || m0);
+
+ // escape() does not properly encode + symbols in any embedded FVF strings.
+ return uri.replace(/\+/g, "%2B");
+ },
+
+ _updatePromptHandler(info) {
+ let oldPerms = info.existingAddon.userPermissions;
+ if (!oldPerms) {
+ // Updating from a legacy add-on, just let it proceed
+ return Promise.resolve();
+ }
+
+ let newPerms = info.addon.userPermissions;
+
+ let difference = Extension.comparePermissions(oldPerms, newPerms);
+
+ // If there are no new permissions, just go ahead with the update
+ if (!difference.origins.length && !difference.permissions.length) {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ let subject = {
+ wrappedJSObject: {
+ addon: info.addon,
+ permissions: difference,
+ resolve,
+ reject,
+ // Reference to the related AddonInstall object (used in AMTelemetry to
+ // link the recorded event to the other events from the same install flow).
+ install: info.install,
+ },
+ };
+ Services.obs.notifyObservers(subject, "webextension-update-permissions");
+ });
+ },
+
+ // Returns true if System Addons should be updated
+ systemUpdateEnabled() {
+ if (!Services.prefs.getBoolPref(PREF_SYS_ADDON_UPDATE_ENABLED)) {
+ return false;
+ }
+ if (Services.policies && !Services.policies.isAllowed("SysAddonUpdate")) {
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Performs a background update check by starting an update for all add-ons
+ * that can be updated.
+ * @return Promise{null} Resolves when the background update check is complete
+ * (the resulting addon installations may still be in progress).
+ */
+ backgroundUpdateCheck() {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ let buPromise = (async () => {
+ logger.debug("Background update check beginning");
+
+ Services.obs.notifyObservers(null, "addons-background-update-start");
+
+ if (this.updateEnabled) {
+ // Keep track of all the async add-on updates happening in parallel
+ let updates = [];
+
+ let allAddons = await this.getAllAddons();
+
+ // Repopulate repository cache first, to ensure compatibility overrides
+ // are up to date before checking for addon updates.
+ await AddonRepository.backgroundUpdateCheck();
+
+ for (let addon of allAddons) {
+ // Check all add-ons for updates so that any compatibility updates will
+ // be applied
+
+ if (!(addon.permissions & AddonManager.PERM_CAN_UPGRADE)) {
+ continue;
+ }
+
+ updates.push(
+ new Promise((resolve, reject) => {
+ addon.findUpdates(
+ {
+ onUpdateAvailable(aAddon, aInstall) {
+ // Start installing updates when the add-on can be updated and
+ // background updates should be applied.
+ logger.debug("Found update for add-on ${id}", aAddon);
+ if (
+ aAddon.permissions & AddonManager.PERM_CAN_UPGRADE &&
+ AddonManager.shouldAutoUpdate(aAddon)
+ ) {
+ // XXX we really should resolve when this install is done,
+ // not when update-available check completes, no?
+ logger.debug(`Starting upgrade install of ${aAddon.id}`);
+ if (WEBEXT_PERMISSION_PROMPTS) {
+ aInstall.promptHandler = (...args) =>
+ AddonManagerInternal._updatePromptHandler(...args);
+ }
+ aInstall.install();
+ }
+ },
+
+ onUpdateFinished: aAddon => {
+ logger.debug("onUpdateFinished for ${id}", aAddon);
+ resolve();
+ },
+ },
+ AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
+ );
+ })
+ );
+ }
+ await Promise.all(updates);
+ }
+
+ if (AddonManagerInternal.systemUpdateEnabled()) {
+ try {
+ await AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).updateSystemAddons();
+ } catch (e) {
+ logger.warn("Failed to update system addons", e);
+ }
+ }
+
+ logger.debug("Background update check complete");
+ Services.obs.notifyObservers(null, "addons-background-update-complete");
+ })();
+ // Fork the promise chain so we can log the error and let our caller see it too.
+ buPromise.catch(e => logger.warn("Error in background update", e));
+ return buPromise;
+ },
+
+ /**
+ * Adds a add-on to the list of detected changes for this startup. If
+ * addStartupChange is called multiple times for the same add-on in the same
+ * startup then only the most recent change will be remembered.
+ *
+ * @param aType
+ * The type of change as a string. Providers can define their own
+ * types of changes or use the existing defined STARTUP_CHANGE_*
+ * constants
+ * @param aID
+ * The ID of the add-on
+ */
+ addStartupChange(aType, aID) {
+ if (!aType || typeof aType != "string") {
+ throw Components.Exception(
+ "aType must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!aID || typeof aID != "string") {
+ throw Components.Exception(
+ "aID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (gStartupComplete) {
+ return;
+ }
+ logger.debug("Registering startup change '" + aType + "' for " + aID);
+
+ // Ensure that an ID is only listed in one type of change
+ for (let type in this.startupChanges) {
+ this.removeStartupChange(type, aID);
+ }
+
+ if (!(aType in this.startupChanges)) {
+ this.startupChanges[aType] = [];
+ }
+ this.startupChanges[aType].push(aID);
+ },
+
+ /**
+ * Removes a startup change for an add-on.
+ *
+ * @param aType
+ * The type of change
+ * @param aID
+ * The ID of the add-on
+ */
+ removeStartupChange(aType, aID) {
+ if (!aType || typeof aType != "string") {
+ throw Components.Exception(
+ "aType must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!aID || typeof aID != "string") {
+ throw Components.Exception(
+ "aID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (gStartupComplete) {
+ return;
+ }
+
+ if (!(aType in this.startupChanges)) {
+ return;
+ }
+
+ this.startupChanges[aType] = this.startupChanges[aType].filter(
+ aItem => aItem != aID
+ );
+ },
+
+ /**
+ * Calls all registered AddonManagerListeners with an event. Any parameters
+ * after the method parameter are passed to the listener.
+ *
+ * @param aMethod
+ * The method on the listeners to call
+ */
+ callManagerListeners(aMethod, ...aArgs) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aMethod || typeof aMethod != "string") {
+ throw Components.Exception(
+ "aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let managerListeners = new Set(this.managerListeners);
+ for (let listener of managerListeners) {
+ try {
+ if (aMethod in listener) {
+ listener[aMethod].apply(listener, aArgs);
+ }
+ } catch (e) {
+ logger.warn(
+ "AddonManagerListener threw exception when calling " + aMethod,
+ e
+ );
+ }
+ }
+ },
+
+ /**
+ * Calls all registered InstallListeners with an event. Any parameters after
+ * the extraListeners parameter are passed to the listener.
+ *
+ * @param aMethod
+ * The method on the listeners to call
+ * @param aExtraListeners
+ * An optional array of extra InstallListeners to also call
+ * @return false if any of the listeners returned false, true otherwise
+ */
+ callInstallListeners(aMethod, aExtraListeners, ...aArgs) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aMethod || typeof aMethod != "string") {
+ throw Components.Exception(
+ "aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aExtraListeners && !Array.isArray(aExtraListeners)) {
+ throw Components.Exception(
+ "aExtraListeners must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let result = true;
+ let listeners;
+ if (aExtraListeners) {
+ listeners = new Set(
+ aExtraListeners.concat(Array.from(this.installListeners))
+ );
+ } else {
+ listeners = new Set(this.installListeners);
+ }
+
+ for (let listener of listeners) {
+ try {
+ if (aMethod in listener) {
+ if (listener[aMethod].apply(listener, aArgs) === false) {
+ result = false;
+ }
+ }
+ } catch (e) {
+ logger.warn(
+ "InstallListener threw exception when calling " + aMethod,
+ e
+ );
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Calls all registered AddonListeners with an event. Any parameters after
+ * the method parameter are passed to the listener.
+ *
+ * @param aMethod
+ * The method on the listeners to call
+ */
+ callAddonListeners(aMethod, ...aArgs) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aMethod || typeof aMethod != "string") {
+ throw Components.Exception(
+ "aMethod must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let addonListeners = new Set(this.addonListeners);
+ for (let listener of addonListeners) {
+ try {
+ if (aMethod in listener) {
+ listener[aMethod].apply(listener, aArgs);
+ }
+ } catch (e) {
+ logger.warn("AddonListener threw exception when calling " + aMethod, e);
+ }
+ }
+ },
+
+ /**
+ * Notifies all providers that an add-on has been enabled when that type of
+ * add-on only supports a single add-on being enabled at a time. This allows
+ * the providers to disable theirs if necessary.
+ *
+ * @param aID
+ * The ID of the enabled add-on
+ * @param aType
+ * The type of the enabled add-on
+ * @param aPendingRestart
+ * A boolean indicating if the change will only take place the next
+ * time the application is restarted
+ */
+ async notifyAddonChanged(aID, aType, aPendingRestart) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (aID && typeof aID != "string") {
+ throw Components.Exception(
+ "aID must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!aType || typeof aType != "string") {
+ throw Components.Exception(
+ "aType must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ // Temporary hack until bug 520124 lands.
+ // We can get here during synchronous startup, at which point it's
+ // considered unsafe (and therefore disallowed by AddonManager.jsm) to
+ // access providers that haven't been initialized yet. Since this is when
+ // XPIProvider is starting up, XPIProvider can't access itself via APIs
+ // going through AddonManager.jsm. Thankfully, this is the only use
+ // of this API, and we know it's safe to use this API with both
+ // providers; so we have this hack to allow bypassing the normal
+ // safetey guard.
+ // The notifyAddonChanged/addonChanged API will be unneeded and therefore
+ // removed by bug 520124, so this is a temporary quick'n'dirty hack.
+ let providers = [...this.providers, ...this.pendingProviders];
+ for (let provider of providers) {
+ let result = callProvider(
+ provider,
+ "addonChanged",
+ null,
+ aID,
+ aType,
+ aPendingRestart
+ );
+ if (result) {
+ await result;
+ }
+ }
+ },
+
+ /**
+ * Notifies all providers they need to update the appDisabled property for
+ * their add-ons in response to an application change such as a blocklist
+ * update.
+ */
+ updateAddonAppDisabledStates() {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ this.callProviders("updateAddonAppDisabledStates");
+ },
+
+ /**
+ * Notifies all providers that the repository has updated its data for
+ * installed add-ons.
+ */
+ updateAddonRepositoryData() {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ return (async () => {
+ for (let provider of this.providers) {
+ await promiseCallProvider(provider, "updateAddonRepositoryData");
+ }
+
+ // only tests should care about this
+ Services.obs.notifyObservers(null, "TEST:addon-repository-data-updated");
+ })();
+ },
+
+ /**
+ * Asynchronously gets an AddonInstall for a URL.
+ *
+ * @param aUrl
+ * The string represenation of the URL where the add-on is located
+ * @param {Object} [aOptions = {}]
+ * Additional options for this install
+ * @param {string} [aOptions.hash]
+ * An optional hash of the add-on
+ * @param {string} [aOptions.name]
+ * An optional placeholder name while the add-on is being downloaded
+ * @param {string|Object} [aOptions.icons]
+ * Optional placeholder icons while the add-on is being downloaded
+ * @param {string} [aOptions.version]
+ * An optional placeholder version while the add-on is being downloaded
+ * @param {XULElement} [aOptions.browser]
+ * An optional element for download permissions prompts.
+ * @param {nsIPrincipal} [aOptions.triggeringPrincipal]
+ * The principal which is attempting to install the add-on.
+ * @param {Object} [aOptions.telemetryInfo]
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
+ * @throws if aUrl is not specified or if an optional argument of
+ * an improper type is passed.
+ */
+ async getInstallForURL(aUrl, aOptions = {}) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aUrl || typeof aUrl != "string") {
+ throw Components.Exception(
+ "aURL must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aOptions.hash && typeof aOptions.hash != "string") {
+ throw Components.Exception(
+ "hash must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aOptions.name && typeof aOptions.name != "string") {
+ throw Components.Exception(
+ "name must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aOptions.icons) {
+ if (typeof aOptions.icons == "string") {
+ aOptions.icons = { "32": aOptions.icons };
+ } else if (typeof aOptions.icons != "object") {
+ throw Components.Exception(
+ "icons must be a string, an object or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+ } else {
+ aOptions.icons = {};
+ }
+
+ if (aOptions.version && typeof aOptions.version != "string") {
+ throw Components.Exception(
+ "version must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aOptions.browser && !Element.isInstance(aOptions.browser)) {
+ throw Components.Exception(
+ "aOptions.browser must be an Element or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ for (let provider of this.providers) {
+ let install = await promiseCallProvider(
+ provider,
+ "getInstallForURL",
+ aUrl,
+ aOptions
+ );
+ if (install) {
+ return install;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Asynchronously gets an AddonInstall for an nsIFile.
+ *
+ * @param aFile
+ * The nsIFile where the add-on is located
+ * @param aMimetype
+ * An optional mimetype hint for the add-on
+ * @param aTelemetryInfo
+ * An optional object which provides details about the installation source
+ * included in the addon manager telemetry events.
+ * @param aUseSystemLocation
+ * If true the addon is installed into the system profile location.
+ * @throws if the aFile or aCallback arguments are not specified
+ */
+ getInstallForFile(aFile, aMimetype, aTelemetryInfo, aUseSystemLocation) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!(aFile instanceof Ci.nsIFile)) {
+ throw Components.Exception(
+ "aFile must be a nsIFile",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aMimetype && typeof aMimetype != "string") {
+ throw Components.Exception(
+ "aMimetype must be a string or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ return (async () => {
+ for (let provider of this.providers) {
+ let install = await promiseCallProvider(
+ provider,
+ "getInstallForFile",
+ aFile,
+ aTelemetryInfo,
+ aUseSystemLocation
+ );
+
+ if (install) {
+ return install;
+ }
+ }
+
+ return null;
+ })();
+ },
+
+ /**
+ * Uninstall an addon from the system profile location.
+ *
+ * @param {string} aID
+ * The ID of the addon to remove.
+ * @returns A promise that resolves when the addon is uninstalled.
+ */
+ uninstallSystemProfileAddon(aID) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).uninstallSystemProfileAddon(aID);
+ },
+
+ /**
+ * Asynchronously gets all current AddonInstalls optionally limiting to a list
+ * of types.
+ *
+ * @param aTypes
+ * An optional array of types to retrieve. Each type is a string name
+ * @throws If the aCallback argument is not specified
+ */
+ getInstallsByTypes(aTypes) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (aTypes && !Array.isArray(aTypes)) {
+ throw Components.Exception(
+ "aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ return (async () => {
+ let installs = [];
+
+ for (let provider of this.providers) {
+ let providerInstalls = await promiseCallProvider(
+ provider,
+ "getInstallsByTypes",
+ aTypes
+ );
+
+ if (providerInstalls) {
+ installs.push(...providerInstalls);
+ }
+ }
+
+ return installs;
+ })();
+ },
+
+ /**
+ * Asynchronously gets all current AddonInstalls.
+ */
+ getAllInstalls() {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ return this.getInstallsByTypes(null);
+ },
+
+ /**
+ * Checks whether installation is enabled for a particular mimetype.
+ *
+ * @param aMimetype
+ * The mimetype to check
+ * @return true if installation is enabled for the mimetype
+ */
+ isInstallEnabled(aMimetype) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aMimetype || typeof aMimetype != "string") {
+ throw Components.Exception(
+ "aMimetype must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ if (
+ callProvider(provider, "supportsMimetype", false, aMimetype) &&
+ callProvider(provider, "isInstallEnabled")
+ ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Checks whether a particular source is allowed to install add-ons of a
+ * given mimetype.
+ *
+ * @param aMimetype
+ * The mimetype of the add-on
+ * @param aInstallingPrincipal
+ * The nsIPrincipal that initiated the install
+ * @return true if the source is allowed to install this mimetype
+ */
+ isInstallAllowed(aMimetype, aInstallingPrincipal) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aMimetype || typeof aMimetype != "string") {
+ throw Components.Exception(
+ "aMimetype must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (
+ !aInstallingPrincipal ||
+ !(aInstallingPrincipal instanceof Ci.nsIPrincipal)
+ ) {
+ throw Components.Exception(
+ "aInstallingPrincipal must be a nsIPrincipal",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (
+ this.isInstallAllowedByPolicy(
+ aInstallingPrincipal,
+ null,
+ true /* explicit */
+ )
+ ) {
+ return true;
+ }
+
+ let providers = [...this.providers];
+ for (let provider of providers) {
+ if (
+ callProvider(provider, "supportsMimetype", false, aMimetype) &&
+ callProvider(provider, "isInstallAllowed", null, aInstallingPrincipal)
+ ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Checks whether a particular source is allowed to install add-ons based
+ * on policy.
+ *
+ * @param aInstallingPrincipal
+ * The nsIPrincipal that initiated the install
+ * @param aInstall
+ * The AddonInstall to be installed
+ * @param explicit
+ * If this is set, we only return true if the source is explicitly
+ * blocked via policy.
+ *
+ * @return boolean
+ * By default, returns true if the source is blocked by policy
+ * or there is no policy.
+ * If explicit is set, only returns true of the source is
+ * blocked by policy, false otherwise. This is needed for
+ * handling inverse cases.
+ */
+ isInstallAllowedByPolicy(aInstallingPrincipal, aInstall, explicit) {
+ if (Services.policies) {
+ let extensionSettings = Services.policies.getExtensionSettings("*");
+ if (extensionSettings && extensionSettings.install_sources) {
+ if (
+ (!aInstall ||
+ Services.policies.allowedInstallSource(aInstall.sourceURI)) &&
+ (!aInstallingPrincipal ||
+ !aInstallingPrincipal.URI ||
+ Services.policies.allowedInstallSource(aInstallingPrincipal.URI))
+ ) {
+ return true;
+ }
+ return false;
+ }
+ }
+ return !explicit;
+ },
+
+ installNotifyObservers(
+ aTopic,
+ aBrowser,
+ aUri,
+ aInstall,
+ aInstallFn,
+ aCancelFn
+ ) {
+ let info = {
+ wrappedJSObject: {
+ browser: aBrowser,
+ originatingURI: aUri,
+ installs: [aInstall],
+ install: aInstallFn,
+ cancel: aCancelFn,
+ },
+ };
+ Services.obs.notifyObservers(info, aTopic);
+ },
+
+ startInstall(browser, url, install) {
+ this.installNotifyObservers("addon-install-started", browser, url, install);
+
+ // Local installs may already be in a failed state in which case
+ // we won't get any further events, detect those cases now.
+ if (
+ install.state == AddonManager.STATE_DOWNLOADED &&
+ install.addon.appDisabled
+ ) {
+ install.cancel();
+ this.installNotifyObservers(
+ "addon-install-failed",
+ browser,
+ url,
+ install
+ );
+ return;
+ }
+
+ let self = this;
+ let listener = {
+ onDownloadCancelled() {
+ install.removeListener(listener);
+ },
+
+ onDownloadFailed() {
+ install.removeListener(listener);
+ self.installNotifyObservers(
+ "addon-install-failed",
+ browser,
+ url,
+ install
+ );
+ },
+
+ onDownloadEnded() {
+ if (install.addon.appDisabled) {
+ // App disabled items are not compatible and so fail to install.
+ install.removeListener(listener);
+ install.cancel();
+ self.installNotifyObservers(
+ "addon-install-failed",
+ browser,
+ url,
+ install
+ );
+ }
+ },
+
+ onInstallCancelled() {
+ install.removeListener(listener);
+ },
+
+ onInstallFailed() {
+ install.removeListener(listener);
+ self.installNotifyObservers(
+ "addon-install-failed",
+ browser,
+ url,
+ install
+ );
+ },
+
+ onInstallEnded() {
+ install.removeListener(listener);
+
+ // If installing a theme that is disabled and can be enabled
+ // then enable it
+ if (
+ install.addon.type == "theme" &&
+ !!install.addon.userDisabled &&
+ !install.addon.appDisabled
+ ) {
+ install.addon.enable();
+ }
+
+ let needsRestart =
+ install.addon.pendingOperations != AddonManager.PENDING_NONE;
+
+ if (WEBEXT_PERMISSION_PROMPTS && !needsRestart) {
+ let subject = {
+ wrappedJSObject: { target: browser, addon: install.addon },
+ };
+ Services.obs.notifyObservers(subject, "webextension-install-notify");
+ } else {
+ self.installNotifyObservers(
+ "addon-install-complete",
+ browser,
+ url,
+ install
+ );
+ }
+ },
+ };
+
+ install.addListener(listener);
+
+ // Start downloading if it hasn't already begun
+ install.install();
+ },
+
+ /**
+ * Starts installation of an AddonInstall notifying the registered
+ * web install listener of a blocked or started install.
+ *
+ * @param aMimetype
+ * The mimetype of the add-on being installed
+ * @param aBrowser
+ * The optional browser element that started the install
+ * @param aInstallingPrincipal
+ * The nsIPrincipal that initiated the install
+ * @param aInstall
+ * The AddonInstall to be installed
+ */
+ installAddonFromWebpage(aMimetype, aBrowser, aInstallingPrincipal, aInstall) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aMimetype || typeof aMimetype != "string") {
+ throw Components.Exception(
+ "aMimetype must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (aBrowser && !Element.isInstance(aBrowser)) {
+ throw Components.Exception(
+ "aSource must be an Element, or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (
+ !aInstallingPrincipal ||
+ !(aInstallingPrincipal instanceof Ci.nsIPrincipal)
+ ) {
+ throw Components.Exception(
+ "aInstallingPrincipal must be a nsIPrincipal",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ // When a chrome in-content UI has loaded a inside to host a
+ // website we want to do our security checks on the inner-browser but
+ // notify front-end that install events came from the outer-browser (the
+ // main tab's browser). Check this by seeing if the browser we've been
+ // passed is in a content type docshell and if so get the outer-browser.
+ let topBrowser = aBrowser;
+ // GeckoView does not pass a browser.
+ if (aBrowser) {
+ let docShell = aBrowser.ownerGlobal.docShell;
+ if (docShell.itemType == Ci.nsIDocShellTreeItem.typeContent) {
+ topBrowser = docShell.chromeEventHandler;
+ }
+ }
+
+ try {
+ // Use fullscreenElement to check for DOM fullscreen, while still allowing
+ // macOS fullscreen, which still has a browser chrome.
+ if (topBrowser && topBrowser.ownerDocument.fullscreenElement) {
+ // Addon installation and the resulting notifications should be
+ // blocked in DOM fullscreen for security and usability reasons.
+ // Installation prompts in fullscreen can trick the user into
+ // installing unwanted addons.
+ // In fullscreen the notification box does not have a clear
+ // visual association with its parent anymore.
+ aInstall.cancel();
+
+ this.installNotifyObservers(
+ "addon-install-fullscreen-blocked",
+ topBrowser,
+ aInstallingPrincipal.URI,
+ aInstall
+ );
+ return;
+ } else if (!this.isInstallEnabled(aMimetype)) {
+ aInstall.cancel();
+
+ this.installNotifyObservers(
+ "addon-install-disabled",
+ topBrowser,
+ aInstallingPrincipal.URI,
+ aInstall
+ );
+ return;
+ } else if (
+ aInstallingPrincipal.isNullPrincipal ||
+ (aBrowser &&
+ (!aBrowser.contentPrincipal ||
+ // When we attempt to handle an XPI load immediately after a
+ // process switch, the DocShell it's being loaded into will have
+ // a null principal, since it won't have been initialized yet.
+ // Allowing installs in this case is relatively safe, since
+ // there isn't much to gain by spoofing an install request from
+ // a null principal in any case. This exception can be removed
+ // once content handlers are triggered by DocumentChannel in the
+ // parent process.
+ !(
+ aBrowser.contentPrincipal.isNullPrincipal ||
+ aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)
+ ))) ||
+ !this.isInstallAllowedByPolicy(
+ aInstallingPrincipal,
+ aInstall,
+ false /* explicit */
+ )
+ ) {
+ aInstall.cancel();
+
+ this.installNotifyObservers(
+ "addon-install-origin-blocked",
+ topBrowser,
+ aInstallingPrincipal.URI,
+ aInstall
+ );
+ return;
+ }
+
+ if (aBrowser) {
+ // The install may start now depending on the web install listener,
+ // listen for the browser navigating to a new origin and cancel the
+ // install in that case.
+ new BrowserListener(aBrowser, aInstallingPrincipal, aInstall);
+ }
+
+ let startInstall = source => {
+ AddonManagerInternal.setupPromptHandler(
+ aBrowser,
+ aInstallingPrincipal.URI,
+ aInstall,
+ true,
+ source
+ );
+
+ AddonManagerInternal.startInstall(
+ aBrowser,
+ aInstallingPrincipal.URI,
+ aInstall
+ );
+ };
+
+ let installAllowed = this.isInstallAllowed(
+ aMimetype,
+ aInstallingPrincipal
+ );
+ let installPerm = Services.perms.testPermissionFromPrincipal(
+ aInstallingPrincipal,
+ "install"
+ );
+
+ if (installAllowed) {
+ startInstall("AMO");
+ } else if (installPerm === Ci.nsIPermissionManager.DENY_ACTION) {
+ // Block without prompt
+ aInstall.cancel();
+ this.installNotifyObservers(
+ "addon-install-blocked-silent",
+ topBrowser,
+ aInstallingPrincipal.URI,
+ aInstall
+ );
+ } else if (!WEBEXT_POSTDOWNLOAD_THIRD_PARTY) {
+ // Block with prompt
+ this.installNotifyObservers(
+ "addon-install-blocked",
+ topBrowser,
+ aInstallingPrincipal.URI,
+ aInstall,
+ () => startInstall("other")
+ );
+ } else {
+ // We download the addon and validate recommended states prior to
+ // showing the third party install panel.
+ startInstall("other");
+ }
+ } catch (e) {
+ // In the event that the weblistener throws during instantiation or when
+ // calling onWebInstallBlocked or onWebInstallRequested the
+ // install should get cancelled.
+ logger.warn("Failure calling web installer", e);
+ aInstall.cancel();
+ }
+ },
+
+ /**
+ * Starts installation of an AddonInstall created from add-ons manager
+ * front-end code (e.g., drag-and-drop of xpis or "Install Add-on from File"
+ *
+ * @param browser
+ * The browser element where the installation was initiated
+ * @param uri
+ * The URI of the page where the installation was initiated
+ * @param install
+ * The AddonInstall to be installed
+ */
+ installAddonFromAOM(browser, uri, install) {
+ if (!this.isInstallAllowedByPolicy(null, install)) {
+ install.cancel();
+
+ this.installNotifyObservers(
+ "addon-install-origin-blocked",
+ browser,
+ install.sourceURI,
+ install
+ );
+ return;
+ }
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ AddonManagerInternal.setupPromptHandler(
+ browser,
+ uri,
+ install,
+ true,
+ "local"
+ );
+ AddonManagerInternal.startInstall(browser, uri, install);
+ },
+
+ /**
+ * Adds a new InstallListener if the listener is not already registered.
+ *
+ * @param aListener
+ * The InstallListener to add
+ */
+ addInstallListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be a InstallListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.installListeners.add(aListener);
+ },
+
+ /**
+ * Removes an InstallListener if the listener is registered.
+ *
+ * @param aListener
+ * The InstallListener to remove
+ */
+ removeInstallListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be a InstallListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.installListeners.delete(aListener);
+ },
+ /**
+ * Adds new or overrides existing UpgradeListener.
+ *
+ * @param aInstanceID
+ * The instance ID of an addon to register a listener for.
+ * @param aCallback
+ * The callback to invoke when updates are available for this addon.
+ * @throws if there is no addon matching the instanceID
+ */
+ addUpgradeListener(aInstanceID, aCallback) {
+ if (!aInstanceID || typeof aInstanceID != "symbol") {
+ throw Components.Exception(
+ "aInstanceID must be a symbol",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!aCallback || typeof aCallback != "function") {
+ throw Components.Exception(
+ "aCallback must be a function",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let addonId = this.syncGetAddonIDByInstanceID(aInstanceID);
+ if (!addonId) {
+ throw Error(`No addon matching instanceID: ${String(aInstanceID)}`);
+ }
+ logger.debug(`Registering upgrade listener for ${addonId}`);
+ this.upgradeListeners.set(addonId, aCallback);
+ },
+
+ /**
+ * Removes an UpgradeListener if the listener is registered.
+ *
+ * @param aInstanceID
+ * The instance ID of the addon to remove
+ */
+ removeUpgradeListener(aInstanceID) {
+ if (!aInstanceID || typeof aInstanceID != "symbol") {
+ throw Components.Exception(
+ "aInstanceID must be a symbol",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let addonId = this.syncGetAddonIDByInstanceID(aInstanceID);
+ if (!addonId) {
+ throw Error(`No addon for instanceID: ${aInstanceID}`);
+ }
+ if (this.upgradeListeners.has(addonId)) {
+ this.upgradeListeners.delete(addonId);
+ } else {
+ throw Error(`No upgrade listener registered for addon ID: ${addonId}`);
+ }
+ },
+
+ addExternalExtensionLoader(loader) {
+ this.externalExtensionLoaders.set(loader.name, loader);
+ },
+
+ /**
+ * Installs a temporary add-on from a local file or directory.
+ *
+ * @param aFile
+ * An nsIFile for the file or directory of the add-on to be
+ * temporarily installed.
+ * @returns a Promise that rejects if the add-on is not a valid restartless
+ * add-on or if the same ID is already temporarily installed.
+ */
+ installTemporaryAddon(aFile) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!(aFile instanceof Ci.nsIFile)) {
+ throw Components.Exception(
+ "aFile must be a nsIFile",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).installTemporaryAddon(aFile);
+ },
+
+ /**
+ * Installs an add-on from a built-in location
+ * (ie a resource: url referencing assets shipped with the application)
+ *
+ * @param aBase
+ * A string containing the base URL. Must be a resource: URL.
+ * @returns a Promise that resolves when the addon is installed.
+ */
+ installBuiltinAddon(aBase) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).installBuiltinAddon(aBase);
+ },
+
+ /**
+ * Like `installBuiltinAddon`, but only installs the addon at `aBase`
+ * if an existing built-in addon with the ID `aID` and version doesn't
+ * already exist.
+ *
+ * @param {string} aID
+ * The ID of the add-on being registered.
+ * @param {string} aVersion
+ * The version of the add-on being registered.
+ * @param {string} aBase
+ * A string containing the base URL. Must be a resource: URL.
+ * @returns a Promise that resolves when the addon is installed.
+ */
+ maybeInstallBuiltinAddon(aID, aVersion, aBase) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).maybeInstallBuiltinAddon(aID, aVersion, aBase);
+ },
+
+ syncGetAddonIDByInstanceID(aInstanceID) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aInstanceID || typeof aInstanceID != "symbol") {
+ throw Components.Exception(
+ "aInstanceID must be a Symbol()",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).getAddonIDByInstanceID(aInstanceID);
+ },
+
+ /**
+ * Gets an icon from the icon set provided by the add-on
+ * that is closest to the specified size.
+ *
+ * The optional window parameter will be used to determine
+ * the screen resolution and select a more appropriate icon.
+ * Calling this method with 48px on retina screens will try to
+ * match an icon of size 96px.
+ *
+ * @param aAddon
+ * An addon object, meaning:
+ * An object with either an icons property that is a key-value list
+ * of icon size and icon URL, or an object having an iconURL property.
+ * @param aSize
+ * Ideal icon size in pixels
+ * @param aWindow
+ * Optional window object for determining the correct scale.
+ * @return {String} The absolute URL of the icon or null if the addon doesn't have icons
+ */
+ getPreferredIconURL(aAddon, aSize, aWindow = undefined) {
+ if (aWindow && aWindow.devicePixelRatio) {
+ aSize *= aWindow.devicePixelRatio;
+ }
+
+ let icons = aAddon.icons;
+
+ // certain addon-types only have iconURLs
+ if (!icons) {
+ icons = {};
+ if (aAddon.iconURL) {
+ icons[32] = aAddon.iconURL;
+ icons[48] = aAddon.iconURL;
+ }
+ }
+
+ // quick return if the exact size was found
+ if (icons[aSize]) {
+ return icons[aSize];
+ }
+
+ let bestSize = null;
+
+ for (let size of Object.keys(icons)) {
+ if (!INTEGER.test(size)) {
+ throw Components.Exception(
+ "Invalid icon size, must be an integer",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ size = parseInt(size, 10);
+
+ if (!bestSize) {
+ bestSize = size;
+ continue;
+ }
+
+ if (size > aSize && bestSize > aSize) {
+ // If both best size and current size are larger than the wanted size then choose
+ // the one closest to the wanted size
+ bestSize = Math.min(bestSize, size);
+ } else {
+ // Otherwise choose the largest of the two so we'll prefer sizes as close to below aSize
+ // or above aSize
+ bestSize = Math.max(bestSize, size);
+ }
+ }
+
+ return icons[bestSize] || null;
+ },
+
+ /**
+ * Asynchronously gets an add-on with a specific ID.
+ *
+ * @type {function}
+ * @param {string} aID
+ * The ID of the add-on to retrieve
+ * @returns {Promise} resolves with the found Addon or null if no such add-on exists. Never rejects.
+ * @throws if the aID argument is not specified
+ */
+ getAddonByID(aID) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aID || typeof aID != "string") {
+ throw Components.Exception(
+ "aID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let promises = Array.from(this.providers, p =>
+ promiseCallProvider(p, "getAddonByID", aID)
+ );
+ return Promise.all(promises).then(aAddons => {
+ return aAddons.find(a => !!a) || null;
+ });
+ },
+
+ /**
+ * Asynchronously get an add-on with a specific Sync GUID.
+ *
+ * @param aGUID
+ * String GUID of add-on to retrieve
+ * @throws if the aGUID argument is not specified
+ */
+ getAddonBySyncGUID(aGUID) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!aGUID || typeof aGUID != "string") {
+ throw Components.Exception(
+ "aGUID must be a non-empty string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ return (async () => {
+ for (let provider of this.providers) {
+ let addon = await promiseCallProvider(
+ provider,
+ "getAddonBySyncGUID",
+ aGUID
+ );
+
+ if (addon) {
+ return addon;
+ }
+ }
+
+ return null;
+ })();
+ },
+
+ /**
+ * Asynchronously gets an array of add-ons.
+ *
+ * @param aIDs
+ * The array of IDs to retrieve
+ * @return {Promise}
+ * @resolves The array of found add-ons.
+ * @rejects Never
+ * @throws if the aIDs argument is not specified
+ */
+ getAddonsByIDs(aIDs) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!Array.isArray(aIDs)) {
+ throw Components.Exception(
+ "aIDs must be an array",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let promises = aIDs.map(a => AddonManagerInternal.getAddonByID(a));
+ return Promise.all(promises);
+ },
+
+ /**
+ * Asynchronously gets add-ons of specific types.
+ *
+ * @param aTypes
+ * An optional array of types to retrieve. Each type is a string name
+ */
+ getAddonsByTypes(aTypes) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (aTypes && !Array.isArray(aTypes)) {
+ throw Components.Exception(
+ "aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ return (async () => {
+ let addons = [];
+
+ for (let provider of this.providers) {
+ let providerAddons = await promiseCallProvider(
+ provider,
+ "getAddonsByTypes",
+ aTypes
+ );
+
+ if (providerAddons) {
+ addons.push(...providerAddons);
+ }
+ }
+
+ return addons;
+ })();
+ },
+
+ /**
+ * Gets active add-ons of specific types.
+ *
+ * This is similar to getAddonsByTypes() but it may return a limited
+ * amount of information about only active addons. Consequently, it
+ * can be implemented by providers using only immediately available
+ * data as opposed to getAddonsByTypes which may require I/O).
+ *
+ * @param aTypes
+ * An optional array of types to retrieve. Each type is a string name
+ *
+ * @resolve {addons: Array, fullData: bool}
+ * fullData is true if addons contains all the data we have on those
+ * addons. It is false if addons only contains partial data.
+ */
+ async getActiveAddons(aTypes) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (aTypes && !Array.isArray(aTypes)) {
+ throw Components.Exception(
+ "aTypes must be an array or null",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ let addons = [],
+ fullData = true;
+
+ for (let provider of this.providers) {
+ let providerAddons, providerFullData;
+ if ("getActiveAddons" in provider) {
+ ({
+ addons: providerAddons,
+ fullData: providerFullData,
+ } = await callProvider(provider, "getActiveAddons", null, aTypes));
+ } else {
+ providerAddons = await promiseCallProvider(
+ provider,
+ "getAddonsByTypes",
+ aTypes
+ );
+ providerAddons = providerAddons.filter(a => a.isActive);
+ providerFullData = true;
+ }
+
+ if (providerAddons) {
+ addons.push(...providerAddons);
+ fullData = fullData && providerFullData;
+ }
+ }
+
+ return { addons, fullData };
+ },
+
+ /**
+ * Asynchronously gets all installed add-ons.
+ */
+ getAllAddons() {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ return this.getAddonsByTypes(null);
+ },
+
+ /**
+ * Adds a new AddonManagerListener if the listener is not already registered.
+ *
+ * @param {AddonManagerListener} aListener
+ * The listener to add
+ */
+ addManagerListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be an AddonManagerListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.managerListeners.add(aListener);
+ },
+
+ /**
+ * Removes an AddonManagerListener if the listener is registered.
+ *
+ * @param {AddonManagerListener} aListener
+ * The listener to remove
+ */
+ removeManagerListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be an AddonManagerListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.managerListeners.delete(aListener);
+ },
+
+ /**
+ * Adds a new AddonListener if the listener is not already registered.
+ *
+ * @param {AddonManagerListener} aListener
+ * The AddonListener to add.
+ */
+ addAddonListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be an AddonListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.addonListeners.add(aListener);
+ },
+
+ /**
+ * Removes an AddonListener if the listener is registered.
+ *
+ * @param {object} aListener
+ * The AddonListener to remove
+ */
+ removeAddonListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be an AddonListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.addonListeners.delete(aListener);
+ },
+
+ /**
+ * Adds a new TypeListener if the listener is not already registered.
+ *
+ * @param {TypeListener} aListener
+ * The TypeListener to add
+ */
+ addTypeListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be a TypeListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.typeListeners.add(aListener);
+ },
+
+ /**
+ * Removes an TypeListener if the listener is registered.
+ *
+ * @param aListener
+ * The TypeListener to remove
+ */
+ removeTypeListener(aListener) {
+ if (!aListener || typeof aListener != "object") {
+ throw Components.Exception(
+ "aListener must be a TypeListener object",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ this.typeListeners.delete(aListener);
+ },
+
+ get addonTypes() {
+ // A read-only wrapper around the types dictionary
+ return new Proxy(this.types, {
+ defineProperty(target, property, descriptor) {
+ // Not allowed to define properties
+ return false;
+ },
+
+ deleteProperty(target, property) {
+ // Not allowed to delete properties
+ return false;
+ },
+
+ get(target, property, receiver) {
+ if (!target.hasOwnProperty(property)) {
+ return undefined;
+ }
+
+ return target[property].type;
+ },
+
+ getOwnPropertyDescriptor(target, property) {
+ if (!target.hasOwnProperty(property)) {
+ return undefined;
+ }
+
+ return {
+ value: target[property].type,
+ writable: false,
+ // Claim configurability to maintain the proxy invariants.
+ configurable: true,
+ enumerable: true,
+ };
+ },
+
+ preventExtensions(target) {
+ // Not allowed to prevent adding new properties
+ return false;
+ },
+
+ set(target, property, value, receiver) {
+ // Not allowed to set properties
+ return false;
+ },
+
+ setPrototypeOf(target, prototype) {
+ // Not allowed to change prototype
+ return false;
+ },
+ });
+ },
+
+ get autoUpdateDefault() {
+ return gAutoUpdateDefault;
+ },
+
+ set autoUpdateDefault(aValue) {
+ aValue = !!aValue;
+ if (aValue != gAutoUpdateDefault) {
+ Services.prefs.setBoolPref(PREF_EM_AUTOUPDATE_DEFAULT, aValue);
+ }
+ return aValue;
+ },
+
+ get checkCompatibility() {
+ return gCheckCompatibility;
+ },
+
+ set checkCompatibility(aValue) {
+ aValue = !!aValue;
+ if (aValue != gCheckCompatibility) {
+ if (!aValue) {
+ Services.prefs.setBoolPref(PREF_EM_CHECK_COMPATIBILITY, false);
+ } else {
+ Services.prefs.clearUserPref(PREF_EM_CHECK_COMPATIBILITY);
+ }
+ }
+ return aValue;
+ },
+
+ get strictCompatibility() {
+ return gStrictCompatibility;
+ },
+
+ set strictCompatibility(aValue) {
+ aValue = !!aValue;
+ if (aValue != gStrictCompatibility) {
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, aValue);
+ }
+ return aValue;
+ },
+
+ get checkUpdateSecurityDefault() {
+ return gCheckUpdateSecurityDefault;
+ },
+
+ get checkUpdateSecurity() {
+ return gCheckUpdateSecurity;
+ },
+
+ set checkUpdateSecurity(aValue) {
+ aValue = !!aValue;
+ if (aValue != gCheckUpdateSecurity) {
+ if (aValue != gCheckUpdateSecurityDefault) {
+ Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, aValue);
+ } else {
+ Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY);
+ }
+ }
+ return aValue;
+ },
+
+ get updateEnabled() {
+ return gUpdateEnabled;
+ },
+
+ set updateEnabled(aValue) {
+ aValue = !!aValue;
+ if (aValue != gUpdateEnabled) {
+ Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue);
+ }
+ return aValue;
+ },
+
+ _verifyThirdPartyInstall(browser, url, install, info, source) {
+ // If this is an install from a recognized source, or it is a recommended addon, we
+ // skip the third party panel. The source param was generated based on the installing
+ // principal and checking against site permissions and enterprise policy, so we
+ // can rely on that rather than re-validating against that principal.
+ if (
+ !WEBEXT_POSTDOWNLOAD_THIRD_PARTY ||
+ ["AMO", "local"].includes(source) ||
+ info.addon.canBypassThirdParyInstallPrompt
+ ) {
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ this.installNotifyObservers(
+ "addon-install-blocked",
+ browser,
+ url,
+ install,
+ resolve,
+ reject
+ );
+ });
+ },
+
+ setupPromptHandler(browser, url, install, requireConfirm, source) {
+ install.promptHandler = info =>
+ new Promise((resolve, _reject) => {
+ let reject = () => {
+ this.installNotifyObservers(
+ "addon-install-cancelled",
+ browser,
+ url,
+ install
+ );
+ _reject();
+ };
+
+ this._verifyThirdPartyInstall(browser, url, install, info, source)
+ .then(() => {
+ // All installs end up in this callback when the add-on is available
+ // for installation. There are numerous different things that can
+ // happen from here though. For webextensions, if the application
+ // implements webextension permission prompts, those always take
+ // precedence.
+ // If this add-on is not a webextension or if the application does not
+ // implement permission prompts, no confirmation is displayed for
+ // installs created from about:addons (in which case requireConfirm
+ // is false).
+ // In the remaining cases, a confirmation prompt is displayed but the
+ // application may override it either by implementing the
+ // "@mozilla.org/addons/web-install-prompt;1" contract or by setting
+ // the customConfirmationUI preference and responding to the
+ // "addon-install-confirmation" notification. If the application
+ // does not implement its own prompt, use the built-in xul dialog.
+ if (info.addon.userPermissions && WEBEXT_PERMISSION_PROMPTS) {
+ let subject = {
+ wrappedJSObject: {
+ target: browser,
+ info: Object.assign({ resolve, reject, source }, info),
+ },
+ };
+ subject.wrappedJSObject.info.permissions =
+ info.addon.userPermissions;
+ Services.obs.notifyObservers(
+ subject,
+ "webextension-permission-prompt"
+ );
+ } else if (requireConfirm) {
+ // The methods below all want to call the install() or cancel()
+ // method on the provided AddonInstall object to either accept
+ // or reject the confirmation. Fit that into our promise-based
+ // control flow by wrapping the install object. However,
+ // xpInstallConfirm.xul matches the install object it is passed
+ // with the argument passed to an InstallListener, so give it
+ // access to the underlying object through the .wrapped property.
+ let proxy = new Proxy(install, {
+ get(target, property) {
+ if (property == "install") {
+ return resolve;
+ } else if (property == "cancel") {
+ return reject;
+ } else if (property == "wrapped") {
+ return target;
+ }
+ let result = target[property];
+ return typeof result == "function"
+ ? result.bind(target)
+ : result;
+ },
+ });
+
+ // Check for a custom installation prompt that may be provided by the
+ // applicaton
+ if ("@mozilla.org/addons/web-install-prompt;1" in Cc) {
+ try {
+ let prompt = Cc[
+ "@mozilla.org/addons/web-install-prompt;1"
+ ].getService(Ci.amIWebInstallPrompt);
+ prompt.confirm(browser, url, [proxy]);
+ return;
+ } catch (e) {}
+ }
+
+ this.installNotifyObservers(
+ "addon-install-confirmation",
+ browser,
+ url,
+ proxy
+ );
+ } else {
+ resolve();
+ }
+ })
+ .catch(e => {
+ // Error is undefined if the promise was rejected.
+ if (e) {
+ Cu.reportError(`Install prompt handler error: ${e}`);
+ }
+ reject();
+ });
+ });
+ },
+
+ webAPI: {
+ // installs maps integer ids to AddonInstall instances.
+ installs: new Map(),
+ nextInstall: 0,
+
+ sendEvent: null,
+ setEventHandler(fn) {
+ this.sendEvent = fn;
+ },
+
+ async getAddonByID(target, id) {
+ return webAPIForAddon(await AddonManager.getAddonByID(id));
+ },
+
+ // helper to copy (and convert) the properties we care about
+ copyProps(install, obj) {
+ obj.state = AddonManager.stateToString(install.state);
+ obj.error = AddonManager.errorToString(install.error);
+ obj.progress = install.progress;
+ obj.maxProgress = install.maxProgress;
+ },
+
+ forgetInstall(id) {
+ let info = this.installs.get(id);
+ if (!info) {
+ throw new Error(`forgetInstall cannot find ${id}`);
+ }
+ info.install.removeListener(info.listener);
+ this.installs.delete(id);
+ },
+
+ createInstall(target, options) {
+ // Throw an appropriate error if the given URL is not valid
+ // as an installation source. Return silently if it is okay.
+ function checkInstallUrl(url) {
+ let host = Services.io.newURI(options.url).host;
+ if (WEBAPI_INSTALL_HOSTS.includes(host)) {
+ return;
+ }
+ if (
+ Services.prefs.getBoolPref(PREF_WEBAPI_TESTING) &&
+ WEBAPI_TEST_INSTALL_HOSTS.includes(host)
+ ) {
+ return;
+ }
+
+ throw new Error(`Install from ${host} not permitted`);
+ }
+
+ const makeListener = (id, mm) => {
+ const events = [
+ "onDownloadStarted",
+ "onDownloadProgress",
+ "onDownloadEnded",
+ "onDownloadCancelled",
+ "onDownloadFailed",
+ "onInstallStarted",
+ "onInstallEnded",
+ "onInstallCancelled",
+ "onInstallFailed",
+ ];
+
+ let listener = {};
+ let installPromise = new Promise((resolve, reject) => {
+ events.forEach(event => {
+ listener[event] = (install, addon) => {
+ let data = { event, id };
+ AddonManager.webAPI.copyProps(install, data);
+ this.sendEvent(mm, data);
+ if (event == "onInstallEnded") {
+ resolve(addon);
+ } else if (
+ event == "onDownloadFailed" ||
+ event == "onInstallFailed"
+ ) {
+ reject({ message: "install failed" });
+ } else if (
+ event == "onDownloadCancelled" ||
+ event == "onInstallCancelled"
+ ) {
+ reject({ message: "install cancelled" });
+ } else if (event == "onDownloadEnded") {
+ if (install.addon.appDisabled) {
+ // App disabled items are not compatible and so fail to install
+ install.cancel();
+ AddonManagerInternal.installNotifyObservers(
+ "addon-install-failed",
+ target,
+ Services.io.newURI(options.url),
+ install
+ );
+ }
+ }
+ };
+ });
+ });
+
+ // We create the promise here since this is where we're setting
+ // up the InstallListener, but if the install is never started,
+ // no handlers will be attached so make sure we terminate errors.
+ installPromise.catch(() => {});
+
+ return { listener, installPromise };
+ };
+
+ try {
+ checkInstallUrl(options.url);
+ } catch (err) {
+ return Promise.reject({ message: err.message });
+ }
+
+ return AddonManagerInternal.getInstallForURL(options.url, {
+ browser: target,
+ triggeringPrincipal: options.triggeringPrincipal,
+ hash: options.hash,
+ telemetryInfo: {
+ source: AddonManager.getInstallSourceFromHost(options.sourceHost),
+ sourceURL: options.sourceURL,
+ method: "amWebAPI",
+ },
+ }).then(install => {
+ let requireConfirm = true;
+ if (
+ target.contentDocument &&
+ target.contentDocument.nodePrincipal.isSystemPrincipal
+ ) {
+ requireConfirm = false;
+ }
+ AddonManagerInternal.setupPromptHandler(
+ target,
+ null,
+ install,
+ requireConfirm,
+ "AMO"
+ );
+
+ let id = this.nextInstall++;
+ let { listener, installPromise } = makeListener(
+ id,
+ target.messageManager
+ );
+ install.addListener(listener);
+
+ this.installs.set(id, {
+ install,
+ target,
+ listener,
+ installPromise,
+ messageManager: target.messageManager,
+ });
+
+ let result = { id };
+ this.copyProps(install, result);
+ return result;
+ });
+ },
+
+ async addonUninstall(target, id) {
+ let addon = await AddonManager.getAddonByID(id);
+ if (!addon) {
+ return false;
+ }
+
+ if (!(addon.permissions & AddonManager.PERM_CAN_UNINSTALL)) {
+ return Promise.reject({ message: "Addon cannot be uninstalled" });
+ }
+
+ try {
+ addon.uninstall();
+ return true;
+ } catch (err) {
+ Cu.reportError(err);
+ return false;
+ }
+ },
+
+ async addonSetEnabled(target, id, value) {
+ let addon = await AddonManager.getAddonByID(id);
+ if (!addon) {
+ throw new Error(`No such addon ${id}`);
+ }
+
+ if (value) {
+ await addon.enable();
+ } else {
+ await addon.disable();
+ }
+ },
+
+ async addonInstallDoInstall(target, id) {
+ let state = this.installs.get(id);
+ if (!state) {
+ throw new Error(`invalid id ${id}`);
+ }
+
+ let addon = await state.install.install();
+
+ if (addon.type == "theme" && !addon.appDisabled) {
+ await addon.enable();
+ }
+
+ if (Services.prefs.getBoolPref(PREF_WEBEXT_PERM_PROMPTS, false)) {
+ await new Promise(resolve => {
+ let subject = {
+ wrappedJSObject: { target, addon, callback: resolve },
+ };
+ Services.obs.notifyObservers(subject, "webextension-install-notify");
+ });
+ }
+ },
+
+ addonInstallCancel(target, id) {
+ let state = this.installs.get(id);
+ if (!state) {
+ return Promise.reject(`invalid id ${id}`);
+ }
+ return Promise.resolve(state.install.cancel());
+ },
+
+ clearInstalls(ids) {
+ for (let id of ids) {
+ this.forgetInstall(id);
+ }
+ },
+
+ clearInstallsFrom(mm) {
+ for (let [id, info] of this.installs) {
+ if (info.messageManager == mm) {
+ this.forgetInstall(id);
+ }
+ }
+ },
+
+ async addonReportAbuse(target, id) {
+ if (!Services.prefs.getBoolPref(PREF_AMO_ABUSEREPORT, false)) {
+ return Promise.reject({
+ message: "amWebAPI reportAbuse not supported",
+ });
+ }
+
+ let existingDialog = AbuseReporter.getOpenDialog();
+ if (existingDialog) {
+ existingDialog.close();
+ }
+
+ const dialog = await AbuseReporter.openDialog(id, "amo", target).catch(
+ err => {
+ Cu.reportError(err);
+ return Promise.reject({
+ message: "Error creating abuse report",
+ });
+ }
+ );
+
+ return dialog.promiseReport.then(
+ async report => {
+ if (!report) {
+ return false;
+ }
+
+ await report.submit().catch(err => {
+ Cu.reportError(err);
+ return Promise.reject({
+ message: "Error submitting abuse report",
+ });
+ });
+
+ return true;
+ },
+ err => {
+ Cu.reportError(err);
+ dialog.close();
+ return Promise.reject({
+ message: "Error creating abuse report",
+ });
+ }
+ );
+ },
+ },
+};
+
+/**
+ * Should not be used outside of core Mozilla code. This is a private API for
+ * the startup and platform integration code to use. Refer to the methods on
+ * AddonManagerInternal for documentation however note that these methods are
+ * subject to change at any time.
+ */
+var AddonManagerPrivate = {
+ startup() {
+ AddonManagerInternal.startup();
+ },
+
+ addonIsActive(addonId) {
+ return AddonManagerInternal._getProviderByName("XPIProvider").addonIsActive(
+ addonId
+ );
+ },
+
+ /**
+ * Gets an array of add-ons which were side-loaded prior to the last
+ * startup, and are currently disabled.
+ *
+ * @returns {Promise>}
+ */
+ getNewSideloads() {
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).getNewSideloads();
+ },
+
+ get browserUpdated() {
+ return gBrowserUpdated;
+ },
+
+ registerProvider(aProvider, aTypes) {
+ AddonManagerInternal.registerProvider(aProvider, aTypes);
+ },
+
+ unregisterProvider(aProvider) {
+ AddonManagerInternal.unregisterProvider(aProvider);
+ },
+
+ markProviderSafe(aProvider) {
+ AddonManagerInternal.markProviderSafe(aProvider);
+ },
+
+ backgroundUpdateCheck() {
+ return AddonManagerInternal.backgroundUpdateCheck();
+ },
+
+ backgroundUpdateTimerHandler() {
+ // Don't return the promise here, since the caller doesn't care.
+ AddonManagerInternal.backgroundUpdateCheck();
+ },
+
+ addStartupChange(aType, aID) {
+ AddonManagerInternal.addStartupChange(aType, aID);
+ },
+
+ removeStartupChange(aType, aID) {
+ AddonManagerInternal.removeStartupChange(aType, aID);
+ },
+
+ notifyAddonChanged(aID, aType, aPendingRestart) {
+ AddonManagerInternal.notifyAddonChanged(aID, aType, aPendingRestart);
+ },
+
+ updateAddonAppDisabledStates() {
+ AddonManagerInternal.updateAddonAppDisabledStates();
+ },
+
+ updateAddonRepositoryData() {
+ return AddonManagerInternal.updateAddonRepositoryData();
+ },
+
+ callInstallListeners(...aArgs) {
+ return AddonManagerInternal.callInstallListeners.apply(
+ AddonManagerInternal,
+ aArgs
+ );
+ },
+
+ callAddonListeners(...aArgs) {
+ AddonManagerInternal.callAddonListeners.apply(AddonManagerInternal, aArgs);
+ },
+
+ AddonAuthor,
+
+ AddonScreenshot,
+
+ AddonType,
+
+ get BOOTSTRAP_REASONS() {
+ return AddonManagerInternal._getProviderByName("XPIProvider")
+ .BOOTSTRAP_REASONS;
+ },
+
+ recordTimestamp(name, value) {
+ AddonManagerInternal.recordTimestamp(name, value);
+ },
+
+ _simpleMeasures: {},
+ recordSimpleMeasure(name, value) {
+ this._simpleMeasures[name] = value;
+ },
+
+ recordException(aModule, aContext, aException) {
+ let report = {
+ module: aModule,
+ context: aContext,
+ };
+
+ if (typeof aException == "number") {
+ report.message = Components.Exception("", aException).name;
+ } else {
+ report.message = aException.toString();
+ if (aException.fileName) {
+ report.file = aException.fileName;
+ report.line = aException.lineNumber;
+ }
+ }
+
+ this._simpleMeasures.exception = report;
+ },
+
+ getSimpleMeasures() {
+ return this._simpleMeasures;
+ },
+
+ getTelemetryDetails() {
+ return AddonManagerInternal.telemetryDetails;
+ },
+
+ setTelemetryDetails(aProvider, aDetails) {
+ AddonManagerInternal.telemetryDetails[aProvider] = aDetails;
+ },
+
+ // Start a timer, record a simple measure of the time interval when
+ // timer.done() is called
+ simpleTimer(aName) {
+ let startTime = Cu.now();
+ return {
+ done: () =>
+ this.recordSimpleMeasure(aName, Math.round(Cu.now() - startTime)),
+ };
+ },
+
+ async recordTiming(name, task) {
+ let timer = this.simpleTimer(name);
+ try {
+ return await task();
+ } finally {
+ timer.done();
+ }
+ },
+
+ /**
+ * Helper to call update listeners when no update is available.
+ *
+ * This can be used as an implementation for Addon.findUpdates() when
+ * no update mechanism is available.
+ */
+ callNoUpdateListeners(addon, listener, reason, appVersion, platformVersion) {
+ if ("onNoCompatibilityUpdateAvailable" in listener) {
+ safeCall(listener.onNoCompatibilityUpdateAvailable.bind(listener), addon);
+ }
+ if ("onNoUpdateAvailable" in listener) {
+ safeCall(listener.onNoUpdateAvailable.bind(listener), addon);
+ }
+ if ("onUpdateFinished" in listener) {
+ safeCall(listener.onUpdateFinished.bind(listener), addon);
+ }
+ },
+
+ get webExtensionsMinPlatformVersion() {
+ return gWebExtensionsMinPlatformVersion;
+ },
+
+ hasUpgradeListener(aId) {
+ return AddonManagerInternal.upgradeListeners.has(aId);
+ },
+
+ getUpgradeListener(aId) {
+ return AddonManagerInternal.upgradeListeners.get(aId);
+ },
+
+ get externalExtensionLoaders() {
+ return AddonManagerInternal.externalExtensionLoaders;
+ },
+
+ /**
+ * Predicate that returns true if we think the given extension ID
+ * might have been generated by XPIProvider.
+ */
+ isTemporaryInstallID(extensionId) {
+ if (!gStarted) {
+ throw Components.Exception(
+ "AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED
+ );
+ }
+
+ if (!extensionId || typeof extensionId != "string") {
+ throw Components.Exception(
+ "extensionId must be a string",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).isTemporaryInstallID(extensionId);
+ },
+
+ isDBLoaded() {
+ let provider = AddonManagerInternal._getProviderByName("XPIProvider");
+ return provider ? provider.isDBLoaded : false;
+ },
+
+ get databaseReady() {
+ let provider = AddonManagerInternal._getProviderByName("XPIProvider");
+ return provider ? provider.databaseReady : new Promise(() => {});
+ },
+
+ /**
+ * Async shutdown barrier which blocks the completion of add-on
+ * manager shutdown. This should generally only be used by add-on
+ * providers (i.e., XPIProvider) to complete their final shutdown
+ * tasks.
+ */
+ get finalShutdown() {
+ return gFinalShutdownBarrier.client;
+ },
+};
+
+/**
+ * This is the public API that UI and developers should be calling. All methods
+ * just forward to AddonManagerInternal.
+ * @class
+ */
+var AddonManager = {
+ // Map used to convert the known install source hostnames into the value to set into the
+ // telemetry events.
+ _installHostSource: new Map([
+ ["addons.mozilla.org", "amo"],
+ ["discovery.addons.mozilla.org", "disco"],
+ ]),
+
+ // Constants for the AddonInstall.state property
+ // These will show up as AddonManager.STATE_* (eg, STATE_AVAILABLE)
+ _states: new Map([
+ // The install is available for download.
+ ["STATE_AVAILABLE", 0],
+ // The install is being downloaded.
+ ["STATE_DOWNLOADING", 1],
+ // The install is checking the update for compatibility information.
+ ["STATE_CHECKING_UPDATE", 2],
+ // The install is downloaded and ready to install.
+ ["STATE_DOWNLOADED", 3],
+ // The download failed.
+ ["STATE_DOWNLOAD_FAILED", 4],
+ // The install may not proceed until the user accepts a prompt
+ ["STATE_AWAITING_PROMPT", 5],
+ // Any prompts are done
+ ["STATE_PROMPTS_DONE", 6],
+ // The install has been postponed.
+ ["STATE_POSTPONED", 7],
+ // The install is ready to be applied.
+ ["STATE_READY", 8],
+ // The add-on is being installed.
+ ["STATE_INSTALLING", 9],
+ // The add-on has been installed.
+ ["STATE_INSTALLED", 10],
+ // The install failed.
+ ["STATE_INSTALL_FAILED", 11],
+ // The install has been cancelled.
+ ["STATE_CANCELLED", 12],
+ ]),
+
+ // Constants representing different types of errors while downloading an
+ // add-on.
+ // These will show up as AddonManager.ERROR_* (eg, ERROR_NETWORK_FAILURE)
+ _errors: new Map([
+ // The download failed due to network problems.
+ ["ERROR_NETWORK_FAILURE", -1],
+ // The downloaded file did not match the provided hash.
+ ["ERROR_INCORRECT_HASH", -2],
+ // The downloaded file seems to be corrupted in some way.
+ ["ERROR_CORRUPT_FILE", -3],
+ // An error occurred trying to write to the filesystem.
+ ["ERROR_FILE_ACCESS", -4],
+ // The add-on must be signed and isn't.
+ ["ERROR_SIGNEDSTATE_REQUIRED", -5],
+ // The downloaded add-on had a different type than expected.
+ ["ERROR_UNEXPECTED_ADDON_TYPE", -6],
+ // The addon did not have the expected ID
+ ["ERROR_INCORRECT_ID", -7],
+ ]),
+ // The update check timed out
+ ERROR_TIMEOUT: -1,
+ // There was an error while downloading the update information.
+ ERROR_DOWNLOAD_ERROR: -2,
+ // The update information was malformed in some way.
+ ERROR_PARSE_ERROR: -3,
+ // The update information was not in any known format.
+ ERROR_UNKNOWN_FORMAT: -4,
+ // The update information was not correctly signed or there was an SSL error.
+ ERROR_SECURITY_ERROR: -5,
+ // The update was cancelled
+ ERROR_CANCELLED: -6,
+ // These must be kept in sync with AddonUpdateChecker.
+ // No error was encountered.
+ UPDATE_STATUS_NO_ERROR: 0,
+ // The update check timed out
+ UPDATE_STATUS_TIMEOUT: -1,
+ // There was an error while downloading the update information.
+ UPDATE_STATUS_DOWNLOAD_ERROR: -2,
+ // The update information was malformed in some way.
+ UPDATE_STATUS_PARSE_ERROR: -3,
+ // The update information was not in any known format.
+ UPDATE_STATUS_UNKNOWN_FORMAT: -4,
+ // The update information was not correctly signed or there was an SSL error.
+ UPDATE_STATUS_SECURITY_ERROR: -5,
+ // The update was cancelled.
+ UPDATE_STATUS_CANCELLED: -6,
+ // Constants to indicate why an update check is being performed
+ // Update check has been requested by the user.
+ UPDATE_WHEN_USER_REQUESTED: 1,
+ // Update check is necessary to see if the Addon is compatibile with a new
+ // version of the application.
+ UPDATE_WHEN_NEW_APP_DETECTED: 2,
+ // Update check is necessary because a new application has been installed.
+ UPDATE_WHEN_NEW_APP_INSTALLED: 3,
+ // Update check is a regular background update check.
+ UPDATE_WHEN_PERIODIC_UPDATE: 16,
+ // Update check is needed to check an Addon that is being installed.
+ UPDATE_WHEN_ADDON_INSTALLED: 17,
+
+ // Constants for operations in Addon.pendingOperations
+ // Indicates that the Addon has no pending operations.
+ PENDING_NONE: 0,
+ // Indicates that the Addon will be enabled after the application restarts.
+ PENDING_ENABLE: 1,
+ // Indicates that the Addon will be disabled after the application restarts.
+ PENDING_DISABLE: 2,
+ // Indicates that the Addon will be uninstalled after the application restarts.
+ PENDING_UNINSTALL: 4,
+ // Indicates that the Addon will be installed after the application restarts.
+ PENDING_INSTALL: 8,
+ PENDING_UPGRADE: 16,
+
+ // Constants for operations in Addon.operationsRequiringRestart
+ // Indicates that restart isn't required for any operation.
+ OP_NEEDS_RESTART_NONE: 0,
+ // Indicates that restart is required for enabling the addon.
+ OP_NEEDS_RESTART_ENABLE: 1,
+ // Indicates that restart is required for disabling the addon.
+ OP_NEEDS_RESTART_DISABLE: 2,
+ // Indicates that restart is required for uninstalling the addon.
+ OP_NEEDS_RESTART_UNINSTALL: 4,
+ // Indicates that restart is required for installing the addon.
+ OP_NEEDS_RESTART_INSTALL: 8,
+
+ // Constants for permissions in Addon.permissions.
+ // Indicates that the Addon can be uninstalled.
+ PERM_CAN_UNINSTALL: 1,
+ // Indicates that the Addon can be enabled by the user.
+ PERM_CAN_ENABLE: 2,
+ // Indicates that the Addon can be disabled by the user.
+ PERM_CAN_DISABLE: 4,
+ // Indicates that the Addon can be upgraded.
+ PERM_CAN_UPGRADE: 8,
+ // Indicates that the Addon can be set to be optionally enabled
+ // on a case-by-case basis.
+ PERM_CAN_ASK_TO_ACTIVATE: 16,
+ // Indicates that the Addon can be set to be allowed/disallowed
+ // in private browsing windows.
+ PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS: 32,
+ // Indicates that internal APIs can uninstall the add-on, even if the
+ // front-end cannot.
+ PERM_API_CAN_UNINSTALL: 64,
+
+ // General descriptions of where items are installed.
+ // Installed in this profile.
+ SCOPE_PROFILE: 1,
+ // Installed for all of this user's profiles.
+ SCOPE_USER: 2,
+ // Installed and owned by the application.
+ SCOPE_APPLICATION: 4,
+ // Installed for all users of the computer.
+ SCOPE_SYSTEM: 8,
+ // Installed temporarily
+ SCOPE_TEMPORARY: 16,
+ // The combination of all scopes.
+ SCOPE_ALL: 31,
+
+ // Add-on type is expected to be displayed in the UI in a list.
+ VIEW_TYPE_LIST: "list",
+
+ // Constants describing how add-on types behave.
+
+ // If no add-ons of a type are installed, then the category for that add-on
+ // type should be hidden in the UI.
+ TYPE_UI_HIDE_EMPTY: 16,
+ // Indicates that this add-on type supports the ask-to-activate state.
+ // That is, add-ons of this type can be set to be optionally enabled
+ // on a case-by-case basis.
+ TYPE_SUPPORTS_ASK_TO_ACTIVATE: 32,
+ // The add-on type natively supports undo for restartless uninstalls.
+ // If this flag is not specified, the UI is expected to handle this via
+ // disabling the add-on, and performing the actual uninstall at a later time.
+ TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL: 64,
+
+ // Constants for Addon.applyBackgroundUpdates.
+ // Indicates that the Addon should not update automatically.
+ AUTOUPDATE_DISABLE: 0,
+ // Indicates that the Addon should update automatically only if
+ // that's the global default.
+ AUTOUPDATE_DEFAULT: 1,
+ // Indicates that the Addon should update automatically.
+ AUTOUPDATE_ENABLE: 2,
+
+ // Constants for how Addon options should be shown.
+ // Options will be displayed in a new tab, if possible
+ OPTIONS_TYPE_TAB: 3,
+ // Similar to OPTIONS_TYPE_INLINE, but rather than generating inline
+ // options from a specially-formatted XUL file, the contents of the
+ // file are simply displayed in an inline element.
+ OPTIONS_TYPE_INLINE_BROWSER: 5,
+
+ // Constants for displayed or hidden options notifications
+ // Options notification will be displayed
+ OPTIONS_NOTIFICATION_DISPLAYED: "addon-options-displayed",
+ // Options notification will be hidden
+ OPTIONS_NOTIFICATION_HIDDEN: "addon-options-hidden",
+
+ // Constants for getStartupChanges, addStartupChange and removeStartupChange
+ // Add-ons that were detected as installed during startup. Doesn't include
+ // add-ons that were pending installation the last time the application ran.
+ STARTUP_CHANGE_INSTALLED: "installed",
+ // Add-ons that were detected as changed during startup. This includes an
+ // add-on moving to a different location, changing version or just having
+ // been detected as possibly changed.
+ STARTUP_CHANGE_CHANGED: "changed",
+ // Add-ons that were detected as uninstalled during startup. Doesn't include
+ // add-ons that were pending uninstallation the last time the application ran.
+ STARTUP_CHANGE_UNINSTALLED: "uninstalled",
+ // Add-ons that were detected as disabled during startup, normally because of
+ // an application change making an add-on incompatible. Doesn't include
+ // add-ons that were pending being disabled the last time the application ran.
+ STARTUP_CHANGE_DISABLED: "disabled",
+ // Add-ons that were detected as enabled during startup, normally because of
+ // an application change making an add-on compatible. Doesn't include
+ // add-ons that were pending being enabled the last time the application ran.
+ STARTUP_CHANGE_ENABLED: "enabled",
+
+ // Constants for Addon.signedState. Any states that should cause an add-on
+ // to be unusable in builds that require signing should have negative values.
+ // Add-on signing is not required, e.g. because the pref is disabled.
+ SIGNEDSTATE_NOT_REQUIRED: undefined,
+ // Add-on is signed but signature verification has failed.
+ SIGNEDSTATE_BROKEN: -2,
+ // Add-on may be signed but by an certificate that doesn't chain to our
+ // our trusted certificate.
+ SIGNEDSTATE_UNKNOWN: -1,
+ // Add-on is unsigned.
+ SIGNEDSTATE_MISSING: 0,
+ // Add-on is preliminarily reviewed.
+ SIGNEDSTATE_PRELIMINARY: 1,
+ // Add-on is fully reviewed.
+ SIGNEDSTATE_SIGNED: 2,
+ // Add-on is system add-on.
+ SIGNEDSTATE_SYSTEM: 3,
+ // Add-on is signed with a "Mozilla Extensions" certificate
+ SIGNEDSTATE_PRIVILEGED: 4,
+
+ // Constants for the Addon.userDisabled property
+ // Indicates that the userDisabled state of this add-on is currently
+ // ask-to-activate. That is, it can be conditionally enabled on a
+ // case-by-case basis.
+ STATE_ASK_TO_ACTIVATE: "askToActivate",
+
+ get __AddonManagerInternal__() {
+ return AppConstants.DEBUG ? AddonManagerInternal : undefined;
+ },
+
+ /** Boolean indicating whether AddonManager startup has completed. */
+ get isReady() {
+ return gStartupComplete && !gShutdownInProgress;
+ },
+
+ /** @constructor */
+ init() {
+ this._stateToString = new Map();
+ for (let [name, value] of this._states) {
+ this[name] = value;
+ this._stateToString.set(value, name);
+ }
+ this._errorToString = new Map();
+ for (let [name, value] of this._errors) {
+ this[name] = value;
+ this._errorToString.set(value, name);
+ }
+ },
+
+ stateToString(state) {
+ return this._stateToString.get(state);
+ },
+
+ errorToString(err) {
+ return err ? this._errorToString.get(err) : null;
+ },
+
+ getInstallSourceFromHost(host) {
+ if (this._installHostSource.has(host)) {
+ return this._installHostSource.get(host);
+ }
+
+ if (WEBAPI_TEST_INSTALL_HOSTS.includes(host)) {
+ return "test-host";
+ }
+
+ return "unknown";
+ },
+
+ getInstallForURL(aUrl, aOptions) {
+ return AddonManagerInternal.getInstallForURL(aUrl, aOptions);
+ },
+
+ getInstallForFile(
+ aFile,
+ aMimetype,
+ aTelemetryInfo,
+ aUseSystemLocation = false
+ ) {
+ return AddonManagerInternal.getInstallForFile(
+ aFile,
+ aMimetype,
+ aTelemetryInfo,
+ aUseSystemLocation
+ );
+ },
+
+ uninstallSystemProfileAddon(aID) {
+ return AddonManagerInternal.uninstallSystemProfileAddon(aID);
+ },
+
+ stageLangpacksForAppUpdate(appVersion, platformVersion) {
+ return AddonManagerInternal._getProviderByName(
+ "XPIProvider"
+ ).stageLangpacksForAppUpdate(appVersion, platformVersion);
+ },
+
+ /**
+ * Gets an array of add-on IDs that changed during the most recent startup.
+ *
+ * @param aType
+ * The type of startup change to get
+ * @return An array of add-on IDs
+ */
+ getStartupChanges(aType) {
+ if (!(aType in AddonManagerInternal.startupChanges)) {
+ return [];
+ }
+ return AddonManagerInternal.startupChanges[aType].slice(0);
+ },
+
+ getAddonByID(aID) {
+ return AddonManagerInternal.getAddonByID(aID);
+ },
+
+ getAddonBySyncGUID(aGUID) {
+ return AddonManagerInternal.getAddonBySyncGUID(aGUID);
+ },
+
+ getAddonsByIDs(aIDs) {
+ return AddonManagerInternal.getAddonsByIDs(aIDs);
+ },
+
+ getAddonsByTypes(aTypes) {
+ return AddonManagerInternal.getAddonsByTypes(aTypes);
+ },
+
+ getActiveAddons(aTypes) {
+ return AddonManagerInternal.getActiveAddons(aTypes);
+ },
+
+ getAllAddons() {
+ return AddonManagerInternal.getAllAddons();
+ },
+
+ getInstallsByTypes(aTypes) {
+ return AddonManagerInternal.getInstallsByTypes(aTypes);
+ },
+
+ getAllInstalls() {
+ return AddonManagerInternal.getAllInstalls();
+ },
+
+ isInstallEnabled(aType) {
+ return AddonManagerInternal.isInstallEnabled(aType);
+ },
+
+ isInstallAllowed(aType, aInstallingPrincipal) {
+ return AddonManagerInternal.isInstallAllowed(aType, aInstallingPrincipal);
+ },
+
+ installAddonFromWebpage(aType, aBrowser, aInstallingPrincipal, aInstall) {
+ AddonManagerInternal.installAddonFromWebpage(
+ aType,
+ aBrowser,
+ aInstallingPrincipal,
+ aInstall
+ );
+ },
+
+ installAddonFromAOM(aBrowser, aUri, aInstall) {
+ AddonManagerInternal.installAddonFromAOM(aBrowser, aUri, aInstall);
+ },
+
+ installTemporaryAddon(aDirectory) {
+ return AddonManagerInternal.installTemporaryAddon(aDirectory);
+ },
+
+ installBuiltinAddon(aBase) {
+ return AddonManagerInternal.installBuiltinAddon(aBase);
+ },
+
+ maybeInstallBuiltinAddon(aID, aVersion, aBase) {
+ return AddonManagerInternal.maybeInstallBuiltinAddon(aID, aVersion, aBase);
+ },
+
+ addManagerListener(aListener) {
+ AddonManagerInternal.addManagerListener(aListener);
+ },
+
+ removeManagerListener(aListener) {
+ AddonManagerInternal.removeManagerListener(aListener);
+ },
+
+ addInstallListener(aListener) {
+ AddonManagerInternal.addInstallListener(aListener);
+ },
+
+ removeInstallListener(aListener) {
+ AddonManagerInternal.removeInstallListener(aListener);
+ },
+
+ getUpgradeListener(aId) {
+ return AddonManagerInternal.upgradeListeners.get(aId);
+ },
+
+ addUpgradeListener(aInstanceID, aCallback) {
+ AddonManagerInternal.addUpgradeListener(aInstanceID, aCallback);
+ },
+
+ removeUpgradeListener(aInstanceID) {
+ return AddonManagerInternal.removeUpgradeListener(aInstanceID);
+ },
+
+ addExternalExtensionLoader(types, loader) {
+ return AddonManagerInternal.addExternalExtensionLoader(types, loader);
+ },
+
+ addAddonListener(aListener) {
+ AddonManagerInternal.addAddonListener(aListener);
+ },
+
+ removeAddonListener(aListener) {
+ AddonManagerInternal.removeAddonListener(aListener);
+ },
+
+ addTypeListener(aListener) {
+ AddonManagerInternal.addTypeListener(aListener);
+ },
+
+ removeTypeListener(aListener) {
+ AddonManagerInternal.removeTypeListener(aListener);
+ },
+
+ get addonTypes() {
+ return AddonManagerInternal.addonTypes;
+ },
+
+ /**
+ * Determines whether an Addon should auto-update or not.
+ *
+ * @param aAddon
+ * The Addon representing the add-on
+ * @return true if the addon should auto-update, false otherwise.
+ */
+ shouldAutoUpdate(aAddon) {
+ if (!aAddon || typeof aAddon != "object") {
+ throw Components.Exception(
+ "aAddon must be specified",
+ Cr.NS_ERROR_INVALID_ARG
+ );
+ }
+
+ if (!("applyBackgroundUpdates" in aAddon)) {
+ return false;
+ }
+ if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_ENABLE) {
+ return true;
+ }
+ if (aAddon.applyBackgroundUpdates == AddonManager.AUTOUPDATE_DISABLE) {
+ return false;
+ }
+ return this.autoUpdateDefault;
+ },
+
+ get checkCompatibility() {
+ return AddonManagerInternal.checkCompatibility;
+ },
+
+ set checkCompatibility(aValue) {
+ AddonManagerInternal.checkCompatibility = aValue;
+ },
+
+ get strictCompatibility() {
+ return AddonManagerInternal.strictCompatibility;
+ },
+
+ set strictCompatibility(aValue) {
+ AddonManagerInternal.strictCompatibility = aValue;
+ },
+
+ get checkUpdateSecurityDefault() {
+ return AddonManagerInternal.checkUpdateSecurityDefault;
+ },
+
+ get checkUpdateSecurity() {
+ return AddonManagerInternal.checkUpdateSecurity;
+ },
+
+ set checkUpdateSecurity(aValue) {
+ AddonManagerInternal.checkUpdateSecurity = aValue;
+ },
+
+ get updateEnabled() {
+ return AddonManagerInternal.updateEnabled;
+ },
+
+ set updateEnabled(aValue) {
+ AddonManagerInternal.updateEnabled = aValue;
+ },
+
+ get autoUpdateDefault() {
+ return AddonManagerInternal.autoUpdateDefault;
+ },
+
+ set autoUpdateDefault(aValue) {
+ AddonManagerInternal.autoUpdateDefault = aValue;
+ },
+
+ escapeAddonURI(aAddon, aUri, aAppVersion) {
+ return AddonManagerInternal.escapeAddonURI(aAddon, aUri, aAppVersion);
+ },
+
+ getPreferredIconURL(aAddon, aSize, aWindow = undefined) {
+ return AddonManagerInternal.getPreferredIconURL(aAddon, aSize, aWindow);
+ },
+
+ get webAPI() {
+ return AddonManagerInternal.webAPI;
+ },
+
+ /**
+ * Async shutdown barrier which blocks the start of AddonManager
+ * shutdown. Callers should add blockers to this barrier if they need
+ * to complete add-on manager operations before it shuts down.
+ */
+ get beforeShutdown() {
+ return gBeforeShutdownBarrier.client;
+ },
+};
+
+/**
+ * Listens to the AddonManager install and addon events and send telemetry events.
+ */
+AMTelemetry = {
+ telemetrySetupDone: false,
+
+ init() {
+ // Enable the addonsManager telemetry event category before the AddonManager
+ // has completed its startup, otherwise telemetry events recorded during the
+ // AddonManager/XPIProvider startup will not be recorded (e.g. the telemetry
+ // events for the extension migrated to the private browsing permission).
+ Services.telemetry.setEventRecordingEnabled("addonsManager", true);
+ },
+
+ // This method is called by the AddonManager, once it has been started, so that we can
+ // init the telemetry event category and start listening for the events related to the
+ // addons installation and management.
+ onStartup() {
+ if (this.telemetrySetupDone) {
+ return;
+ }
+
+ this.telemetrySetupDone = true;
+
+ Services.obs.addObserver(this, "addon-install-origin-blocked");
+ Services.obs.addObserver(this, "addon-install-disabled");
+ Services.obs.addObserver(this, "addon-install-blocked");
+
+ AddonManager.addInstallListener(this);
+ AddonManager.addAddonListener(this);
+ },
+
+ // Observer Service notification callback.
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "addon-install-blocked": {
+ const { installs } = subject.wrappedJSObject;
+ this.recordInstallEvent(installs[0], { step: "site_warning" });
+ break;
+ }
+ case "addon-install-origin-blocked": {
+ const { installs } = subject.wrappedJSObject;
+ this.recordInstallEvent(installs[0], { step: "site_blocked" });
+ break;
+ }
+ case "addon-install-disabled": {
+ const { installs } = subject.wrappedJSObject;
+ this.recordInstallEvent(installs[0], {
+ step: "install_disabled_warning",
+ });
+ break;
+ }
+ }
+ },
+
+ // AddonManager install listener callbacks.
+
+ onNewInstall(install) {
+ this.recordInstallEvent(install, { step: "started" });
+ },
+
+ onInstallCancelled(install) {
+ this.recordInstallEvent(install, { step: "cancelled" });
+ },
+
+ onInstallPostponed(install) {
+ this.recordInstallEvent(install, { step: "postponed" });
+ },
+
+ onInstallFailed(install) {
+ this.recordInstallEvent(install, { step: "failed" });
+ },
+
+ onInstallEnded(install) {
+ this.recordInstallEvent(install, { step: "completed" });
+ // Skip install_stats events for install objects related to.
+ // add-on updates.
+ if (!install.existingAddon) {
+ this.recordInstallStatsEvent(install);
+ }
+ },
+
+ onDownloadStarted(install) {
+ this.recordInstallEvent(install, { step: "download_started" });
+ },
+
+ onDownloadCancelled(install) {
+ this.recordInstallEvent(install, { step: "cancelled" });
+ },
+
+ onDownloadEnded(install) {
+ let download_time = Math.round(Cu.now() - install.downloadStartedAt);
+ this.recordInstallEvent(install, {
+ step: "download_completed",
+ download_time,
+ });
+ },
+
+ onDownloadFailed(install) {
+ let download_time = Math.round(Cu.now() - install.downloadStartedAt);
+ this.recordInstallEvent(install, {
+ step: "download_failed",
+ download_time,
+ });
+ },
+
+ // Addon listeners callbacks.
+
+ onUninstalled(addon) {
+ this.recordManageEvent(addon, "uninstall");
+ },
+
+ onEnabled(addon) {
+ this.recordManageEvent(addon, "enable");
+ },
+
+ onDisabled(addon) {
+ this.recordManageEvent(addon, "disable");
+ },
+
+ // Internal helpers methods.
+
+ /**
+ * Get a trimmed version of the given string if it is longer than 80 chars.
+ *
+ * @param {string} str
+ * The original string content.
+ *
+ * @returns {string}
+ * The trimmed version of the string when longer than 80 chars, or the given string
+ * unmodified otherwise.
+ */
+ getTrimmedString(str) {
+ if (str.length <= 80) {
+ return str;
+ }
+
+ const length = str.length;
+
+ // Trim the string to prevent a flood of warnings messages logged internally by recordEvent,
+ // the trimmed version is going to be composed by the first 40 chars and the last 37 and 3 dots
+ // that joins the two parts, to visually indicate that the string has been trimmed.
+ return `${str.slice(0, 40)}...${str.slice(length - 37, length)}`;
+ },
+
+ /**
+ * Retrieve the addonId for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to retrieve the addonId from.
+ *
+ * @returns {string | null}
+ * The addonId for the given AddonInstall instance (if any).
+ */
+ getAddonIdFromInstall(install) {
+ // Returns the id of the extension that is being installed, as soon as the
+ // addon is available in the AddonInstall instance (after being downloaded
+ // and validated successfully).
+ if (install.addon) {
+ return install.addon.id;
+ }
+
+ // While updating an addon, the existing addon can be
+ // used to retrieve the addon id since the first update event.
+ if (install.existingAddon) {
+ return install.existingAddon.id;
+ }
+
+ return null;
+ },
+
+ /**
+ * Retrieve the telemetry event's object property value for the given
+ * AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to retrieve the event object from.
+ *
+ * @returns {string}
+ * The object for the given AddonInstall instance.
+ */
+ getEventObjectFromInstall(install) {
+ let addonType;
+
+ if (install.type) {
+ // The AddonInstall wrapper already provides a type (if it was known when the
+ // install object has been created).
+ addonType = install.type;
+ } else if (install.addon) {
+ // The install flow has reached a step that has an addon instance which we can
+ // check to know the extension type (e.g. after download for the DownloadAddonInstall).
+ addonType = install.addon.type;
+ } else if (install.existingAddon) {
+ // The install flow is an update and we can look the existingAddon to check which was
+ // the add-on type that is being installed.
+ addonType = install.existingAddon.type;
+ }
+
+ return this.getEventObjectFromAddonType(addonType);
+ },
+
+ /**
+ * Retrieve the telemetry event source for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to retrieve the source from.
+ *
+ * @returns {Object | null}
+ * The telemetry infor ({source, method}) from the given AddonInstall instance.
+ */
+ getInstallTelemetryInfo(install) {
+ if (install.installTelemetryInfo) {
+ return install.installTelemetryInfo;
+ } else if (
+ install.existingAddon &&
+ install.existingAddon.installTelemetryInfo
+ ) {
+ // Get the install source from the existing addon (e.g. for an extension update).
+ return install.existingAddon.installTelemetryInfo;
+ }
+
+ return null;
+ },
+
+ /**
+ * Get the telemetry event's object property for the given addon type
+ *
+ * @param {string} addonType
+ * The addon type to convert into the related telemetry event object.
+ *
+ * @returns {string}
+ * The object for the given addon type.
+ */
+ getEventObjectFromAddonType(addonType) {
+ switch (addonType) {
+ case undefined:
+ return "unknown";
+ case "extension":
+ case "theme":
+ case "locale":
+ case "dictionary":
+ return addonType;
+ default:
+ // Currently this should only include plugins and gmp-plugins
+ return "other";
+ }
+ },
+
+ convertToString(value) {
+ if (value == null) {
+ // Convert null and undefined to empty strings.
+ return "";
+ }
+ switch (typeof value) {
+ case "string":
+ return value;
+ case "boolean":
+ return value ? "1" : "0";
+ }
+ return String(value);
+ },
+
+ /**
+ * Return the UTM parameters found in `sourceURL` for AMO attribution data.
+ *
+ * @param {string} sourceURL
+ * The source URL from where the add-on has been installed.
+ *
+ * @returns {object}
+ * An object containing the attribution data for AMO if any. Keys
+ * are defined in `AMO_ATTRIBUTION_DATA_KEYS`. Values are strings.
+ */
+ parseAttributionDataForAMO(sourceURL) {
+ let searchParams;
+
+ try {
+ searchParams = new URL(sourceURL).searchParams;
+ } catch {
+ return {};
+ }
+
+ const utmKeys = [...searchParams.keys()].filter(key =>
+ AMO_ATTRIBUTION_DATA_KEYS.includes(key)
+ );
+
+ return utmKeys.reduce((params, key) => {
+ let value = searchParams.get(key);
+ if (typeof value === "string") {
+ value = value.slice(0, AMO_ATTRIBUTION_DATA_MAX_LENGTH);
+ }
+
+ return { ...params, [key]: value };
+ }, {});
+ },
+
+ /**
+ * Record an "install stats" event when the source is included in
+ * `AMO_ATTRIBUTION_ALLOWED_SOURCES`.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to record an install_stats event for.
+ */
+ recordInstallStatsEvent(install) {
+ const telemetryInfo = this.getInstallTelemetryInfo(install);
+
+ if (!AMO_ATTRIBUTION_ALLOWED_SOURCES.includes(telemetryInfo?.source)) {
+ return;
+ }
+
+ const method = "install_stats";
+ const object = this.getEventObjectFromInstall(install);
+ const addonId = this.getAddonIdFromInstall(install);
+
+ if (!addonId) {
+ Cu.reportError(
+ "Missing addonId when trying to record an install_stats event"
+ );
+ return;
+ }
+
+ let extra = {
+ addon_id: this.getTrimmedString(addonId),
+ };
+
+ if (
+ telemetryInfo?.source === "amo" &&
+ typeof telemetryInfo?.sourceURL === "string"
+ ) {
+ extra = {
+ ...extra,
+ ...this.parseAttributionDataForAMO(telemetryInfo.sourceURL),
+ };
+ }
+
+ this.recordEvent({ method, object, value: install.hashedAddonId, extra });
+ },
+
+ /**
+ * Convert all the telemetry event's extra_vars into strings, if needed.
+ *
+ * @param {object} extraVars
+ * @returns {object} The formatted extra vars.
+ */
+ formatExtraVars({ addon, ...extraVars }) {
+ if (addon) {
+ extraVars.addonId = addon.id;
+ extraVars.type = addon.type;
+ }
+
+ // All the extra_vars in a telemetry event have to be strings.
+ for (var [key, value] of Object.entries(extraVars)) {
+ if (value == undefined) {
+ delete extraVars[key];
+ } else {
+ extraVars[key] = this.convertToString(value);
+ }
+ }
+
+ if (extraVars.addonId) {
+ extraVars.addonId = this.getTrimmedString(extraVars.addonId);
+ }
+
+ return extraVars;
+ },
+
+ /**
+ * Record an install or update event for the given AddonInstall instance.
+ *
+ * @param {AddonInstall} install
+ * The AddonInstall instance to record an install or update event for.
+ * @param {object} extraVars
+ * The additional extra_vars to include in the recorded event.
+ * @param {string} extraVars.step
+ * The current step in the install or update flow.
+ * @param {string} extraVars.download_time
+ * The number of ms needed to download the extension.
+ * @param {string} extraVars.num_strings
+ * The number of permission description string for the extension
+ * permission doorhanger.
+ */
+ recordInstallEvent(install, extraVars) {
+ // Early exit if AMTelemetry's telemetry setup has not been done yet.
+ if (!this.telemetrySetupDone) {
+ return;
+ }
+
+ let extra = {};
+
+ let telemetryInfo = this.getInstallTelemetryInfo(install);
+ if (telemetryInfo && typeof telemetryInfo.source === "string") {
+ extra.source = telemetryInfo.source;
+ }
+
+ if (extra.source === "internal") {
+ // Do not record the telemetry event for installation sources
+ // that are marked as "internal".
+ return;
+ }
+
+ // Also include the install source's method when applicable (e.g. install events with
+ // source "about:addons" may have "install-from-file" or "url" as their source method).
+ if (telemetryInfo && typeof telemetryInfo.method === "string") {
+ extra.method = telemetryInfo.method;
+ }
+
+ let addonId = this.getAddonIdFromInstall(install);
+ let object = this.getEventObjectFromInstall(install);
+
+ let installId = String(install.installId);
+ let eventMethod = install.existingAddon ? "update" : "install";
+
+ if (addonId) {
+ extra.addon_id = this.getTrimmedString(addonId);
+ }
+
+ if (install.error) {
+ extra.error = AddonManager.errorToString(install.error);
+ }
+
+ if (eventMethod === "update") {
+ // For "update" telemetry events, also include an extra var which determine
+ // if the update has been requested by the user.
+ extra.updated_from = install.isUserRequestedUpdate ? "user" : "app";
+ }
+
+ // All the extra vars in a telemetry event have to be strings.
+ extra = this.formatExtraVars({ ...extraVars, ...extra });
+
+ this.recordEvent({ method: eventMethod, object, value: installId, extra });
+ },
+
+ /**
+ * Record a manage event for the given addon.
+ *
+ * @param {AddonWrapper} addon
+ * The AddonWrapper instance.
+ * @param {object} extraVars
+ * The additional extra_vars to include in the recorded event.
+ * @param {string} extraVars.num_strings
+ * The number of permission description string for the extension
+ * permission doorhanger.
+ */
+ recordManageEvent(addon, method, extraVars) {
+ // Early exit if AMTelemetry's telemetry setup has not been done yet.
+ if (!this.telemetrySetupDone) {
+ return;
+ }
+
+ let extra = {};
+
+ if (addon.installTelemetryInfo) {
+ if ("source" in addon.installTelemetryInfo) {
+ extra.source = addon.installTelemetryInfo.source;
+ }
+
+ // Also include the install source's method when applicable (e.g. install events with
+ // source "about:addons" may have "install-from-file" or "url" as their source method).
+ if ("method" in addon.installTelemetryInfo) {
+ extra.method = addon.installTelemetryInfo.method;
+ }
+ }
+
+ if (extra.source === "internal") {
+ // Do not record the telemetry event for installation sources
+ // that are marked as "internal".
+ return;
+ }
+
+ let object = this.getEventObjectFromAddonType(addon.type);
+ let value = this.getTrimmedString(addon.id);
+
+ extra = { ...extraVars, ...extra };
+
+ let hasExtraVars = !!Object.keys(extra).length;
+ extra = this.formatExtraVars(extra);
+
+ this.recordEvent({
+ method,
+ object,
+ value,
+ extra: hasExtraVars ? extra : null,
+ });
+ },
+
+ /**
+ * Record an event for when a link is clicked.
+ *
+ * @param {object} opts
+ * @param {string} opts.object
+ * The object of the event, should be an identifier for where the link
+ * is located. The accepted values are listed in the
+ * addonsManager.link object of the Events.yaml file.
+ * @param {string} opts.value The identifier for the link destination.
+ * @param {object} opts.extra
+ * The extra data to be sent, all keys must be registered in the
+ * extra_keys section of addonsManager.link in Events.yaml.
+ */
+ recordLinkEvent({ object, value, extra = null }) {
+ this.recordEvent({ method: "link", object, value, extra });
+ },
+
+ /**
+ * Record an event for an action that took place.
+ *
+ * @param {object} opts
+ * @param {string} opts.object
+ * The object of the event, should an identifier for where the action
+ * took place. The accepted values are listed in the
+ * addonsManager.action object of the Events.yaml file.
+ * @param {string} opts.action The identifier for the action.
+ * @param {string} opts.value An optional value for the action.
+ * @param {object} opts.addon
+ * An optional object with the "id" and "type" properties, for example
+ * an AddonWrapper object. Passing this will set some extra properties.
+ * @param {string} opts.addon.id
+ * The add-on ID to assign to extra.addonId.
+ * @param {string} opts.addon.type
+ * The add-on type to assign to extra.type.
+ * @param {string} opts.view The current view, when object is aboutAddons.
+ * @param {object} opts.extra
+ * The extra data to be sent, all keys must be registered in the
+ * extra_keys section of addonsManager.action in Events.yaml. If
+ * opts.addon is passed then it will overwrite the addonId and type
+ * properties in this object, if they are set.
+ */
+ recordActionEvent({ object, action, value, addon, view, extra }) {
+ extra = { ...extra, action, addon, view };
+ this.recordEvent({
+ method: "action",
+ object,
+ // Treat null and undefined as null.
+ value: value == null ? null : this.convertToString(value),
+ extra: this.formatExtraVars(extra),
+ });
+ },
+
+ /**
+ * Record an event for a view load in about:addons.
+ *
+ * @param {object} opts
+ * @param {string} opts.view
+ * The identifier for the view. The accepted values are listed in the
+ * object property of addonsManager.view object of the Events.yaml
+ * file.
+ * @param {AddonWrapper} opts.addon
+ * An optional add-on object related to the event.
+ * @param {string} opts.type
+ * An optional type for the view. If opts.addon is set it will
+ * overwrite this value with the type of the add-on.
+ */
+ recordViewEvent({ view, addon, type }) {
+ this.recordEvent({
+ method: "view",
+ object: "aboutAddons",
+ value: view,
+ extra: this.formatExtraVars({ type, addon }),
+ });
+ },
+
+ /**
+ * Record an event on abuse report submissions.
+ *
+ * @params {object} opts
+ * @params {string} opts.addonId
+ * The id of the addon being reported.
+ * @params {string} [opts.addonType]
+ * The type of the addon being reported (only present for an existing
+ * addonId).
+ * @params {string} [opts.errorType]
+ * The AbuseReport errorType for a submission failure.
+ * @params {string} opts.reportEntryPoint
+ * The entry point of the abuse report.
+ */
+ recordReportEvent({ addonId, addonType, errorType, reportEntryPoint }) {
+ this.recordEvent({
+ method: "report",
+ object: reportEntryPoint,
+ value: addonId,
+ extra: this.formatExtraVars({
+ addon_type: addonType,
+ error_type: errorType,
+ }),
+ });
+ },
+
+ recordEvent({ method, object, value, extra }) {
+ if (typeof value != "string") {
+ // The value must be a string or null, make sure it's valid so sending
+ // the event doesn't fail.
+ value = null;
+ }
+ try {
+ Services.telemetry.recordEvent(
+ "addonsManager",
+ method,
+ object,
+ value,
+ extra
+ );
+ } catch (err) {
+ // If the telemetry throws just log the error so it doesn't break any
+ // functionality.
+ Cu.reportError(err);
+ }
+ },
+};
+
+AddonManager.init();
+
+// Setup the AMTelemetry once the AddonManager has been started.
+AddonManager.addManagerListener(AMTelemetry);
+
+// load the timestamps module into AddonManagerInternal
+ChromeUtils.import(
+ "resource://gre/modules/TelemetryTimestamps.jsm",
+ AddonManagerInternal
+);
+Object.freeze(AddonManagerInternal);
+Object.freeze(AddonManagerPrivate);
+Object.freeze(AddonManager);
diff --git a/toolkit/mozapps/extensions/AddonManagerStartup-inlines.h b/toolkit/mozapps/extensions/AddonManagerStartup-inlines.h
new file mode 100644
index 0000000000..876b03cdbf
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerStartup-inlines.h
@@ -0,0 +1,226 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AddonManagerStartup_inlines_h
+#define AddonManagerStartup_inlines_h
+
+#include
+
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "jsapi.h"
+#include "mozilla/Maybe.h"
+#include "nsJSUtils.h"
+
+namespace mozilla {
+
+class ArrayIterElem;
+class PropertyIterElem;
+
+/*****************************************************************************
+ * Object iterator base classes
+ *****************************************************************************/
+
+template
+class MOZ_STACK_CLASS BaseIter {
+ public:
+ typedef T SelfType;
+
+ PropertyType begin() const { return PropertyType(Self()); }
+
+ PropertyType end() const {
+ PropertyType elem(Self());
+ return elem.End();
+ }
+
+ void* Context() const { return mContext; }
+
+ protected:
+ BaseIter(JSContext* cx, JS::HandleObject object, void* context = nullptr)
+ : mCx(cx), mObject(object), mContext(context) {}
+
+ const SelfType& Self() const { return *static_cast(this); }
+ SelfType& Self() { return *static_cast(this); }
+
+ JSContext* mCx;
+
+ JS::HandleObject mObject;
+
+ void* mContext;
+};
+
+template
+class MOZ_STACK_CLASS BaseIterElem {
+ public:
+ typedef T SelfType;
+
+ explicit BaseIterElem(const IterType& iter, uint32_t index = 0)
+ : mIter(iter), mIndex(index) {}
+
+ uint32_t Length() const { return mIter.Length(); }
+
+ JS::Value Value() {
+ JS::RootedValue value(mIter.mCx, JS::UndefinedValue());
+
+ auto& self = Self();
+ if (!self.GetValue(&value)) {
+ JS_ClearPendingException(mIter.mCx);
+ }
+
+ return value;
+ }
+
+ SelfType& operator*() { return Self(); }
+
+ SelfType& operator++() {
+ MOZ_ASSERT(mIndex < Length());
+ mIndex++;
+ return Self();
+ }
+
+ bool operator!=(const SelfType& other) const {
+ return &mIter != &other.mIter || mIndex != other.mIndex;
+ }
+
+ SelfType End() const {
+ SelfType end(mIter);
+ end.mIndex = Length();
+ return end;
+ }
+
+ void* Context() const { return mIter.Context(); }
+
+ protected:
+ const SelfType& Self() const { return *static_cast(this); }
+ SelfType& Self() { return *static_cast(this); }
+
+ const IterType& mIter;
+
+ uint32_t mIndex;
+};
+
+/*****************************************************************************
+ * Property iteration
+ *****************************************************************************/
+
+class MOZ_STACK_CLASS PropertyIter
+ : public BaseIter {
+ friend class PropertyIterElem;
+ friend class BaseIterElem;
+
+ public:
+ PropertyIter(JSContext* cx, JS::HandleObject object, void* context = nullptr)
+ : BaseIter(cx, object, context), mIds(cx, JS::IdVector(cx)) {
+ if (!JS_Enumerate(cx, object, &mIds)) {
+ JS_ClearPendingException(cx);
+ }
+ }
+
+ PropertyIter(const PropertyIter& other)
+ : PropertyIter(other.mCx, other.mObject, other.mContext) {}
+
+ PropertyIter& operator=(const PropertyIter& other) {
+ MOZ_ASSERT(other.mObject == mObject);
+ mCx = other.mCx;
+ mContext = other.mContext;
+
+ mIds.clear();
+ if (!JS_Enumerate(mCx, mObject, &mIds)) {
+ JS_ClearPendingException(mCx);
+ }
+ return *this;
+ }
+
+ int32_t Length() const { return mIds.length(); }
+
+ protected:
+ JS::Rooted mIds;
+};
+
+class MOZ_STACK_CLASS PropertyIterElem
+ : public BaseIterElem {
+ friend class BaseIterElem;
+
+ public:
+ using BaseIterElem::BaseIterElem;
+
+ PropertyIterElem(const PropertyIterElem& other)
+ : BaseIterElem(other.mIter, other.mIndex) {}
+
+ jsid Id() {
+ MOZ_ASSERT(mIndex < mIter.mIds.length());
+
+ return mIter.mIds[mIndex];
+ }
+
+ const nsAString& Name() {
+ if (mName.isNothing()) {
+ mName.emplace();
+ mName.ref().init(mIter.mCx, Id());
+ }
+ return mName.ref();
+ }
+
+ JSContext* Cx() { return mIter.mCx; }
+
+ protected:
+ bool GetValue(JS::MutableHandleValue value) {
+ MOZ_ASSERT(mIndex < Length());
+ JS::Rooted id(mIter.mCx, Id());
+
+ return JS_GetPropertyById(mIter.mCx, mIter.mObject, id, value);
+ }
+
+ private:
+ Maybe mName;
+};
+
+/*****************************************************************************
+ * Array iteration
+ *****************************************************************************/
+
+class MOZ_STACK_CLASS ArrayIter : public BaseIter {
+ friend class ArrayIterElem;
+ friend class BaseIterElem;
+
+ public:
+ ArrayIter(JSContext* cx, JS::HandleObject object)
+ : BaseIter(cx, object), mLength(0) {
+ bool isArray;
+ if (!JS::IsArrayObject(cx, object, &isArray) || !isArray) {
+ JS_ClearPendingException(cx);
+ return;
+ }
+
+ if (!JS::GetArrayLength(cx, object, &mLength)) {
+ JS_ClearPendingException(cx);
+ }
+ }
+
+ uint32_t Length() const { return mLength; }
+
+ private:
+ uint32_t mLength;
+};
+
+class MOZ_STACK_CLASS ArrayIterElem
+ : public BaseIterElem {
+ friend class BaseIterElem;
+
+ public:
+ using BaseIterElem::BaseIterElem;
+
+ ArrayIterElem(const ArrayIterElem& other)
+ : BaseIterElem(other.mIter, other.mIndex) {}
+
+ protected:
+ bool GetValue(JS::MutableHandleValue value) {
+ MOZ_ASSERT(mIndex < Length());
+ return JS_GetElement(mIter.mCx, mIter.mObject, mIndex, value);
+ }
+};
+
+} // namespace mozilla
+
+#endif // AddonManagerStartup_inlines_h
diff --git a/toolkit/mozapps/extensions/AddonManagerStartup.cpp b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
new file mode 100644
index 0000000000..04b8321938
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.cpp
@@ -0,0 +1,876 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AddonManagerStartup.h"
+#include "AddonManagerStartup-inlines.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/Array.h" // JS::IsArrayObject
+#include "js/ArrayBuffer.h"
+#include "js/JSON.h"
+#include "js/TracingAPI.h"
+#include "xpcpublic.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/Compression.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Services.h"
+#include "mozilla/URLPreloader.h"
+#include "mozilla/Unused.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/ipc/StructuredCloneData.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsAppRunner.h"
+#include "nsContentUtils.h"
+#include "nsChromeRegistry.h"
+#include "nsIAppStartup.h"
+#include "nsIDOMWindowUtils.h" // for nsIJSRAIIHelper
+#include "nsIFileURL.h"
+#include "nsIIOService.h"
+#include "nsIJARURI.h"
+#include "nsIStringEnumerator.h"
+#include "nsIZipReader.h"
+#include "nsJARProtocolHandler.h"
+#include "nsJSUtils.h"
+#include "nsReadableUtils.h"
+#include "nsXULAppAPI.h"
+
+#include
+
+namespace mozilla {
+
+using Compression::LZ4;
+using dom::ipc::StructuredCloneData;
+
+AddonManagerStartup& AddonManagerStartup::GetSingleton() {
+ static RefPtr singleton;
+ if (!singleton) {
+ singleton = new AddonManagerStartup();
+ ClearOnShutdown(&singleton);
+ }
+ return *singleton;
+}
+
+AddonManagerStartup::AddonManagerStartup() = default;
+
+nsIFile* AddonManagerStartup::ProfileDir() {
+ if (!mProfileDir) {
+ nsresult rv;
+
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mProfileDir));
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ return mProfileDir;
+}
+
+NS_IMPL_ISUPPORTS(AddonManagerStartup, amIAddonManagerStartup, nsIObserver)
+
+/*****************************************************************************
+ * URI utils
+ *****************************************************************************/
+
+static nsresult ParseJARURI(nsIJARURI* uri, nsIURI** jarFile,
+ nsCString& entry) {
+ MOZ_TRY(uri->GetJARFile(jarFile));
+ MOZ_TRY(uri->GetJAREntry(entry));
+
+ // The entry portion of a jar: URI is required to begin with a '/', but for
+ // nested JAR URIs, the leading / of the outer entry is currently stripped.
+ // This is a bug which should be fixed in the JAR URI code, but...
+ if (entry.IsEmpty() || entry[0] != '/') {
+ entry.Insert('/', 0);
+ }
+ return NS_OK;
+}
+
+static nsresult ParseJARURI(nsIURI* uri, nsIURI** jarFile, nsCString& entry) {
+ nsresult rv;
+ nsCOMPtr jarURI = do_QueryInterface(uri, &rv);
+ MOZ_TRY(rv);
+
+ return ParseJARURI(jarURI, jarFile, entry);
+}
+
+static Result, nsresult> GetFile(nsIURI* uri) {
+ nsresult rv;
+ nsCOMPtr fileURL = do_QueryInterface(uri, &rv);
+ MOZ_TRY(rv);
+
+ nsCOMPtr file;
+ MOZ_TRY(fileURL->GetFile(getter_AddRefs(file)));
+ MOZ_ASSERT(file);
+
+ return std::move(file);
+}
+
+/*****************************************************************************
+ * File utils
+ *****************************************************************************/
+
+static already_AddRefed CloneAndAppend(nsIFile* aFile,
+ const char* name) {
+ nsCOMPtr file;
+ aFile->Clone(getter_AddRefs(file));
+ file->AppendNative(nsDependentCString(name));
+ return file.forget();
+}
+
+static bool IsNormalFile(nsIFile* file) {
+ bool result;
+ return NS_SUCCEEDED(file->IsFile(&result)) && result;
+}
+
+static const char STRUCTURED_CLONE_MAGIC[] = "mozJSSCLz40v001";
+
+template
+static Result DecodeLZ4(const nsACString& lz4,
+ const T& magicNumber) {
+ constexpr auto HEADER_SIZE = sizeof(magicNumber) + 4;
+
+ // Note: We want to include the null terminator here.
+ nsDependentCSubstring magic(magicNumber, sizeof(magicNumber));
+
+ if (lz4.Length() < HEADER_SIZE || StringHead(lz4, magic.Length()) != magic) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ auto data = lz4.BeginReading() + magic.Length();
+ auto size = LittleEndian::readUint32(data);
+ data += 4;
+
+ size_t dataLen = lz4.EndReading() - data;
+ size_t outputSize;
+
+ nsCString result;
+ if (!result.SetLength(size, fallible) ||
+ !LZ4::decompress(data, dataLen, result.BeginWriting(), size,
+ &outputSize)) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(size == outputSize);
+
+ return std::move(result);
+}
+
+// Our zlib headers redefine this to MOZ_Z_compress, which breaks LZ4::compress
+#undef compress
+
+template
+static Result EncodeLZ4(const nsACString& data,
+ const T& magicNumber) {
+ // Note: We want to include the null terminator here.
+ nsDependentCSubstring magic(magicNumber, sizeof(magicNumber));
+
+ nsAutoCString result;
+ result.Append(magic);
+
+ auto off = result.Length();
+ if (!result.SetLength(off + 4, fallible)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ LittleEndian::writeUint32(result.BeginWriting() + off, data.Length());
+ off += 4;
+
+ auto size = LZ4::maxCompressedSize(data.Length());
+ if (!result.SetLength(off + size, fallible)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ size = LZ4::compress(data.BeginReading(), data.Length(),
+ result.BeginWriting() + off);
+
+ if (!result.SetLength(off + size, fallible)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ return std::move(result);
+}
+
+static_assert(sizeof STRUCTURED_CLONE_MAGIC % 8 == 0,
+ "Magic number should be an array of uint64_t");
+
+/**
+ * Reads the contents of a LZ4-compressed file, as stored by the OS.File
+ * module, and returns the decompressed contents on success.
+ */
+static Result ReadFileLZ4(nsIFile* file) {
+ static const char MAGIC_NUMBER[] = "mozLz40";
+
+ nsCString lz4;
+ MOZ_TRY_VAR(lz4, URLPreloader::ReadFile(file));
+
+ if (lz4.IsEmpty()) {
+ return lz4;
+ }
+
+ return DecodeLZ4(lz4, MAGIC_NUMBER);
+}
+
+static bool ParseJSON(JSContext* cx, nsACString& jsonData,
+ JS::MutableHandleValue result) {
+ NS_ConvertUTF8toUTF16 str(jsonData);
+ jsonData.Truncate();
+
+ return JS_ParseJSON(cx, str.Data(), str.Length(), result);
+}
+
+static Result, nsresult> GetJarCache() {
+ nsCOMPtr ios = services::GetIOService();
+ NS_ENSURE_TRUE(ios, Err(NS_ERROR_FAILURE));
+
+ nsCOMPtr jarProto;
+ MOZ_TRY(ios->GetProtocolHandler("jar", getter_AddRefs(jarProto)));
+
+ auto jar = static_cast(jarProto.get());
+ MOZ_ASSERT(jar);
+
+ nsCOMPtr zipCache = jar->JarCache();
+ return std::move(zipCache);
+}
+
+static Result GetFileLocation(nsIURI* uri) {
+ FileLocation location;
+
+ nsCOMPtr fileURL = do_QueryInterface(uri);
+ nsCOMPtr file;
+ if (fileURL) {
+ MOZ_TRY(fileURL->GetFile(getter_AddRefs(file)));
+ location.Init(file);
+ } else {
+ nsCOMPtr fileURI;
+ nsCString entry;
+ MOZ_TRY(ParseJARURI(uri, getter_AddRefs(fileURI), entry));
+
+ MOZ_TRY_VAR(file, GetFile(fileURI));
+
+ location.Init(file, entry.get());
+ }
+
+ return std::move(location);
+}
+
+/*****************************************************************************
+ * JSON data handling
+ *****************************************************************************/
+
+class MOZ_STACK_CLASS WrapperBase {
+ protected:
+ WrapperBase(JSContext* cx, JSObject* object) : mCx(cx), mObject(cx, object) {}
+
+ WrapperBase(JSContext* cx, const JS::Value& value) : mCx(cx), mObject(cx) {
+ if (value.isObject()) {
+ mObject = &value.toObject();
+ } else {
+ mObject = JS_NewPlainObject(cx);
+ }
+ }
+
+ protected:
+ JSContext* mCx;
+ JS::RootedObject mObject;
+
+ bool GetBool(const char* name, bool defVal = false);
+
+ double GetNumber(const char* name, double defVal = 0);
+
+ nsString GetString(const char* name, const char* defVal = "");
+
+ JSObject* GetObject(const char* name);
+};
+
+bool WrapperBase::GetBool(const char* name, bool defVal) {
+ JS::RootedObject obj(mCx, mObject);
+
+ JS::RootedValue val(mCx, JS::UndefinedValue());
+ if (!JS_GetProperty(mCx, obj, name, &val)) {
+ JS_ClearPendingException(mCx);
+ }
+
+ if (val.isBoolean()) {
+ return val.toBoolean();
+ }
+ return defVal;
+}
+
+double WrapperBase::GetNumber(const char* name, double defVal) {
+ JS::RootedObject obj(mCx, mObject);
+
+ JS::RootedValue val(mCx, JS::UndefinedValue());
+ if (!JS_GetProperty(mCx, obj, name, &val)) {
+ JS_ClearPendingException(mCx);
+ }
+
+ if (val.isNumber()) {
+ return val.toNumber();
+ }
+ return defVal;
+}
+
+nsString WrapperBase::GetString(const char* name, const char* defVal) {
+ JS::RootedObject obj(mCx, mObject);
+
+ JS::RootedValue val(mCx, JS::UndefinedValue());
+ if (!JS_GetProperty(mCx, obj, name, &val)) {
+ JS_ClearPendingException(mCx);
+ }
+
+ nsString res;
+ if (val.isString()) {
+ AssignJSString(mCx, res, val.toString());
+ } else {
+ res.AppendASCII(defVal);
+ }
+ return res;
+}
+
+JSObject* WrapperBase::GetObject(const char* name) {
+ JS::RootedObject obj(mCx, mObject);
+
+ JS::RootedValue val(mCx, JS::UndefinedValue());
+ if (!JS_GetProperty(mCx, obj, name, &val)) {
+ JS_ClearPendingException(mCx);
+ }
+
+ if (val.isObject()) {
+ return &val.toObject();
+ }
+ return nullptr;
+}
+
+class MOZ_STACK_CLASS InstallLocation : public WrapperBase {
+ public:
+ InstallLocation(JSContext* cx, const JS::Value& value);
+
+ MOZ_IMPLICIT InstallLocation(PropertyIterElem& iter)
+ : InstallLocation(iter.Cx(), iter.Value()) {}
+
+ InstallLocation(const InstallLocation& other)
+ : InstallLocation(other.mCx, JS::ObjectValue(*other.mObject)) {}
+
+ void SetChanged(bool changed) {
+ JS::RootedObject obj(mCx, mObject);
+
+ JS::RootedValue val(mCx, JS::BooleanValue(changed));
+ if (!JS_SetProperty(mCx, obj, "changed", val)) {
+ JS_ClearPendingException(mCx);
+ }
+ }
+
+ PropertyIter& Addons() { return mAddonsIter.ref(); }
+
+ nsString Path() { return GetString("path"); }
+
+ bool ShouldCheckStartupModifications() {
+ return GetBool("checkStartupModifications");
+ }
+
+ private:
+ JS::RootedObject mAddonsObj;
+ Maybe mAddonsIter;
+};
+
+class MOZ_STACK_CLASS Addon : public WrapperBase {
+ public:
+ Addon(JSContext* cx, InstallLocation& location, const nsAString& id,
+ JSObject* object)
+ : WrapperBase(cx, object), mId(id), mLocation(location) {}
+
+ MOZ_IMPLICIT Addon(PropertyIterElem& iter)
+ : WrapperBase(iter.Cx(), iter.Value()),
+ mId(iter.Name()),
+ mLocation(*static_cast(iter.Context())) {}
+
+ Addon(const Addon& other)
+ : WrapperBase(other.mCx, other.mObject),
+ mId(other.mId),
+ mLocation(other.mLocation) {}
+
+ const nsString& Id() { return mId; }
+
+ nsString Path() { return GetString("path"); }
+
+ nsString Type() { return GetString("type", "extension"); }
+
+ bool Enabled() { return GetBool("enabled"); }
+
+ double LastModifiedTime() { return GetNumber("lastModifiedTime"); }
+
+ bool ShouldCheckStartupModifications() {
+ return Type().EqualsLiteral("locale");
+ }
+
+ Result, nsresult> FullPath();
+
+ Result UpdateLastModifiedTime();
+
+ private:
+ nsString mId;
+ InstallLocation& mLocation;
+};
+
+Result, nsresult> Addon::FullPath() {
+ nsString path = Path();
+
+ // First check for an absolute path, in case we have a proxy file.
+ nsCOMPtr file;
+ if (NS_SUCCEEDED(NS_NewLocalFile(path, false, getter_AddRefs(file)))) {
+ return std::move(file);
+ }
+
+ // If not an absolute path, fall back to a relative path from the location.
+ MOZ_TRY(NS_NewLocalFile(mLocation.Path(), false, getter_AddRefs(file)));
+
+ MOZ_TRY(file->AppendRelativePath(path));
+ return std::move(file);
+}
+
+Result Addon::UpdateLastModifiedTime() {
+ nsCOMPtr file;
+ MOZ_TRY_VAR(file, FullPath());
+
+ JS::RootedObject obj(mCx, mObject);
+
+ bool result;
+ if (NS_FAILED(file->Exists(&result)) || !result) {
+ JS::RootedValue value(mCx, JS::NullValue());
+ if (!JS_SetProperty(mCx, obj, "currentModifiedTime", value)) {
+ JS_ClearPendingException(mCx);
+ }
+
+ return true;
+ }
+
+ PRTime time;
+
+ nsCOMPtr manifest = file;
+ if (!IsNormalFile(manifest)) {
+ manifest = CloneAndAppend(file, "manifest.json");
+ if (!IsNormalFile(manifest)) {
+ return true;
+ }
+ }
+
+ if (NS_FAILED(manifest->GetLastModifiedTime(&time))) {
+ return true;
+ }
+
+ double lastModified = time;
+ JS::RootedValue value(mCx, JS::NumberValue(lastModified));
+ if (!JS_SetProperty(mCx, obj, "currentModifiedTime", value)) {
+ JS_ClearPendingException(mCx);
+ }
+
+ return lastModified != LastModifiedTime();
+}
+
+InstallLocation::InstallLocation(JSContext* cx, const JS::Value& value)
+ : WrapperBase(cx, value), mAddonsObj(cx), mAddonsIter() {
+ mAddonsObj = GetObject("addons");
+ if (!mAddonsObj) {
+ mAddonsObj = JS_NewPlainObject(cx);
+ }
+ mAddonsIter.emplace(cx, mAddonsObj, this);
+}
+
+/*****************************************************************************
+ * XPC interfacing
+ *****************************************************************************/
+
+nsresult AddonManagerStartup::ReadStartupData(
+ JSContext* cx, JS::MutableHandleValue locations) {
+ locations.set(JS::UndefinedValue());
+
+ nsCOMPtr file =
+ CloneAndAppend(ProfileDir(), "addonStartup.json.lz4");
+
+ nsCString data;
+ auto res = ReadFileLZ4(file);
+ if (res.isOk()) {
+ data = res.unwrap();
+ } else if (res.inspectErr() != NS_ERROR_FILE_NOT_FOUND) {
+ return res.unwrapErr();
+ }
+
+ if (data.IsEmpty() || !ParseJSON(cx, data, locations)) {
+ return NS_OK;
+ }
+
+ if (!locations.isObject()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ JS::RootedObject locs(cx, &locations.toObject());
+ for (auto e1 : PropertyIter(cx, locs)) {
+ InstallLocation loc(e1);
+
+ bool shouldCheck = loc.ShouldCheckStartupModifications();
+
+ for (auto e2 : loc.Addons()) {
+ Addon addon(e2);
+
+ if (addon.Enabled() &&
+ (shouldCheck || addon.ShouldCheckStartupModifications())) {
+ bool changed;
+ MOZ_TRY_VAR(changed, addon.UpdateLastModifiedTime());
+ if (changed) {
+ loc.SetChanged(true);
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult AddonManagerStartup::EncodeBlob(JS::HandleValue value, JSContext* cx,
+ JS::MutableHandleValue result) {
+ StructuredCloneData holder;
+
+ ErrorResult rv;
+ holder.Write(cx, value, rv);
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+
+ nsAutoCString scData;
+
+ holder.Data().ForEachDataChunk([&](const char* aData, size_t aSize) {
+ scData.Append(nsDependentCSubstring(aData, aSize));
+ return true;
+ });
+
+ nsCString lz4;
+ MOZ_TRY_VAR(lz4, EncodeLZ4(scData, STRUCTURED_CLONE_MAGIC));
+
+ JS::RootedObject obj(cx);
+ MOZ_TRY(nsContentUtils::CreateArrayBuffer(cx, lz4, &obj.get()));
+
+ result.set(JS::ObjectValue(*obj));
+ return NS_OK;
+}
+
+nsresult AddonManagerStartup::DecodeBlob(JS::HandleValue value, JSContext* cx,
+ JS::MutableHandleValue result) {
+ NS_ENSURE_TRUE(value.isObject() &&
+ JS::IsArrayBufferObject(&value.toObject()) &&
+ JS::ArrayBufferHasData(&value.toObject()),
+ NS_ERROR_INVALID_ARG);
+
+ StructuredCloneData holder;
+
+ nsCString data;
+ {
+ JS::AutoCheckCannotGC nogc;
+
+ auto obj = &value.toObject();
+ bool isShared;
+
+ nsDependentCSubstring lz4(
+ reinterpret_cast(JS::GetArrayBufferData(obj, &isShared, nogc)),
+ JS::GetArrayBufferByteLength(obj));
+
+ MOZ_TRY_VAR(data, DecodeLZ4(lz4, STRUCTURED_CLONE_MAGIC));
+ }
+
+ bool ok = holder.CopyExternalData(data.get(), data.Length());
+ NS_ENSURE_TRUE(ok, NS_ERROR_OUT_OF_MEMORY);
+
+ ErrorResult rv;
+ holder.Read(cx, result, rv);
+ return rv.StealNSResult();
+ ;
+}
+
+static nsresult EnumerateZip(nsIZipReader* zip, const nsACString& pattern,
+ nsTArray& results) {
+ nsCOMPtr entries;
+ MOZ_TRY(zip->FindEntries(pattern, getter_AddRefs(entries)));
+
+ bool hasMore;
+ while (NS_SUCCEEDED(entries->HasMore(&hasMore)) && hasMore) {
+ nsAutoCString name;
+ MOZ_TRY(entries->GetNext(name));
+
+ results.AppendElement(NS_ConvertUTF8toUTF16(name));
+ }
+
+ return NS_OK;
+}
+
+nsresult AddonManagerStartup::EnumerateJAR(nsIURI* uri,
+ const nsACString& pattern,
+ nsTArray& results) {
+ nsCOMPtr zipCache;
+ MOZ_TRY_VAR(zipCache, GetJarCache());
+
+ nsCOMPtr zip;
+ nsCOMPtr file;
+ if (nsCOMPtr jarURI = do_QueryInterface(uri)) {
+ nsCOMPtr fileURI;
+ nsCString entry;
+ MOZ_TRY(ParseJARURI(jarURI, getter_AddRefs(fileURI), entry));
+
+ MOZ_TRY_VAR(file, GetFile(fileURI));
+ MOZ_TRY(
+ zipCache->GetInnerZip(file, Substring(entry, 1), getter_AddRefs(zip)));
+ } else {
+ MOZ_TRY_VAR(file, GetFile(uri));
+ MOZ_TRY(zipCache->GetZip(file, getter_AddRefs(zip)));
+ }
+ MOZ_ASSERT(zip);
+
+ return EnumerateZip(zip, pattern, results);
+}
+
+nsresult AddonManagerStartup::EnumerateJARSubtree(nsIURI* uri,
+ nsTArray& results) {
+ nsCOMPtr fileURI;
+ nsCString entry;
+ MOZ_TRY(ParseJARURI(uri, getter_AddRefs(fileURI), entry));
+
+ // Mangle the path into a pattern to match all child entries by escaping any
+ // existing pattern matching metacharacters it contains and appending "/*".
+ constexpr auto metaChars = "[]()?*~|$\\"_ns;
+
+ nsCString pattern;
+ pattern.SetCapacity(entry.Length());
+
+ // The first character of the entry name is "/", which we want to skip.
+ for (auto chr : Span(Substring(entry, 1))) {
+ if (metaChars.FindChar(chr) >= 0) {
+ pattern.Append('\\');
+ }
+ pattern.Append(chr);
+ }
+ if (!pattern.IsEmpty() && !StringEndsWith(pattern, "/"_ns)) {
+ pattern.Append('/');
+ }
+ pattern.Append('*');
+
+ return EnumerateJAR(fileURI, pattern, results);
+}
+
+nsresult AddonManagerStartup::InitializeURLPreloader() {
+ MOZ_RELEASE_ASSERT(xpc::IsInAutomation());
+
+ URLPreloader::ReInitialize();
+
+ return NS_OK;
+}
+
+/******************************************************************************
+ * RegisterChrome
+ ******************************************************************************/
+
+namespace {
+static bool sObserverRegistered;
+
+struct ContentEntry final {
+ explicit ContentEntry(nsTArray&& aArgs, uint8_t aFlags = 0)
+ : mArgs(std::move(aArgs)), mFlags(aFlags) {}
+
+ AutoTArray mArgs;
+ uint8_t mFlags;
+};
+
+}; // anonymous namespace
+}; // namespace mozilla
+
+MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::ContentEntry);
+
+namespace mozilla {
+namespace {
+
+class RegistryEntries final : public nsIJSRAIIHelper,
+ public LinkedListElement {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIJSRAIIHELPER
+
+ using Override = AutoTArray;
+ using Locale = AutoTArray;
+
+ RegistryEntries(FileLocation& location, nsTArray&& overrides,
+ nsTArray&& content, nsTArray&& locales)
+ : mLocation(location),
+ mOverrides(std::move(overrides)),
+ mContent(std::move(content)),
+ mLocales(std::move(locales)) {}
+
+ void Register();
+
+ protected:
+ virtual ~RegistryEntries() { Unused << Destruct(); }
+
+ private:
+ FileLocation mLocation;
+ const nsTArray mOverrides;
+ const nsTArray mContent;
+ const nsTArray mLocales;
+};
+
+NS_IMPL_ISUPPORTS(RegistryEntries, nsIJSRAIIHelper)
+
+void RegistryEntries::Register() {
+ RefPtr cr = nsChromeRegistry::GetSingleton();
+
+ nsChromeRegistry::ManifestProcessingContext context(NS_EXTENSION_LOCATION,
+ mLocation);
+
+ for (auto& override : mOverrides) {
+ const char* args[] = {override[0].get(), override[1].get()};
+ cr->ManifestOverride(context, 0, const_cast(args), 0);
+ }
+
+ for (auto& content : mContent) {
+ const char* args[] = {content.mArgs[0].get(), content.mArgs[1].get()};
+ cr->ManifestContent(context, 0, const_cast(args), content.mFlags);
+ }
+
+ for (auto& locale : mLocales) {
+ const char* args[] = {locale[0].get(), locale[1].get(), locale[2].get()};
+ cr->ManifestLocale(context, 0, const_cast(args), 0);
+ }
+}
+
+NS_IMETHODIMP
+RegistryEntries::Destruct() {
+ if (isInList()) {
+ remove();
+
+ // No point in doing I/O to check for new chrome during shutdown, return
+ // early in that case.
+ nsCOMPtr appStartup = components::AppStartup::Service();
+ if (!appStartup || appStartup->GetShuttingDown()) {
+ return NS_OK;
+ }
+
+ // When we remove dynamic entries from the registry, we need to rebuild it
+ // in order to ensure a consistent state. See comments in Observe().
+ RefPtr cr = nsChromeRegistry::GetSingleton();
+ return cr->CheckForNewChrome();
+ }
+ return NS_OK;
+}
+
+static LinkedList& GetRegistryEntries() {
+ static LinkedList sEntries;
+ return sEntries;
+}
+}; // anonymous namespace
+
+NS_IMETHODIMP
+AddonManagerStartup::RegisterChrome(nsIURI* manifestURI,
+ JS::HandleValue locations, JSContext* cx,
+ nsIJSRAIIHelper** result) {
+ auto IsArray = [cx](JS::HandleValue val) -> bool {
+ bool isArray;
+ return JS::IsArrayObject(cx, val, &isArray) && isArray;
+ };
+
+ NS_ENSURE_ARG_POINTER(manifestURI);
+ NS_ENSURE_TRUE(IsArray(locations), NS_ERROR_INVALID_ARG);
+
+ FileLocation location;
+ MOZ_TRY_VAR(location, GetFileLocation(manifestURI));
+
+ nsTArray locales;
+ nsTArray content;
+ nsTArray overrides;
+
+ JS::RootedObject locs(cx, &locations.toObject());
+ JS::RootedValue arrayVal(cx);
+ JS::RootedObject array(cx);
+
+ for (auto elem : ArrayIter(cx, locs)) {
+ arrayVal = elem.Value();
+ NS_ENSURE_TRUE(IsArray(arrayVal), NS_ERROR_INVALID_ARG);
+
+ array = &arrayVal.toObject();
+
+ AutoTArray vals;
+ for (auto val : ArrayIter(cx, array)) {
+ nsAutoJSString str;
+ NS_ENSURE_TRUE(str.init(cx, val.Value()), NS_ERROR_OUT_OF_MEMORY);
+
+ vals.AppendElement(NS_ConvertUTF16toUTF8(str));
+ }
+ NS_ENSURE_TRUE(vals.Length() > 0, NS_ERROR_INVALID_ARG);
+
+ nsCString type = vals[0];
+ vals.RemoveElementAt(0);
+
+ if (type.EqualsLiteral("override")) {
+ NS_ENSURE_TRUE(vals.Length() == 2, NS_ERROR_INVALID_ARG);
+ overrides.AppendElement(std::move(vals));
+ } else if (type.EqualsLiteral("content")) {
+ if (vals.Length() == 3 &&
+ vals[2].EqualsLiteral("contentaccessible=yes")) {
+ NS_ENSURE_TRUE(xpc::IsInAutomation(), NS_ERROR_INVALID_ARG);
+ vals.RemoveElementAt(2);
+ content.AppendElement(ContentEntry(
+ std::move(vals), nsChromeRegistry::CONTENT_ACCESSIBLE));
+ } else {
+ NS_ENSURE_TRUE(vals.Length() == 2, NS_ERROR_INVALID_ARG);
+ content.AppendElement(ContentEntry(std::move(vals)));
+ }
+ } else if (type.EqualsLiteral("locale")) {
+ NS_ENSURE_TRUE(vals.Length() == 3, NS_ERROR_INVALID_ARG);
+ locales.AppendElement(std::move(vals));
+ } else {
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ if (!sObserverRegistered) {
+ nsCOMPtr obs = services::GetObserverService();
+ NS_ENSURE_TRUE(obs, NS_ERROR_UNEXPECTED);
+ obs->AddObserver(this, "chrome-manifests-loaded", false);
+
+ sObserverRegistered = true;
+ }
+
+ auto entry = MakeRefPtr(
+ location, std::move(overrides), std::move(content), std::move(locales));
+
+ entry->Register();
+ GetRegistryEntries().insertBack(entry);
+
+ entry.forget(result);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AddonManagerStartup::Observe(nsISupports* subject, const char* topic,
+ const char16_t* data) {
+ // The chrome registry is maintained as a set of global resource mappings
+ // generated mainly from manifest files, on-the-fly, as they're parsed.
+ // Entries added later override entries added earlier, and no record is kept
+ // of the former state.
+ //
+ // As a result, if we remove a dynamically-added manifest file, or a set of
+ // dynamic entries, the registry needs to be rebuilt from scratch, from the
+ // manifests and dynamic entries that remain. The chrome registry itself
+ // takes care of re-parsing manifes files. This observer notification lets
+ // us know when we need to re-register our dynamic entries.
+ if (!strcmp(topic, "chrome-manifests-loaded")) {
+ for (auto entry : GetRegistryEntries()) {
+ entry->Register();
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/mozapps/extensions/AddonManagerStartup.h b/toolkit/mozapps/extensions/AddonManagerStartup.h
new file mode 100644
index 0000000000..a1cb5c9506
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerStartup.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AddonManagerStartup_h
+#define AddonManagerStartup_h
+
+#include "amIAddonManagerStartup.h"
+#include "mozilla/Result.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIObserver.h"
+#include "nsISupports.h"
+
+namespace mozilla {
+
+class Addon;
+
+class AddonManagerStartup final : public amIAddonManagerStartup,
+ public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_AMIADDONMANAGERSTARTUP
+ NS_DECL_NSIOBSERVER
+
+ AddonManagerStartup();
+
+ static AddonManagerStartup& GetSingleton();
+
+ static already_AddRefed GetInstance() {
+ RefPtr inst = &GetSingleton();
+ return inst.forget();
+ }
+
+ private:
+ nsIFile* ProfileDir();
+
+ nsCOMPtr mProfileDir;
+
+ protected:
+ virtual ~AddonManagerStartup() = default;
+};
+
+} // namespace mozilla
+
+#define NS_ADDONMANAGERSTARTUP_CONTRACTID \
+ "@mozilla.org/addons/addon-manager-startup;1"
+
+// {17a59a6b-92b8-42e5-bce0-ab434c7a7135
+#define NS_ADDON_MANAGER_STARTUP_CID \
+ { \
+ 0x17a59a6b, 0x92b8, 0x42e5, { \
+ 0xbc, 0xe0, 0xab, 0x43, 0x4c, 0x7a, 0x71, 0x35 \
+ } \
+ }
+
+#endif // AddonManagerStartup_h
diff --git a/toolkit/mozapps/extensions/AddonManagerWebAPI.cpp b/toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
new file mode 100644
index 0000000000..9d619bbd87
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AddonManagerWebAPI.h"
+
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/NavigatorBinding.h"
+
+#include "mozilla/Preferences.h"
+#include "nsGlobalWindow.h"
+#include "xpcpublic.h"
+
+#include "nsIDocShell.h"
+#include "nsIScriptObjectPrincipal.h"
+
+namespace mozilla {
+using namespace mozilla::dom;
+
+static bool IsValidHost(const nsACString& host) {
+ // This hidden pref allows users to disable mozAddonManager entirely if they
+ // want for fingerprinting resistance. Someone like Tor browser will use this
+ // pref.
+ if (Preferences::GetBool(
+ "privacy.resistFingerprinting.block_mozAddonManager")) {
+ return false;
+ }
+
+ if (host.EqualsLiteral("addons.mozilla.org")) {
+ return true;
+ }
+
+ // When testing allow access to the developer sites.
+ if (Preferences::GetBool("extensions.webapi.testing", false)) {
+ if (host.LowerCaseEqualsLiteral("addons.allizom.org") ||
+ host.LowerCaseEqualsLiteral("addons-dev.allizom.org") ||
+ host.LowerCaseEqualsLiteral("example.com")) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Checks if the given uri is secure and matches one of the hosts allowed to
+// access the API.
+bool AddonManagerWebAPI::IsValidSite(nsIURI* uri) {
+ if (!uri) {
+ return false;
+ }
+
+ if (!uri->SchemeIs("https")) {
+ if (!(xpc::IsInAutomation() &&
+ Preferences::GetBool("extensions.webapi.testing.http", false))) {
+ return false;
+ }
+ }
+
+ nsAutoCString host;
+ nsresult rv = uri->GetHost(host);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ return IsValidHost(host);
+}
+
+#ifndef ANDROID
+bool AddonManagerWebAPI::IsAPIEnabled(JSContext* aCx, JSObject* aGlobal) {
+ MOZ_DIAGNOSTIC_ASSERT(JS_IsGlobalObject(aGlobal));
+ nsCOMPtr win = xpc::WindowOrNull(aGlobal);
+ if (!win) {
+ return false;
+ }
+
+ // Check that the current window and all parent frames are allowed access to
+ // the API.
+ while (win) {
+ nsCOMPtr sop = do_QueryInterface(win);
+ if (!sop) {
+ return false;
+ }
+
+ nsCOMPtr principal = sop->GetPrincipal();
+ if (!principal) {
+ return false;
+ }
+
+ // Reaching a window with a system principal means we have reached
+ // privileged UI of some kind so stop at this point and allow access.
+ if (principal->IsSystemPrincipal()) {
+ return true;
+ }
+
+ nsCOMPtr docShell = win->GetDocShell();
+ if (!docShell) {
+ // This window has been torn down so don't allow access to the API.
+ return false;
+ }
+
+ if (!IsValidSite(win->GetDocumentURI())) {
+ return false;
+ }
+
+ // Checks whether there is a parent frame of the same type. This won't cross
+ // mozbrowser or chrome or fission/process boundaries.
+ nsCOMPtr parent;
+ nsresult rv = docShell->GetInProcessSameTypeParent(getter_AddRefs(parent));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // No parent means we've hit a mozbrowser or chrome or process boundary.
+ if (!parent) {
+ // With Fission, a cross-origin iframe has an out-of-process parent, but
+ // DocShell knows nothing about it. We need to ask BrowsingContext here,
+ // and only allow API access if AMO is actually at the top, not framed
+ // by evilleagueofevil.com.
+ return docShell->GetBrowsingContext()->IsTopContent();
+ }
+
+ Document* doc = win->GetDoc();
+ if (!doc) {
+ return false;
+ }
+
+ doc = doc->GetInProcessParentDocument();
+ if (!doc) {
+ // Getting here means something has been torn down so fail safe.
+ return false;
+ }
+
+ win = doc->GetInnerWindow();
+ }
+
+ // Found a document with no inner window, don't grant access to the API.
+ return false;
+}
+#else // We don't support mozAddonManager on Android
+bool AddonManagerWebAPI::IsAPIEnabled(JSContext* aCx, JSObject* aGlobal) {
+ return false;
+}
+#endif // ifndef ANDROID
+
+namespace dom {
+
+bool AddonManagerPermissions::IsHostPermitted(const GlobalObject& /*unused*/,
+ const nsAString& host) {
+ return IsValidHost(NS_ConvertUTF16toUTF8(host));
+}
+
+} // namespace dom
+
+} // namespace mozilla
diff --git a/toolkit/mozapps/extensions/AddonManagerWebAPI.h b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
new file mode 100644
index 0000000000..4f34b366a4
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef addonmanagerwebapi_h_
+#define addonmanagerwebapi_h_
+
+#include "nsPIDOMWindow.h"
+
+namespace mozilla {
+
+class AddonManagerWebAPI {
+ public:
+ static bool IsAPIEnabled(JSContext* aCx, JSObject* aGlobal);
+
+ static bool IsValidSite(nsIURI* uri);
+};
+
+namespace dom {
+
+class AddonManagerPermissions {
+ public:
+ static bool IsHostPermitted(const GlobalObject&, const nsAString& host);
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif // addonmanagerwebapi_h_
diff --git a/toolkit/mozapps/extensions/Blocklist.jsm b/toolkit/mozapps/extensions/Blocklist.jsm
new file mode 100644
index 0000000000..d8301ceb8b
--- /dev/null
+++ b/toolkit/mozapps/extensions/Blocklist.jsm
@@ -0,0 +1,1898 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* eslint "valid-jsdoc": [2, {requireReturn: false}] */
+
+var EXPORTED_SYMBOLS = ["Blocklist"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManager",
+ "resource://gre/modules/AddonManager.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "RemoteSettings",
+ "resource://services-settings/remote-settings.js"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "jexlFilterFunc",
+ "resource://services-settings/remote-settings.js"
+);
+
+const CascadeFilter = Components.Constructor(
+ "@mozilla.org/cascade-filter;1",
+ "nsICascadeFilter",
+ "setFilterData"
+);
+
+// The whole ID should be surrounded by literal ().
+// IDs may contain alphanumerics, _, -, {}, @ and a literal '.'
+// They may also contain backslashes (needed to escape the {} and dot)
+// We filter out backslash escape sequences (like `\w`) separately
+// (see kEscapeSequences).
+const kIdSubRegex =
+ "\\([" +
+ "\\\\" + // note: just a backslash, but between regex and string it needs escaping.
+ "\\w .{}@-]+\\)";
+
+// prettier-ignore
+// Find regular expressions of the form:
+// /^((id1)|(id2)|(id3)|...|(idN))$/
+// The outer set of parens enclosing the entire list of IDs is optional.
+const kIsMultipleIds = new RegExp(
+ // Start with literal sequence /^(
+ // (the `(` is optional)
+ "^/\\^\\(?" +
+ // Then at least one ID in parens ().
+ kIdSubRegex +
+ // Followed by any number of IDs in () separated by pipes.
+ // Note: using a non-capturing group because we don't care about the value.
+ "(?:\\|" + kIdSubRegex + ")*" +
+ // Finally, we need to end with literal sequence )$/
+ // (the leading `)` is optional like at the start)
+ "\\)?\\$/$"
+);
+
+// Check for a backslash followed by anything other than a literal . or curlies
+const kEscapeSequences = /\\[^.{}]/;
+
+// Used to remove the following 3 things:
+// leading literal /^(
+// plus an optional (
+// any backslash
+// trailing literal )$/
+// plus an optional ) before the )$/
+const kRegExpRemovalRegExp = /^\/\^\(\(?|\\|\)\)?\$\/$/g;
+
+// For a given input string matcher, produce either a string to compare with,
+// a regular expression, or a set of strings to compare with.
+function processMatcher(str) {
+ if (!str.startsWith("/")) {
+ return str;
+ }
+ // Process regexes matching multiple IDs into a set.
+ if (kIsMultipleIds.test(str) && !kEscapeSequences.test(str)) {
+ // Remove the regexp gunk at the start and end of the string, as well
+ // as all backslashes, and split by )|( to leave the list of IDs.
+ return new Set(str.replace(kRegExpRemovalRegExp, "").split(")|("));
+ }
+ let lastSlash = str.lastIndexOf("/");
+ let pattern = str.slice(1, lastSlash);
+ let flags = str.slice(lastSlash + 1);
+ return new RegExp(pattern, flags);
+}
+
+// Returns true if the addonProps object passes the constraints set by matches.
+// (For every non-null property in matches, the same key must exist in
+// addonProps and its value must match)
+function doesAddonEntryMatch(matches, addonProps) {
+ for (let [key, value] of Object.entries(matches)) {
+ if (value === null || value === undefined) {
+ continue;
+ }
+ if (addonProps[key]) {
+ // If this property matches (member of the set, matches regex, or
+ // an exact string match), continue to look at the other properties of
+ // the `matches` object.
+ // If not, return false immediately.
+ if (value.has && value.has(addonProps[key])) {
+ continue;
+ }
+ if (value.test && value.test(addonProps[key])) {
+ continue;
+ }
+ if (typeof value == "string" && value === addonProps[key]) {
+ continue;
+ }
+ }
+ // If we get here, the property doesn't match, so this entry doesn't match.
+ return false;
+ }
+ // If we get here, all the properties must have matched.
+ return true;
+}
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
+const PREF_BLOCKLIST_ADDONITEM_URL = "extensions.blocklist.addonItemURL";
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level";
+const PREF_BLOCKLIST_SUPPRESSUI = "extensions.blocklist.suppressUI";
+const PREF_BLOCKLIST_USE_MLBF = "extensions.blocklist.useMLBF";
+const PREF_BLOCKLIST_USE_MLBF_STASHES = "extensions.blocklist.useMLBF.stashes";
+const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
+const URI_BLOCKLIST_DIALOG =
+ "chrome://mozapps/content/extensions/blocklist.xhtml";
+const DEFAULT_SEVERITY = 3;
+const DEFAULT_LEVEL = 2;
+const MAX_BLOCK_LEVEL = 3;
+const SEVERITY_OUTDATED = 0;
+const VULNERABILITYSTATUS_NONE = 0;
+const VULNERABILITYSTATUS_UPDATE_AVAILABLE = 1;
+const VULNERABILITYSTATUS_NO_UPDATE = 2;
+
+// Remote Settings blocklist constants
+const PREF_BLOCKLIST_BUCKET = "services.blocklist.bucket";
+const PREF_BLOCKLIST_GFX_COLLECTION = "services.blocklist.gfx.collection";
+const PREF_BLOCKLIST_GFX_CHECKED_SECONDS = "services.blocklist.gfx.checked";
+const PREF_BLOCKLIST_GFX_SIGNER = "services.blocklist.gfx.signer";
+const PREF_BLOCKLIST_PLUGINS_COLLECTION =
+ "services.blocklist.plugins.collection";
+const PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS =
+ "services.blocklist.plugins.checked";
+const PREF_BLOCKLIST_PLUGINS_SIGNER = "services.blocklist.plugins.signer";
+// Blocklist v2 - legacy JSON format.
+const PREF_BLOCKLIST_ADDONS_COLLECTION = "services.blocklist.addons.collection";
+const PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS =
+ "services.blocklist.addons.checked";
+const PREF_BLOCKLIST_ADDONS_SIGNER = "services.blocklist.addons.signer";
+// Blocklist v3 - MLBF format.
+const PREF_BLOCKLIST_ADDONS3_COLLECTION =
+ "services.blocklist.addons-mlbf.collection";
+const PREF_BLOCKLIST_ADDONS3_CHECKED_SECONDS =
+ "services.blocklist.addons-mlbf.checked";
+const PREF_BLOCKLIST_ADDONS3_SIGNER = "services.blocklist.addons-mlbf.signer";
+
+const BlocklistTelemetry = {
+ /**
+ * Record the RemoteSettings Blocklist lastModified server time into the
+ * "blocklist.lastModified_rs keyed scalar (or "Missing Date" when unable
+ * to retrieve a valid timestamp).
+ *
+ * @param {string} blocklistType
+ * The blocklist type that has been updated (one of "addons" or "plugins",
+ * or "addons_mlbf";
+ * the "gfx" blocklist is not covered by this telemetry).
+ * @param {RemoteSettingsClient} remoteSettingsClient
+ * The RemoteSettings client to retrieve the lastModified timestamp from.
+ */
+ async recordRSBlocklistLastModified(blocklistType, remoteSettingsClient) {
+ // In some tests overrides ensureInitialized and remoteSettingsClient
+ // can be undefined, and in that case we don't want to record any
+ // telemetry scalar.
+ if (!remoteSettingsClient) {
+ return;
+ }
+
+ let lastModified = await remoteSettingsClient.getLastModified();
+ BlocklistTelemetry.recordTimeScalar(
+ "lastModified_rs_" + blocklistType,
+ lastModified
+ );
+ },
+
+ /**
+ * Record a timestamp in telemetry as a UTC string or "Missing Date" if the
+ * input is not a valid timestamp.
+ *
+ * @param {string} telemetryKey
+ * The part of after "blocklist.", as defined in Scalars.yaml.
+ * @param {number} time
+ * A timestamp to record. If invalid, "Missing Date" will be recorded.
+ */
+ recordTimeScalar(telemetryKey, time) {
+ if (time > 0) {
+ // convert from timestamp in ms into UTC datetime string, so it is going
+ // to be record in the same format previously used by blocklist.lastModified_xml.
+ let dateString = new Date(time).toUTCString();
+ Services.telemetry.scalarSet("blocklist." + telemetryKey, dateString);
+ } else {
+ Services.telemetry.scalarSet("blocklist." + telemetryKey, "Missing Date");
+ }
+ },
+};
+
+this.BlocklistTelemetry = BlocklistTelemetry;
+
+const Utils = {
+ /**
+ * Checks whether this entry is valid for the current OS and ABI.
+ * If the entry has an "os" property then the current OS must appear in
+ * its comma separated list for it to be valid. Similarly for the
+ * xpcomabi property.
+ *
+ * @param {Object} item
+ * The blocklist item.
+ * @returns {bool}
+ * Whether the entry matches the current OS.
+ */
+ matchesOSABI(item) {
+ if (item.os) {
+ let os = item.os.split(",");
+ if (!os.includes(gAppOS)) {
+ return false;
+ }
+ }
+
+ if (item.xpcomabi) {
+ let xpcomabi = item.xpcomabi.split(",");
+ if (!xpcomabi.includes(gApp.XPCOMABI)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Checks if a version is higher than or equal to the minVersion (if provided)
+ * and lower than or equal to the maxVersion (if provided).
+ * @param {string} version
+ * The version to test.
+ * @param {string?} minVersion
+ * The minimum version. If null it is assumed that version is always
+ * larger.
+ * @param {string?} maxVersion
+ * The maximum version. If null it is assumed that version is always
+ * smaller.
+ * @returns {boolean}
+ * Whether the item matches the range.
+ */
+ versionInRange(version, minVersion, maxVersion) {
+ if (minVersion && Services.vc.compare(version, minVersion) < 0) {
+ return false;
+ }
+ if (maxVersion && Services.vc.compare(version, maxVersion) > 0) {
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * Tests if this versionRange matches the item specified, and has a matching
+ * targetApplication id and version.
+ * @param {Object} versionRange
+ * The versionRange to check against
+ * @param {string} itemVersion
+ * The version of the actual addon/plugin to test for.
+ * @param {string} appVersion
+ * The version of the application to test for.
+ * @param {string} toolkitVersion
+ * The version of toolkit to check for.
+ * @returns {boolean}
+ * True if this version range covers the item and app/toolkit version given.
+ */
+ versionsMatch(versionRange, itemVersion, appVersion, toolkitVersion) {
+ // Some platforms have no version for plugins, these don't match if there
+ // was a min/maxVersion provided
+ if (!itemVersion && (versionRange.minVersion || versionRange.maxVersion)) {
+ return false;
+ }
+
+ // Check if the item version matches
+ if (
+ !this.versionInRange(
+ itemVersion,
+ versionRange.minVersion,
+ versionRange.maxVersion
+ )
+ ) {
+ return false;
+ }
+
+ // Check if the application or toolkit version matches
+ for (let tA of versionRange.targetApplication) {
+ if (
+ tA.guid == gAppID &&
+ this.versionInRange(appVersion, tA.minVersion, tA.maxVersion)
+ ) {
+ return true;
+ }
+ if (
+ tA.guid == TOOLKIT_ID &&
+ this.versionInRange(toolkitVersion, tA.minVersion, tA.maxVersion)
+ ) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Given a blocklist JS object entry, ensure it has a versionRange property, where
+ * each versionRange property has valid severity and vulnerabilityStatus properties,
+ * and at least 1 valid targetApplication.
+ * If it didn't have a valid targetApplication array before and/or it was empty,
+ * fill it with an entry with null min/maxVersion properties, which will match
+ * every version.
+ *
+ * If there *are* targetApplications, if any of them don't have a guid property,
+ * assign them the current app's guid.
+ *
+ * @param {Object} entry
+ * blocklist entry object.
+ */
+ ensureVersionRangeIsSane(entry) {
+ if (!entry.versionRange.length) {
+ entry.versionRange.push({});
+ }
+ for (let vr of entry.versionRange) {
+ if (!vr.hasOwnProperty("severity")) {
+ vr.severity = DEFAULT_SEVERITY;
+ }
+ if (!vr.hasOwnProperty("vulnerabilityStatus")) {
+ vr.vulnerabilityStatus = VULNERABILITYSTATUS_NONE;
+ }
+
+ if (!Array.isArray(vr.targetApplication)) {
+ vr.targetApplication = [];
+ }
+ if (!vr.targetApplication.length) {
+ vr.targetApplication.push({ minVersion: null, maxVersion: null });
+ }
+ vr.targetApplication.forEach(tA => {
+ if (!tA.guid) {
+ tA.guid = gAppID;
+ }
+ });
+ }
+ },
+
+ /**
+ * Create a blocklist URL for the given blockID
+ * @param {String} id the blockID to use
+ * @returns {String} the blocklist URL.
+ */
+ _createBlocklistURL(id) {
+ let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
+ return url.replace(/%blockID%/g, id);
+ },
+};
+
+/**
+ * This custom filter function is used to limit the entries returned
+ * by `RemoteSettings("...").get()` depending on the target app information
+ * defined on entries.
+ *
+ * Note that this is async because `jexlFilterFunc` is async.
+ *
+ * @param {Object} entry a Remote Settings record
+ * @param {Object} environment the JEXL environment object.
+ * @returns {Object} The entry if it matches, `null` otherwise.
+ */
+async function targetAppFilter(entry, environment) {
+ // If the entry has a JEXL filter expression, it should prevail.
+ // The legacy target app mechanism will be kept in place for old entries.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1463377
+ const { filter_expression } = entry;
+ if (filter_expression) {
+ return jexlFilterFunc(entry, environment);
+ }
+
+ // Keep entries without target information.
+ if (!("versionRange" in entry)) {
+ return entry;
+ }
+
+ const { versionRange } = entry;
+
+ // Everywhere in this method, we avoid checking the minVersion, because
+ // we want to retain items whose minVersion is higher than the current
+ // app version, so that we have the items around for app updates.
+
+ // Gfx blocklist has a specific versionRange object, which is not a list.
+ if (!Array.isArray(versionRange)) {
+ const { maxVersion = "*" } = versionRange;
+ const matchesRange = Services.vc.compare(gApp.version, maxVersion) <= 0;
+ return matchesRange ? entry : null;
+ }
+
+ // Iterate the targeted applications, at least one of them must match.
+ // If no target application, keep the entry.
+ if (!versionRange.length) {
+ return entry;
+ }
+ for (const vr of versionRange) {
+ const { targetApplication = [] } = vr;
+ if (!targetApplication.length) {
+ return entry;
+ }
+ for (const ta of targetApplication) {
+ const { guid } = ta;
+ if (!guid) {
+ return entry;
+ }
+ const { maxVersion = "*" } = ta;
+ if (
+ guid == gAppID &&
+ Services.vc.compare(gApp.version, maxVersion) <= 0
+ ) {
+ return entry;
+ }
+ if (
+ guid == "toolkit@mozilla.org" &&
+ Services.vc.compare(Services.appinfo.platformVersion, maxVersion) <= 0
+ ) {
+ return entry;
+ }
+ }
+ }
+ // Skip this entry.
+ return null;
+}
+
+/**
+ * The Graphics blocklist implementation. The JSON objects for graphics blocks look
+ * something like:
+ *
+ * {
+ * "blockID": "g35",
+ * "os": "WINNT 6.1",
+ * "vendor": "0xabcd",
+ * "devices": [
+ * "0x2783",
+ * "0x1234",
+ * ],
+ * "feature": " DIRECT2D ",
+ * "featureStatus": " BLOCKED_DRIVER_VERSION ",
+ * "driverVersion": " 8.52.322.2202 ",
+ * "driverVersionComparator": " LESS_THAN ",
+ * "versionRange": {"minVersion": "5.0", "maxVersion: "25.0"},
+ * }
+ *
+ * The RemoteSetttings client takes care of filtering out versions that don't apply.
+ * The code here stores entries in memory and sends them to the gfx component in
+ * serialized text form, using ',', '\t' and '\n' as separators.
+ *
+ * Note: we assign to the global to allow tests to reach the object directly.
+ */
+this.GfxBlocklistRS = {
+ _ensureInitialized() {
+ if (this._initialized || !gBlocklistEnabled) {
+ return;
+ }
+ this._initialized = true;
+ this._client = RemoteSettings(
+ Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_COLLECTION),
+ {
+ bucketNamePref: PREF_BLOCKLIST_BUCKET,
+ lastCheckTimePref: PREF_BLOCKLIST_GFX_CHECKED_SECONDS,
+ signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_GFX_SIGNER),
+ filterFunc: targetAppFilter,
+ }
+ );
+ this.checkForEntries = this.checkForEntries.bind(this);
+ this._client.on("sync", this.checkForEntries);
+ },
+
+ shutdown() {
+ if (this._client) {
+ this._client.off("sync", this.checkForEntries);
+ }
+ },
+
+ sync() {
+ this._ensureInitialized();
+ return this._client.sync();
+ },
+
+ async checkForEntries() {
+ this._ensureInitialized();
+ if (!gBlocklistEnabled) {
+ return []; // return value expected by tests.
+ }
+ let entries = await this._client.get().catch(ex => Cu.reportError(ex));
+ // Handle error silently. This can happen if our request to fetch data is aborted,
+ // e.g. by application shutdown.
+ if (!entries) {
+ return [];
+ }
+ // Trim helper (spaces, tabs, no-break spaces..)
+ const trim = s =>
+ (s || "").replace(/(^[\s\uFEFF\xA0]+)|([\s\uFEFF\xA0]+$)/g, "");
+
+ entries = entries.map(entry => {
+ let props = [
+ "blockID",
+ "driverVersion",
+ "driverVersionMax",
+ "driverVersionComparator",
+ "feature",
+ "featureStatus",
+ "os",
+ "vendor",
+ "devices",
+ ];
+ let rv = {};
+ for (let p of props) {
+ let val = entry[p];
+ // Ignore falsy values or empty arrays.
+ if (!val || (Array.isArray(val) && !val.length)) {
+ continue;
+ }
+ if (typeof val == "string") {
+ val = trim(val);
+ } else if (p == "devices") {
+ let invalidDevices = [];
+ let validDevices = [];
+ // We serialize the array of devices as a comma-separated string, so
+ // we need to ensure that none of the entries contain commas, also in
+ // the future.
+ val.forEach(v =>
+ v.includes(",") ? invalidDevices.push(v) : validDevices.push(v)
+ );
+ for (let dev of invalidDevices) {
+ const e = new Error(
+ `Block ${entry.blockID} contains unsupported device: ${dev}`
+ );
+ Cu.reportError(e);
+ }
+ if (!validDevices) {
+ continue;
+ }
+ val = validDevices;
+ }
+ rv[p] = val;
+ }
+ if (entry.versionRange) {
+ rv.versionRange = {
+ minVersion: trim(entry.versionRange.minVersion) || "0",
+ maxVersion: trim(entry.versionRange.maxVersion) || "*",
+ };
+ }
+ return rv;
+ });
+ if (entries.length) {
+ let sortedProps = [
+ "blockID",
+ "devices",
+ "driverVersion",
+ "driverVersionComparator",
+ "driverVersionMax",
+ "feature",
+ "featureStatus",
+ "hardware",
+ "manufacturer",
+ "model",
+ "os",
+ "osversion",
+ "product",
+ "vendor",
+ "versionRange",
+ ];
+ // Notify `GfxInfoBase`, by passing a string serialization.
+ let payload = [];
+ for (let gfxEntry of entries) {
+ let entryLines = [];
+ for (let key of sortedProps) {
+ if (gfxEntry[key]) {
+ let value = gfxEntry[key];
+ if (Array.isArray(value)) {
+ value = value.join(",");
+ } else if (value.maxVersion) {
+ // Both minVersion and maxVersion are always set on each entry.
+ value = value.minVersion + "," + value.maxVersion;
+ }
+ entryLines.push(key + ":" + value);
+ }
+ }
+ payload.push(entryLines.join("\t"));
+ }
+ Services.obs.notifyObservers(
+ null,
+ "blocklist-data-gfxItems",
+ payload.join("\n")
+ );
+ }
+ // The return value is only used by tests.
+ return entries;
+ },
+};
+
+/**
+ * The plugins blocklist implementation. The JSON objects for plugin blocks look
+ * something like:
+ *
+ * {
+ * "blockID":"p906",
+ * "details": {
+ * "bug":"https://bugzilla.mozilla.org/show_bug.cgi?id=1159917",
+ * "who":"Which users it affects",
+ * "why":"Why it's being blocklisted",
+ * "name":"Java Plugin 7 update 45 to 78 (click-to-play), Windows",
+ * "created":"2015-05-19T09:02:45Z"
+ * },
+ * "enabled":true,
+ * "infoURL":"https://java.com/",
+ * "matchName":"Java\\(TM\\) Platform SE 7 U(4[5-9]|(5|6)\\d|7[0-8])(\\s[^\\d\\._U]|$)",
+ * "versionRange":[
+ * {
+ * "severity":0,
+ * "targetApplication":[
+ * {
+ * "guid":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ * "maxVersion":"57.0.*",
+ * "minVersion":"0"
+ * }
+ * ],
+ * "vulnerabilityStatus":1
+ * }
+ * ],
+ * "matchFilename":"npjp2\\.dll",
+ * "id":"f254e5bc-12c7-7954-fe6b-8f1fdab0ae88",
+ * "last_modified":1519390914542,
+ * }
+ *
+ * Note: we assign to the global to allow tests to reach the object directly.
+ */
+this.PluginBlocklistRS = {
+ _matchProps: {
+ matchDescription: "description",
+ matchFilename: "filename",
+ matchName: "name",
+ },
+
+ async _ensureEntries() {
+ await this.ensureInitialized();
+ if (!this._entries && gBlocklistEnabled) {
+ await this._updateEntries();
+
+ // Dispatch to mainthread because consumers may try to construct nsIPluginHost
+ // again based on this notification, while we were called from nsIPluginHost
+ // anyway, leading to re-entrancy.
+ Services.tm.dispatchToMainThread(function() {
+ Services.obs.notifyObservers(null, "plugin-blocklist-loaded");
+ });
+ }
+ },
+
+ async _updateEntries() {
+ if (!gBlocklistEnabled) {
+ this._entries = [];
+ return;
+ }
+ this._entries = await this._client.get().catch(ex => Cu.reportError(ex));
+ // Handle error silently. This can happen if our request to fetch data is aborted,
+ // e.g. by application shutdown.
+ if (!this._entries) {
+ this._entries = [];
+ return;
+ }
+ this._entries.forEach(entry => {
+ entry.matches = {};
+ for (let k of Object.keys(this._matchProps)) {
+ if (entry[k]) {
+ try {
+ entry.matches[this._matchProps[k]] = new RegExp(entry[k], "m");
+ } catch (ex) {
+ /* Ignore invalid regexes */
+ }
+ }
+ }
+ Utils.ensureVersionRangeIsSane(entry);
+ });
+
+ BlocklistTelemetry.recordRSBlocklistLastModified("plugins", this._client);
+ },
+
+ async _filterItem(entry, environment) {
+ if (!(await targetAppFilter(entry, environment))) {
+ return null;
+ }
+ if (!Utils.matchesOSABI(entry)) {
+ return null;
+ }
+ if (!entry.matchFilename && !entry.matchName && !entry.matchDescription) {
+ let blockID = entry.blockID || entry.id;
+ Cu.reportError(new Error(`Nothing to filter plugin item ${blockID}`));
+ return null;
+ }
+ return entry;
+ },
+
+ sync() {
+ this.ensureInitialized();
+ return this._client.sync();
+ },
+
+ ensureInitialized() {
+ if (!gBlocklistEnabled || this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ this._client = RemoteSettings(
+ Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_COLLECTION),
+ {
+ bucketNamePref: PREF_BLOCKLIST_BUCKET,
+ lastCheckTimePref: PREF_BLOCKLIST_PLUGINS_CHECKED_SECONDS,
+ signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_PLUGINS_SIGNER),
+ filterFunc: this._filterItem,
+ }
+ );
+ this._onUpdate = this._onUpdate.bind(this);
+ this._client.on("sync", this._onUpdate);
+ },
+
+ shutdown() {
+ if (this._client) {
+ this._client.off("sync", this._onUpdate);
+ }
+ },
+
+ async _onUpdate() {
+ let oldEntries = this._entries || [];
+ this.ensureInitialized();
+ await this._updateEntries();
+ const pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(
+ Ci.nsIPluginHost
+ );
+ const plugins = pluginHost.getPluginTags();
+
+ let blockedItems = [];
+
+ for (let plugin of plugins) {
+ let oldState = this._getState(plugin, oldEntries);
+ let state = this._getState(plugin, this._entries);
+ LOG(
+ "Blocklist state for " +
+ plugin.name +
+ " changed from " +
+ oldState +
+ " to " +
+ state
+ );
+ // We don't want to re-warn about items
+ if (state == oldState) {
+ continue;
+ }
+
+ if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
+ plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ }
+ } else if (
+ !plugin.disabled &&
+ state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED
+ ) {
+ if (
+ state != Ci.nsIBlocklistService.STATE_OUTDATED &&
+ state != Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE &&
+ state != Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE
+ ) {
+ blockedItems.push({
+ name: plugin.name,
+ version: plugin.version,
+ icon: "chrome://mozapps/skin/plugins/plugin.svg",
+ disable: false,
+ blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+ item: plugin,
+ url: await this.getURL(plugin),
+ });
+ }
+ }
+ }
+
+ if (blockedItems.length) {
+ this._showBlockedPluginsPrompt(blockedItems);
+ } else {
+ this._notifyUpdate();
+ }
+ },
+
+ _showBlockedPluginsPrompt(blockedPlugins) {
+ let args = {
+ restart: false,
+ list: blockedPlugins,
+ };
+ // This lets the dialog get the raw js object
+ args.wrappedJSObject = args;
+
+ /*
+ Some tests run without UI, so the async code listens to a message
+ that can be sent programatically
+ */
+ let applyBlocklistChanges = async () => {
+ Services.obs.removeObserver(
+ applyBlocklistChanges,
+ "addon-blocklist-closed"
+ );
+
+ for (let blockedData of blockedPlugins) {
+ if (!blockedData.disable) {
+ continue;
+ }
+
+ // This will disable all the plugins immediately.
+ if (blockedData.item instanceof Ci.nsIPluginTag) {
+ blockedData.item.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ }
+ }
+
+ if (!args.restart) {
+ this._notifyUpdate();
+ return;
+ }
+
+ // We need to ensure the new blocklist state is written to disk before restarting.
+ // We'll notify about the blocklist update, then wait for nsIPluginHost
+ // to finish processing it, then restart the browser.
+ let pluginUpdatesFinishedPromise = new Promise(resolve => {
+ Services.obs.addObserver(function updatesFinished() {
+ Services.obs.removeObserver(
+ updatesFinished,
+ "plugin-blocklist-updates-finished"
+ );
+ resolve();
+ }, "plugin-blocklist-updates-finished");
+ });
+ this._notifyUpdate();
+ await pluginUpdatesFinishedPromise;
+
+ // Notify all windows that an application quit has been requested.
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+
+ // Something aborted the quit process.
+ if (cancelQuit.data) {
+ return;
+ }
+
+ Services.startup.quit(
+ Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit
+ );
+ };
+
+ Services.obs.addObserver(applyBlocklistChanges, "addon-blocklist-closed");
+
+ if (Services.prefs.getBoolPref(PREF_BLOCKLIST_SUPPRESSUI, false)) {
+ applyBlocklistChanges();
+ return;
+ }
+
+ function blocklistUnloadHandler(event) {
+ if (event.target.location == URI_BLOCKLIST_DIALOG) {
+ applyBlocklistChanges();
+ blocklistWindow.removeEventListener("unload", blocklistUnloadHandler);
+ }
+ }
+
+ let blocklistWindow = Services.ww.openWindow(
+ null,
+ URI_BLOCKLIST_DIALOG,
+ "",
+ "chrome,centerscreen,dialog,titlebar",
+ args
+ );
+ if (blocklistWindow) {
+ blocklistWindow.addEventListener("unload", blocklistUnloadHandler);
+ }
+ },
+
+ _notifyUpdate() {
+ Services.obs.notifyObservers(null, "plugin-blocklist-updated");
+ },
+
+ async getURL(plugin) {
+ await this._ensureEntries();
+ let r = this._getEntry(plugin, this._entries);
+ if (!r) {
+ return null;
+ }
+ let blockEntry = r.entry;
+ let blockID = blockEntry.blockID || blockEntry.id;
+ return blockEntry.infoURL || Utils._createBlocklistURL(blockID);
+ },
+
+ async getState(plugin, appVersion, toolkitVersion) {
+ if (AppConstants.platform == "android") {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+ await this._ensureEntries();
+ return this._getState(plugin, this._entries, appVersion, toolkitVersion);
+ },
+
+ /**
+ * Private helper to get the blocklist entry for a plugin given a set of
+ * blocklist entries and versions.
+ *
+ * @param {nsIPluginTag} plugin
+ * The nsIPluginTag to get the blocklist state for.
+ * @param {object[]} pluginEntries
+ * The plugin blocklist entries to compare against.
+ * @param {string?} appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param {string?} toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns {object?}
+ * {entry: blocklistEntry, version: blocklistEntryVersion},
+ * or null if there is no matching entry.
+ */
+ _getEntry(plugin, pluginEntries, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled) {
+ return null;
+ }
+
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!appVersion && !gApp.version) {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+
+ if (!appVersion) {
+ appVersion = gApp.version;
+ }
+ if (!toolkitVersion) {
+ toolkitVersion = gApp.platformVersion;
+ }
+
+ const pluginProperties = {
+ description: plugin.description,
+ filename: plugin.filename,
+ name: plugin.name,
+ version: plugin.version,
+ };
+ if (!pluginEntries) {
+ Cu.reportError(
+ new Error("There are no plugin entries. This should never happen.")
+ );
+ }
+ for (let blockEntry of pluginEntries) {
+ var matchFailed = false;
+ for (var name in blockEntry.matches) {
+ let pluginProperty = pluginProperties[name];
+ if (
+ typeof pluginProperty != "string" ||
+ !blockEntry.matches[name].test(pluginProperty)
+ ) {
+ matchFailed = true;
+ break;
+ }
+ }
+
+ if (matchFailed) {
+ continue;
+ }
+
+ for (let versionRange of blockEntry.versionRange) {
+ if (
+ Utils.versionsMatch(
+ versionRange,
+ pluginProperties.version,
+ appVersion,
+ toolkitVersion
+ )
+ ) {
+ return { entry: blockEntry, version: versionRange };
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Private version of getState that allows the caller to pass in
+ * the plugin blocklist entries.
+ *
+ * @param {nsIPluginTag} plugin
+ * The nsIPluginTag to get the blocklist state for.
+ * @param {object[]} pluginEntries
+ * The plugin blocklist entries to compare against.
+ * @param {string?} appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param {string?} toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns {integer}
+ * The blocklist state for the item, one of the STATE constants as
+ * defined in nsIBlocklistService.
+ */
+ _getState(plugin, pluginEntries, appVersion, toolkitVersion) {
+ let r = this._getEntry(plugin, pluginEntries, appVersion, toolkitVersion);
+ if (!r) {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+
+ let { version: versionRange } = r;
+
+ if (versionRange.severity >= gBlocklistLevel) {
+ return Ci.nsIBlocklistService.STATE_BLOCKED;
+ }
+ if (versionRange.severity == SEVERITY_OUTDATED) {
+ let vulnerabilityStatus = versionRange.vulnerabilityStatus;
+ if (vulnerabilityStatus == VULNERABILITYSTATUS_UPDATE_AVAILABLE) {
+ return Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE;
+ }
+ if (vulnerabilityStatus == VULNERABILITYSTATUS_NO_UPDATE) {
+ return Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE;
+ }
+ return Ci.nsIBlocklistService.STATE_OUTDATED;
+ }
+ return Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ },
+};
+
+/**
+ * The extensions blocklist implementation. The JSON objects for extension
+ * blocks look something like:
+ *
+ * {
+ * "guid": "someguid@addons.mozilla.org",
+ * "prefs": ["i.am.a.pref.that.needs.resetting"],
+ * "schema": 1480349193877,
+ * "blockID": "i12345",
+ * "details": {
+ * "bug": "https://bugzilla.mozilla.org/show_bug.cgi?id=1234567",
+ * "who": "All Firefox users who have this add-on installed. If you wish to continue using this add-on, you can enable it in the Add-ons Manager.",
+ * "why": "This add-on is in violation of the Add-on Guidelines, using multiple add-on IDs and potentially doing other unwanted activities.",
+ * "name": "Some pretty name",
+ * "created": "2019-05-06T19:52:20Z"
+ * },
+ * "enabled": true,
+ * "versionRange": [
+ * {
+ * "severity": 1,
+ * "maxVersion": "*",
+ * "minVersion": "0",
+ * "targetApplication": []
+ * }
+ * ],
+ * "id": "",
+ * "last_modified": 1480349215672,
+ * }
+ *
+ * This is a legacy format, and implements deprecated operations (bug 1620580).
+ * ExtensionBlocklistMLBF supersedes this implementation.
+ *
+ * Note: we assign to the global to allow tests to reach the object directly.
+ */
+this.ExtensionBlocklistRS = {
+ async _ensureEntries() {
+ this.ensureInitialized();
+ if (!this._entries && gBlocklistEnabled) {
+ await this._updateEntries();
+ }
+ },
+
+ async _updateEntries() {
+ if (!gBlocklistEnabled) {
+ this._entries = [];
+ return;
+ }
+ this._entries = await this._client.get().catch(ex => Cu.reportError(ex));
+ // Handle error silently. This can happen if our request to fetch data is aborted,
+ // e.g. by application shutdown.
+ if (!this._entries) {
+ this._entries = [];
+ return;
+ }
+ this._entries.forEach(entry => {
+ entry.matches = {};
+ if (entry.guid) {
+ entry.matches.id = processMatcher(entry.guid);
+ }
+ for (let key of EXTENSION_BLOCK_FILTERS) {
+ if (key == "id" || !entry[key]) {
+ continue;
+ }
+ entry.matches[key] = processMatcher(entry[key]);
+ }
+ Utils.ensureVersionRangeIsSane(entry);
+ });
+
+ BlocklistTelemetry.recordRSBlocklistLastModified("addons", this._client);
+ },
+
+ async _filterItem(entry, environment) {
+ if (!(await targetAppFilter(entry, environment))) {
+ return null;
+ }
+ if (!Utils.matchesOSABI(entry)) {
+ return null;
+ }
+ // Need something to filter on - at least a guid or name (either could be a regex):
+ if (!entry.guid && !entry.name) {
+ let blockID = entry.blockID || entry.id;
+ Cu.reportError(new Error(`Nothing to filter add-on item ${blockID} on`));
+ return null;
+ }
+ return entry;
+ },
+
+ sync() {
+ this.ensureInitialized();
+ return this._client.sync();
+ },
+
+ ensureInitialized() {
+ if (!gBlocklistEnabled || this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ this._client = RemoteSettings(
+ Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_COLLECTION),
+ {
+ bucketNamePref: PREF_BLOCKLIST_BUCKET,
+ lastCheckTimePref: PREF_BLOCKLIST_ADDONS_CHECKED_SECONDS,
+ signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS_SIGNER),
+ filterFunc: this._filterItem,
+ }
+ );
+ this._onUpdate = this._onUpdate.bind(this);
+ this._client.on("sync", this._onUpdate);
+ },
+
+ shutdown() {
+ if (this._client) {
+ this._client.off("sync", this._onUpdate);
+ this._didShutdown = true;
+ }
+ },
+
+ // Called when the blocklist implementation is changed via a pref.
+ undoShutdown() {
+ if (this._didShutdown) {
+ this._client.on("sync", this._onUpdate);
+ this._didShutdown = false;
+ }
+ },
+
+ async _onUpdate() {
+ let oldEntries = this._entries || [];
+ await this.ensureInitialized();
+ await this._updateEntries();
+
+ const types = ["extension", "theme", "locale", "dictionary", "service"];
+ let addons = await AddonManager.getAddonsByTypes(types);
+ for (let addon of addons) {
+ let oldState = addon.blocklistState;
+ if (addon.updateBlocklistState) {
+ await addon.updateBlocklistState(false);
+ } else if (oldEntries) {
+ let oldEntry = this._getEntry(addon, oldEntries);
+ oldState = oldEntry
+ ? oldEntry.state
+ : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ } else {
+ oldState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+ let state = addon.blocklistState;
+
+ LOG(
+ "Blocklist state for " +
+ addon.id +
+ " changed from " +
+ oldState +
+ " to " +
+ state
+ );
+
+ // We don't want to re-warn about add-ons
+ if (state == oldState) {
+ continue;
+ }
+
+ // Ensure that softDisabled is false if the add-on is not soft blocked
+ if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
+ await addon.setSoftDisabled(false);
+ }
+
+ // If an add-on has dropped from hard to soft blocked just mark it as
+ // soft disabled and don't warn about it.
+ if (
+ state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
+ oldState == Ci.nsIBlocklistService.STATE_BLOCKED
+ ) {
+ await addon.setSoftDisabled(true);
+ }
+
+ if (
+ state == Ci.nsIBlocklistService.STATE_BLOCKED ||
+ state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED
+ ) {
+ // Mark it as softblocked if necessary. Note that we avoid setting
+ // softDisabled at the same time as userDisabled to make it clear
+ // which was the original cause of the add-on becoming disabled in a
+ // way that the user can change.
+ if (
+ state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
+ !addon.userDisabled
+ ) {
+ await addon.setSoftDisabled(true);
+ }
+ // It's a block. We must reset certain preferences.
+ let entry = this._getEntry(addon, this._entries);
+ if (entry.prefs && entry.prefs.length) {
+ for (let pref of entry.prefs) {
+ Services.prefs.clearUserPref(pref);
+ }
+ }
+ }
+ }
+
+ AddonManagerPrivate.updateAddonAppDisabledStates();
+ },
+
+ async getState(addon, appVersion, toolkitVersion) {
+ let entry = await this.getEntry(addon, appVersion, toolkitVersion);
+ return entry ? entry.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ async getEntry(addon, appVersion, toolkitVersion) {
+ await this._ensureEntries();
+ return this._getEntry(addon, this._entries, appVersion, toolkitVersion);
+ },
+
+ _getEntry(addon, addonEntries, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled || !addon) {
+ return null;
+ }
+
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!appVersion && !gApp.version) {
+ return null;
+ }
+
+ if (!appVersion) {
+ appVersion = gApp.version;
+ }
+ if (!toolkitVersion) {
+ toolkitVersion = gApp.platformVersion;
+ }
+
+ let addonProps = {};
+ for (let key of EXTENSION_BLOCK_FILTERS) {
+ addonProps[key] = addon[key];
+ }
+ if (addonProps.creator) {
+ addonProps.creator = addonProps.creator.name;
+ }
+
+ for (let entry of addonEntries) {
+ // First check if it matches our properties. If not, just skip to the next item.
+ if (!doesAddonEntryMatch(entry.matches, addonProps)) {
+ continue;
+ }
+ // If those match, check the app or toolkit version works:
+ for (let versionRange of entry.versionRange) {
+ if (
+ Utils.versionsMatch(
+ versionRange,
+ addon.version,
+ appVersion,
+ toolkitVersion
+ )
+ ) {
+ let blockID = entry.blockID || entry.id;
+ return {
+ state:
+ versionRange.severity >= gBlocklistLevel
+ ? Ci.nsIBlocklistService.STATE_BLOCKED
+ : Ci.nsIBlocklistService.STATE_SOFTBLOCKED,
+ url: Utils._createBlocklistURL(blockID),
+ prefs: entry.prefs || [],
+ };
+ }
+ }
+ }
+ return null;
+ },
+};
+
+/**
+ * The extensions blocklist implementation, the third version.
+ *
+ * The current blocklist is represented by a multi-level bloom filter (MLBF)
+ * (aka "Cascade Bloom Filter") that works like a set, i.e. supports a has()
+ * operation, except it is probabilistic. The MLBF is 100% accurate for known
+ * entries and unreliable for unknown entries. When the backend generates the
+ * MLBF, all known add-ons are recorded, including their block state. Unknown
+ * add-ons are identified by their signature date being newer than the MLBF's
+ * generation time, and they are considered to not be blocked.
+ *
+ * Legacy blocklists used to distinguish between "soft block" and "hard block",
+ * but the current blocklist only supports one type of block ("hard block").
+ * After checking the blocklist states, any previous "soft blocked" addons will
+ * either be (hard) blocked or unblocked based on the blocklist.
+ *
+ * The MLBF is attached to a RemoteSettings record, as follows:
+ *
+ * {
+ * "generation_time": 1585692000000,
+ * "attachment": { ... RemoteSettings attachment ... }
+ * "attachment_type": "bloomfilter-base",
+ * }
+ *
+ * To update the blocklist, a replacement MLBF is published:
+ *
+ * {
+ * "generation_time": 1585692000000,
+ * "attachment": { ... RemoteSettings attachment ... }
+ * "attachment_type": "bloomfilter-full",
+ * }
+ *
+ * The collection can also contain stashes:
+ *
+ * {
+ * "stash_time": 1585692000001,
+ * "stash": {
+ * "blocked": [ "addonid:1.0", ... ],
+ * "unblocked": [ "addonid:1.0", ... ]
+ * }
+ *
+ * Stashes can be used to update the blocklist without forcing the whole MLBF
+ * to be downloaded again. These stashes are applied on top of the base MLBF.
+ * The use of stashes is currently optional, and toggled via the
+ * extensions.blocklist.useMLBF.stashes preference (true = use stashes).
+ *
+ * Note: we assign to the global to allow tests to reach the object directly.
+ */
+this.ExtensionBlocklistMLBF = {
+ RS_ATTACHMENT_ID: "addons-mlbf.bin",
+
+ async _fetchMLBF(record) {
+ // |record| may be unset. In that case, the MLBF dump is used instead
+ // (provided that the client has been built with it included).
+ let hash = record?.attachment.hash;
+ if (this._mlbfData && hash && this._mlbfData.cascadeHash === hash) {
+ // MLBF not changed, save the efforts of downloading the data again.
+
+ // Although the MLBF has not changed, the time in the record has. This
+ // means that the MLBF is known to provide accurate results for add-ons
+ // that were signed after the previously known date (but before the newly
+ // given date). To ensure that add-ons in this time range are also blocked
+ // as expected, update the cached generationTime.
+ if (record.generation_time > this._mlbfData.generationTime) {
+ this._mlbfData.generationTime = record.generation_time;
+ }
+ return this._mlbfData;
+ }
+ const {
+ buffer,
+ record: actualRecord,
+ } = await this._client.attachments.download(record, {
+ attachmentId: this.RS_ATTACHMENT_ID,
+ useCache: true,
+ fallbackToCache: true,
+ fallbackToDump: true,
+ });
+ return {
+ cascadeHash: actualRecord.attachment.hash,
+ cascadeFilter: new CascadeFilter(new Uint8Array(buffer)),
+ // Note: generation_time is semantically distinct from last_modified.
+ // generation_time is compared with the signing date of the add-on, so it
+ // should be in sync with the signing service's clock.
+ // In contrast, last_modified does not have such strong requirements.
+ generationTime: actualRecord.generation_time,
+ };
+ },
+
+ async _updateMLBF(forceUpdate = false) {
+ // The update process consists of fetching the collection, followed by
+ // potentially multiple network requests. As long as the collection has not
+ // been changed, repeated update requests can be coalesced. But when the
+ // collection has been updated, all pending update requests should await the
+ // new update request instead of the previous one.
+ if (!forceUpdate && this._updatePromise) {
+ return this._updatePromise;
+ }
+ const isUpdateReplaced = () => this._updatePromise != updatePromise;
+ const updatePromise = (async () => {
+ if (!gBlocklistEnabled) {
+ this._mlbfData = null;
+ this._stashes = null;
+ return;
+ }
+ let records = await this._client.get();
+ if (isUpdateReplaced()) {
+ return;
+ }
+
+ let mlbfRecords = records
+ .filter(r => r.attachment)
+ // Newest attachments first.
+ .sort((a, b) => b.generation_time - a.generation_time);
+ let mlbfRecord;
+ if (this.stashesEnabled) {
+ mlbfRecord = mlbfRecords.find(
+ r => r.attachment_type == "bloomfilter-base"
+ );
+ this._stashes = records
+ .filter(({ stash }) => {
+ return (
+ // Exclude non-stashes, e.g. MLBF attachments.
+ stash &&
+ // Sanity check for type.
+ Array.isArray(stash.blocked) &&
+ Array.isArray(stash.unblocked)
+ );
+ })
+ // Sort by stash time - newest first.
+ .sort((a, b) => b.stash_time - a.stash_time)
+ .map(({ stash, stash_time }) => ({
+ blocked: new Set(stash.blocked),
+ unblocked: new Set(stash.unblocked),
+ stash_time,
+ }));
+ } else {
+ mlbfRecord = mlbfRecords.find(
+ r =>
+ r.attachment_type == "bloomfilter-full" ||
+ r.attachment_type == "bloomfilter-base"
+ );
+ this._stashes = null;
+ }
+
+ let mlbf = await this._fetchMLBF(mlbfRecord);
+ // When a MLBF dump is packaged with the browser, mlbf will always be
+ // non-null at this point.
+ if (isUpdateReplaced()) {
+ return;
+ }
+ this._mlbfData = mlbf;
+ })()
+ .catch(e => {
+ Cu.reportError(e);
+ })
+ .then(() => {
+ if (!isUpdateReplaced()) {
+ this._updatePromise = null;
+ this._recordPostUpdateTelemetry();
+ }
+ return this._updatePromise;
+ });
+ this._updatePromise = updatePromise;
+ return updatePromise;
+ },
+
+ // Update the telemetry of the blocklist. This is always called, even if
+ // the update request failed (e.g. due to network errors or data corruption).
+ _recordPostUpdateTelemetry() {
+ BlocklistTelemetry.recordRSBlocklistLastModified(
+ "addons_mlbf",
+ this._client
+ );
+ BlocklistTelemetry.recordTimeScalar(
+ "mlbf_generation_time",
+ this._mlbfData?.generationTime
+ );
+ // stashes has conveniently already been sorted by stash_time, newest first.
+ let stashes = this._stashes || [];
+ BlocklistTelemetry.recordTimeScalar(
+ "mlbf_stash_time_oldest",
+ stashes[stashes.length - 1]?.stash_time
+ );
+ BlocklistTelemetry.recordTimeScalar(
+ "mlbf_stash_time_newest",
+ stashes[0]?.stash_time
+ );
+ },
+
+ ensureInitialized() {
+ if (!gBlocklistEnabled || this._initialized) {
+ return;
+ }
+ this._initialized = true;
+ this._client = RemoteSettings(
+ Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS3_COLLECTION),
+ {
+ bucketNamePref: PREF_BLOCKLIST_BUCKET,
+ lastCheckTimePref: PREF_BLOCKLIST_ADDONS3_CHECKED_SECONDS,
+ signerName: Services.prefs.getCharPref(PREF_BLOCKLIST_ADDONS3_SIGNER),
+ }
+ );
+ this._onUpdate = this._onUpdate.bind(this);
+ this._client.on("sync", this._onUpdate);
+ this.stashesEnabled = Services.prefs.getBoolPref(
+ PREF_BLOCKLIST_USE_MLBF_STASHES,
+ false
+ );
+ Services.telemetry.scalarSet("blocklist.mlbf_stashes", this.stashesEnabled);
+ },
+
+ shutdown() {
+ if (this._client) {
+ this._client.off("sync", this._onUpdate);
+ this._didShutdown = true;
+ }
+ },
+
+ // Called when the blocklist implementation is changed via a pref.
+ undoShutdown() {
+ if (this._didShutdown) {
+ this._client.on("sync", this._onUpdate);
+ this._didShutdown = false;
+ }
+ },
+
+ async _onUpdate() {
+ this.ensureInitialized();
+ await this._updateMLBF(true);
+
+ // Check add-ons from XPIProvider.
+ const types = ["extension", "theme", "locale", "dictionary"];
+ let addons = await AddonManager.getAddonsByTypes(types);
+ for (let addon of addons) {
+ let oldState = addon.blocklistState;
+ await addon.updateBlocklistState(false);
+ let state = addon.blocklistState;
+
+ LOG(
+ "Blocklist state for " +
+ addon.id +
+ " changed from " +
+ oldState +
+ " to " +
+ state
+ );
+
+ // We don't want to re-warn about add-ons
+ if (state == oldState) {
+ continue;
+ }
+
+ // Ensure that softDisabled is false if the add-on is not soft blocked
+ // (by a previous implementation of the blocklist).
+ if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED) {
+ await addon.setSoftDisabled(false);
+ }
+ }
+
+ AddonManagerPrivate.updateAddonAppDisabledStates();
+ },
+
+ async getState(addon) {
+ let state = await this.getEntry(addon);
+ return state ? state.state : Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ async getEntry(addon) {
+ if (!this._mlbfData) {
+ this.ensureInitialized();
+ await this._updateMLBF(false);
+ }
+
+ let blockKey = addon.id + ":" + addon.version;
+
+ if (this._stashes) {
+ // Stashes are ordered by newest first.
+ for (let stash of this._stashes) {
+ // blocked and unblocked do not have overlapping entries.
+ if (stash.blocked.has(blockKey)) {
+ return this._createBlockEntry(addon);
+ }
+ if (stash.unblocked.has(blockKey)) {
+ return null;
+ }
+ }
+ }
+
+ // signedDate is a Date if the add-on is signed, null if not signed,
+ // undefined if it's an addon update descriptor instead of an addon wrapper.
+ let { signedDate } = addon;
+ if (!signedDate) {
+ // The MLBF does not apply to unsigned add-ons.
+ return null;
+ }
+
+ if (!this._mlbfData) {
+ // This could happen in theory in any of the following cases:
+ // - the blocklist is disabled.
+ // - The RemoteSettings backend served a malformed MLBF.
+ // - The RemoteSettings backend is unreachable, and this client was built
+ // without including a dump of the MLBF.
+ //
+ // ... in other words, this shouldn't happen in practice.
+ return null;
+ }
+ let { cascadeFilter, generationTime } = this._mlbfData;
+ if (!cascadeFilter.has(blockKey)) {
+ // Add-on not blocked or unknown.
+ return null;
+ }
+ // Add-on blocked, or unknown add-on inadvertently labeled as blocked.
+
+ let { signedState } = addon;
+ if (
+ signedState !== AddonManager.SIGNEDSTATE_PRELIMINARY &&
+ signedState !== AddonManager.SIGNEDSTATE_SIGNED
+ ) {
+ // The block decision can only be relied upon for known add-ons, i.e.
+ // signed via AMO. Anything else is unknown and ignored:
+ //
+ // - SIGNEDSTATE_SYSTEM and SIGNEDSTATE_PRIVILEGED are signed
+ // independently of AMO.
+ //
+ // - SIGNEDSTATE_NOT_REQUIRED already has an early return above due to
+ // signedDate being unset for these kinds of add-ons.
+ //
+ // - SIGNEDSTATE_BROKEN, SIGNEDSTATE_UNKNOWN and SIGNEDSTATE_MISSING
+ // means that the signature cannot be relied upon. It is equivalent to
+ // removing the signature from the XPI file, which already causes them
+ // to be disabled on release builds (where MOZ_REQUIRE_SIGNING=true).
+ return null;
+ }
+
+ if (signedDate.getTime() > generationTime) {
+ // The bloom filter only reports 100% accurate results for known add-ons.
+ // Since the add-on was unknown when the bloom filter was generated, the
+ // block decision is incorrect and should be treated as unblocked.
+ return null;
+ }
+
+ if (AppConstants.NIGHTLY_BUILD && addon.type === "locale") {
+ // Only Mozilla can create langpacks with a valid signature.
+ // Langpacks for Release, Beta and ESR are submitted to AMO.
+ // DevEd does not support external langpacks (bug 1563923), only builtins.
+ // (and built-in addons are not subjected to the blocklist).
+ // Langpacks for Nightly are not known to AMO, so the MLBF cannot be used.
+ return null;
+ }
+
+ return this._createBlockEntry(addon);
+ },
+
+ _createBlockEntry(addon) {
+ return {
+ state: Ci.nsIBlocklistService.STATE_BLOCKED,
+ url: this.createBlocklistURL(addon.id, addon.version),
+ };
+ },
+
+ createBlocklistURL(id, version) {
+ let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ADDONITEM_URL);
+ return url.replace(/%addonID%/g, id).replace(/%addonVersion%/g, version);
+ },
+};
+
+const EXTENSION_BLOCK_FILTERS = [
+ "id",
+ "name",
+ "creator",
+ "homepageURL",
+ "updateURL",
+];
+
+var gLoggingEnabled = null;
+var gBlocklistEnabled = true;
+var gBlocklistLevel = DEFAULT_LEVEL;
+
+/**
+ * @class nsIBlocklistPrompt
+ *
+ * nsIBlocklistPrompt is used, if available, by the default implementation of
+ * nsIBlocklistService to display a confirmation UI to the user before blocking
+ * extensions/plugins.
+ */
+/**
+ * @method prompt
+ *
+ * Prompt the user about newly blocked addons. The prompt is then resposible
+ * for soft-blocking any addons that need to be afterwards
+ *
+ * @param {object[]} aAddons
+ * An array of addons and plugins that are blocked. These are javascript
+ * objects with properties:
+ * name - the plugin or extension name,
+ * version - the version of the extension or plugin,
+ * icon - the plugin or extension icon,
+ * disable - can be used by the nsIBlocklistPrompt to allows users to decide
+ * whether a soft-blocked add-on should be disabled,
+ * blocked - true if the item is hard-blocked, false otherwise,
+ * item - the nsIPluginTag or Addon object
+ */
+
+// From appinfo in Services.jsm. It is not possible to use the one in
+// Services.jsm since it will not successfully QueryInterface nsIXULAppInfo in
+// xpcshell tests due to other code calling Services.appinfo before the
+// nsIXULAppInfo is created by the tests.
+XPCOMUtils.defineLazyGetter(this, "gApp", function() {
+ // eslint-disable-next-line mozilla/use-services
+ let appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+ try {
+ appinfo.QueryInterface(Ci.nsIXULAppInfo);
+ } catch (ex) {
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (
+ !(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_NOINTERFACE
+ ) {
+ throw ex;
+ }
+ }
+ return appinfo;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gAppID", function() {
+ return gApp.ID;
+});
+XPCOMUtils.defineLazyGetter(this, "gAppOS", function() {
+ return gApp.OS;
+});
+
+/**
+ * Logs a string to the error console.
+ * @param {string} string
+ * The string to write to the error console..
+ */
+function LOG(string) {
+ if (gLoggingEnabled) {
+ dump("*** " + string + "\n");
+ Services.console.logStringMessage(string);
+ }
+}
+
+let Blocklist = {
+ _init() {
+ Services.obs.addObserver(this, "xpcom-shutdown");
+ gLoggingEnabled = Services.prefs.getBoolPref(
+ PREF_EM_LOGGING_ENABLED,
+ false
+ );
+ gBlocklistEnabled = Services.prefs.getBoolPref(
+ PREF_BLOCKLIST_ENABLED,
+ true
+ );
+ gBlocklistLevel = Math.min(
+ Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
+ MAX_BLOCK_LEVEL
+ );
+ this._chooseExtensionBlocklistImplementationFromPref();
+ Services.prefs.addObserver("extensions.blocklist.", this);
+ Services.prefs.addObserver(PREF_EM_LOGGING_ENABLED, this);
+
+ // If the stub blocklist service deferred any queries because we
+ // weren't loaded yet, execute them now.
+ for (let entry of Services.blocklist.pluginQueries.splice(0)) {
+ entry.resolve(
+ this.getPluginBlocklistState(
+ entry.plugin,
+ entry.appVersion,
+ entry.toolkitVersion
+ )
+ );
+ }
+ },
+ isLoaded: true,
+
+ shutdown() {
+ GfxBlocklistRS.shutdown();
+ PluginBlocklistRS.shutdown();
+ this.ExtensionBlocklist.shutdown();
+
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.prefs.removeObserver("extensions.blocklist.", this);
+ Services.prefs.removeObserver(PREF_EM_LOGGING_ENABLED, this);
+ },
+
+ observe(subject, topic, prefName) {
+ switch (topic) {
+ case "xpcom-shutdown":
+ this.shutdown();
+ break;
+ case "nsPref:changed":
+ switch (prefName) {
+ case PREF_EM_LOGGING_ENABLED:
+ gLoggingEnabled = Services.prefs.getBoolPref(
+ PREF_EM_LOGGING_ENABLED,
+ false
+ );
+ break;
+ case PREF_BLOCKLIST_ENABLED:
+ gBlocklistEnabled = Services.prefs.getBoolPref(
+ PREF_BLOCKLIST_ENABLED,
+ true
+ );
+ this._blocklistUpdated();
+ break;
+ case PREF_BLOCKLIST_LEVEL:
+ gBlocklistLevel = Math.min(
+ Services.prefs.getIntPref(PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
+ MAX_BLOCK_LEVEL
+ );
+ this._blocklistUpdated();
+ break;
+ case PREF_BLOCKLIST_USE_MLBF:
+ let oldImpl = this.ExtensionBlocklist;
+ this._chooseExtensionBlocklistImplementationFromPref();
+ if (oldImpl._initialized) {
+ oldImpl.shutdown();
+ this.ExtensionBlocklist.undoShutdown();
+ this.ExtensionBlocklist._onUpdate();
+ } // else neither has been initialized yet. Wait for it to happen.
+ break;
+ case PREF_BLOCKLIST_USE_MLBF_STASHES:
+ ExtensionBlocklistMLBF.stashesEnabled = Services.prefs.getBoolPref(
+ PREF_BLOCKLIST_USE_MLBF_STASHES,
+ false
+ );
+ if (
+ ExtensionBlocklistMLBF._initialized &&
+ !ExtensionBlocklistMLBF._didShutdown
+ ) {
+ Services.telemetry.scalarSet(
+ "blocklist.mlbf_stashes",
+ ExtensionBlocklistMLBF.stashesEnabled
+ );
+ ExtensionBlocklistMLBF._onUpdate();
+ }
+ break;
+ }
+ break;
+ }
+ },
+
+ loadBlocklistAsync() {
+ // Need to ensure we notify gfx of new stuff.
+ GfxBlocklistRS.checkForEntries();
+ this.ExtensionBlocklist.ensureInitialized();
+ PluginBlocklistRS.ensureInitialized();
+ },
+
+ getPluginBlocklistState(plugin, appVersion, toolkitVersion) {
+ return PluginBlocklistRS.getState(plugin, appVersion, toolkitVersion);
+ },
+
+ getPluginBlockURL(plugin) {
+ return PluginBlocklistRS.getURL(plugin);
+ },
+
+ getAddonBlocklistState(addon, appVersion, toolkitVersion) {
+ // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS.
+ return this.ExtensionBlocklist.getState(addon, appVersion, toolkitVersion);
+ },
+
+ getAddonBlocklistEntry(addon, appVersion, toolkitVersion) {
+ // NOTE: appVersion/toolkitVersion are only used by ExtensionBlocklistRS.
+ return this.ExtensionBlocklist.getEntry(addon, appVersion, toolkitVersion);
+ },
+
+ _chooseExtensionBlocklistImplementationFromPref() {
+ if (Services.prefs.getBoolPref(PREF_BLOCKLIST_USE_MLBF, false)) {
+ this.ExtensionBlocklist = ExtensionBlocklistMLBF;
+ Services.telemetry.scalarSet("blocklist.mlbf_enabled", true);
+ } else {
+ this.ExtensionBlocklist = ExtensionBlocklistRS;
+ Services.telemetry.scalarSet("blocklist.mlbf_enabled", false);
+ }
+ },
+
+ _blocklistUpdated() {
+ this.ExtensionBlocklist._onUpdate();
+ PluginBlocklistRS._onUpdate();
+ },
+};
+
+Blocklist._init();
diff --git a/toolkit/mozapps/extensions/LightweightThemeManager.jsm b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
new file mode 100644
index 0000000000..78c2135e61
--- /dev/null
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["LightweightThemeManager"];
+
+// Holds optional fallback theme data that will be returned when no data for an
+// active theme can be found. This the case for WebExtension Themes, for example.
+var _fallbackThemeData = null;
+
+var LightweightThemeManager = {
+ set fallbackThemeData(data) {
+ if (data && Object.getOwnPropertyNames(data).length) {
+ _fallbackThemeData = Object.assign({}, data);
+ } else {
+ _fallbackThemeData = null;
+ }
+ },
+
+ /*
+ * Returns the currently active theme, taking the fallback theme into account
+ * if we'd be using the default theme otherwise.
+ *
+ * This will always return the original theme data and not make use of
+ * locally persisted resources.
+ */
+ get currentThemeWithFallback() {
+ return _fallbackThemeData && _fallbackThemeData.theme;
+ },
+
+ get themeData() {
+ return _fallbackThemeData || { theme: null };
+ },
+};
diff --git a/toolkit/mozapps/extensions/addonManager.js b/toolkit/mozapps/extensions/addonManager.js
new file mode 100644
index 0000000000..ca82283b18
--- /dev/null
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -0,0 +1,409 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This component serves as integration between the platform and AddonManager.
+ * It is responsible for initializing and shutting down the AddonManager as well
+ * as passing new installs from webpages to the AddonManager.
+ */
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "separatePrivilegedMozillaWebContentProcess",
+ "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "extensionsWebAPITesting",
+ "extensions.webapi.testing",
+ false
+);
+
+// The old XPInstall error codes
+const EXECUTION_ERROR = -203;
+const CANT_READ_ARCHIVE = -207;
+const USER_CANCELLED = -210;
+const DOWNLOAD_ERROR = -228;
+const UNSUPPORTED_TYPE = -244;
+const SUCCESS = 0;
+
+const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled";
+const MSG_INSTALL_ADDON = "WebInstallerInstallAddonFromWebpage";
+const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
+
+const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
+const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
+const MSG_INSTALL_EVENT = "WebAPIInstallEvent";
+const MSG_INSTALL_CLEANUP = "WebAPICleanup";
+const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest";
+const MSG_ADDON_EVENT = "WebAPIAddonEvent";
+
+const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var gSingleton = null;
+
+var AddonManager, AddonManagerPrivate;
+function amManager() {
+ ({ AddonManager, AddonManagerPrivate } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+ ));
+
+ Services.mm.loadFrameScript(CHILD_SCRIPT, true, true);
+ Services.mm.addMessageListener(MSG_INSTALL_ENABLED, this);
+ Services.mm.addMessageListener(MSG_PROMISE_REQUEST, this);
+ Services.mm.addMessageListener(MSG_INSTALL_CLEANUP, this);
+ Services.mm.addMessageListener(MSG_ADDON_EVENT_REQ, this);
+
+ Services.ppmm.addMessageListener(MSG_INSTALL_ADDON, this);
+
+ Services.obs.addObserver(this, "message-manager-close");
+ Services.obs.addObserver(this, "message-manager-disconnect");
+
+ AddonManager.webAPI.setEventHandler(this.sendEvent);
+
+ // Needed so receiveMessage can be called directly by JS callers
+ this.wrappedJSObject = this;
+}
+
+amManager.prototype = {
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "addons-startup":
+ AddonManagerPrivate.startup();
+ break;
+
+ case "message-manager-close":
+ case "message-manager-disconnect":
+ this.childClosed(aSubject);
+ break;
+ }
+ },
+
+ installAddonFromWebpage(aPayload, aBrowser, aCallback) {
+ let retval = true;
+
+ const { mimetype, triggeringPrincipal, hash, icon, name, uri } = aPayload;
+
+ if (!AddonManager.isInstallAllowed(mimetype, triggeringPrincipal)) {
+ aCallback = null;
+ retval = false;
+ }
+
+ let telemetryInfo = {
+ source: AddonManager.getInstallSourceFromHost(aPayload.sourceHost),
+ sourceURL: aPayload.sourceURL,
+ };
+
+ if ("method" in aPayload) {
+ telemetryInfo.method = aPayload.method;
+ }
+
+ AddonManager.getInstallForURL(uri, {
+ hash,
+ name,
+ icon,
+ browser: aBrowser,
+ triggeringPrincipal,
+ telemetryInfo,
+ sendCookies: true,
+ }).then(aInstall => {
+ function callCallback(status) {
+ try {
+ aCallback.onInstallEnded(uri, status);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ if (!aInstall) {
+ aCallback.onInstallEnded(uri, UNSUPPORTED_TYPE);
+ return;
+ }
+
+ if (aCallback) {
+ aInstall.addListener({
+ onDownloadCancelled(aInstall) {
+ callCallback(USER_CANCELLED);
+ },
+
+ onDownloadFailed(aInstall) {
+ if (aInstall.error == AddonManager.ERROR_CORRUPT_FILE) {
+ callCallback(CANT_READ_ARCHIVE);
+ } else {
+ callCallback(DOWNLOAD_ERROR);
+ }
+ },
+
+ onInstallFailed(aInstall) {
+ callCallback(EXECUTION_ERROR);
+ },
+
+ onInstallEnded(aInstall, aStatus) {
+ callCallback(SUCCESS);
+ },
+ });
+ }
+
+ AddonManager.installAddonFromWebpage(
+ mimetype,
+ aBrowser,
+ triggeringPrincipal,
+ aInstall
+ );
+ });
+
+ return retval;
+ },
+
+ notify(aTimer) {
+ AddonManagerPrivate.backgroundUpdateTimerHandler();
+ },
+
+ // Maps message manager instances for content processes to the associated
+ // AddonListener instances.
+ addonListeners: new Map(),
+
+ _addAddonListener(target) {
+ if (!this.addonListeners.has(target)) {
+ let handler = (event, id) => {
+ target.sendAsyncMessage(MSG_ADDON_EVENT, { event, id });
+ };
+ let listener = {
+ onEnabling: addon => handler("onEnabling", addon.id),
+ onEnabled: addon => handler("onEnabled", addon.id),
+ onDisabling: addon => handler("onDisabling", addon.id),
+ onDisabled: addon => handler("onDisabled", addon.id),
+ onInstalling: addon => handler("onInstalling", addon.id),
+ onInstalled: addon => handler("onInstalled", addon.id),
+ onUninstalling: addon => handler("onUninstalling", addon.id),
+ onUninstalled: addon => handler("onUninstalled", addon.id),
+ onOperationCancelled: addon =>
+ handler("onOperationCancelled", addon.id),
+ };
+ this.addonListeners.set(target, listener);
+ AddonManager.addAddonListener(listener);
+ }
+ },
+
+ _removeAddonListener(target) {
+ if (this.addonListeners.has(target)) {
+ AddonManager.removeAddonListener(this.addonListeners.get(target));
+ this.addonListeners.delete(target);
+ }
+ },
+
+ /**
+ * messageManager callback function.
+ *
+ * Listens to requests from child processes for InstallTrigger
+ * activity, and sends back callbacks.
+ */
+ receiveMessage(aMessage) {
+ let payload = aMessage.data;
+
+ switch (aMessage.name) {
+ case MSG_INSTALL_ENABLED:
+ return AddonManager.isInstallEnabled(payload.mimetype);
+
+ case MSG_INSTALL_ADDON: {
+ let browser = payload.browsingContext.top.embedderElement;
+
+ let callback = null;
+ if (payload.callbackID != -1) {
+ let mm = browser.messageManager;
+ callback = {
+ onInstallEnded(url, status) {
+ mm.sendAsyncMessage(MSG_INSTALL_CALLBACK, {
+ callbackID: payload.callbackID,
+ url,
+ status,
+ });
+ },
+ };
+ }
+
+ return this.installAddonFromWebpage(payload, browser, callback);
+ }
+
+ case MSG_PROMISE_REQUEST: {
+ if (
+ !extensionsWebAPITesting &&
+ separatePrivilegedMozillaWebContentProcess &&
+ aMessage.target &&
+ aMessage.target.remoteType != null &&
+ aMessage.target.remoteType !== "privilegedmozilla"
+ ) {
+ return undefined;
+ }
+
+ let mm = aMessage.target.messageManager;
+ let resolve = value => {
+ mm.sendAsyncMessage(MSG_PROMISE_RESULT, {
+ callbackID: payload.callbackID,
+ resolve: value,
+ });
+ };
+ let reject = value => {
+ mm.sendAsyncMessage(MSG_PROMISE_RESULT, {
+ callbackID: payload.callbackID,
+ reject: value,
+ });
+ };
+
+ let API = AddonManager.webAPI;
+ if (payload.type in API) {
+ API[payload.type](aMessage.target, ...payload.args).then(
+ resolve,
+ reject
+ );
+ } else {
+ reject("Unknown Add-on API request.");
+ }
+ break;
+ }
+
+ case MSG_INSTALL_CLEANUP: {
+ if (
+ !extensionsWebAPITesting &&
+ separatePrivilegedMozillaWebContentProcess &&
+ aMessage.target &&
+ aMessage.target.remoteType != null &&
+ aMessage.target.remoteType !== "privilegedmozilla"
+ ) {
+ return undefined;
+ }
+
+ AddonManager.webAPI.clearInstalls(payload.ids);
+ break;
+ }
+
+ case MSG_ADDON_EVENT_REQ: {
+ if (
+ !extensionsWebAPITesting &&
+ separatePrivilegedMozillaWebContentProcess &&
+ aMessage.target &&
+ aMessage.target.remoteType != null &&
+ aMessage.target.remoteType !== "privilegedmozilla"
+ ) {
+ return undefined;
+ }
+
+ let target = aMessage.target.messageManager;
+ if (payload.enabled) {
+ this._addAddonListener(target);
+ } else {
+ this._removeAddonListener(target);
+ }
+ }
+ }
+ return undefined;
+ },
+
+ childClosed(target) {
+ AddonManager.webAPI.clearInstallsFrom(target);
+ this._removeAddonListener(target);
+ },
+
+ sendEvent(mm, data) {
+ mm.sendAsyncMessage(MSG_INSTALL_EVENT, data);
+ },
+
+ classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"),
+ _xpcom_factory: {
+ createInstance(aOuter, aIid) {
+ if (aOuter != null) {
+ throw Components.Exception(
+ "Component does not support aggregation",
+ Cr.NS_ERROR_NO_AGGREGATION
+ );
+ }
+
+ if (!gSingleton) {
+ gSingleton = new amManager();
+ }
+ return gSingleton.QueryInterface(aIid);
+ },
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "amIAddonManager",
+ "nsITimerCallback",
+ "nsIObserver",
+ ]),
+};
+
+const BLOCKLIST_JSM = "resource://gre/modules/Blocklist.jsm";
+ChromeUtils.defineModuleGetter(this, "Blocklist", BLOCKLIST_JSM);
+
+function BlocklistService() {
+ this.wrappedJSObject = this;
+ this.pluginQueries = [];
+}
+
+BlocklistService.prototype = {
+ STATE_NOT_BLOCKED: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ STATE_SOFTBLOCKED: Ci.nsIBlocklistService.STATE_SOFTBLOCKED,
+ STATE_BLOCKED: Ci.nsIBlocklistService.STATE_BLOCKED,
+ STATE_OUTDATED: Ci.nsIBlocklistService.STATE_OUTDATED,
+ STATE_VULNERABLE_UPDATE_AVAILABLE:
+ Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE,
+ STATE_VULNERABLE_NO_UPDATE: Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE,
+
+ get isLoaded() {
+ return Cu.isModuleLoaded(BLOCKLIST_JSM) && Blocklist.isLoaded;
+ },
+
+ async getPluginBlocklistState(plugin, appVersion, toolkitVersion) {
+ if (AppConstants.platform == "android") {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+ if (Cu.isModuleLoaded(BLOCKLIST_JSM)) {
+ return Blocklist.getPluginBlocklistState(
+ plugin,
+ appVersion,
+ toolkitVersion
+ );
+ }
+
+ // Blocklist module isn't loaded yet. Queue the query until it is.
+ let request = { plugin, appVersion, toolkitVersion };
+ let promise = new Promise(resolve => {
+ request.resolve = resolve;
+ });
+
+ this.pluginQueries.push(request);
+ return promise;
+ },
+
+ observe(...args) {
+ return Blocklist.observe(...args);
+ },
+
+ notify() {
+ Blocklist.notify();
+ },
+
+ classID: Components.ID("{66354bc9-7ed1-4692-ae1d-8da97d6b205e}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIBlocklistService",
+ "nsITimerCallback",
+ ]),
+};
+
+// eslint-disable-next-line no-unused-vars
+var EXPORTED_SYMBOLS = ["amManager", "BlocklistService"];
diff --git a/toolkit/mozapps/extensions/amContentHandler.jsm b/toolkit/mozapps/extensions/amContentHandler.jsm
new file mode 100644
index 0000000000..b655409850
--- /dev/null
+++ b/toolkit/mozapps/extensions/amContentHandler.jsm
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const XPI_CONTENT_TYPE = "application/x-xpinstall";
+const MSG_INSTALL_ADDON = "WebInstallerInstallAddonFromWebpage";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+function amContentHandler() {}
+
+amContentHandler.prototype = {
+ /**
+ * Handles a new request for an application/x-xpinstall file.
+ *
+ * @param aMimetype
+ * The mimetype of the file
+ * @param aContext
+ * The context passed to nsIChannel.asyncOpen
+ * @param aRequest
+ * The nsIRequest dealing with the content
+ */
+ handleContent(aMimetype, aContext, aRequest) {
+ if (aMimetype != XPI_CONTENT_TYPE) {
+ throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT);
+ }
+
+ if (!(aRequest instanceof Ci.nsIChannel)) {
+ throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT);
+ }
+
+ let uri = aRequest.URI;
+
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+
+ let { loadInfo } = aRequest;
+ const { triggeringPrincipal } = loadInfo;
+ let browsingContext = loadInfo.targetBrowsingContext;
+
+ let sourceHost;
+ let sourceURL;
+
+ try {
+ sourceURL =
+ triggeringPrincipal.spec != "" ? triggeringPrincipal.spec : undefined;
+ sourceHost = triggeringPrincipal.host;
+ } catch (error) {
+ // Ignore errors when retrieving the host for the principal (e.g. data URIs return
+ // an NS_ERROR_FAILURE when principal.host is accessed).
+ }
+
+ let install = {
+ uri: uri.spec,
+ hash: null,
+ name: null,
+ icon: null,
+ mimetype: XPI_CONTENT_TYPE,
+ triggeringPrincipal,
+ callbackID: -1,
+ method: "link",
+ sourceHost,
+ sourceURL,
+ browsingContext,
+ };
+
+ Services.cpmm.sendAsyncMessage(MSG_INSTALL_ADDON, install);
+ },
+
+ classID: Components.ID("{7beb3ba8-6ec3-41b4-b67c-da89b8518922}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIContentHandler"]),
+
+ log(aMsg) {
+ let msg = "amContentHandler.js: " + (aMsg.join ? aMsg.join("") : aMsg);
+ Services.console.logStringMessage(msg);
+ dump(msg + "\n");
+ },
+};
+
+var EXPORTED_SYMBOLS = ["amContentHandler"];
diff --git a/toolkit/mozapps/extensions/amIAddonManagerStartup.idl b/toolkit/mozapps/extensions/amIAddonManagerStartup.idl
new file mode 100644
index 0000000000..779b9f4cfe
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIAddonManagerStartup.idl
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIJSRAIIHelper;
+interface nsIURI;
+
+[scriptable, builtinclass, uuid(01dfa47b-87e4-4135-877b-586d033e1b5d)]
+interface amIAddonManagerStartup : nsISupports
+{
+ /**
+ * Reads and parses startup data from the addonState.json.lz4 file, checks
+ * for modifications, and returns the result.
+ *
+ * Returns null for an empty or nonexistent state file, but throws for an
+ * invalid one.
+ */
+ [implicit_jscontext]
+ jsval readStartupData();
+
+ /**
+ * Registers a set of dynamic chrome registry entries, and returns an object
+ * with a `destruct()` method which must be called in order to unregister
+ * the entries.
+ *
+ * @param manifestURI The base manifest URI for the entries. URL values are
+ * resolved relative to this URI.
+ * @param entries An array of arrays, each containing a registry entry as it
+ * would appar in a chrome.manifest file. Only the following entry
+ * types are currently accepted:
+ *
+ * - "locale" A locale package entry. Must be a 4-element array.
+ * - "override" A URL override entry. Must be a 3-element array.
+ */
+ [implicit_jscontext]
+ nsIJSRAIIHelper registerChrome(in nsIURI manifestURI, in jsval entries);
+
+ [implicit_jscontext]
+ jsval encodeBlob(in jsval value);
+
+ [implicit_jscontext]
+ jsval decodeBlob(in jsval value);
+
+ /**
+ * Enumerates over all entries in the JAR file at the given URI, and returns
+ * an array of entry paths which match the given pattern. The URI may be
+ * either a file: URL pointing directly to a zip file, or a jar: URI
+ * pointing to a zip file nested within another zip file. Only one level of
+ * nesting is supported.
+ *
+ * This should be used in preference to manually opening or retrieving a
+ * ZipReader from the zip cache, since the former causes main thread IO and
+ * the latter can lead to file locking issues due to unpredictable GC behavior
+ * keeping the cached ZipReader alive after the cache is flushed.
+ *
+ * @param uri The URI of the zip file to enumerate.
+ * @param pattern The pattern to match, as passed to nsIZipReader.findEntries.
+ */
+ Array enumerateJAR(in nsIURI uri, in AUTF8String pattern);
+
+ /**
+ * Similar to |enumerateJAR| above, but accepts the URI of a directory
+ * within a JAR file, and returns a list of all entries below it.
+ *
+ * The given URI must be a jar: URI, and its JAR file must point either to a
+ * file: URI, or to a singly-nested JAR within another JAR file (i.e.,
+ * "jar:file:///thing.jar!/" or "jar:jar:file:///thing.jar!/stuff.jar!/").
+ * Multiple levels of nesting are not supported.
+ */
+ Array enumerateJARSubtree(in nsIURI uri);
+
+ /**
+ * Initializes the URL Preloader.
+ *
+ * NOT FOR USE OUTSIDE OF UNIT TESTS.
+ */
+ void initializeURLPreloader();
+
+};
+
diff --git a/toolkit/mozapps/extensions/amIWebInstallPrompt.idl b/toolkit/mozapps/extensions/amIWebInstallPrompt.idl
new file mode 100644
index 0000000000..6724303cba
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIWebInstallPrompt.idl
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIVariant;
+
+webidl Element;
+
+/**
+ * amIWebInstallPrompt is used, if available, by the default implementation of
+ * amIWebInstallInfo to display a confirmation UI to the user before running
+ * installs.
+ */
+[scriptable, uuid(386906f1-4d18-45bf-bc81-5dcd68e42c3b)]
+interface amIWebInstallPrompt : nsISupports
+{
+ /**
+ * Get a confirmation that the user wants to start the installs.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were requested
+ */
+ void confirm(in Element aBrowser, in nsIURI aUri,
+ in Array aInstalls);
+};
diff --git a/toolkit/mozapps/extensions/amInstallTrigger.jsm b/toolkit/mozapps/extensions/amInstallTrigger.jsm
new file mode 100644
index 0000000000..8acfa65ab3
--- /dev/null
+++ b/toolkit/mozapps/extensions/amInstallTrigger.jsm
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { Preferences } = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm"
+);
+const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+
+const XPINSTALL_MIMETYPE = "application/x-xpinstall";
+
+const MSG_INSTALL_ENABLED = "WebInstallerIsInstallEnabled";
+const MSG_INSTALL_ADDON = "WebInstallerInstallAddonFromWebpage";
+const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
+
+var log = Log.repository.getLogger("AddonManager.InstallTrigger");
+log.level =
+ Log.Level[
+ Preferences.get("extensions.logging.enabled", false) ? "Warn" : "Trace"
+ ];
+
+function CallbackObject(id, callback, mediator) {
+ this.id = id;
+ this.callback = callback;
+ this.callCallback = function(url, status) {
+ try {
+ this.callback(url, status);
+ } catch (e) {
+ log.warn("InstallTrigger callback threw an exception: " + e);
+ }
+
+ mediator._callbacks.delete(id);
+ };
+}
+
+function RemoteMediator(window) {
+ this._windowID = window.windowGlobalChild.innerWindowId;
+
+ this.mm = window.docShell.messageManager;
+ this.mm.addWeakMessageListener(MSG_INSTALL_CALLBACK, this);
+
+ this._lastCallbackID = 0;
+ this._callbacks = new Map();
+}
+
+RemoteMediator.prototype = {
+ receiveMessage(message) {
+ if (message.name == MSG_INSTALL_CALLBACK) {
+ let payload = message.data;
+ let callbackHandler = this._callbacks.get(payload.callbackID);
+ if (callbackHandler) {
+ callbackHandler.callCallback(payload.url, payload.status);
+ }
+ }
+ },
+
+ enabled(url) {
+ let params = {
+ mimetype: XPINSTALL_MIMETYPE,
+ };
+ return this.mm.sendSyncMessage(MSG_INSTALL_ENABLED, params)[0];
+ },
+
+ install(install, principal, callback, window) {
+ let callbackID = this._addCallback(callback);
+
+ install.mimetype = XPINSTALL_MIMETYPE;
+ install.triggeringPrincipal = principal;
+ install.callbackID = callbackID;
+ install.browsingContext = BrowsingContext.getFromWindow(window);
+
+ return Services.cpmm.sendSyncMessage(MSG_INSTALL_ADDON, install)[0];
+ },
+
+ _addCallback(callback) {
+ if (!callback || typeof callback != "function") {
+ return -1;
+ }
+
+ let callbackID = this._windowID + "-" + ++this._lastCallbackID;
+ let callbackObject = new CallbackObject(callbackID, callback, this);
+ this._callbacks.set(callbackID, callbackObject);
+ return callbackID;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+};
+
+function InstallTrigger() {}
+
+InstallTrigger.prototype = {
+ // We've declared ourselves as providing the nsIDOMGlobalPropertyInitializer
+ // interface. This means that when the InstallTrigger property is gotten from
+ // the window that will createInstance this object and then call init(),
+ // passing the window were bound to. It will then automatically create the
+ // WebIDL wrapper (InstallTriggerImpl) for this object. This indirection is
+ // necessary because webidl does not (yet) support statics (bug 863952). See
+ // bug 926712 and then bug 1442360 for more details about this implementation.
+ init(window) {
+ this._window = window;
+ this._principal = window.document.nodePrincipal;
+ this._url = window.document.documentURIObject;
+
+ this._mediator = new RemoteMediator(window);
+ // If we can't set up IPC (e.g., because this is a top-level window or
+ // something), then don't expose InstallTrigger. The Window code handles
+ // that, if we throw an exception here.
+ },
+
+ enabled() {
+ return this._mediator.enabled(this._url.spec);
+ },
+
+ updateEnabled() {
+ return this.enabled();
+ },
+
+ install(installs, callback) {
+ let keys = Object.keys(installs);
+ if (keys.length > 1) {
+ throw new this._window.Error("Only one XPI may be installed at a time");
+ }
+
+ let item = installs[keys[0]];
+
+ if (typeof item === "string") {
+ item = { URL: item };
+ }
+ if (!item.URL) {
+ throw new this._window.Error(
+ "Missing URL property for '" + keys[0] + "'"
+ );
+ }
+
+ let url = this._resolveURL(item.URL);
+ if (!this._checkLoadURIFromScript(url)) {
+ throw new this._window.Error(
+ "Insufficient permissions to install: " + url.spec
+ );
+ }
+
+ let iconUrl = null;
+ if (item.IconURL) {
+ iconUrl = this._resolveURL(item.IconURL);
+ if (!this._checkLoadURIFromScript(iconUrl)) {
+ iconUrl = null; // If page can't load the icon, just ignore it
+ }
+ }
+
+ let installData = {
+ uri: url.spec,
+ hash: item.Hash || null,
+ name: item.name,
+ icon: iconUrl ? iconUrl.spec : null,
+ method: "installTrigger",
+ sourceHost: this._window.location?.host,
+ sourceURL: this._window.location?.href,
+ };
+
+ return this._mediator.install(
+ installData,
+ this._principal,
+ callback,
+ this._window
+ );
+ },
+
+ startSoftwareUpdate(url, flags) {
+ let filename = Services.io.newURI(url).QueryInterface(Ci.nsIURL).filename;
+ let args = {};
+ args[filename] = { URL: url };
+ return this.install(args);
+ },
+
+ installChrome(type, url, skin) {
+ return this.startSoftwareUpdate(url);
+ },
+
+ _resolveURL(url) {
+ return Services.io.newURI(url, null, this._url);
+ },
+
+ _checkLoadURIFromScript(uri) {
+ let secman = Services.scriptSecurityManager;
+ try {
+ secman.checkLoadURIWithPrincipal(
+ this._principal,
+ uri,
+ secman.DISALLOW_INHERIT_PRINCIPAL
+ );
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ classID: Components.ID("{9df8ef2b-94da-45c9-ab9f-132eb55fddf1}"),
+ contractID: "@mozilla.org/addons/installtrigger;1",
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]),
+};
+
+var EXPORTED_SYMBOLS = ["InstallTrigger"];
diff --git a/toolkit/mozapps/extensions/amWebAPI.jsm b/toolkit/mozapps/extensions/amWebAPI.jsm
new file mode 100644
index 0000000000..b4a1b8d3e6
--- /dev/null
+++ b/toolkit/mozapps/extensions/amWebAPI.jsm
@@ -0,0 +1,307 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "WEBEXT_PERMISSION_PROMPTS",
+ "extensions.webextPermissionPrompts",
+ false
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "AMO_ABUSEREPORT",
+ "extensions.abuseReport.amWebAPI.enabled",
+ false
+);
+
+const MSG_PROMISE_REQUEST = "WebAPIPromiseRequest";
+const MSG_PROMISE_RESULT = "WebAPIPromiseResult";
+const MSG_INSTALL_EVENT = "WebAPIInstallEvent";
+const MSG_INSTALL_CLEANUP = "WebAPICleanup";
+const MSG_ADDON_EVENT_REQ = "WebAPIAddonEventRequest";
+const MSG_ADDON_EVENT = "WebAPIAddonEvent";
+
+class APIBroker {
+ constructor(mm) {
+ this.mm = mm;
+
+ this._promises = new Map();
+
+ // _installMap maps integer ids to DOM AddonInstall instances
+ this._installMap = new Map();
+
+ this.mm.addMessageListener(MSG_PROMISE_RESULT, this);
+ this.mm.addMessageListener(MSG_INSTALL_EVENT, this);
+
+ this._eventListener = null;
+ }
+
+ receiveMessage(message) {
+ let payload = message.data;
+
+ switch (message.name) {
+ case MSG_PROMISE_RESULT: {
+ if (!this._promises.has(payload.callbackID)) {
+ return;
+ }
+
+ let resolve = this._promises.get(payload.callbackID);
+ this._promises.delete(payload.callbackID);
+ resolve(payload);
+ break;
+ }
+
+ case MSG_INSTALL_EVENT: {
+ let install = this._installMap.get(payload.id);
+ if (!install) {
+ let err = new Error(
+ `Got install event for unknown install ${payload.id}`
+ );
+ Cu.reportError(err);
+ return;
+ }
+ install._dispatch(payload);
+ break;
+ }
+
+ case MSG_ADDON_EVENT: {
+ if (this._eventListener) {
+ this._eventListener(payload);
+ }
+ }
+ }
+ }
+
+ sendRequest(type, ...args) {
+ return new Promise(resolve => {
+ let callbackID = APIBroker._nextID++;
+
+ this._promises.set(callbackID, resolve);
+ this.mm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
+ });
+ }
+
+ setAddonListener(callback) {
+ this._eventListener = callback;
+ if (callback) {
+ this.mm.addMessageListener(MSG_ADDON_EVENT, this);
+ this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, { enabled: true });
+ } else {
+ this.mm.removeMessageListener(MSG_ADDON_EVENT, this);
+ this.mm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, { enabled: false });
+ }
+ }
+
+ sendCleanup(ids) {
+ this.setAddonListener(null);
+ this.mm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids });
+ }
+}
+
+APIBroker._nextID = 0;
+
+// Base class for building classes to back content-exposed interfaces.
+class APIObject {
+ init(window, broker, properties) {
+ this.window = window;
+ this.broker = broker;
+
+ // Copy any provided properties onto this object, webidl bindings
+ // will only expose to content what should be exposed.
+ for (let key of Object.keys(properties)) {
+ this[key] = properties[key];
+ }
+ }
+
+ /**
+ * Helper to implement an asychronous method visible to content, where
+ * the method is implemented by sending a message to the parent process
+ * and then wrapping the returned object or error in an appropriate object.
+ * This helper method ensures that:
+ * - Returned Promise objects are from the content window
+ * - Rejected Promises have Error objects from the content window
+ * - Only non-internal errors are exposed to the caller
+ *
+ * @param {string} apiRequest The command to invoke in the parent process.
+ * @param {array} apiArgs The arguments to include with the
+ * request to the parent process.
+ * @param {function} resultConvert If provided, a function called with the
+ * result from the parent process as an
+ * argument. Used to convert the result
+ * into something appropriate for content.
+ * @returns {Promise} A Promise suitable for passing directly to content.
+ */
+ _apiTask(apiRequest, apiArgs, resultConverter) {
+ let win = this.window;
+ let broker = this.broker;
+ return new win.Promise((resolve, reject) => {
+ (async function() {
+ let result = await broker.sendRequest(apiRequest, ...apiArgs);
+ if ("reject" in result) {
+ let err = new win.Error(result.reject.message);
+ // We don't currently put any other properties onto Errors
+ // generated by mozAddonManager. If/when we do, they will
+ // need to get copied here.
+ reject(err);
+ return;
+ }
+
+ let obj = result.resolve;
+ if (resultConverter) {
+ obj = resultConverter(obj);
+ }
+ resolve(obj);
+ })().catch(err => {
+ Cu.reportError(err);
+ reject(new win.Error("Unexpected internal error"));
+ });
+ });
+ }
+}
+
+class Addon extends APIObject {
+ constructor(...args) {
+ super();
+ this.init(...args);
+ }
+
+ uninstall() {
+ return this._apiTask("addonUninstall", [this.id]);
+ }
+
+ setEnabled(value) {
+ return this._apiTask("addonSetEnabled", [this.id, value]);
+ }
+}
+
+class AddonInstall extends APIObject {
+ constructor(window, broker, properties) {
+ super();
+ this.init(window, broker, properties);
+
+ broker._installMap.set(properties.id, this);
+ }
+
+ _dispatch(data) {
+ // The message for the event includes updated copies of all install
+ // properties. Use the usual "let webidl filter visible properties" trick.
+ for (let key of Object.keys(data)) {
+ this[key] = data[key];
+ }
+
+ let event = new this.window.Event(data.event);
+ this.__DOM_IMPL__.dispatchEvent(event);
+ }
+
+ install() {
+ return this._apiTask("addonInstallDoInstall", [this.id]);
+ }
+
+ cancel() {
+ return this._apiTask("addonInstallCancel", [this.id]);
+ }
+}
+
+class WebAPI extends APIObject {
+ constructor() {
+ super();
+ this.allInstalls = [];
+ this.listenerCount = 0;
+ }
+
+ init(window) {
+ let mm = window.docShell.messageManager;
+ let broker = new APIBroker(mm);
+
+ super.init(window, broker, {});
+
+ window.addEventListener("unload", event => {
+ this.broker.sendCleanup(this.allInstalls);
+ });
+ }
+
+ getAddonByID(id) {
+ return this._apiTask("getAddonByID", [id], addonInfo => {
+ if (!addonInfo) {
+ return null;
+ }
+ let addon = new Addon(this.window, this.broker, addonInfo);
+ return this.window.Addon._create(this.window, addon);
+ });
+ }
+
+ createInstall(options) {
+ if (!Services.prefs.getBoolPref("xpinstall.enabled", true)) {
+ throw new this.window.Error("Software installation is disabled.");
+ }
+
+ const triggeringPrincipal = this.window.document.nodePrincipal;
+
+ let installOptions = {
+ ...options,
+ triggeringPrincipal,
+ // Provide the host from which the amWebAPI is being called
+ // (so that we can detect if the API is being used from the disco pane,
+ // AMO, testpilot or another unknown webpage).
+ sourceHost: this.window.location?.host,
+ sourceURL: this.window.location?.href,
+ };
+ return this._apiTask("createInstall", [installOptions], installInfo => {
+ if (!installInfo) {
+ return null;
+ }
+ let install = new AddonInstall(this.window, this.broker, installInfo);
+ this.allInstalls.push(installInfo.id);
+ return this.window.AddonInstall._create(this.window, install);
+ });
+ }
+
+ reportAbuse(id) {
+ return this._apiTask("addonReportAbuse", [id]);
+ }
+
+ get permissionPromptsEnabled() {
+ return WEBEXT_PERMISSION_PROMPTS;
+ }
+
+ get abuseReportPanelEnabled() {
+ return AMO_ABUSEREPORT;
+ }
+
+ eventListenerAdded(type) {
+ if (this.listenerCount == 0) {
+ this.broker.setAddonListener(data => {
+ let event = new this.window.AddonEvent(data.event, data);
+ this.__DOM_IMPL__.dispatchEvent(event);
+ });
+ }
+ this.listenerCount++;
+ }
+
+ eventListenerRemoved(type) {
+ this.listenerCount--;
+ if (this.listenerCount == 0) {
+ this.broker.setAddonListener(null);
+ }
+ }
+}
+WebAPI.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIDOMGlobalPropertyInitializer",
+]);
+WebAPI.prototype.classID = Components.ID(
+ "{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"
+);
+var EXPORTED_SYMBOLS = ["WebAPI"];
diff --git a/toolkit/mozapps/extensions/components.conf b/toolkit/mozapps/extensions/components.conf
new file mode 100644
index 0000000000..9d73b6d797
--- /dev/null
+++ b/toolkit/mozapps/extensions/components.conf
@@ -0,0 +1,44 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'js_name': 'blocklist',
+ 'cid': '{66354bc9-7ed1-4692-ae1d-8da97d6b205e}',
+ 'contract_ids': ['@mozilla.org/extensions/blocklist;1'],
+ 'jsm': 'resource://gre/modules/addonManager.js',
+ 'constructor': 'BlocklistService',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{4399533d-08d1-458c-a87a-235f74451cfa}',
+ 'contract_ids': ['@mozilla.org/addons/integration;1'],
+ 'jsm': 'resource://gre/modules/addonManager.js',
+ 'constructor': 'amManager',
+ },
+ {
+ 'cid': '{9df8ef2b-94da-45c9-ab9f-132eb55fddf1}',
+ 'contract_ids': ['@mozilla.org/addons/installtrigger;1'],
+ 'jsm': 'resource://gre/modules/amInstallTrigger.jsm',
+ 'constructor': 'InstallTrigger',
+ },
+]
+
+if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android':
+ Classes += [
+ {
+ 'cid': '{7beb3ba8-6ec3-41b4-b67c-da89b8518922}',
+ 'contract_ids': ['@mozilla.org/uriloader/content-handler;1?type=application/x-xpinstall'],
+ 'jsm': 'resource://gre/modules/amContentHandler.jsm',
+ 'constructor': 'amContentHandler',
+ },
+ {
+ 'cid': '{8866d8e3-4ea5-48b7-a891-13ba0ac15235}',
+ 'contract_ids': ['@mozilla.org/addon-web-api/manager;1'],
+ 'jsm': 'resource://gre/modules/amWebAPI.jsm',
+ 'constructor': 'WebAPI',
+ },
+ ]
diff --git a/toolkit/mozapps/extensions/content/OpenH264-license.txt b/toolkit/mozapps/extensions/content/OpenH264-license.txt
new file mode 100644
index 0000000000..ad37989b8c
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/OpenH264-license.txt
@@ -0,0 +1,59 @@
+-------------------------------------------------------
+About The Cisco-Provided Binary of OpenH264 Video Codec
+-------------------------------------------------------
+
+Cisco provides this program under the terms of the BSD license.
+
+Additionally, this binary is licensed under Cisco’s AVC/H.264 Patent Portfolio License from MPEG LA, at no cost to you, provided that the requirements and conditions shown below in the AVC/H.264 Patent Portfolio sections are met.
+
+As with all AVC/H.264 codecs, you may also obtain your own patent license from MPEG LA or from the individual patent owners, or proceed at your own risk. Your rights from Cisco under the BSD license are not affected by this choice.
+
+For more information on the OpenH264 binary licensing, please see the OpenH264 FAQ found at http://www.openh264.org/faq.html#binary
+
+A corresponding source code to this binary program is available under the same BSD terms, which can be found at http://www.openh264.org
+
+-----------
+BSD License
+-----------
+
+Copyright © 2014 Cisco Systems, Inc.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+-----------------------------------------
+AVC/H.264 Patent Portfolio License Notice
+-----------------------------------------
+
+The binary form of this Software is distributed by Cisco under the AVC/H.264 Patent Portfolio License from MPEG LA, and is subject to the following requirements, which may or may not be applicable to your use of this software:
+
+THIS PRODUCT IS LICENSED UNDER THE AVC PATENT PORTFOLIO LICENSE FOR THE PERSONAL USE OF A CONSUMER OR OTHER USES IN WHICH IT DOES NOT RECEIVE REMUNERATION TO (i) ENCODE VIDEO IN COMPLIANCE WITH THE AVC STANDARD (“AVC VIDEO”) AND/OR (ii) DECODE AVC VIDEO THAT WAS ENCODED BY A CONSUMER ENGAGED IN A PERSONAL ACTIVITY AND/OR WAS OBTAINED FROM A VIDEO PROVIDER LICENSED TO PROVIDE AVC VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR ANY OTHER USE. ADDITIONAL INFORMATION MAY BE OBTAINED FROM MPEG LA, L.L.C. SEE HTTP://WWW.MPEGLA.COM
+
+Accordingly, please be advised that content providers and broadcasters using AVC/H.264 in their service may be required to obtain a separate use license from MPEG LA, referred to as "(b) sublicenses" in the SUMMARY OF AVC/H.264 LICENSE TERMS from MPEG LA found at http://www.openh264.org/mpegla
+
+---------------------------------------------
+AVC/H.264 Patent Portfolio License Conditions
+---------------------------------------------
+
+In addition, the Cisco-provided binary of this Software is licensed under Cisco's license from MPEG LA only if the following conditions are met:
+
+1. The Cisco-provided binary is separately downloaded to an end user’s device, and not integrated into or combined with third party software prior to being downloaded to the end user’s device;
+
+2. The end user must have the ability to control (e.g., to enable, disable, or re-enable) the use of the Cisco-provided binary;
+
+3. Third party software, in the location where end users can control the use of the Cisco-provided binary, must display the following text:
+
+ "OpenH264 Video Codec provided by Cisco Systems, Inc."
+
+4. Any third-party software that makes use of the Cisco-provided binary must reproduce all of the above text, as well as this last condition, in the EULA and/or in another location where licensing information is to be presented to the end user.
+
+
+
+ v1.0
diff --git a/toolkit/mozapps/extensions/content/aboutaddons.css b/toolkit/mozapps/extensions/content/aboutaddons.css
new file mode 100644
index 0000000000..c7dbf90af6
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/aboutaddons.css
@@ -0,0 +1,739 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:root {
+ --addon-icon-size: 32px;
+ --main-margin-start: 28px;
+ --section-width: 664px;
+ --sidebar-width: var(--in-content-sidebar-width);
+ --z-index-sticky-container: 1;
+ --z-index-popup: 10;
+}
+
+@media (max-width: 830px) {
+ :root {
+ --main-margin-start: 16px;
+ /* Maintain a main margin so card shadows don't overlap the sidebar. */
+ --sidebar-width: calc(var(--in-content-sidebar-width) - var(--main-margin-start));
+ }
+}
+
+*|*[hidden] {
+ display: none !important;
+}
+
+body {
+ cursor: default;
+ /* The page starts to look really bad lower than this. */
+ min-width: 500px;
+}
+
+#full {
+ display: grid;
+ grid-template-columns: var(--sidebar-width) 1fr;
+}
+
+#sidebar {
+ position: sticky;
+ top: 0;
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ overflow: hidden auto;
+}
+
+@media (prefers-reduced-motion) {
+ /* Setting border-inline-end on #sidebar makes it a focusable element */
+ #sidebar::after {
+ content: "";
+ width: 1px;
+ height: 100%;
+ background-color: var(--in-content-border-color);
+ top: 0;
+ inset-inline-end: 0;
+ position: absolute;
+ }
+}
+
+#categories {
+ display: flex;
+ flex-direction: column;
+ padding-inline-end: 4px; /* Leave space for the button focus styles. */
+}
+
+.category {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ margin-block: 0;
+ align-items: center;
+}
+
+.category[badge-count]::after {
+ display: inline-block;
+ min-width: 20px;
+ background-color: var(--blue-50);
+ color: #fff;
+ font-weight: bold;
+ /* Use a large border-radius to get semi-circles on the sides. */
+ border-radius: 1000px;
+ padding: 2px 6px;
+ content: attr(badge-count);
+ text-align: center;
+ margin-inline-start: 8px;
+ grid-column: 2;
+}
+
+.category[name="discover"] {
+ background-image: url("chrome://mozapps/skin/extensions/category-discover.svg");
+}
+.category[name="locale"] {
+ background-image: url("chrome://mozapps/skin/extensions/category-languages.svg");
+}
+.category[name="extension"] {
+ background-image: url("chrome://mozapps/skin/extensions/category-extensions.svg");
+}
+.category[name="theme"] {
+ background-image: url("chrome://mozapps/skin/extensions/category-themes.svg");
+}
+.category[name="plugin"] {
+ background-image: url("chrome://global/skin/plugins/plugin.svg");
+}
+.category[name="dictionary"] {
+ background-image: url("chrome://mozapps/skin/extensions/category-dictionaries.svg");
+}
+.category[name="available-updates"] {
+ background-image: url("chrome://mozapps/skin/extensions/category-available.svg");
+}
+.category[name="recent-updates"] {
+ background-image: url("chrome://mozapps/skin/extensions/category-recent.svg");
+}
+
+.header-name {
+ user-select: initial;
+}
+
+.sticky-container {
+ background: var(--in-content-page-background);
+ width: 100%;
+ position: sticky;
+ top: 0;
+ z-index: var(--z-index-sticky-container);
+}
+
+.main-search {
+ background: var(--in-content-page-background);
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding-inline-start: 28px;
+ padding-top: 20px;
+ padding-bottom: 30px;
+ max-width: var(--section-width);
+}
+
+search-addons > search-textbox {
+ margin: 0;
+ width: 20em;
+ min-height: 32px;
+}
+
+.search-label {
+ margin-inline-end: 8px;
+}
+
+.main-heading {
+ background: var(--in-content-page-background);
+ display: flex;
+ margin-inline-start: var(--main-margin-start);
+ padding-bottom: 16px;
+ max-width: var(--section-width);
+}
+
+.spacer {
+ flex-grow: 1;
+}
+
+#updates-message {
+ display: flex;
+ align-items: center;
+ margin-inline-end: 8px;
+}
+
+.back-button {
+ margin-inline-end: 16px;
+}
+
+/* Plugins aren't yet disabled by safemode (bug 342333),
+ so don't show that warning when viewing plugins. */
+#page-header[current-param="plugin"] message-bar[warning-type="safe-mode"] {
+ display: none;
+}
+
+#main {
+ margin-inline-start: var(--main-margin-start);
+ margin-bottom: 28px;
+ max-width: var(--section-width);
+}
+
+global-warnings,
+#abuse-reports-messages {
+ margin-inline-start: var(--main-margin-start);
+ max-width: var(--section-width);
+}
+
+/* The margin between message bars. */
+message-bar-stack > * {
+ margin-bottom: 8px;
+}
+
+/* List sections */
+
+.list-section-heading {
+ font-size: 17px;
+ font-weight: 600;
+ margin-bottom: 16px;
+}
+
+.section {
+ margin-bottom: 32px;
+}
+
+/* Add-on cards */
+
+.addon.card {
+ margin-bottom: 16px;
+}
+
+addon-card:not([expanded]) > .addon.card[active="false"] {
+ opacity: 0.6;
+ transition: opacity 150ms, box-shadow 150ms;
+}
+
+addon-card:not([expanded])[panelopen] > .addon.card[active="false"],
+addon-card:not([expanded]) > .addon.card[active="false"]:focus-within,
+addon-card:not([expanded]) > .addon.card[active="false"]:hover {
+ opacity: 1;
+}
+
+.addon.card:hover {
+ box-shadow: var(--card-shadow);
+}
+
+addon-card:not([expanded]) > .addon.card:hover {
+ box-shadow: var(--card-shadow-hover);
+ cursor: pointer;
+}
+
+addon-card[expanded] .addon.card {
+ padding-bottom: 0;
+}
+
+.addon-card-collapsed {
+ display: flex;
+}
+
+addon-list addon-card > .addon.card {
+ user-select: none;
+}
+
+.addon-card-message,
+.update-postponed-bar {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ margin: 8px calc(var(--card-padding) * -1) calc(var(--card-padding) * -1);
+}
+
+addon-card[expanded] .addon-card-message,
+addon-card[expanded] .update-postponed-bar {
+ border-radius: 0;
+ margin-bottom: 0;
+}
+
+addon-card[expanded] .update-postponed-bar + .addon-card-message {
+ /* Remove margin between the two message bars when they are both
+ * visible in the detail view */
+ margin-top: 0px;
+}
+
+.update-postponed-bar + .addon-card-message {
+ /* Prevent the small overlapping between the two message bars
+ * when they are both visible at the same time one after the
+ * other on the same addon card */
+ margin-top: 12px;
+}
+
+/* Theme preview image. */
+.card-heading-image {
+ /* If the width, height or aspect ratio changes, don't forget to update the
+ * getScreenshotUrlForAddon function in aboutaddons.js */
+ width: var(--section-width);
+ /* Adjust height so that the image preserves the aspect ratio from AMO.
+ * For details, see https://bugzilla.mozilla.org/show_bug.cgi?id=1546123 */
+ height: calc(var(--section-width) * 92 / 680);
+ object-fit: cover;
+}
+
+.card-heading-icon {
+ flex-shrink: 0;
+ width: var(--addon-icon-size);
+ height: var(--addon-icon-size);
+ margin-inline-end: 16px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.card-contents {
+ word-break: break-word;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.addon-name-container {
+ /* Subtract the top line-height so the text and icon align at the top. */
+ margin-top: -3px;
+ display: flex;
+ align-items: center;
+}
+
+.addon-name {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 22px;
+ margin: 0;
+ margin-inline-end: 8px;
+}
+
+.addon-name-link,
+.addon-name-link:hover {
+ color: var(--in-content-text-color);
+ text-decoration: none;
+}
+
+.addon-badge {
+ display: inline-block;
+ margin-inline-end: 8px;
+ width: 22px;
+ height: 22px;
+ background-repeat: no-repeat;
+ background-position: center;
+ flex-shrink: 0;
+ border-radius: 11px;
+ -moz-context-properties: fill;
+ fill: #fff;
+}
+
+.addon-badge-private-browsing-allowed {
+ background-image: url("chrome://browser/skin/private-browsing.svg");
+}
+
+.addon-badge-recommended {
+ background-color: var(--orange-50);
+ background-image: url("chrome://mozapps/skin/extensions/recommended.svg");
+}
+
+.addon-badge-line {
+ background-color: #fff;
+ background-image: url("chrome://mozapps/skin/extensions/line.svg");
+ background-size: 16px;
+ border-radius: 10px;
+ border: 1px solid #CFCFD8;
+ width: 20px;
+ height: 20px;
+}
+
+.addon-badge-verified {
+ background-color: var(--green-70);
+ background-image: url("chrome://global/skin/icons/check.svg");
+}
+
+.theme-enable-button {
+ min-width: auto;
+ height: auto;
+ font-size: 13px;
+ min-height: auto;
+ height: 24px;
+ margin: 0;
+}
+
+.addon-description {
+ font-size: 14px;
+ line-height: 20px;
+ color: var(--in-content-deemphasized-text);
+ font-weight: 400;
+}
+
+/* Prevent the content from wrapping unless expanded. */
+addon-card:not([expanded]) .card-contents {
+ /* We're hiding the content when it's too long, so we need to define the
+ * width. As long as this is less than the width of its parent it works. */
+ width: 1px;
+ white-space: nowrap;
+}
+
+/* Ellipsize if the content is too long. */
+addon-card:not([expanded]) .addon-name,
+addon-card:not([expanded]) .addon-description {
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+}
+
+.page-options-menu {
+ align-self: center;
+}
+
+.page-options-menu > .more-options-button {
+ background-image: url("chrome://global/skin/icons/settings.svg");
+ width: 32px;
+ height: 32px;
+}
+
+/* Recommended add-ons on list views */
+.recommended-heading {
+ margin-bottom: 24px;
+ margin-top: 48px;
+}
+
+/* Discopane extensions to the add-on card */
+
+recommended-addon-card .addon-description:not(:empty) {
+ margin-top: 0.5em;
+}
+
+.disco-card-head {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.disco-addon-name {
+ font-size: inherit;
+ font-weight: normal;
+ line-height: normal;
+ margin: 0;
+}
+
+.disco-addon-author {
+ font-size: 12px;
+ font-weight: normal;
+}
+
+.disco-description-statistics {
+ margin-top: 1em;
+ display: grid;
+ grid-template-columns: repeat(2, max-content);
+ grid-column-gap: 2em;
+ align-items: center;
+}
+
+.disco-cta-button {
+ font-size: 14px;
+ flex-shrink: 0;
+ flex-grow: 0;
+ align-self: baseline;
+ margin-inline-end: 0;
+}
+
+.disco-cta-button[action="install-addon"]::before {
+ content: "+";
+ padding-inline-end: 4px;
+}
+
+.discopane-notice {
+ margin: 24px 0;
+}
+
+.discopane-notice-content {
+ align-items: center;
+ display: flex;
+ width: 100%;
+}
+
+.discopane-notice-content > span {
+ flex-grow: 1;
+ margin-inline-end: 4px;
+}
+
+.discopane-notice-content > button {
+ flex-grow: 0;
+ flex-shrink: 0;
+}
+
+.view-footer {
+ text-align: center;
+}
+
+.view-footer-item {
+ margin-top: 30px;
+}
+
+.privacy-policy-link {
+ font-size: small;
+}
+
+.theme-recommendation {
+ text-align: start;
+}
+
+addon-details {
+ color: var(--in-content-deemphasized-text);
+}
+
+.addon-detail-description {
+ margin: 16px 0;
+}
+
+.addon-detail-contribute {
+ display: flex;
+ padding: var(--card-padding);
+ border: 1px solid var(--in-content-box-border-color);
+ border-radius: var(--panel-border-radius);
+ margin-bottom: var(--card-padding);
+ flex-direction: column;
+}
+
+.addon-detail-contribute > label {
+ font-style: italic;
+}
+
+.addon-detail-contribute-button {
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background-image: url("chrome://global/skin/icons/heart.svg");
+ background-repeat: no-repeat;
+ background-position: 8px;
+ padding-inline-start: 28px;
+ margin-top: var(--card-padding);
+ margin-bottom: 0;
+ align-self: flex-end;
+}
+
+.addon-detail-contribute-button:dir(rtl) {
+ background-position-x: right 8px;
+}
+
+.addon-detail-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-top: 1px solid var(--in-content-box-border-color);
+ margin: 0 calc(var(--card-padding) * -1);
+ padding: var(--card-padding);
+ color: var(--in-content-text-color);
+}
+
+.addon-detail-row.addon-detail-help-row {
+ display: block;
+ color: var(--in-content-deemphasized-text);
+ padding-top: 4px;
+ padding-bottom: var(--card-padding);
+ border: none;
+}
+
+.addon-detail-row-has-help {
+ padding-bottom: 0;
+}
+
+.addon-detail-row input[type="checkbox"] {
+ margin: 0;
+}
+
+.addon-detail-rating {
+ display: flex;
+}
+
+.addon-detail-rating > a {
+ margin-inline-start: 8px;
+}
+
+.more-options-button {
+ min-width: auto;
+ min-height: auto;
+ width: 24px;
+ height: 24px;
+ margin: 0;
+ margin-inline-start: 8px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background-image: url("chrome://global/skin/icons/more.svg");
+ background-repeat: no-repeat;
+ background-position: center center;
+ /* Get the -badged ::after element in the right spot. */
+ padding: 1px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.more-options-button-badged::after {
+ content: "";
+ display: block;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--blue-50);
+}
+
+panel-item {
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+panel-item[action="remove"] {
+ --icon: url("chrome://global/skin/icons/delete.svg");
+}
+
+panel-item[action="install-update"] {
+ --icon: url("chrome://global/skin/icons/update-icon.svg");
+}
+
+panel-item[action="report"] {
+ --icon: url(chrome://global/skin/icons/warning.svg);
+}
+
+panel-item-separator {
+ display: block;
+ height: 1px;
+ background: var(--in-content-box-border-color);
+ padding: 0;
+ margin: 6px 0;
+}
+
+.hide-amo-link .amo-link-container {
+ display: none;
+}
+
+.button-link {
+ min-height: auto;
+ background: none !important;
+ padding: 0;
+ margin: 0;
+ color: var(--in-content-link-color) !important;
+ cursor: pointer;
+ border: none;
+}
+
+.button-link:hover {
+ color: var(--in-content-link-color-hover) !important;
+ text-decoration: underline;
+}
+
+.button-link:active {
+ color: var(--in-content-link-color-active) !important;
+ text-decoration: none;
+}
+
+.inline-options-stack {
+ /* If the options browser triggers an alert we need room to show it. */
+ min-height: 250px;
+ width: 100%;
+ background-color: white;
+ margin-block: 4px;
+}
+
+addon-permissions-list > .addon-detail-row {
+ border-top: none;
+}
+
+.addon-permissions-list {
+ list-style-type: none;
+ margin: 0;
+ padding-inline-start: 8px;
+}
+
+.addon-permissions-list > li {
+ border: none;
+ padding-block: 4px;
+ padding-inline-start: 2rem;
+ background-image: none;
+ background-position: 0 center;
+ background-size: 1.6rem 1.6rem;
+ background-repeat: no-repeat;
+}
+
+.addon-permissions-list > li:dir(rtl) {
+ background-position-x: right 0;
+}
+
+/* justify the permission toggle */
+li.permission-info > label {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+/* using a list-style-image prevents aligning the image */
+.addon-permissions-list > li.permission-checked {
+ background-image: url("chrome://global/skin/icons/check.svg");
+ -moz-context-properties: fill;
+ fill: var(--green-60);
+}
+
+.permission-header {
+ font-size: 1em;
+}
+
+.tab-group {
+ display: block;
+ margin-top: 8px;
+ /* Pull the buttons flush with the side of the card */
+ margin-inline: calc(var(--card-padding) * -1);
+ border-bottom: 1px solid var(--in-content-box-border-color);
+ border-top: 1px solid var(--in-content-box-border-color);
+ font-size: 0;
+ line-height: 0;
+}
+
+button.tab-button {
+ appearance: none;
+ border-inline: none;
+ border-block: 2px solid transparent;
+ border-radius: 0;
+ background: transparent;
+ font-size: 14px;
+ line-height: 20px;
+ margin: 0;
+ padding: 4px 16px;
+ color: var(--in-content-text-color);
+}
+
+button.tab-button:hover {
+ background-color: var(--in-content-button-background);
+ border-top-color: var(--in-content-box-border-color);
+}
+
+button.tab-button:hover:active {
+ background-color: var(--in-content-button-background-hover);
+}
+
+button.tab-button[selected] {
+ border-top-color: var(--in-content-border-highlight);
+ color: var(--in-content-category-text-selected) !important;
+}
+
+button.tab-button:-moz-focusring {
+ outline-offset: -2px;
+ -moz-outline-radius: 0;
+}
+
+.tab-group[last-input-type="mouse"] > button.tab-button:-moz-focusring {
+ outline: none;
+ box-shadow: none;
+}
+
+panel-list {
+ font-size: 13px;
+}
+
+@media (max-width: 830px) {
+ .category[badge-count]::after {
+ content: "";
+ display: block;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ min-width: auto;
+ padding: 0;
+ }
+}
diff --git a/toolkit/mozapps/extensions/content/aboutaddons.html b/toolkit/mozapps/extensions/content/aboutaddons.html
new file mode 100644
index 0000000000..5d9ebd3e50
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/aboutaddons.html
@@ -0,0 +1,427 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/content/aboutaddons.js b/toolkit/mozapps/extensions/content/aboutaddons.js
new file mode 100644
index 0000000000..0ad41fd91c
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/aboutaddons.js
@@ -0,0 +1,4811 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+/* exported hide, initialize, show */
+/* import-globals-from aboutaddonsCommon.js */
+/* import-globals-from abuse-reports.js */
+/* global MozXULElement, MessageBarStackElement, windowRoot */
+
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.jsm",
+ AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
+ AMTelemetry: "resource://gre/modules/AddonManager.jsm",
+ ClientID: "resource://gre/modules/ClientID.jsm",
+ DeferredTask: "resource://gre/modules/DeferredTask.jsm",
+ E10SUtils: "resource://gre/modules/E10SUtils.jsm",
+ ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
+ ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
+ ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "browserBundle", () => {
+ return Services.strings.createBundle(
+ "chrome://browser/locale/browser.properties"
+ );
+});
+XPCOMUtils.defineLazyGetter(this, "brandBundle", () => {
+ return Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+});
+XPCOMUtils.defineLazyGetter(this, "extBundle", function() {
+ return Services.strings.createBundle(
+ "chrome://mozapps/locale/extensions/extensions.properties"
+ );
+});
+XPCOMUtils.defineLazyGetter(this, "extensionStylesheets", () => {
+ const { ExtensionParent } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionParent.jsm"
+ );
+ return ExtensionParent.extensionStylesheets;
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "allowPrivateBrowsingByDefault",
+ "extensions.allowPrivateBrowsingByDefault",
+ true
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "SUPPORT_URL",
+ "app.support.baseURL",
+ "",
+ null,
+ val => Services.urlFormatter.formatURL(val)
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "XPINSTALL_ENABLED",
+ "xpinstall.enabled",
+ true
+);
+
+const UPDATES_RECENT_TIMESPAN = 2 * 24 * 3600000; // 2 days (in milliseconds)
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "ABUSE_REPORT_ENABLED",
+ "extensions.abuseReport.enabled",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "LIST_RECOMMENDATIONS_ENABLED",
+ "extensions.htmlaboutaddons.recommendations.enabled",
+ false
+);
+
+const PLUGIN_ICON_URL = "chrome://global/skin/plugins/plugin.svg";
+const EXTENSION_ICON_URL =
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+const BUILTIN_THEME_PREVIEWS = new Map([
+ [
+ "default-theme@mozilla.org",
+ "chrome://mozapps/content/extensions/default-theme.svg",
+ ],
+ [
+ "firefox-compact-light@mozilla.org",
+ "chrome://mozapps/content/extensions/firefox-compact-light.svg",
+ ],
+ [
+ "firefox-compact-dark@mozilla.org",
+ "chrome://mozapps/content/extensions/firefox-compact-dark.svg",
+ ],
+ [
+ "firefox-alpenglow@mozilla.org",
+ "chrome://mozapps/content/extensions/firefox-alpenglow.svg",
+ ],
+]);
+
+const PERMISSION_MASKS = {
+ "ask-to-activate": AddonManager.PERM_CAN_ASK_TO_ACTIVATE,
+ enable: AddonManager.PERM_CAN_ENABLE,
+ "always-activate": AddonManager.PERM_CAN_ENABLE,
+ disable: AddonManager.PERM_CAN_DISABLE,
+ "never-activate": AddonManager.PERM_CAN_DISABLE,
+ uninstall: AddonManager.PERM_CAN_UNINSTALL,
+ upgrade: AddonManager.PERM_CAN_UPGRADE,
+ "change-privatebrowsing": AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS,
+};
+
+const PREF_DISCOVERY_API_URL = "extensions.getAddons.discovery.api_url";
+const PREF_THEME_RECOMMENDATION_URL =
+ "extensions.recommendations.themeRecommendationUrl";
+const PREF_RECOMMENDATION_HIDE_NOTICE = "extensions.recommendations.hideNotice";
+const PREF_PRIVACY_POLICY_URL = "extensions.recommendations.privacyPolicyUrl";
+const PREF_RECOMMENDATION_ENABLED = "browser.discovery.enabled";
+const PREF_TELEMETRY_ENABLED = "datareporting.healthreport.uploadEnabled";
+const PRIVATE_BROWSING_PERM_NAME = "internal:privateBrowsingAllowed";
+const PRIVATE_BROWSING_PERMS = {
+ permissions: [PRIVATE_BROWSING_PERM_NAME],
+ origins: [],
+};
+
+function shouldSkipAnimations() {
+ return (
+ document.body.hasAttribute("skip-animations") ||
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
+ );
+}
+
+function callListeners(name, args, listeners) {
+ for (let listener of listeners) {
+ try {
+ if (name in listener) {
+ listener[name](...args);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+}
+
+function getUpdateInstall(addon) {
+ return (
+ // Install object for a pending update.
+ addon.updateInstall ||
+ // Install object for a postponed upgrade (only for extensions,
+ // because is the only addon type that can postpone their own
+ // updates).
+ (addon.type === "extension" &&
+ addon.pendingUpgrade &&
+ addon.pendingUpgrade.install)
+ );
+}
+
+function isManualUpdate(install) {
+ let isManual =
+ install.existingAddon &&
+ !AddonManager.shouldAutoUpdate(install.existingAddon);
+ let isExtension =
+ install.existingAddon && install.existingAddon.type == "extension";
+ return (
+ (isManual && isInState(install, "available")) ||
+ (isExtension && isInState(install, "postponed"))
+ );
+}
+
+const AddonManagerListenerHandler = {
+ listeners: new Set(),
+
+ addListener(listener) {
+ this.listeners.add(listener);
+ },
+
+ removeListener(listener) {
+ this.listeners.delete(listener);
+ },
+
+ delegateEvent(name, args) {
+ callListeners(name, args, this.listeners);
+ },
+
+ startup() {
+ this._listener = new Proxy(
+ {},
+ {
+ has: () => true,
+ get: (_, name) => (...args) => this.delegateEvent(name, args),
+ }
+ );
+ AddonManager.addAddonListener(this._listener);
+ AddonManager.addInstallListener(this._listener);
+ AddonManager.addManagerListener(this._listener);
+ this._permissionHandler = (type, data) => {
+ if (type == "change-permissions") {
+ this.delegateEvent("onChangePermissions", [data]);
+ }
+ };
+ ExtensionPermissions.addListener(this._permissionHandler);
+ },
+
+ shutdown() {
+ AddonManager.removeAddonListener(this._listener);
+ AddonManager.removeInstallListener(this._listener);
+ AddonManager.removeManagerListener(this._listener);
+ ExtensionPermissions.removeListener(this._permissionHandler);
+ },
+};
+
+/**
+ * This object wires the AddonManager event listeners into addon-card and
+ * addon-details elements rather than needing to add/remove listeners all the
+ * time as the view changes.
+ */
+const AddonCardListenerHandler = new Proxy(
+ {},
+ {
+ has: () => true,
+ get(_, name) {
+ return (...args) => {
+ let elements = [];
+ let addonId;
+
+ // We expect args[0] to be of type:
+ // - AddonInstall, on AddonManager install events
+ // - AddonWrapper, on AddonManager addon events
+ // - undefined, on AddonManager manage events
+ if (args[0]) {
+ addonId =
+ args[0].addon?.id ||
+ args[0].existingAddon?.id ||
+ args[0].extensionId ||
+ args[0].id;
+ }
+
+ if (addonId) {
+ let cardSelector = `addon-card[addon-id="${addonId}"]`;
+ elements = document.querySelectorAll(
+ `${cardSelector}, ${cardSelector} addon-details`
+ );
+ } else if (name == "onUpdateModeChanged") {
+ elements = document.querySelectorAll("addon-card");
+ }
+
+ callListeners(name, args, elements);
+ };
+ },
+ }
+);
+AddonManagerListenerHandler.addListener(AddonCardListenerHandler);
+
+function isAbuseReportSupported(addon) {
+ return (
+ ABUSE_REPORT_ENABLED &&
+ ["extension", "theme"].includes(addon.type) &&
+ !(addon.isBuiltin || addon.isSystem)
+ );
+}
+
+async function isAllowedInPrivateBrowsing(addon) {
+ // Use the Promise directly so this function stays sync for the other case.
+ let perms = await ExtensionPermissions.get(addon.id);
+ return perms.permissions.includes(PRIVATE_BROWSING_PERM_NAME);
+}
+
+function hasPermission(addon, permission) {
+ return !!(addon.permissions & PERMISSION_MASKS[permission]);
+}
+
+function isInState(install, state) {
+ return install.state == AddonManager["STATE_" + state.toUpperCase()];
+}
+
+async function getAddonMessageInfo(addon) {
+ const { name } = addon;
+ const appName = brandBundle.GetStringFromName("brandShortName");
+ const {
+ STATE_BLOCKED,
+ STATE_OUTDATED,
+ STATE_SOFTBLOCKED,
+ STATE_VULNERABLE_UPDATE_AVAILABLE,
+ STATE_VULNERABLE_NO_UPDATE,
+ } = Ci.nsIBlocklistService;
+
+ const formatString = (name, args) =>
+ extBundle.formatStringFromName(
+ `details.notification.${name}`,
+ args,
+ args.length
+ );
+ const getString = name =>
+ extBundle.GetStringFromName(`details.notification.${name}`);
+
+ if (addon.blocklistState === STATE_BLOCKED) {
+ return {
+ linkText: getString("blocked.link"),
+ linkUrl: await addon.getBlocklistURL(),
+ message: formatString("blocked", [name]),
+ type: "error",
+ };
+ } else if (isDisabledUnsigned(addon)) {
+ return {
+ linkText: getString("unsigned.link"),
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ message: formatString("unsignedAndDisabled", [name, appName]),
+ type: "error",
+ };
+ } else if (
+ !addon.isCompatible &&
+ (AddonManager.checkCompatibility ||
+ addon.blocklistState !== STATE_SOFTBLOCKED)
+ ) {
+ return {
+ message: formatString("incompatible", [
+ name,
+ appName,
+ Services.appinfo.version,
+ ]),
+ type: "warning",
+ };
+ } else if (!isCorrectlySigned(addon)) {
+ return {
+ linkText: getString("unsigned.link"),
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ message: formatString("unsigned", [name, appName]),
+ type: "warning",
+ };
+ } else if (addon.blocklistState === STATE_SOFTBLOCKED) {
+ return {
+ linkText: getString("softblocked.link"),
+ linkUrl: await addon.getBlocklistURL(),
+ message: formatString("softblocked", [name]),
+ type: "warning",
+ };
+ } else if (addon.blocklistState === STATE_OUTDATED) {
+ return {
+ linkText: getString("outdated.link"),
+ linkUrl: await addon.getBlocklistURL(),
+ message: formatString("outdated", [name]),
+ type: "warning",
+ };
+ } else if (addon.blocklistState === STATE_VULNERABLE_UPDATE_AVAILABLE) {
+ return {
+ linkText: getString("vulnerableUpdatable.link"),
+ linkUrl: await addon.getBlocklistURL(),
+ message: formatString("vulnerableUpdatable", [name]),
+ type: "error",
+ };
+ } else if (addon.blocklistState === STATE_VULNERABLE_NO_UPDATE) {
+ return {
+ linkText: getString("vulnerableNoUpdate.link"),
+ linkUrl: await addon.getBlocklistURL(),
+ message: formatString("vulnerableNoUpdate", [name]),
+ type: "error",
+ };
+ } else if (addon.isGMPlugin && !addon.isInstalled && addon.isActive) {
+ return {
+ message: formatString("gmpPending", [name]),
+ type: "warning",
+ };
+ }
+ return {};
+}
+
+function checkForUpdate(addon) {
+ return new Promise(resolve => {
+ let listener = {
+ onUpdateAvailable(addon, install) {
+ if (AddonManager.shouldAutoUpdate(addon)) {
+ // Make sure that an update handler is attached to all the install
+ // objects when updated xpis are going to be installed automatically.
+ attachUpdateHandler(install);
+
+ let failed = () => {
+ detachUpdateHandler(install);
+ install.removeListener(updateListener);
+ resolve({ installed: false, pending: false, found: true });
+ };
+ let updateListener = {
+ onDownloadFailed: failed,
+ onInstallCancelled: failed,
+ onInstallFailed: failed,
+ onInstallEnded: (...args) => {
+ detachUpdateHandler(install);
+ install.removeListener(updateListener);
+ resolve({ installed: true, pending: false, found: true });
+ },
+ onInstallPostponed: (...args) => {
+ detachUpdateHandler(install);
+ install.removeListener(updateListener);
+ resolve({ installed: false, pending: true, found: true });
+ },
+ };
+ install.addListener(updateListener);
+ install.install();
+ } else {
+ resolve({ installed: false, pending: true, found: true });
+ }
+ },
+ onNoUpdateAvailable() {
+ resolve({ found: false });
+ },
+ };
+ addon.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
+
+async function checkForUpdates() {
+ let addons = await AddonManager.getAddonsByTypes(null);
+ addons = addons.filter(addon => hasPermission(addon, "upgrade"));
+ let updates = await Promise.all(addons.map(addon => checkForUpdate(addon)));
+ Services.obs.notifyObservers(null, "EM-update-check-finished");
+ return updates.reduce(
+ (counts, update) => ({
+ installed: counts.installed + (update.installed ? 1 : 0),
+ pending: counts.pending + (update.pending ? 1 : 0),
+ found: counts.found + (update.found ? 1 : 0),
+ }),
+ { installed: 0, pending: 0, found: 0 }
+ );
+}
+
+// Don't change how we handle this while the page is open.
+const INLINE_OPTIONS_ENABLED = Services.prefs.getBoolPref(
+ "extensions.htmlaboutaddons.inline-options.enabled"
+);
+const OPTIONS_TYPE_MAP = {
+ [AddonManager.OPTIONS_TYPE_TAB]: "tab",
+ [AddonManager.OPTIONS_TYPE_INLINE_BROWSER]: INLINE_OPTIONS_ENABLED
+ ? "inline"
+ : "tab",
+};
+
+// Check if an add-on has the provided options type, accounting for the pref
+// to disable inline options.
+function getOptionsType(addon, type) {
+ return OPTIONS_TYPE_MAP[addon.optionsType];
+}
+
+// Check whether the options page can be loaded in the current browser window.
+async function isAddonOptionsUIAllowed(addon) {
+ if (addon.type !== "extension" || !getOptionsType(addon)) {
+ // Themes never have options pages.
+ // Some plugins have preference pages, and they can always be shown.
+ // Extensions do not need to be checked if they do not have options pages.
+ return true;
+ }
+ if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
+ return true;
+ }
+ if (addon.incognito === "not_allowed") {
+ return false;
+ }
+ // The current page is in a private browsing window, and the add-on does not
+ // have the permission to access private browsing windows. Block access.
+ return (
+ allowPrivateBrowsingByDefault ||
+ // Note: This function is async because isAllowedInPrivateBrowsing is async.
+ isAllowedInPrivateBrowsing(addon)
+ );
+}
+
+/**
+ * This function is set in initialize() by the parent about:addons window. It
+ * is a helper for gViewController.loadView().
+ *
+ * @param {string} type The view type to load.
+ * @param {string} param The (optional) param for the view.
+ */
+let loadViewFn;
+
+/**
+ * This function is set in initialize() by the parent about:addons window. It
+ * is a helper for gViewController.replaceView(defaultViewId). This should be
+ * used to reset the view if we try to load an invalid view.
+ */
+let replaceWithDefaultViewFn;
+
+let _templates = {};
+
+/**
+ * Import a template from the main document.
+ */
+function importTemplate(name) {
+ if (!_templates.hasOwnProperty(name)) {
+ _templates[name] = document.querySelector(`template[name="${name}"]`);
+ }
+ let template = _templates[name];
+ if (template) {
+ return document.importNode(template.content, true);
+ }
+ throw new Error(`Unknown template: ${name}`);
+}
+
+function nl2br(text) {
+ let frag = document.createDocumentFragment();
+ let hasAppended = false;
+ for (let part of text.split("\n")) {
+ if (hasAppended) {
+ frag.appendChild(document.createElement("br"));
+ }
+ frag.appendChild(new Text(part));
+ hasAppended = true;
+ }
+ return frag;
+}
+
+/**
+ * Select the screeenshot to display above an add-on card.
+ *
+ * @param {AddonWrapper|DiscoAddonWrapper} addon
+ * @returns {string|null}
+ * The URL of the best fitting screenshot, if any.
+ */
+function getScreenshotUrlForAddon(addon) {
+ if (BUILTIN_THEME_PREVIEWS.has(addon.id)) {
+ return BUILTIN_THEME_PREVIEWS.get(addon.id);
+ }
+
+ let { screenshots } = addon;
+ if (!screenshots || !screenshots.length) {
+ return null;
+ }
+
+ // The image size is defined at .card-heading-image in aboutaddons.css, and
+ // is based on the aspect ratio for a 680x92 image. Use the image if possible,
+ // and otherwise fall back to the first image and hope for the best.
+ let screenshot = screenshots.find(s => s.width === 680 && s.height === 92);
+ if (!screenshot) {
+ console.warn(`Did not find screenshot with desired size for ${addon.id}.`);
+ screenshot = screenshots[0];
+ }
+ return screenshot.url;
+}
+
+/**
+ * Adds UTM parameters to a given URL, if it is an AMO URL.
+ *
+ * @param {string} contentAttribute
+ * Identifies the part of the UI with which the link is associated.
+ * @param {string} url
+ * @returns {string}
+ * The url with UTM parameters if it is an AMO URL.
+ * Otherwise the url in unmodified form.
+ */
+function formatUTMParams(contentAttribute, url) {
+ let parsedUrl = new URL(url);
+ let domain = `.${parsedUrl.hostname}`;
+ if (
+ !domain.endsWith(".mozilla.org") &&
+ // For testing: addons-dev.allizom.org and addons.allizom.org
+ !domain.endsWith(".allizom.org")
+ ) {
+ return url;
+ }
+
+ parsedUrl.searchParams.set("utm_source", "firefox-browser");
+ parsedUrl.searchParams.set("utm_medium", "firefox-browser");
+ parsedUrl.searchParams.set("utm_content", contentAttribute);
+ return parsedUrl.href;
+}
+
+// A wrapper around an item from the "results" array from AMO's discovery API.
+// See https://addons-server.readthedocs.io/en/latest/topics/api/discovery.html
+class DiscoAddonWrapper {
+ /**
+ * @param {object} details
+ * An item in the "results" array from AMO's discovery API.
+ */
+ constructor(details) {
+ // Reuse AddonRepository._parseAddon to have the AMO response parsing logic
+ // in one place.
+ let repositoryAddon = AddonRepository._parseAddon(details.addon);
+
+ // Note: Any property used by RecommendedAddonCard should appear here.
+ // The property names and values should have the same semantics as
+ // AddonWrapper, to ease the reuse of helper functions in this file.
+ this.id = repositoryAddon.id;
+ this.type = repositoryAddon.type;
+ this.name = repositoryAddon.name;
+ this.screenshots = repositoryAddon.screenshots;
+ this.sourceURI = repositoryAddon.sourceURI;
+ this.creator = repositoryAddon.creator;
+ this.averageRating = repositoryAddon.averageRating;
+
+ this.dailyUsers = details.addon.average_daily_users;
+
+ this.editorialDescription = details.description_text;
+ this.iconURL = details.addon.icon_url;
+ this.amoListingUrl = details.addon.url;
+ }
+}
+
+/**
+ * A helper to retrieve the list of recommended add-ons via AMO's discovery API.
+ */
+var DiscoveryAPI = {
+ // Map Promises from fetching the API results with or
+ // without a client ID. The `false` (no client ID) case could actually
+ // have been fetched with a client ID. See getResults() for more info.
+ _resultPromises: new Map(),
+
+ /**
+ * Fetch the list of recommended add-ons. The results are cached.
+ *
+ * Pending requests are coalesced, so there is only one request at any given
+ * time. If a request fails, the pending promises are rejected, but a new
+ * call will result in a new request. A succesful response is cached for the
+ * lifetime of the document.
+ *
+ * @param {boolean} preferClientId
+ * A boolean indicating a preference for using a client ID.
+ * This will not overwrite the user preference but will
+ * avoid sending a client ID if no request has been made yet.
+ * @returns {Promise}
+ */
+ async getResults(preferClientId = true) {
+ // Allow a caller to set preferClientId to false, but not true if discovery
+ // is disabled.
+ preferClientId = preferClientId && this.clientIdDiscoveryEnabled;
+
+ // Reuse a request for this preference first.
+ let resultPromise =
+ this._resultPromises.get(preferClientId) ||
+ // If the client ID isn't preferred, we can still reuse a request with the
+ // client ID.
+ (!preferClientId && this._resultPromises.get(true));
+
+ if (resultPromise) {
+ return resultPromise;
+ }
+
+ // Nothing is prepared for this preference, make a new request.
+ resultPromise = this._fetchRecommendedAddons(preferClientId).catch(e => {
+ // Delete the pending promise, so _fetchRecommendedAddons can be
+ // called again at the next property access.
+ this._resultPromises.delete(preferClientId);
+ Cu.reportError(e);
+ throw e;
+ });
+
+ // Store the new result for the preference.
+ this._resultPromises.set(preferClientId, resultPromise);
+
+ return resultPromise;
+ },
+
+ get clientIdDiscoveryEnabled() {
+ // These prefs match Discovery.jsm for enabling clientId cookies.
+ return (
+ Services.prefs.getBoolPref(PREF_RECOMMENDATION_ENABLED, false) &&
+ Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED, false) &&
+ !PrivateBrowsingUtils.isContentWindowPrivate(window)
+ );
+ },
+
+ async _fetchRecommendedAddons(useClientId) {
+ let discoveryApiUrl = new URL(
+ Services.urlFormatter.formatURLPref(PREF_DISCOVERY_API_URL)
+ );
+
+ if (useClientId) {
+ let clientId = await ClientID.getClientIdHash();
+ discoveryApiUrl.searchParams.set("telemetry-client-id", clientId);
+ }
+ let res = await fetch(discoveryApiUrl.href, {
+ credentials: "omit",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to fetch recommended add-ons, ${res.status}`);
+ }
+ let { results } = await res.json();
+ return results.map(details => new DiscoAddonWrapper(details));
+ },
+};
+
+class SupportLink extends HTMLAnchorElement {
+ static get observedAttributes() {
+ return ["support-page"];
+ }
+
+ connectedCallback() {
+ this.setHref();
+ this.setAttribute("target", "_blank");
+ }
+
+ attributeChangedCallback(name, oldVal, newVal) {
+ if (name === "support-page") {
+ this.setHref();
+ }
+ }
+
+ setHref() {
+ let base = SUPPORT_URL + this.getAttribute("support-page");
+ this.href = this.hasAttribute("utmcontent")
+ ? formatUTMParams(this.getAttribute("utmcontent"), base)
+ : base;
+ }
+}
+customElements.define("support-link", SupportLink, { extends: "a" });
+
+class PanelList extends HTMLElement {
+ static get observedAttributes() {
+ return ["open"];
+ }
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ // Ensure that the element is hidden even if its main stylesheet hasn't
+ // loaded yet. On initial load, or with cache disabled, the element could
+ // briefly flicker before the stylesheet is loaded without this.
+ let style = document.createElement("style");
+ style.textContent = `
+ :host(:not([open])) {
+ display: none;
+ }
+ `;
+ this.shadowRoot.appendChild(style);
+ this.shadowRoot.appendChild(importTemplate("panel-list"));
+ }
+
+ connectedCallback() {
+ this.setAttribute("role", "menu");
+ }
+
+ attributeChangedCallback(name, oldVal, newVal) {
+ if (name == "open" && newVal != oldVal) {
+ if (this.open) {
+ this.onShow();
+ } else {
+ this.onHide();
+ }
+ }
+ }
+
+ get open() {
+ return this.hasAttribute("open");
+ }
+
+ set open(val) {
+ this.toggleAttribute("open", val);
+ }
+
+ show(triggeringEvent) {
+ this.triggeringEvent = triggeringEvent;
+ this.open = true;
+ }
+
+ hide(triggeringEvent) {
+ let openingEvent = this.triggeringEvent;
+ this.triggeringEvent = triggeringEvent;
+ this.open = false;
+ // Refocus the button that opened the menu if we have one.
+ if (openingEvent && openingEvent.target) {
+ openingEvent.target.focus();
+ }
+ }
+
+ toggle(triggeringEvent) {
+ if (this.open) {
+ this.hide(triggeringEvent);
+ } else {
+ this.show(triggeringEvent);
+ }
+ }
+
+ async setAlign() {
+ // Set the showing attribute to hide the panel until its alignment is set.
+ this.setAttribute("showing", "true");
+ // Tell the parent node to hide any overflow in case the panel extends off
+ // the page before the alignment is set.
+ this.parentNode.style.overflow = "hidden";
+
+ // Wait for a layout flush, then find the bounds.
+ let {
+ anchorHeight,
+ anchorLeft,
+ anchorTop,
+ anchorWidth,
+ panelHeight,
+ panelWidth,
+ winHeight,
+ winScrollY,
+ winScrollX,
+ winWidth,
+ } = await new Promise(resolve => {
+ this.style.left = 0;
+ this.style.top = 0;
+
+ requestAnimationFrame(() =>
+ setTimeout(() => {
+ let anchorNode =
+ (this.triggeringEvent && this.triggeringEvent.target) ||
+ this.parentNode;
+ // Use y since top is reserved.
+ let anchorBounds = window.windowUtils.getBoundsWithoutFlushing(
+ anchorNode
+ );
+ let panelBounds = window.windowUtils.getBoundsWithoutFlushing(this);
+ resolve({
+ anchorHeight: anchorBounds.height,
+ anchorLeft: anchorBounds.left,
+ anchorTop: anchorBounds.top,
+ anchorWidth: anchorBounds.width,
+ panelHeight: panelBounds.height,
+ panelWidth: panelBounds.width,
+ winHeight: innerHeight,
+ winWidth: innerWidth,
+ winScrollX: scrollX,
+ winScrollY: scrollY,
+ });
+ }, 0)
+ );
+ });
+
+ // Calculate the left/right alignment.
+ let align;
+ let leftOffset;
+ // The tip of the arrow is 25px from the edge of the panel,
+ // but 26px looks right.
+ let arrowOffset = 26;
+ let leftAlignX = anchorLeft + anchorWidth / 2 - arrowOffset;
+ let rightAlignX = anchorLeft + anchorWidth / 2 - panelWidth + arrowOffset;
+ if (Services.locale.isAppLocaleRTL) {
+ // Prefer aligning on the right.
+ align = rightAlignX < 0 ? "left" : "right";
+ } else {
+ // Prefer aligning on the left.
+ align = leftAlignX + panelWidth > winWidth ? "right" : "left";
+ }
+ leftOffset = align === "left" ? leftAlignX : rightAlignX;
+
+ let bottomAlignY = anchorTop + anchorHeight;
+ let valign;
+ let topOffset;
+ if (bottomAlignY + panelHeight > winHeight) {
+ topOffset = anchorTop - panelHeight;
+ valign = "top";
+ } else {
+ topOffset = bottomAlignY;
+ valign = "bottom";
+ }
+
+ // Set the alignments and show the panel.
+ this.setAttribute("align", align);
+ this.setAttribute("valign", valign);
+ this.parentNode.style.overflow = "";
+
+ this.style.left = `${leftOffset + winScrollX}px`;
+ this.style.top = `${topOffset + winScrollY}px`;
+
+ this.removeAttribute("showing");
+ }
+
+ addHideListeners() {
+ // Hide when a panel-item is clicked in the list.
+ this.addEventListener("click", this);
+ document.addEventListener("keydown", this);
+ // Hide when a click is initiated outside the panel.
+ document.addEventListener("mousedown", this);
+ // Hide if focus changes and the panel isn't in focus.
+ document.addEventListener("focusin", this);
+ // Reset or focus tracking, we treat the first focusin differently.
+ this.focusHasChanged = false;
+ // Hide on resize, scroll or losing window focus.
+ window.addEventListener("resize", this);
+ window.addEventListener("scroll", this);
+ window.addEventListener("blur", this);
+ }
+
+ removeHideListeners() {
+ this.removeEventListener("click", this);
+ document.removeEventListener("keydown", this);
+ document.removeEventListener("mousedown", this);
+ document.removeEventListener("focusin", this);
+ window.removeEventListener("resize", this);
+ window.removeEventListener("scroll", this);
+ window.removeEventListener("blur", this);
+ }
+
+ handleEvent(e) {
+ // Ignore the event if it caused the panel to open.
+ if (e == this.triggeringEvent) {
+ return;
+ }
+
+ switch (e.type) {
+ case "resize":
+ case "scroll":
+ case "blur":
+ this.hide();
+ break;
+ case "click":
+ if (e.target.tagName == "PANEL-ITEM") {
+ this.hide();
+ } else {
+ // Avoid falling through to the default click handler of the
+ // add-on card, which would expand the add-on card.
+ e.stopPropagation();
+ }
+ break;
+ case "keydown":
+ if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Tab") {
+ // Ignore tabbing with a modifer other than shift.
+ if (e.key === "Tab" && (e.altKey || e.ctrlKey || e.metaKey)) {
+ return;
+ }
+
+ // Don't scroll the page or let the regular tab order take effect.
+ e.preventDefault();
+
+ // Keep moving to the next/previous element sibling until we find a
+ // panel-item that isn't hidden.
+ let moveForward =
+ e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey);
+
+ // If the menu is opened with the mouse, the active element might be
+ // somewhere else in the document. In that case we should ignore it
+ // to avoid walking unrelated DOM nodes.
+ this.focusWalker.currentNode = this.contains(document.activeElement)
+ ? document.activeElement
+ : this;
+ let nextItem = moveForward
+ ? this.focusWalker.nextNode()
+ : this.focusWalker.previousNode();
+
+ // If the next item wasn't found, try looping to the top/bottom.
+ if (!nextItem) {
+ this.focusWalker.currentNode = this;
+ if (moveForward) {
+ nextItem = this.focusWalker.firstChild();
+ } else {
+ nextItem = this.focusWalker.lastChild();
+ }
+ }
+ break;
+ } else if (e.key === "Escape") {
+ this.hide();
+ } else if (!e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
+ // Check if any of the children have an accesskey for this letter.
+ let item = this.querySelector(
+ `[accesskey="${e.key.toLowerCase()}"],
+ [accesskey="${e.key.toUpperCase()}"]`
+ );
+ if (item) {
+ item.click();
+ }
+ }
+ break;
+ case "mousedown":
+ case "focusin":
+ // There will be a focusin after the mousedown that opens the panel
+ // using the mouse. Ignore the first focusin event if it's on the
+ // triggering target.
+ if (
+ this.triggeringEvent &&
+ e.target == this.triggeringEvent.target &&
+ !this.focusHasChanged
+ ) {
+ this.focusHasChanged = true;
+ // If the target isn't in the panel, hide. This will close when focus
+ // moves out of the panel, or there's a click started outside the
+ // panel.
+ } else if (!e.target || e.target.closest("panel-list") != this) {
+ this.hide();
+ // Just record that there was a focusin event.
+ } else {
+ this.focusHasChanged = true;
+ }
+ break;
+ }
+ }
+
+ /**
+ * A TreeWalker that can be used to focus elements. The returned element will
+ * be the element that has gained focus based on the requested movement
+ * through the tree.
+ *
+ * Example:
+ *
+ * this.focusWalker.currentNode = this;
+ * // Focus and get the first focusable child.
+ * let focused = this.focusWalker.nextNode();
+ * // Focus the second focusable child.
+ * this.focusWalker.nextNode();
+ */
+ get focusWalker() {
+ if (!this._focusWalker) {
+ this._focusWalker = document.createTreeWalker(
+ this,
+ NodeFilter.SHOW_ELEMENT,
+ {
+ acceptNode: node => {
+ // No need to look at hidden nodes.
+ if (node.hidden) {
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ // Focus the node, if it worked then this is the node we want.
+ node.focus();
+ if (node === document.activeElement) {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+
+ // Continue into child nodes if the parent couldn't be focused.
+ return NodeFilter.FILTER_SKIP;
+ },
+ }
+ );
+ }
+ return this._focusWalker;
+ }
+
+ async onShow() {
+ this.sendEvent("showing");
+ this.addHideListeners();
+ await this.setAlign();
+
+ // Wait until the next paint for the alignment to be set and panel to be
+ // visible.
+ requestAnimationFrame(() => {
+ // Focus the first focusable panel-item.
+ this.focusWalker.currentNode = this;
+ this.focusWalker.nextNode();
+
+ this.sendEvent("shown");
+ });
+ }
+
+ onHide() {
+ requestAnimationFrame(() => this.sendEvent("hidden"));
+ this.removeHideListeners();
+ }
+
+ sendEvent(name, detail) {
+ this.dispatchEvent(new CustomEvent(name, { detail }));
+ }
+}
+customElements.define("panel-list", PanelList);
+
+class PanelItem extends HTMLElement {
+ static get observedAttributes() {
+ return ["accesskey"];
+ }
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+
+ let style = document.createElement("link");
+ style.rel = "stylesheet";
+ style.href = "chrome://mozapps/content/extensions/panel-item.css";
+
+ this.button = document.createElement("button");
+ this.button.setAttribute("role", "menuitem");
+
+ // Use a XUL label element to show the accesskey.
+ this.label = document.createXULElement("label");
+ this.button.appendChild(this.label);
+
+ let supportLinkSlot = document.createElement("slot");
+ supportLinkSlot.name = "support-link";
+
+ let defaultSlot = document.createElement("slot");
+ defaultSlot.style.display = "none";
+
+ this.shadowRoot.append(style, this.button, supportLinkSlot, defaultSlot);
+
+ // When our content changes, move the text into the label. It doesn't work
+ // with a , unfortunately.
+ new MutationObserver(() => {
+ this.label.textContent = defaultSlot
+ .assignedNodes()
+ .map(node => node.textContent)
+ .join("");
+ }).observe(this, { characterData: true, childList: true, subtree: true });
+ }
+
+ connectedCallback() {
+ this.panel = this.closest("panel-list");
+
+ if (this.panel) {
+ this.panel.addEventListener("hidden", this);
+ this.panel.addEventListener("shown", this);
+ }
+ }
+
+ disconnectedCallback() {
+ if (this.panel) {
+ this.panel.removeEventListener("hidden", this);
+ this.panel.removeEventListener("shown", this);
+ this.panel = null;
+ }
+ }
+
+ attributeChangedCallback(name, oldVal, newVal) {
+ if (name === "accesskey") {
+ // Bug 1037709 - Accesskey doesn't work in shadow DOM.
+ // Ideally we'd have the accesskey set in shadow DOM, and on
+ // attributeChangedCallback we'd just update the shadow DOM accesskey.
+
+ // Skip this change event if we caused it.
+ if (this._modifyingAccessKey) {
+ this._modifyingAccessKey = false;
+ return;
+ }
+
+ this.label.accessKey = newVal || "";
+
+ // Bug 1588156 - Accesskey is not ignored for hidden non-input elements.
+ // Since the accesskey won't be ignored, we need to remove it ourselves
+ // when the panel is closed, and move it back when it opens.
+ if (!this.panel || !this.panel.open) {
+ // When the panel isn't open, just store the key for later.
+ this._accessKey = newVal || null;
+ this._modifyingAccessKey = true;
+ this.accessKey = "";
+ } else {
+ this._accessKey = null;
+ }
+ }
+ }
+
+ get disabled() {
+ return this.button.hasAttribute("disabled");
+ }
+
+ set disabled(val) {
+ this.button.toggleAttribute("disabled", val);
+ }
+
+ get checked() {
+ return this.hasAttribute("checked");
+ }
+
+ set checked(val) {
+ this.toggleAttribute("checked", val);
+ }
+
+ focus() {
+ this.button.focus();
+ }
+
+ handleEvent(e) {
+ // Bug 1588156 - Accesskey is not ignored for hidden non-input elements.
+ // Since the accesskey won't be ignored, we need to remove it ourselves
+ // when the panel is closed, and move it back when it opens.
+ switch (e.type) {
+ case "shown":
+ if (this._accessKey) {
+ this.accessKey = this._accessKey;
+ this._accessKey = null;
+ }
+ break;
+ case "hidden":
+ if (this.accessKey) {
+ this._accessKey = this.accessKey;
+ this._modifyingAccessKey = true;
+ this.accessKey = "";
+ }
+ break;
+ }
+ }
+}
+customElements.define("panel-item", PanelItem);
+
+class SearchAddons extends HTMLElement {
+ connectedCallback() {
+ if (this.childElementCount === 0) {
+ this.input = document.createXULElement("search-textbox");
+ this.input.setAttribute("searchbutton", true);
+ this.input.setAttribute("maxlength", 100);
+ this.input.setAttribute("data-l10n-attrs", "placeholder");
+ document.l10n.setAttributes(this.input, "addons-heading-search-input");
+ this.append(this.input);
+ }
+ this.input.addEventListener("command", this);
+ document.addEventListener("keypress", this);
+ }
+
+ disconnectedCallback() {
+ this.input.removeEventListener("command", this);
+ document.removeEventListener("keypress", this);
+ }
+
+ focus() {
+ this.input.focus();
+ }
+
+ get focusKey() {
+ return this.getAttribute("key");
+ }
+
+ handleEvent(e) {
+ if (e.type === "command") {
+ this.searchAddons(this.value);
+ } else if (e.type === "keypress") {
+ if (e.key === "/" && !e.ctrlKey && !e.metaKey && !e.altKey) {
+ this.focus();
+ } else if (e.key == this.focusKey) {
+ if (e.altKey || e.shiftKey) {
+ return;
+ }
+
+ if (Services.appinfo.OS === "Darwin") {
+ if (e.metaKey && !e.ctrlKey) {
+ this.focus();
+ }
+ } else if (e.ctrlKey && !e.metaKey) {
+ this.focus();
+ }
+ }
+ }
+ }
+
+ get value() {
+ return this.input.value;
+ }
+
+ searchAddons(query) {
+ if (query.length === 0) {
+ return;
+ }
+
+ let url = formatUTMParams(
+ "addons-manager-search",
+ AddonRepository.getSearchURL(query)
+ );
+
+ let browser = getBrowserElement();
+ let chromewin = browser.ownerGlobal;
+ chromewin.openLinkIn(url, "tab", {
+ fromChrome: true,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ });
+
+ AMTelemetry.recordLinkEvent({
+ object: "aboutAddons",
+ value: "search",
+ extra: {
+ type: this.closest("addon-page-header").getAttribute("type"),
+ view: getTelemetryViewName(this),
+ },
+ });
+ }
+}
+customElements.define("search-addons", SearchAddons);
+
+class GlobalWarnings extends MessageBarStackElement {
+ constructor() {
+ super();
+ // This won't change at runtime, but we'll want to fake it in tests.
+ this.inSafeMode = Services.appinfo.inSafeMode;
+ this.globalWarning = null;
+ }
+
+ connectedCallback() {
+ this.refresh();
+ this.addEventListener("click", this);
+ AddonManagerListenerHandler.addListener(this);
+ }
+
+ disconnectedCallback() {
+ this.removeEventListener("click", this);
+ AddonManagerListenerHandler.removeListener(this);
+ }
+
+ refresh() {
+ if (this.inSafeMode) {
+ this.setWarning("safe-mode");
+ } else if (
+ AddonManager.checkUpdateSecurityDefault &&
+ !AddonManager.checkUpdateSecurity
+ ) {
+ this.setWarning("update-security", { action: true });
+ } else if (!AddonManager.checkCompatibility) {
+ this.setWarning("check-compatibility", { action: true });
+ } else {
+ this.removeWarning();
+ }
+ }
+
+ setWarning(type, opts) {
+ if (
+ this.globalWarning &&
+ this.globalWarning.getAttribute("warning-type") !== type
+ ) {
+ this.removeWarning();
+ }
+ if (!this.globalWarning) {
+ this.globalWarning = document.createElement("message-bar");
+ this.globalWarning.setAttribute("warning-type", type);
+ let textContainer = document.createElement("span");
+ document.l10n.setAttributes(textContainer, `extensions-warning-${type}`);
+ this.globalWarning.appendChild(textContainer);
+ if (opts && opts.action) {
+ let button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ `extensions-warning-${type}-button`
+ );
+ button.setAttribute("action", type);
+ this.globalWarning.appendChild(button);
+ }
+ this.appendChild(this.globalWarning);
+ }
+ }
+
+ removeWarning() {
+ if (this.globalWarning) {
+ this.globalWarning.remove();
+ this.globalWarning = null;
+ }
+ }
+
+ handleEvent(e) {
+ if (e.type === "click") {
+ switch (e.target.getAttribute("action")) {
+ case "update-security":
+ AddonManager.checkUpdateSecurity = true;
+ break;
+ case "check-compatibility":
+ AddonManager.checkCompatibility = true;
+ break;
+ }
+ }
+ }
+
+ /**
+ * AddonManager listener events.
+ */
+
+ onCompatibilityModeChanged() {
+ this.refresh();
+ }
+
+ onCheckUpdateSecurityChanged() {
+ this.refresh();
+ }
+}
+customElements.define("global-warnings", GlobalWarnings);
+
+class AddonPageHeader extends HTMLElement {
+ connectedCallback() {
+ if (this.childElementCount === 0) {
+ this.appendChild(importTemplate("addon-page-header"));
+ this.heading = this.querySelector(".header-name");
+ this.backButton = this.querySelector(".back-button");
+ this.pageOptionsMenuButton = this.querySelector(
+ '[action="page-options"]'
+ );
+ // The addon-page-options element is outside of this element since this is
+ // position: sticky and that would break the positioning of the menu.
+ this.pageOptionsMenu = document.getElementById(
+ this.getAttribute("page-options-id")
+ );
+ }
+ this.addEventListener("click", this);
+ this.addEventListener("mousedown", this);
+ // Use capture since the event is actually triggered on the internal
+ // panel-list and it doesn't bubble.
+ this.pageOptionsMenu.addEventListener("shown", this, true);
+ this.pageOptionsMenu.addEventListener("hidden", this, true);
+ }
+
+ disconnectedCallback() {
+ this.removeEventListener("click", this);
+ this.removeEventListener("mousedown", this);
+ this.pageOptionsMenu.removeEventListener("shown", this, true);
+ this.pageOptionsMenu.removeEventListener("hidden", this, true);
+ }
+
+ setViewInfo({ type, param }) {
+ this.setAttribute("current-view", type);
+ this.setAttribute("current-param", param);
+ let viewType = type === "list" ? param : type;
+ this.setAttribute("type", viewType);
+
+ this.heading.hidden = viewType === "detail";
+ this.backButton.hidden = viewType !== "detail" && viewType !== "shortcuts";
+
+ let { contentWindow } = getBrowserElement();
+ this.backButton.disabled = !contentWindow.history.state?.previousView;
+
+ if (viewType !== "detail") {
+ document.l10n.setAttributes(this.heading, `${viewType}-heading`);
+ }
+ }
+
+ handleEvent(e) {
+ let { backButton, pageOptionsMenu, pageOptionsMenuButton } = this;
+ if (e.type === "click") {
+ switch (e.target) {
+ case backButton:
+ window.history.back();
+ break;
+ case pageOptionsMenuButton:
+ if (e.mozInputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
+ this.pageOptionsMenu.toggle(e);
+ }
+ break;
+ }
+ } else if (
+ e.type == "mousedown" &&
+ e.target == pageOptionsMenuButton &&
+ e.button == 0
+ ) {
+ this.pageOptionsMenu.toggle(e);
+ } else if (
+ e.target == pageOptionsMenu.panel &&
+ (e.type == "shown" || e.type == "hidden")
+ ) {
+ this.pageOptionsMenuButton.setAttribute(
+ "aria-expanded",
+ this.pageOptionsMenu.open
+ );
+ }
+ }
+}
+customElements.define("addon-page-header", AddonPageHeader);
+
+class AddonUpdatesMessage extends HTMLElement {
+ static get observedAttributes() {
+ return ["state"];
+ }
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ let style = document.createElement("style");
+ style.textContent = `
+ @import "chrome://global/skin/in-content/common.css";
+ button {
+ margin: 0;
+ }
+ `;
+ this.message = document.createElement("span");
+ this.message.hidden = true;
+ this.button = document.createElement("button");
+ this.button.addEventListener("click", e => {
+ if (e.button === 0) {
+ loadViewFn("updates/available");
+ }
+ });
+ this.button.hidden = true;
+ this.shadowRoot.append(style, this.message, this.button);
+ }
+
+ connectedCallback() {
+ document.l10n.connectRoot(this.shadowRoot);
+ document.l10n.translateFragment(this.shadowRoot);
+ }
+
+ disconnectedCallback() {
+ document.l10n.disconnectRoot(this.shadowRoot);
+ }
+
+ attributeChangedCallback(name, oldVal, newVal) {
+ if (name === "state" && oldVal !== newVal) {
+ let l10nId = `addon-updates-${newVal}`;
+ switch (newVal) {
+ case "updating":
+ case "installed":
+ case "none-found":
+ this.button.hidden = true;
+ this.message.hidden = false;
+ document.l10n.setAttributes(this.message, l10nId);
+ break;
+ case "manual-updates-found":
+ this.message.hidden = true;
+ this.button.hidden = false;
+ document.l10n.setAttributes(this.button, l10nId);
+ break;
+ }
+ }
+ }
+
+ set state(val) {
+ this.setAttribute("state", val);
+ }
+}
+customElements.define("addon-updates-message", AddonUpdatesMessage);
+
+class AddonPageOptions extends HTMLElement {
+ connectedCallback() {
+ if (this.childElementCount === 0) {
+ this.render();
+ }
+ this.addEventListener("click", this);
+ this.panel.addEventListener("showing", this);
+ AddonManagerListenerHandler.addListener(this);
+ }
+
+ disconnectedCallback() {
+ this.removeEventListener("click", this);
+ this.panel.removeEventListener("showing", this);
+ AddonManagerListenerHandler.removeListener(this);
+ }
+
+ toggle(...args) {
+ return this.panel.toggle(...args);
+ }
+
+ get open() {
+ return this.panel.open;
+ }
+
+ render() {
+ this.appendChild(importTemplate("addon-page-options"));
+ this.panel = this.querySelector("panel-list");
+ this.installFromFile = this.querySelector('[action="install-from-file"]');
+ this.toggleUpdatesEl = this.querySelector(
+ '[action="set-update-automatically"]'
+ );
+ this.resetUpdatesEl = this.querySelector('[action="reset-update-states"]');
+ this.onUpdateModeChanged();
+ }
+
+ async handleEvent(e) {
+ if (e.type === "click") {
+ e.target.disabled = true;
+ try {
+ await this.onClick(e);
+ } finally {
+ e.target.disabled = false;
+ }
+ } else if (e.type === "showing") {
+ this.installFromFile.hidden = !XPINSTALL_ENABLED;
+ }
+ }
+
+ async onClick(e) {
+ switch (e.target.getAttribute("action")) {
+ case "check-for-updates":
+ await this.checkForUpdates();
+ break;
+ case "view-recent-updates":
+ loadViewFn("updates/recent");
+ break;
+ case "install-from-file":
+ if (XPINSTALL_ENABLED) {
+ installAddonsFromFilePicker().then(installs => {
+ for (let install of installs) {
+ this.recordActionEvent({
+ action: "installFromFile",
+ value: install.installId,
+ });
+ }
+ });
+ }
+ break;
+ case "debug-addons":
+ this.openAboutDebugging();
+ break;
+ case "set-update-automatically":
+ await this.toggleAutomaticUpdates();
+ break;
+ case "reset-update-states":
+ await this.resetAutomaticUpdates();
+ break;
+ case "manage-shortcuts":
+ loadViewFn("shortcuts/shortcuts");
+ break;
+ }
+ }
+
+ async checkForUpdates(e) {
+ this.recordActionEvent({ action: "checkForUpdates" });
+ let message = document.getElementById("updates-message");
+ message.state = "updating";
+ message.hidden = false;
+ let { installed, pending } = await checkForUpdates();
+ if (pending > 0) {
+ message.state = "manual-updates-found";
+ } else if (installed > 0) {
+ message.state = "installed";
+ } else {
+ message.state = "none-found";
+ }
+ }
+
+ openAboutDebugging() {
+ let mainWindow = window.windowRoot.ownerGlobal;
+ this.recordLinkEvent({ value: "about:debugging" });
+ if ("switchToTabHavingURI" in mainWindow) {
+ let principal = Services.scriptSecurityManager.getSystemPrincipal();
+ mainWindow.switchToTabHavingURI(
+ `about:debugging#/runtime/this-firefox`,
+ true,
+ {
+ ignoreFragment: "whenComparing",
+ triggeringPrincipal: principal,
+ }
+ );
+ }
+ }
+
+ automaticUpdatesEnabled() {
+ return AddonManager.updateEnabled && AddonManager.autoUpdateDefault;
+ }
+
+ toggleAutomaticUpdates() {
+ if (!this.automaticUpdatesEnabled()) {
+ // One or both of the prefs is false, i.e. the checkbox is not
+ // checked. Now toggle both to true. If the user wants us to
+ // auto-update add-ons, we also need to auto-check for updates.
+ AddonManager.updateEnabled = true;
+ AddonManager.autoUpdateDefault = true;
+ } else {
+ // Both prefs are true, i.e. the checkbox is checked.
+ // Toggle the auto pref to false, but don't touch the enabled check.
+ AddonManager.autoUpdateDefault = false;
+ }
+ // Record telemetry for changing the update policy.
+ let updatePolicy = [];
+ if (AddonManager.autoUpdateDefault) {
+ updatePolicy.push("default");
+ }
+ if (AddonManager.updateEnabled) {
+ updatePolicy.push("enabled");
+ }
+ this.recordActionEvent({
+ action: "setUpdatePolicy",
+ value: updatePolicy.join(","),
+ });
+ }
+
+ async resetAutomaticUpdates() {
+ let addons = await AddonManager.getAllAddons();
+ for (let addon of addons) {
+ if ("applyBackgroundUpdates" in addon) {
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+ }
+ }
+ this.recordActionEvent({ action: "resetUpdatePolicy" });
+ }
+
+ getTelemetryViewName() {
+ return getTelemetryViewName(document.getElementById("page-header"));
+ }
+
+ recordActionEvent({ action, value }) {
+ AMTelemetry.recordActionEvent({
+ object: "aboutAddons",
+ view: this.getTelemetryViewName(),
+ action,
+ addon: this.addon,
+ value,
+ });
+ }
+
+ recordLinkEvent({ value }) {
+ AMTelemetry.recordLinkEvent({
+ object: "aboutAddons",
+ value,
+ extra: {
+ view: this.getTelemetryViewName(),
+ },
+ });
+ }
+
+ /**
+ * AddonManager listener events.
+ */
+
+ onUpdateModeChanged() {
+ let updatesEnabled = this.automaticUpdatesEnabled();
+ this.toggleUpdatesEl.checked = updatesEnabled;
+ let resetType = updatesEnabled ? "automatic" : "manual";
+ let resetStringId = `addon-updates-reset-updates-to-${resetType}`;
+ document.l10n.setAttributes(this.resetUpdatesEl, resetStringId);
+ }
+}
+customElements.define("addon-page-options", AddonPageOptions);
+
+class CategoryButton extends HTMLButtonElement {
+ connectedCallback() {
+ if (this.childElementCount != 0) {
+ return;
+ }
+
+ // Make sure the aria-selected attribute is set correctly.
+ this.selected = this.hasAttribute("selected");
+
+ document.l10n.setAttributes(this, `addon-category-${this.name}-title`);
+
+ let text = document.createElement("span");
+ text.classList.add("category-name");
+ document.l10n.setAttributes(text, `addon-category-${this.name}`);
+
+ this.append(text);
+ }
+
+ load() {
+ loadViewFn(this.viewId);
+ }
+
+ get isVisible() {
+ return true;
+ }
+
+ get badgeCount() {
+ return parseInt(this.getAttribute("badge-count"), 10) || 0;
+ }
+
+ set badgeCount(val) {
+ let count = parseInt(val, 10);
+ if (count) {
+ this.setAttribute("badge-count", count);
+ } else {
+ this.removeAttribute("badge-count");
+ }
+ }
+
+ get selected() {
+ return this.hasAttribute("selected");
+ }
+
+ set selected(val) {
+ this.toggleAttribute("selected", !!val);
+ this.setAttribute("aria-selected", !!val);
+ }
+
+ get name() {
+ return this.getAttribute("name");
+ }
+
+ get viewId() {
+ return this.getAttribute("viewid");
+ }
+
+ // Just setting the hidden attribute isn't enough in case the category gets
+ // hidden while about:addons is closed since it could be the last active view
+ // which will unhide the button when it gets selected.
+ get defaultHidden() {
+ return this.hasAttribute("default-hidden");
+ }
+}
+customElements.define("category-button", CategoryButton, { extends: "button" });
+
+class DiscoverButton extends CategoryButton {
+ get isVisible() {
+ return isDiscoverEnabled();
+ }
+}
+customElements.define("discover-button", DiscoverButton, { extends: "button" });
+
+class CategoriesBox extends customElements.get("button-group") {
+ constructor() {
+ super();
+ // This will resolve when the initial category states have been set from
+ // our cached prefs. This is intended for use in testing to verify that we
+ // are caching the previous state.
+ this.promiseRendered = new Promise(resolve => {
+ this._resolveRendered = resolve;
+ });
+ // This will resolve when the final category states have been set by
+ // checking the AddonManager state and showing/hiding categories. The page
+ // won't be "initialized" until this resolves.
+ this.promiseInitialized = new Promise(resolve => {
+ this._resolveInitialized = resolve;
+ });
+ }
+
+ async initialize() {
+ let addonTypesObjects = AddonManager.addonTypes;
+ let addonTypes = new Set();
+ for (let type in addonTypesObjects) {
+ addonTypes.add(type);
+ }
+
+ let hiddenTypes = new Set([]);
+
+ for (let button of this.children) {
+ let { defaultHidden, name } = button;
+ button.hidden =
+ !button.isVisible || (defaultHidden && this.shouldHideCategory(name));
+
+ if (defaultHidden && addonTypes.has(name)) {
+ hiddenTypes.add(name);
+ }
+ }
+
+ let hiddenUpdated;
+ if (hiddenTypes.size) {
+ hiddenUpdated = this.updateHiddenCategories(Array.from(hiddenTypes));
+ }
+
+ this.updateAvailableCount();
+
+ this.addEventListener("click", e => {
+ let button = e.target.closest("[viewid]");
+ if (button) {
+ button.load();
+ }
+ });
+ this.addEventListener("button-group:key-selected", e => {
+ this.activeChild.load();
+ });
+
+ AddonManagerListenerHandler.addListener(this);
+
+ this._resolveRendered();
+ await hiddenUpdated;
+ this._resolveInitialized();
+ }
+
+ get initialViewId() {
+ let viewId = Services.prefs.getStringPref(PREF_UI_LASTCATEGORY, "");
+ // If the pref value is a valid top-level view then use that viewId.
+ if (this.getButtonByViewId(viewId)) {
+ return viewId;
+ }
+ // Otherwise, use the first viewId that can be shown.
+ for (let button of this.children) {
+ if (!button.defaultHidden && !button.hidden && button.isVisible) {
+ return button.viewId;
+ }
+ }
+ // If there aren't any available views then there's nothing to load. This
+ // shouldn't happen though since the extension list should always be valid.
+ throw new Error("Couldn't find initial view to load");
+ }
+
+ shouldHideCategory(name) {
+ return Services.prefs.getBoolPref(`extensions.ui.${name}.hidden`, true);
+ }
+
+ setShouldHideCategory(name, hide) {
+ Services.prefs.setBoolPref(`extensions.ui.${name}.hidden`, hide);
+ }
+
+ getButtonByName(name) {
+ return this.querySelector(`[name="${name}"]`);
+ }
+
+ getButtonByViewId(id) {
+ return this.querySelector(`[viewid="${id}"]`);
+ }
+
+ get selectedChild() {
+ return this._selectedChild;
+ }
+
+ set selectedChild(node) {
+ if (node && this.contains(node)) {
+ if (this._selectedChild) {
+ this._selectedChild.selected = false;
+ }
+ this._selectedChild = node;
+ this._selectedChild.selected = true;
+ }
+ }
+
+ select(viewId) {
+ let button = this.querySelector(`[viewid="${viewId}"]`);
+ if (button) {
+ this.activeChild = button;
+ this.selectedChild = button;
+ button.hidden = false;
+ Services.prefs.setStringPref(PREF_UI_LASTCATEGORY, viewId);
+ }
+ }
+
+ selectType(type) {
+ this.select(`addons://list/${type}`);
+ }
+
+ onInstalled(addon) {
+ let button = this.getButtonByName(addon.type);
+ if (button) {
+ button.hidden = false;
+ this.setShouldHideCategory(addon.type, false);
+ }
+ this.updateAvailableCount();
+ }
+
+ onInstallStarted(install) {
+ this.onInstalled(install);
+ }
+
+ onNewInstall() {
+ this.updateAvailableCount();
+ }
+
+ onInstallPostponed() {
+ this.updateAvailableCount();
+ }
+
+ onInstallCancelled() {
+ this.updateAvailableCount();
+ }
+
+ async updateAvailableCount() {
+ let installs = await AddonManager.getAllInstalls();
+ var count = installs.filter(install => {
+ return isManualUpdate(install) && !install.installed;
+ }).length;
+ let availableButton = this.getButtonByName("available-updates");
+ availableButton.hidden = !availableButton.selected && count == 0;
+ availableButton.badgeCount = count;
+ }
+
+ async updateHiddenCategories(types) {
+ let hiddenTypes = new Set(types);
+ let getAddons = AddonManager.getAddonsByTypes(types);
+ let getInstalls = AddonManager.getInstallsByTypes(types);
+
+ for (let addon of await getAddons) {
+ if (addon.hidden) {
+ continue;
+ }
+
+ this.onInstalled(addon);
+ hiddenTypes.delete(addon.type);
+
+ if (!hiddenTypes.size) {
+ return;
+ }
+ }
+
+ for (let install of await getInstalls) {
+ if (
+ install.existingAddon ||
+ install.state == AddonManager.STATE_AVAILABLE
+ ) {
+ continue;
+ }
+
+ this.onInstalled(install);
+ hiddenTypes.delete(install.type);
+
+ if (!hiddenTypes.size) {
+ return;
+ }
+ }
+
+ for (let type of hiddenTypes) {
+ let button = this.getButtonByName(type);
+ if (button.selected) {
+ // Cancel the load if this view should be hidden.
+ replaceWithDefaultViewFn();
+ }
+ this.setShouldHideCategory(type, true);
+ button.hidden = true;
+ }
+ }
+}
+customElements.define("categories-box", CategoriesBox);
+
+class SidebarFooter extends HTMLElement {
+ connectedCallback() {
+ let list = document.createElement("ul");
+ list.classList.add("sidebar-footer-list");
+
+ let prefsItem = document.createElement("li");
+ prefsItem.classList.add("sidebar-footer-item");
+ let prefsLink = document.createElement("a");
+ prefsLink.classList.add("sidebar-footer-link", "preferences-icon");
+ prefsLink.id = "preferencesButton";
+ prefsLink.href = "about:preferences";
+ document.l10n.setAttributes(prefsLink, "sidebar-preferences-button-title");
+ let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+ prefsLink.addEventListener("click", e => {
+ e.preventDefault();
+ AMTelemetry.recordLinkEvent({
+ object: "aboutAddons",
+ value: "about:preferences",
+ extra: {
+ view: getTelemetryViewName(this),
+ },
+ });
+ windowRoot.ownerGlobal.switchToTabHavingURI("about:preferences", true, {
+ ignoreFragment: "whenComparing",
+ triggeringPrincipal: systemPrincipal,
+ });
+ });
+ let prefsText = document.createElement("span");
+ prefsText.classList.add("sidebar-footer-link-text");
+ document.l10n.setAttributes(prefsText, "preferences");
+ prefsLink.append(prefsText);
+ prefsItem.append(prefsLink);
+
+ let supportItem = document.createElement("li");
+ supportItem.classList.add("sidebar-footer-item");
+ let supportLink = document.createElement("a", { is: "support-link" });
+ document.l10n.setAttributes(supportLink, "sidebar-help-button-title");
+ supportLink.classList.add("sidebar-footer-link", "help-icon");
+ supportLink.id = "help-button";
+ supportLink.setAttribute("support-page", "addons-help");
+ supportLink.addEventListener("click", e => {
+ AMTelemetry.recordLinkEvent({
+ object: "aboutAddons",
+ value: "support",
+ extra: {
+ view: getTelemetryViewName(this),
+ },
+ });
+ });
+ let supportText = document.createElement("span");
+ supportText.classList.add("sidebar-footer-link-text");
+ document.l10n.setAttributes(supportText, "help-button");
+ supportLink.append(supportText);
+ supportItem.append(supportLink);
+
+ list.append(prefsItem, supportItem);
+ this.append(list);
+ }
+}
+customElements.define("sidebar-footer", SidebarFooter, { extends: "footer" });
+
+class AddonOptions extends HTMLElement {
+ connectedCallback() {
+ if (!this.children.length) {
+ this.render();
+ }
+ }
+
+ get panel() {
+ return this.querySelector("panel-list");
+ }
+
+ updateSeparatorsVisibility() {
+ let lastSeparator;
+ let elWasVisible = false;
+
+ // Collect the panel-list children that are not already hidden.
+ const children = Array.from(this.panel.children).filter(el => !el.hidden);
+
+ for (let child of children) {
+ if (child.tagName == "PANEL-ITEM-SEPARATOR") {
+ child.hidden = !elWasVisible;
+ if (!child.hidden) {
+ lastSeparator = child;
+ }
+ elWasVisible = false;
+ } else {
+ elWasVisible = true;
+ }
+ }
+ if (!elWasVisible && lastSeparator) {
+ lastSeparator.hidden = true;
+ }
+ }
+
+ get template() {
+ return "addon-options";
+ }
+
+ render() {
+ this.appendChild(importTemplate(this.template));
+ }
+
+ setElementState(el, card, addon, updateInstall) {
+ switch (el.getAttribute("action")) {
+ case "remove":
+ if (hasPermission(addon, "uninstall")) {
+ // Regular add-on that can be uninstalled.
+ el.disabled = false;
+ el.hidden = false;
+ document.l10n.setAttributes(el, "remove-addon-button");
+ } else if (addon.isBuiltin) {
+ // Likely the built-in themes, can't be removed, that's fine.
+ el.hidden = true;
+ } else {
+ // Likely sideloaded, mention that it can't be removed with a link.
+ el.hidden = false;
+ el.disabled = true;
+ if (!el.querySelector('[slot="support-link"]')) {
+ let link = document.createElement("a", { is: "support-link" });
+ link.setAttribute("data-l10n-name", "link");
+ link.setAttribute("support-page", "cant-remove-addon");
+ link.setAttribute("slot", "support-link");
+ el.appendChild(link);
+ document.l10n.setAttributes(el, "remove-addon-disabled-button");
+ }
+ }
+ break;
+ case "report":
+ el.hidden = !isAbuseReportSupported(addon);
+ break;
+ case "install-update":
+ el.hidden = !updateInstall;
+ break;
+ case "expand":
+ el.hidden = card.expanded;
+ break;
+ case "preferences":
+ el.hidden =
+ getOptionsType(addon) !== "tab" &&
+ (getOptionsType(addon) !== "inline" || card.expanded);
+ if (!el.hidden) {
+ isAddonOptionsUIAllowed(addon).then(allowed => {
+ el.hidden = !allowed;
+ });
+ }
+ break;
+ }
+ }
+
+ update(card, addon, updateInstall) {
+ for (let el of this.items) {
+ this.setElementState(el, card, addon, updateInstall);
+ }
+
+ // Update the separators visibility based on the updated visibility
+ // of the actions in the panel-list.
+ this.updateSeparatorsVisibility();
+ }
+
+ get items() {
+ return this.querySelectorAll("panel-item");
+ }
+
+ get visibleItems() {
+ return Array.from(this.items).filter(item => !item.hidden);
+ }
+}
+customElements.define("addon-options", AddonOptions);
+
+class PluginOptions extends AddonOptions {
+ get template() {
+ return "plugin-options";
+ }
+
+ setElementState(el, card, addon) {
+ const userDisabledStates = {
+ "ask-to-activate": AddonManager.STATE_ASK_TO_ACTIVATE,
+ "always-activate": false,
+ "never-activate": true,
+ };
+ const action = el.getAttribute("action");
+ if (action in userDisabledStates) {
+ let userDisabled = userDisabledStates[action];
+ el.checked = addon.userDisabled === userDisabled;
+ let resultProp =
+ action == "always-activate" && addon.isFlashPlugin
+ ? "hidden"
+ : "disabled";
+ el[resultProp] = !(el.checked || hasPermission(addon, action));
+ } else {
+ super.setElementState(el, card, addon);
+ }
+ }
+}
+customElements.define("plugin-options", PluginOptions);
+
+class FiveStarRating extends HTMLElement {
+ static get observedAttributes() {
+ return ["rating"];
+ }
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot.append(importTemplate("five-star-rating"));
+ }
+
+ set rating(v) {
+ this.setAttribute("rating", v);
+ }
+
+ get rating() {
+ let v = parseFloat(this.getAttribute("rating"), 10);
+ if (v >= 0 && v <= 5) {
+ return v;
+ }
+ return 0;
+ }
+
+ get ratingBuckets() {
+ // 0 <= x < 0.25 = empty
+ // 0.25 <= x < 0.75 = half
+ // 0.75 <= x <= 1 = full
+ // ... et cetera, until x <= 5.
+ let { rating } = this;
+ return [0, 1, 2, 3, 4].map(ratingStart => {
+ let distanceToFull = rating - ratingStart;
+ if (distanceToFull < 0.25) {
+ return "empty";
+ }
+ if (distanceToFull < 0.75) {
+ return "half";
+ }
+ return "full";
+ });
+ }
+
+ connectedCallback() {
+ this.renderRating();
+ }
+
+ attributeChangedCallback() {
+ this.renderRating();
+ }
+
+ renderRating() {
+ let starElements = this.shadowRoot.querySelectorAll(".rating-star");
+ for (let [i, part] of this.ratingBuckets.entries()) {
+ starElements[i].setAttribute("fill", part);
+ }
+ document.l10n.setAttributes(this, "five-star-rating", {
+ rating: this.rating,
+ });
+ }
+}
+customElements.define("five-star-rating", FiveStarRating);
+
+class ContentSelectDropdown extends HTMLElement {
+ connectedCallback() {
+ if (this.children.length) {
+ return;
+ }
+ // This creates the menulist and menupopup elements needed for the inline
+ // browser to support
+
+
+ `,
+ "options.js": () => {
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "get-version") {
+ let version = document.querySelector("pre").textContent;
+ browser.test.sendMessage("version", version);
+ }
+ });
+ window.onload = () => browser.test.sendMessage("options-loaded");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ return extension;
+ }
+
+ let firstExtension = await loadExtension("1");
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(doc, id);
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+ let browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ await browserAdded;
+
+ await firstExtension.awaitMessage("options-loaded");
+ await firstExtension.sendMessage("get-version");
+ let version = await firstExtension.awaitMessage("version");
+ is(version, "1", "Version 1 page is loaded");
+
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ browserAdded = waitOptionsBrowserInserted();
+ let secondExtension = await loadExtension("2");
+ await updated;
+ await browserAdded;
+ await secondExtension.awaitMessage("options-loaded");
+
+ await secondExtension.sendMessage("get-version");
+ version = await secondExtension.awaitMessage("version");
+ is(version, "2", "Version 2 page is loaded");
+ let { deck } = card.details;
+ is(deck.selectedViewName, "preferences", "Preferences are still shown");
+
+ await closeView(win);
+ await firstExtension.unload();
+ await secondExtension.unload();
+});
+
+add_task(async function testReloadExtension() {
+ let id = "reload@mochi.test";
+ let xpiFile = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ applications: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+
+
+ Options
+
+
+ `,
+ },
+ });
+ let addon = await AddonManager.installTemporaryAddon(xpiFile);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = getAddonCard(doc, id);
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+ let { deck } = card.details;
+ is(deck.selectedViewName, "details", "Details load first");
+
+ let browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ await browserAdded;
+
+ is(deck.selectedViewName, "preferences", "Preferences are shown");
+
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ browserAdded = waitOptionsBrowserInserted();
+ let addonStarted = AddonTestUtils.promiseWebExtensionStartup(id);
+ await addon.reload();
+ await addonStarted;
+ await updated;
+ await browserAdded;
+ is(deck.selectedViewName, "preferences", "Preferences are still shown");
+
+ await closeView(win);
+ await addon.uninstall();
+});
+
+async function testOptionsZoom(type = "full") {
+ let id = `${type}-zoom@mochi.test`;
+ let zoomProp = `${type}Zoom`;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+ files: {
+ "options.html": `
+
+
+ Some text
+
+
+ `,
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ gBrowser.selectedBrowser[zoomProp] = 2;
+
+ let card = getAddonCard(doc, id);
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+
+ let browserAdded = waitOptionsBrowserInserted();
+ card.querySelector('.tab-button[name="preferences"]').click();
+ let optionsBrowser = await browserAdded;
+
+ is(optionsBrowser[zoomProp], 2, `Options browser inherited ${zoomProp}`);
+
+ gBrowser.selectedBrowser[zoomProp] = 0.5;
+
+ is(
+ optionsBrowser[zoomProp],
+ 0.5,
+ `Options browser reacts to ${zoomProp} change`
+ );
+
+ await closeView(win);
+ await extension.unload();
+}
+
+add_task(function testOptionsFullZoom() {
+ return testOptionsZoom("full");
+});
+
+add_task(function testOptionsTextZoom() {
+ return testOptionsZoom("text");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js
new file mode 100644
index 0000000000..94375c68b3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_options_ui_in_tab.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+add_task(async function enableHtmlViews() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.htmlaboutaddons.inline-options.enabled", true]],
+ });
+});
+
+async function testOptionsInTab({ id, options_ui_options }) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Prefs extension",
+ applications: { gecko: { id } },
+ options_ui: {
+ page: "options.html",
+ ...options_ui_options,
+ },
+ },
+ background() {
+ browser.test.sendMessage(
+ "options-url",
+ browser.runtime.getURL("options.html")
+ );
+ },
+ files: {
+ "options.html": ``,
+ "options.js": () => {
+ browser.test.sendMessage("options-loaded");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let optionsUrl = await extension.awaitMessage("options-url");
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let aboutAddonsTab = gBrowser.selectedTab;
+
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ let prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(!prefsBtn.hidden, "The button is not hidden");
+
+ info("Open the preferences page from list");
+ let tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, optionsUrl);
+ prefsBtn.click();
+ await extension.awaitMessage("options-loaded");
+ BrowserTestUtils.removeTab(await tabLoaded);
+
+ info("Load details page");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Find the expanded card.
+ card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ info("Check that the button is still visible");
+ prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(!prefsBtn.hidden, "The button is not hidden");
+
+ info("Open the preferences page from details");
+ tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, optionsUrl);
+ prefsBtn.click();
+ let prefsTab = await tabLoaded;
+ await extension.awaitMessage("options-loaded");
+
+ info("Switch back to about:addons and open prefs again");
+ await BrowserTestUtils.switchTab(gBrowser, aboutAddonsTab);
+ let tabSwitched = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone");
+ prefsBtn.click();
+ await tabSwitched;
+ is(gBrowser.selectedTab, prefsTab, "The prefs tab was selected");
+
+ BrowserTestUtils.removeTab(prefsTab);
+
+ await closeView(win);
+ await extension.unload();
+}
+
+add_task(async function testPreferencesLink() {
+ let id = "prefs@mochi.test";
+ await testOptionsInTab({ id, options_ui_options: { open_in_tab: true } });
+});
+
+add_task(async function testPreferencesInlineDisabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.htmlaboutaddons.inline-options.enabled", false]],
+ });
+
+ let id = "inline-disabled@mochi.test";
+ await testOptionsInTab({ id, options_ui_options: {} });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testNoPreferences() {
+ let id = "no-prefs@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "No Prefs extension",
+ applications: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ info("Check button on list");
+ let prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(prefsBtn.hidden, "The button is hidden");
+
+ info("Load details page");
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Find the expanded card.
+ card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+
+ info("Check that the button is still hidden on detail");
+ prefsBtn = card.querySelector('panel-item[action="preferences"]');
+ ok(prefsBtn.hidden, "The button is hidden");
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js
new file mode 100644
index 0000000000..2bc5160518
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_pending_updates.js
@@ -0,0 +1,312 @@
+/* eslint max-len: ["error", 80] */
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const server = AddonTestUtils.createHttpServer();
+
+const LOCALE_ADDON_ID = "postponed-langpack@mochi.test";
+
+let gProvider;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.checkUpdateSecurity", false]],
+ });
+
+ // Also include a langpack with a pending postponed install.
+ const fakeLocalePostponedInstall = {
+ name: "updated langpack",
+ version: "2.0",
+ state: AddonManager.STATE_POSTPONED,
+ };
+
+ gProvider = new MockProvider();
+ gProvider.createAddons([
+ {
+ id: LOCALE_ADDON_ID,
+ name: "Postponed Langpack",
+ type: "locale",
+ version: "1.0",
+ // Mock pending upgrade property on the mocked langpack add-on.
+ pendingUpgrade: {
+ install: fakeLocalePostponedInstall,
+ },
+ },
+ ]);
+
+ fakeLocalePostponedInstall.existingAddon = gProvider.addons[0];
+ gProvider.createInstalls([fakeLocalePostponedInstall]);
+
+ registerCleanupFunction(() => {
+ cleanupPendingNotifications();
+ });
+});
+
+function createTestExtension({
+ id = "test-pending-update@test",
+ newManifest = {},
+}) {
+ function background() {
+ browser.runtime.onUpdateAvailable.addListener(() => {
+ browser.test.sendMessage("update-available");
+ });
+
+ browser.test.sendMessage("bgpage-ready");
+ }
+
+ const serverHost = `http://localhost:${server.identity.primaryPort}`;
+ const updatesPath = `/ext-updates-${id}.json`;
+ const update_url = `${serverHost}${updatesPath}`;
+
+ const manifest = {
+ name: "Test Pending Update",
+ applications: {
+ gecko: { id, update_url },
+ },
+ version: "1",
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest,
+ // Use permanent so the add-on can be updated.
+ useAddonManager: "permanent",
+ });
+
+ let updateXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ ...manifest,
+ ...newManifest,
+ version: "2",
+ },
+ });
+
+ let xpiFilename = `/update-${id}.xpi`;
+ server.registerFile(xpiFilename, updateXpi);
+ AddonTestUtils.registerJSON(server, updatesPath, {
+ addons: {
+ [id]: {
+ updates: [
+ {
+ version: "2",
+ update_link: serverHost + xpiFilename,
+ },
+ ],
+ },
+ },
+ });
+
+ return { extension, updateXpi };
+}
+
+async function promiseUpdateAvailable(extension) {
+ info("Wait for the extension to receive onUpdateAvailable event");
+ await extension.awaitMessage("update-available");
+}
+
+function expectUpdatesAvailableBadgeCount({ win, expectedNumber }) {
+ const categoriesSidebar = win.document.querySelector("categories-box");
+ ok(categoriesSidebar, "Found the categories-box element");
+ const availableButton = categoriesSidebar.getButtonByName(
+ "available-updates"
+ );
+ is(
+ availableButton.badgeCount,
+ 1,
+ `Expect only ${expectedNumber} available updates`
+ );
+ ok(
+ !availableButton.hidden,
+ "Expecte the available updates category to be visible"
+ );
+}
+
+async function expectAddonInstallStatePostponed(id) {
+ const [addonInstall] = (await AddonManager.getAllInstalls()).filter(
+ install => install.existingAddon && install.existingAddon.id == id
+ );
+ is(
+ addonInstall && addonInstall.state,
+ AddonManager.STATE_POSTPONED,
+ "AddonInstall is in the postponed state"
+ );
+}
+
+function expectCardOptionsButtonBadged({ id, win, hasBadge = true }) {
+ const card = getAddonCard(win, id);
+ const moreOptionsEl = card.querySelector(".more-options-button");
+ is(
+ moreOptionsEl.classList.contains("more-options-button-badged"),
+ hasBadge,
+ `The options button should${hasBadge || "n't"} have the update badge`
+ );
+}
+
+function getCardPostponedBar({ id, win }) {
+ const card = getAddonCard(win, id);
+ return card.querySelector(".update-postponed-bar");
+}
+
+function waitCardAndAddonUpdated({ id, win }) {
+ const card = getAddonCard(win, id);
+ const updatedExtStarted = AddonTestUtils.promiseWebExtensionStartup(id);
+ const updatedCard = BrowserTestUtils.waitForEvent(card, "update");
+ return Promise.all([updatedExtStarted, updatedCard]);
+}
+
+async function testPostponedBarVisibility({ id, win, hidden = false }) {
+ const postponedBar = getCardPostponedBar({ id, win });
+ is(
+ postponedBar.hidden,
+ hidden,
+ `${id} update postponed message bar should be ${
+ hidden ? "hidden" : "visible"
+ }`
+ );
+
+ if (!hidden) {
+ await expectAddonInstallStatePostponed(id);
+ }
+}
+
+async function assertPostponedBarVisibleInAllViews({ id, win }) {
+ info("Test postponed bar visibility in extension list view");
+ await testPostponedBarVisibility({ id, win });
+
+ info("Test postponed bar visibility in available view");
+ await switchView(win, "available-updates");
+ await testPostponedBarVisibility({ id, win });
+
+ info("Test that available updates count do not include postponed langpacks");
+ expectUpdatesAvailableBadgeCount({ win, expectedNumber: 1 });
+
+ info("Test postponed langpacks are not listed in the available updates view");
+ ok(
+ !getAddonCard(win, LOCALE_ADDON_ID),
+ "Locale addon is expected to not be listed in the updates view"
+ );
+
+ info("Test that postponed bar isn't visible on postponed langpacks");
+ await switchView(win, "locale");
+ await testPostponedBarVisibility({ id: LOCALE_ADDON_ID, win, hidden: true });
+
+ info("Test postponed bar visibility in extension detail view");
+ await switchView(win, "extension");
+ await switchToDetailView({ win, id });
+ await testPostponedBarVisibility({ id, win });
+}
+
+async function completePostponedUpdate({ id, win }) {
+ expectCardOptionsButtonBadged({ id, win, hasBadge: false });
+
+ await testPostponedBarVisibility({ id, win });
+
+ let addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "1", "Addon version is 1");
+
+ const promiseUpdated = waitCardAndAddonUpdated({ id, win });
+ const postponedBar = getCardPostponedBar({ id, win });
+ postponedBar.querySelector("button").click();
+ await promiseUpdated;
+
+ addon = await AddonManager.getAddonByID(id);
+ is(addon.version, "2", "Addon version is 2");
+
+ await testPostponedBarVisibility({ id, win, hidden: true });
+}
+
+add_task(async function test_pending_update_with_prompted_permission() {
+ const id = "test-pending-update-with-prompted-permission@mochi.test";
+
+ const { extension } = createTestExtension({
+ id,
+ newManifest: { permissions: [""] },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage-ready");
+
+ const win = await loadInitialView("extension");
+
+ // Force about:addons to check for updates.
+ let promisePermissionHandled = handlePermissionPrompt({
+ addonId: extension.id,
+ assertIcon: false,
+ });
+ win.checkForUpdates();
+ await promisePermissionHandled;
+
+ await promiseUpdateAvailable(extension);
+ await expectAddonInstallStatePostponed(id);
+
+ await completePostponedUpdate({ id, win });
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function test_pending_manual_install_over_existing() {
+ const id = "test-pending-manual-install-over-existing@mochi.test";
+
+ const { extension, updateXpi } = createTestExtension({
+ id,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage-ready");
+
+ let win = await loadInitialView("extension");
+
+ info("Manually install new xpi over the existing extension");
+ const promiseInstalled = AddonTestUtils.promiseInstallFile(updateXpi);
+ await promiseUpdateAvailable(extension);
+
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ info("Test postponed bar visibility after reopening about:addons");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ await completePostponedUpdate({ id, win });
+ await promiseInstalled;
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function test_pending_update_no_prompted_permission() {
+ const id = "test-pending-update-no-prompted-permission@mochi.test";
+
+ const { extension } = createTestExtension({ id });
+
+ await extension.startup();
+ await extension.awaitMessage("bgpage-ready");
+
+ let win = await loadInitialView("extension");
+
+ info("Force about:addons to check for updates");
+ win.checkForUpdates();
+ await promiseUpdateAvailable(extension);
+
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ info("Test postponed bar visibility after reopening about:addons");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ await assertPostponedBarVisibleInAllViews({ id, win });
+
+ await completePostponedUpdate({ id, win });
+
+ info("Reopen about:addons again and verify postponed bar hidden");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ await testPostponedBarVisibility({ id, win, hidden: true });
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js
new file mode 100644
index 0000000000..0adfebeb3b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_recent_updates.js
@@ -0,0 +1,180 @@
+/* eslint max-len: ["error", 80] */
+let gProvider;
+
+function dateHoursAgo(hours) {
+ let date = new Date();
+ date.setTime(date.getTime() - hours * 3600000);
+ return date;
+}
+
+add_task(async function enableHtmlViews() {
+ gProvider = new MockProvider();
+ gProvider.createAddons([
+ {
+ id: "addon-today-2@mochi.test",
+ name: "Updated today two",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(6),
+ },
+ {
+ id: "addon-today-3@mochi.test",
+ name: "Updated today three",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(9),
+ },
+ {
+ id: "addon-today-1@mochi.test",
+ name: "Updated today",
+ creator: { name: "The creator" },
+ version: "3.1",
+ type: "extension",
+ releaseNotesURI: "http://example.com/notes.txt",
+ updateDate: dateHoursAgo(1),
+ },
+ {
+ id: "addon-yesterday-1@mochi.test",
+ name: "Updated yesterday one",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(15),
+ },
+ {
+ id: "addon-earlier@mochi.test",
+ name: "Updated earlier",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(49),
+ },
+ {
+ id: "addon-yesterday-2@mochi.test",
+ name: "Updated yesterday",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(24),
+ },
+ {
+ id: "addon-lastweek@mochi.test",
+ name: "Updated last week",
+ creator: { name: "The creator" },
+ version: "3.3",
+ type: "extension",
+ updateDate: dateHoursAgo(192),
+ },
+ ]);
+});
+
+add_task(async function testRecentUpdatesList() {
+ // Load extension view first so we can mock the startOfDay property.
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let categoryUtils = new CategoryUtilities(win.managerWindow);
+ const RECENT_URL = "addons://updates/recent";
+ let recentCat = categoryUtils.get("recent-updates");
+
+ ok(recentCat.hidden, "Recent updates category is initially hidden");
+
+ // Load the recent updates view.
+ let loaded = waitForViewLoad(win);
+ doc.querySelector('#page-options [action="view-recent-updates"]').click();
+ await loaded;
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ RECENT_URL,
+ "Recent updates is selected"
+ );
+ ok(!recentCat.hidden, "Recent updates category is now shown");
+
+ // Find all the add-on ids.
+ let list = doc.querySelector("addon-list");
+ let addonsInOrder = () =>
+ Array.from(list.querySelectorAll("addon-card"))
+ .map(card => card.addon.id)
+ .filter(id => id.endsWith("@mochi.test"));
+
+ // Verify that the add-ons are in the right order.
+ Assert.deepEqual(
+ addonsInOrder(),
+ [
+ "addon-today-1@mochi.test",
+ "addon-today-2@mochi.test",
+ "addon-today-3@mochi.test",
+ "addon-yesterday-1@mochi.test",
+ "addon-yesterday-2@mochi.test",
+ ],
+ "The add-ons are in the right order"
+ );
+
+ info("Check that release notes are shown on the details page");
+ let card = list.querySelector(
+ 'addon-card[addon-id="addon-today-1@mochi.test"]'
+ );
+ loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ card = doc.querySelector("addon-card");
+ ok(card.expanded, "The card is expanded");
+ ok(!card.details.tabGroup.hidden, "The tabs are shown");
+ ok(
+ !card.details.tabGroup.querySelector('[name="release-notes"]').hidden,
+ "The release notes button is shown"
+ );
+
+ info("Go back to the recent updates view");
+ loaded = waitForViewLoad(win);
+ doc.querySelector('#page-options [action="view-recent-updates"]').click();
+ await loaded;
+
+ // Find the list again.
+ list = doc.querySelector("addon-list");
+
+ info("Install a new add-on, it should be first in the list");
+ // Install a new extension.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "New extension",
+ applications: { gecko: { id: "new@mochi.test" } },
+ },
+ useAddonManager: "temporary",
+ });
+ let added = BrowserTestUtils.waitForEvent(list, "add");
+ await extension.startup();
+ await added;
+
+ // The new extension should now be at the top of the list.
+ Assert.deepEqual(
+ addonsInOrder(),
+ [
+ "new@mochi.test",
+ "addon-today-1@mochi.test",
+ "addon-today-2@mochi.test",
+ "addon-today-3@mochi.test",
+ "addon-yesterday-1@mochi.test",
+ "addon-yesterday-2@mochi.test",
+ ],
+ "The new add-on went to the top"
+ );
+
+ // Open the detail view for the new add-on.
+ card = list.querySelector('addon-card[addon-id="new@mochi.test"]');
+ loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/extension",
+ "The extensions category is selected"
+ );
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js b/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js
new file mode 100644
index 0000000000..f7654089d4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_recommendations.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const SUPPORT_URL = "http://support.allizom.org/support-dummy/";
+const SUMO_URL = SUPPORT_URL + "add-on-badges";
+const SUPPORTED_BADGES = ["recommended", "line", "verified"];
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["app.support.baseURL", SUPPORT_URL]],
+ });
+});
+
+const server = AddonTestUtils.createHttpServer({
+ hosts: ["support.allizom.org"],
+});
+server.registerPathHandler("/support-dummy", (request, response) => {
+ response.write("Dummy");
+});
+
+async function checkRecommendedBadge(id, badges = []) {
+ async function checkBadge() {
+ let card = win.document.querySelector(`addon-card[addon-id="${id}"]`);
+ for (let badgeName of SUPPORTED_BADGES) {
+ let badge = card.querySelector(`.addon-badge-${badgeName}`);
+ let hidden = !badges.includes(badgeName);
+ is(
+ badge.hidden,
+ hidden,
+ `badge ${badgeName} is ${hidden ? "hidden" : "shown"}`
+ );
+ // Verify the utm params.
+ ok(
+ badge.href.startsWith(SUMO_URL),
+ "links to sumo correctly " + badge.href
+ );
+ if (!hidden) {
+ info(`Verify the ${badgeName} badge links to the support page`);
+ let tabLoaded = BrowserTestUtils.waitForNewTab(gBrowser, badge.href);
+ EventUtils.synthesizeMouseAtCenter(badge, {}, win);
+ BrowserTestUtils.removeTab(await tabLoaded);
+ }
+ let url = new URL(badge.href);
+ is(
+ url.searchParams.get("utm_content"),
+ "promoted-addon-badge",
+ "content param correct"
+ );
+ is(
+ url.searchParams.get("utm_source"),
+ "firefox-browser",
+ "source param correct"
+ );
+ is(
+ url.searchParams.get("utm_medium"),
+ "firefox-browser",
+ "medium param correct"
+ );
+ }
+ for (let badgeName of badges) {
+ if (!SUPPORTED_BADGES.includes(badgeName)) {
+ ok(
+ !card.querySelector(`.addon-badge-${badgeName}`),
+ `no badge element for ${badgeName}`
+ );
+ }
+ }
+ return card;
+ }
+
+ let win = await loadInitialView("extension");
+
+ // Check list view.
+ let card = await checkBadge();
+
+ // Load detail view.
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Check detail view.
+ await checkBadge();
+
+ await closeView(win);
+}
+
+add_task(async function testNotRecommended() {
+ let id = "not-recommended@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { applications: { gecko: { id } } },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ await checkRecommendedBadge(id);
+
+ await extension.unload();
+});
+
+async function test_badged_addon(addon) {
+ let provider = new MockProvider();
+ provider.createAddons([addon]);
+ await checkRecommendedBadge(addon.id, addon.recommendationStates);
+
+ provider.unregister();
+}
+
+add_task(async function testRecommended() {
+ await test_badged_addon({
+ id: "recommended@mochi.test",
+ isRecommended: true,
+ recommendationStates: ["recommended"],
+ name: "Recommended",
+ type: "extension",
+ });
+});
+
+add_task(async function testLine() {
+ await test_badged_addon({
+ id: "line@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["line"],
+ name: "Line",
+ type: "extension",
+ });
+});
+
+add_task(async function testVerified() {
+ await test_badged_addon({
+ id: "verified@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["verified"],
+ name: "Verified",
+ type: "extension",
+ });
+});
+
+add_task(async function testOther() {
+ await test_badged_addon({
+ id: "other@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["other"],
+ name: "No Badge",
+ type: "extension",
+ });
+});
+
+add_task(async function testMultiple() {
+ await test_badged_addon({
+ id: "multiple@mochi.test",
+ isRecommended: false,
+ recommendationStates: ["verified", "recommended", "other"],
+ name: "Multiple",
+ type: "extension",
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js
new file mode 100644
index 0000000000..d1d00c6cae
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js
@@ -0,0 +1,226 @@
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+AddonTestUtils.initMochitest(this);
+const server = AddonTestUtils.createHttpServer();
+const TEST_API_URL = `http://localhost:${server.identity.primaryPort}/discoapi`;
+
+const EXT_ID_EXTENSION = "extension@example.com";
+const EXT_ID_THEME = "theme@example.com";
+
+let requestCount = 0;
+server.registerPathHandler("/discoapi", (request, response) => {
+ // This test is expected to load the results only once, and then cache the
+ // results.
+ is(++requestCount, 1, "Expect only one discoapi request");
+
+ let results = {
+ results: [
+ {
+ addon: {
+ authors: [{ name: "Some author" }],
+ current_version: {
+ files: [{ platform: "all", url: "data:," }],
+ },
+ url: "data:,",
+ guid: "recommendation@example.com",
+ type: "extension",
+ },
+ },
+ ],
+ };
+ response.write(JSON.stringify(results));
+});
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.getAddons.discovery.api_url", TEST_API_URL]],
+ });
+
+ let mockProvider = new MockProvider();
+ mockProvider.createAddons([
+ {
+ id: EXT_ID_EXTENSION,
+ name: "Mock 1",
+ type: "extension",
+ userPermissions: {
+ origins: [""],
+ permissions: ["tabs"],
+ },
+ },
+ {
+ id: EXT_ID_THEME,
+ name: "Mock 2",
+ type: "theme",
+ },
+ ]);
+});
+
+async function switchToView(win, type, param = "") {
+ let loaded = waitForViewLoad(win);
+ win.managerWindow.gViewController.loadView(`addons://${type}/${param}`);
+ await loaded;
+ await waitForStableLayout(win);
+}
+
+// delta = -1 = go back.
+// delta = +1 = go forwards.
+async function historyGo(win, delta, expectedViewType) {
+ let loaded = waitForViewLoad(win);
+ win.managerWindow.history.go(delta);
+ await loaded;
+ is(
+ win.managerWindow.gViewController.currentViewId,
+ expectedViewType,
+ "Expected view after history navigation"
+ );
+ await waitForStableLayout(win);
+}
+
+async function waitForStableLayout(win) {
+ // In the test, it is important that the layout is fully stable before we
+ // consider the view loaded, because those affect the offset calculations.
+ await TestUtils.waitForCondition(
+ () => isLayoutStable(win),
+ "Waiting for layout to stabilize"
+ );
+}
+
+function isLayoutStable(win) {
+ // elements may affect the layout of a page, and therefore we
+ // should check whether its embedded style sheet has finished loading.
+ for (let bar of win.document.querySelectorAll("message-bar")) {
+ // Check for the existence of a CSS property from message-bar.css.
+ if (!win.getComputedStyle(bar).getPropertyValue("--message-bar-icon-url")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function getAddonCard(win, addonId) {
+ return win.document.querySelector(`addon-card[addon-id="${addonId}"]`);
+}
+
+function getScrollOffset(win) {
+ let { scrollTop: top, scrollLeft: left } = win.document.documentElement;
+ return { top, left };
+}
+
+// Scroll an element into view. The purpose of this is to simulate a real-world
+// scenario where the user has moved part of the UI is in the viewport.
+function scrollTopLeftIntoView(elem) {
+ elem.scrollIntoView({ block: "start", inline: "start" });
+ // Sanity check: In this test, a large padding has been added to the top and
+ // left of the document. So when an element has been scrolled into view, the
+ // top and left offsets must be non-zero.
+ assertNonZeroScrollOffsets(getScrollOffset(elem.ownerGlobal));
+}
+
+function assertNonZeroScrollOffsets(offsets) {
+ ok(offsets.left, "Should have scrolled to the right");
+ ok(offsets.top, "Should have scrolled down");
+}
+
+function checkScrollOffset(win, expected, msg = "") {
+ let actual = getScrollOffset(win);
+ is(actual.top, expected.top, `Top scroll offset - ${msg}`);
+ is(actual.left, expected.left, `Left scroll offset - ${msg}`);
+}
+
+add_task(async function test_scroll_restoration() {
+ let win = await loadInitialView("discover");
+
+ // Wait until the recommendations have been loaded. These are cached after
+ // the first load, so we only need to wait once, at the start of the test.
+ await win.document.querySelector("recommended-addon-list").cardsReady;
+
+ // Force scrollbar to appear, by adding enough space at the top and left.
+ win.document.body.style.paddingTop = "200vh";
+ win.document.body.style.paddingLeft = "100vw";
+ win.document.body.style.width = "200vw";
+
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial page load");
+
+ scrollTopLeftIntoView(win.document.querySelector("recommended-addon-card"));
+ let discoOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(discoOffsets);
+
+ // Switch from disco pane to extension list
+
+ await switchToView(win, "list", "extension");
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial extension list");
+
+ scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION));
+ let extListOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(extListOffsets);
+
+ // Switch from extension list to details view.
+
+ let loaded = waitForViewLoad(win);
+ getAddonCard(win, EXT_ID_EXTENSION).click();
+ await loaded;
+
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial details view");
+ scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION));
+ let detailsOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(detailsOffsets);
+
+ // Switch from details view back to extension list.
+
+ await historyGo(win, -1, "addons://list/extension");
+ checkScrollOffset(win, extListOffsets, "back to extension list");
+
+ // Now scroll to the bottom-right corner, so we can check whether the scroll
+ // offset is correctly restored when the extension view is loaded, even when
+ // the recommendations are loaded after the initial render.
+ ok(
+ win.document.querySelector("recommended-addon-card"),
+ "Recommendations have already been loaded"
+ );
+ win.document.body.scrollIntoView({ block: "end", inline: "end" });
+ extListOffsets = getScrollOffset(win);
+ assertNonZeroScrollOffsets(extListOffsets);
+
+ // Switch back from the extension list to the details view.
+
+ await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`);
+ checkScrollOffset(win, detailsOffsets, "details view with default tab");
+
+ // Switch from the default details tab to the permissions tab.
+ // (this does not change the history).
+ win.document.querySelector(".tab-button[name='permissions']").click();
+
+ // Switch back from the details view to the extension list.
+
+ await historyGo(win, -1, "addons://list/extension");
+ checkScrollOffset(win, extListOffsets, "bottom-right of extension list");
+ ok(
+ win.document.querySelector("recommended-addon-card"),
+ "Recommendations should have been loaded again"
+ );
+
+ // Switch back from extension list to the details view.
+
+ await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`);
+ // Scroll offsets are not remembered for the details view, because at the
+ // time of leaving the details view, the non-default tab was selected.
+ checkScrollOffset(win, { top: 0, left: 0 }, "details view, non-default tab");
+
+ // Switch back from the details view to the disco pane.
+
+ await historyGo(win, -2, "addons://discover/");
+ checkScrollOffset(win, discoOffsets, "after switching back to disco pane");
+
+ // Switch from disco pane to theme list.
+
+ // Verifies that the extension list and theme lists are independent.
+ await switchToView(win, "list", "theme");
+ checkScrollOffset(win, { top: 0, left: 0 }, "initial theme list");
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_updates.js b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
new file mode 100644
index 0000000000..aaf409d83c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_updates.js
@@ -0,0 +1,829 @@
+/* eslint max-len: ["error", 80] */
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const server = AddonTestUtils.createHttpServer();
+
+const initialAutoUpdate = AddonManager.autoUpdateDefault;
+registerCleanupFunction(() => {
+ AddonManager.autoUpdateDefault = initialAutoUpdate;
+});
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.checkUpdateSecurity", false]],
+ });
+
+ Services.telemetry.clearEvents();
+ registerCleanupFunction(() => {
+ cleanupPendingNotifications();
+ });
+});
+
+function loadDetailView(win, id) {
+ let doc = win.document;
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ let loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win);
+ return loaded;
+}
+
+add_task(async function testChangeAutoUpdates() {
+ let id = "test@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test",
+ applications: { gecko: { id } },
+ },
+ // Use permanent so the add-on can be updated.
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+ let addon = await AddonManager.getAddonByID(id);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let getInputs = updateRow => ({
+ default: updatesRow.querySelector('input[value="1"]'),
+ on: updatesRow.querySelector('input[value="2"]'),
+ off: updatesRow.querySelector('input[value="0"]'),
+ checkForUpdate: updatesRow.querySelector('[action="update-check"]'),
+ });
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ ok(card.querySelector("addon-details"), "The card now has details");
+
+ let updatesRow = card.querySelector(".addon-detail-row-updates");
+ let inputs = getInputs(updatesRow);
+ is(addon.applyBackgroundUpdates, 1, "Default is set");
+ ok(inputs.default.checked, "The default option is selected");
+ ok(inputs.checkForUpdate.hidden, "Update check is hidden");
+
+ inputs.on.click();
+ is(addon.applyBackgroundUpdates, "2", "Updates are now enabled");
+ ok(inputs.on.checked, "The on option is selected");
+ ok(inputs.checkForUpdate.hidden, "Update check is hidden");
+
+ inputs.off.click();
+ is(addon.applyBackgroundUpdates, "0", "Updates are now disabled");
+ ok(inputs.off.checked, "The off option is selected");
+ ok(!inputs.checkForUpdate.hidden, "Update check is visible");
+
+ // Go back to the list view and check the details view again.
+ let loaded = waitForViewLoad(win);
+ doc.querySelector(".back-button").click();
+ await loaded;
+
+ // Load the detail view again.
+ await loadDetailView(win, id);
+
+ card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ updatesRow = card.querySelector(".addon-detail-row-updates");
+ inputs = getInputs(updatesRow);
+
+ ok(inputs.off.checked, "Off is still selected");
+
+ // Disable global updates.
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ AddonManager.autoUpdateDefault = false;
+ await updated;
+
+ // Updates are still the same.
+ is(addon.applyBackgroundUpdates, "0", "Updates are now disabled");
+ ok(inputs.off.checked, "The off option is selected");
+ ok(!inputs.checkForUpdate.hidden, "Update check is visible");
+
+ // Check default.
+ inputs.default.click();
+ is(addon.applyBackgroundUpdates, "1", "Default is set");
+ ok(inputs.default.checked, "The default option is selected");
+ ok(!inputs.checkForUpdate.hidden, "Update check is visible");
+
+ inputs.on.click();
+ is(addon.applyBackgroundUpdates, "2", "Updates are now enabled");
+ ok(inputs.on.checked, "The on option is selected");
+ ok(inputs.checkForUpdate.hidden, "Update check is hidden");
+
+ // Enable updates again.
+ updated = BrowserTestUtils.waitForEvent(card, "update");
+ AddonManager.autoUpdateDefault = true;
+ await updated;
+
+ await closeView(win);
+ await extension.unload();
+
+ assertAboutAddonsTelemetryEvents([
+ ["addonsManager", "view", "aboutAddons", "list", { type: "extension" }],
+ [
+ "addonsManager",
+ "view",
+ "aboutAddons",
+ "detail",
+ { type: "extension", addonId: id },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "enabled",
+ { type: "extension", addonId: id, action: "setAddonUpdate" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "",
+ { type: "extension", addonId: id, action: "setAddonUpdate" },
+ ],
+ ["addonsManager", "view", "aboutAddons", "list", { type: "extension" }],
+ [
+ "addonsManager",
+ "view",
+ "aboutAddons",
+ "detail",
+ { type: "extension", addonId: id },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "default",
+ { type: "extension", addonId: id, action: "setAddonUpdate" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "enabled",
+ { type: "extension", addonId: id, action: "setAddonUpdate" },
+ ],
+ ]);
+});
+
+async function setupExtensionWithUpdate(
+ id,
+ { releaseNotes, cancelUpdate } = {}
+) {
+ let serverHost = `http://localhost:${server.identity.primaryPort}`;
+ let updatesPath = `/ext-updates-${id}.json`;
+
+ let baseManifest = {
+ name: "Updates",
+ icons: { "48": "an-icon.png" },
+ applications: {
+ gecko: {
+ id,
+ update_url: serverHost + updatesPath,
+ },
+ },
+ };
+
+ let updateXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ ...baseManifest,
+ version: "2",
+ // Include a permission in the updated extension, to make
+ // sure that we trigger the permission prompt as expected
+ // (and that we can accept or cancel the update by observing
+ // the underlying observerService notification).
+ permissions: ["http://*.example.com/*"],
+ },
+ });
+
+ let releaseNotesExtra = {};
+ if (releaseNotes) {
+ let notesPath = "/notes.txt";
+ server.registerPathHandler(notesPath, (request, response) => {
+ if (releaseNotes == "ERROR") {
+ response.setStatusLine(null, 404, "Not Found");
+ } else {
+ response.setStatusLine(null, 200, "OK");
+ response.write(releaseNotes);
+ }
+ response.processAsync();
+ response.finish();
+ });
+ releaseNotesExtra.update_info_url = serverHost + notesPath;
+ }
+
+ let xpiFilename = `/update-${id}.xpi`;
+ server.registerFile(xpiFilename, updateXpi);
+ AddonTestUtils.registerJSON(server, updatesPath, {
+ addons: {
+ [id]: {
+ updates: [
+ {
+ version: "2",
+ update_link: serverHost + xpiFilename,
+ ...releaseNotesExtra,
+ },
+ ],
+ },
+ },
+ });
+
+ handlePermissionPrompt({ addonId: id, reject: cancelUpdate });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ ...baseManifest,
+ version: "1",
+ },
+ // Use permanent so the add-on can be updated.
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+ return extension;
+}
+
+function disableAutoUpdates(card) {
+ // Check button should be hidden.
+ let updateCheckButton = card.querySelector('button[action="update-check"]');
+ ok(updateCheckButton.hidden, "The button is initially hidden");
+
+ // Disable updates, update check button is now visible.
+ card.querySelector('input[name="autoupdate"][value="0"]').click();
+ ok(!updateCheckButton.hidden, "The button is now visible");
+
+ // There shouldn't be an update shown to the user.
+ assertUpdateState({ card, shown: false });
+}
+
+function checkForUpdate(card, expected) {
+ let updateCheckButton = card.querySelector('button[action="update-check"]');
+ let updateFound = BrowserTestUtils.waitForEvent(card, expected);
+ updateCheckButton.click();
+ return updateFound;
+}
+
+function installUpdate(card, expected) {
+ // Install the update.
+ let updateInstalled = BrowserTestUtils.waitForEvent(card, expected);
+ let updated = BrowserTestUtils.waitForEvent(card, "update");
+ card.querySelector('panel-item[action="install-update"]').click();
+ return Promise.all([updateInstalled, updated]);
+}
+
+async function findUpdatesForAddonId(id) {
+ let addon = await AddonManager.getAddonByID(id);
+ await new Promise(resolve => {
+ addon.findUpdates(
+ { onUpdateAvailable: resolve },
+ AddonManager.UPDATE_WHEN_USER_REQUESTED
+ );
+ });
+}
+
+function assertUpdateState({
+ card,
+ shown,
+ expanded = true,
+ releaseNotes = false,
+}) {
+ let menuButton = card.querySelector(".more-options-button");
+ ok(
+ menuButton.classList.contains("more-options-button-badged") == shown,
+ "The menu button is badged"
+ );
+ let installButton = card.querySelector('panel-item[action="install-update"]');
+ ok(
+ installButton.hidden != shown,
+ `The install button is ${shown ? "hidden" : "shown"}`
+ );
+ if (expanded) {
+ let updateCheckButton = card.querySelector('button[action="update-check"]');
+ ok(
+ updateCheckButton.hidden == shown,
+ `The update check button is ${shown ? "hidden" : "shown"}`
+ );
+
+ let { tabGroup } = card.details;
+ is(tabGroup.hidden, false, "The tab group is shown");
+ let notesBtn = tabGroup.querySelector('[name="release-notes"]');
+ is(
+ notesBtn.hidden,
+ !releaseNotes,
+ `The release notes button is ${releaseNotes ? "shown" : "hidden"}`
+ );
+ }
+}
+
+add_task(async function testUpdateAvailable() {
+ let id = "update@mochi.test";
+ let extension = await setupExtensionWithUpdate(id);
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector("addon-card");
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true });
+
+ // The version was 1.
+ let versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "1", "The version started as 1");
+
+ await installUpdate(card, "update-installed");
+
+ // The version is now 2.
+ versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "2", "The version has updated");
+
+ // No update is shown again.
+ assertUpdateState({ card, shown: false });
+
+ // Check for updates again, there shouldn't be an update.
+ await checkForUpdate(card, "no-update");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testReleaseNotesLoad() {
+ Services.telemetry.clearEvents();
+ let id = "update-with-notes@mochi.test";
+ let extension = await setupExtensionWithUpdate(id, {
+ releaseNotes: `
+
+
+
+
+ My release notes
+
+
+ Go somewhere
+
+
+ `,
+ });
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector("addon-card");
+ let { deck, tabGroup } = card.details;
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true, releaseNotes: true });
+
+ info("Check release notes");
+ let notesBtn = tabGroup.querySelector('[name="release-notes"]');
+ let notes = card.querySelector("update-release-notes");
+ let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading");
+ let loaded = BrowserTestUtils.waitForEvent(notes, "release-notes-loaded");
+ // Don't use notesBtn.click() since it causes an assertion to fail.
+ // See bug 1551621 for more info.
+ EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win);
+ await loading;
+ is(
+ doc.l10n.getAttributes(notes.firstElementChild).id,
+ "release-notes-loading",
+ "The loading message is shown"
+ );
+ await loaded;
+ info("Checking HTML release notes");
+ let [h1, ul, a] = notes.children;
+ is(h1.tagName, "H1", "There's a heading");
+ is(h1.textContent, "My release notes", "The heading has content");
+ is(ul.tagName, "UL", "There's a list");
+ is(ul.children.length, 1, "There's one item in the list");
+ let [li] = ul.children;
+ is(li.tagName, "LI", "There's a list item");
+ is(li.textContent, "A thing", "The text is set");
+ ok(!li.hasAttribute("onclick"), "The onclick was removed");
+ ok(!notes.querySelector("link"), "The link tag was removed");
+ ok(!notes.querySelector("script"), "The script tag was removed");
+ is(a.textContent, "Go somewhere", "The link text is preserved");
+ is(a.href, "http://example.com/", "The link href is preserved");
+
+ info("Verify the link opened in a new tab");
+ let tabOpened = BrowserTestUtils.waitForNewTab(gBrowser, a.href);
+ a.click();
+ let tab = await tabOpened;
+ BrowserTestUtils.removeTab(tab);
+
+ let originalContent = notes.innerHTML;
+
+ info("Switch away and back to release notes");
+ // Load details view.
+ let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]');
+ let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ detailsBtn.click();
+ await viewChanged;
+
+ // Load release notes again, verify they weren't loaded.
+ viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ let notesCached = BrowserTestUtils.waitForEvent(
+ notes,
+ "release-notes-cached"
+ );
+ notesBtn.click();
+ await viewChanged;
+ await notesCached;
+ is(notes.innerHTML, originalContent, "The content didn't change");
+
+ info("Install the update to clean it up");
+ await installUpdate(card, "update-installed");
+
+ // There's no more update but release notes are still shown.
+ assertUpdateState({ card, shown: false, releaseNotes: true });
+
+ await closeView(win);
+ await extension.unload();
+
+ assertAboutAddonsTelemetryEvents([
+ ["addonsManager", "view", "aboutAddons", "list", { type: "extension" }],
+ [
+ "addonsManager",
+ "view",
+ "aboutAddons",
+ "detail",
+ { type: "extension", addonId: id },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "",
+ { type: "extension", addonId: id, action: "setAddonUpdate" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { type: "extension", addonId: id, action: "checkForUpdate" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { type: "extension", addonId: id, action: "releaseNotes" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { type: "extension", addonId: id, action: "releaseNotes" },
+ ],
+ ]);
+});
+
+add_task(async function testReleaseNotesError() {
+ let id = "update-with-notes-error@mochi.test";
+ let extension = await setupExtensionWithUpdate(id, { releaseNotes: "ERROR" });
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, id);
+
+ let card = doc.querySelector("addon-card");
+ let { deck, tabGroup } = card.details;
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true, releaseNotes: true });
+
+ info("Check release notes");
+ let notesBtn = tabGroup.querySelector('[name="release-notes"]');
+ let notes = card.querySelector("update-release-notes");
+ let loading = BrowserTestUtils.waitForEvent(notes, "release-notes-loading");
+ let errored = BrowserTestUtils.waitForEvent(notes, "release-notes-error");
+ // Don't use notesBtn.click() since it causes an assertion to fail.
+ // See bug 1551621 for more info.
+ EventUtils.synthesizeMouseAtCenter(notesBtn, {}, win);
+ await loading;
+ is(
+ doc.l10n.getAttributes(notes.firstElementChild).id,
+ "release-notes-loading",
+ "The loading message is shown"
+ );
+ await errored;
+ is(
+ doc.l10n.getAttributes(notes.firstElementChild).id,
+ "release-notes-error",
+ "The error message is shown"
+ );
+
+ info("Switch away and back to release notes");
+ // Load details view.
+ let detailsBtn = tabGroup.querySelector('.tab-button[name="details"]');
+ let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ detailsBtn.click();
+ await viewChanged;
+
+ // Load release notes again, verify they weren't loaded.
+ viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
+ let notesCached = BrowserTestUtils.waitForEvent(
+ notes,
+ "release-notes-cached"
+ );
+ notesBtn.click();
+ await viewChanged;
+ await notesCached;
+
+ info("Install the update to clean it up");
+ await installUpdate(card, "update-installed");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testUpdateCancelled() {
+ let id = "update@mochi.test";
+ let extension = await setupExtensionWithUpdate(id, { cancelUpdate: true });
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ await loadDetailView(win, "update@mochi.test");
+ let card = doc.querySelector("addon-card");
+
+ // Disable updates and then check.
+ disableAutoUpdates(card);
+ await checkForUpdate(card, "update-found");
+
+ // There should now be an update.
+ assertUpdateState({ card, shown: true });
+
+ // The add-on starts as version 1.
+ let versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "1", "The version started as 1");
+
+ // Force the install to be cancelled.
+ let install = card.updateInstall;
+ ok(install, "There was an install found");
+
+ await installUpdate(card, "update-cancelled");
+
+ // The add-on is still version 1.
+ versionRow = card.querySelector(".addon-detail-row-version");
+ is(versionRow.lastChild.textContent, "1", "The version hasn't changed");
+
+ // The update has been removed.
+ assertUpdateState({ card, shown: false });
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testAvailableUpdates() {
+ let ids = ["update1@mochi.test", "update2@mochi.test", "update3@mochi.test"];
+ let addons = await Promise.all(ids.map(id => setupExtensionWithUpdate(id)));
+
+ // Disable global add-on updates.
+ AddonManager.autoUpdateDefault = false;
+
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let updatesMessage = doc.getElementById("updates-message");
+ let categoryUtils = new CategoryUtilities(win.managerWindow);
+
+ let availableCat = categoryUtils.get("available-updates");
+
+ ok(availableCat.hidden, "Available updates is hidden");
+ is(availableCat.badgeCount, 0, "There are no updates");
+ ok(updatesMessage, "There is an updates message");
+ is_element_hidden(updatesMessage, "The message is hidden");
+ ok(!updatesMessage.message.textContent, "The message is empty");
+ ok(!updatesMessage.button.textContent, "The button is empty");
+
+ // Check for all updates.
+ let updatesFound = TestUtils.topicObserved("EM-update-check-finished");
+ doc.querySelector('#page-options [action="check-for-updates"]').click();
+
+ is_element_visible(updatesMessage, "The message is visible");
+ ok(!updatesMessage.message.textContent, "The message is empty");
+ ok(updatesMessage.button.hidden, "The view updates button is hidden");
+
+ // Make sure the message gets populated by fluent.
+ await TestUtils.waitForCondition(
+ () => updatesMessage.message.textContent,
+ "wait for message text"
+ );
+
+ await updatesFound;
+
+ // The button should be visible, and should get some text from fluent.
+ ok(!updatesMessage.button.hidden, "The view updates button is visible");
+ await TestUtils.waitForCondition(
+ () => updatesMessage.button.textContent,
+ "wait for button text"
+ );
+
+ // Wait for the available updates count to finalize, it's async.
+ await BrowserTestUtils.waitForCondition(() => availableCat.badgeCount == 3);
+
+ // The category shows the correct update count.
+ ok(!availableCat.hidden, "Available updates is visible");
+ is(availableCat.badgeCount, 3, "There are 3 updates");
+
+ // Go to the available updates page.
+ let loaded = waitForViewLoad(win);
+ availableCat.click();
+ await loaded;
+
+ // Check the updates are shown.
+ let cards = doc.querySelectorAll("addon-card");
+ is(cards.length, 3, "There are 3 cards");
+
+ // Each card should have an update.
+ for (let card of cards) {
+ assertUpdateState({ card, shown: true, expanded: false });
+ }
+
+ // Check the detail page for the first add-on.
+ await loadDetailView(win, ids[0]);
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/extension",
+ "The extensions category is selected"
+ );
+
+ // Go back to the last view.
+ loaded = waitForViewLoad(win);
+ doc.querySelector(".back-button").click();
+ await loaded;
+
+ // We're back on the updates view.
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://updates/available",
+ "The available updates category is selected"
+ );
+
+ // Find the cards again.
+ cards = doc.querySelectorAll("addon-card");
+ is(cards.length, 3, "There are 3 cards");
+
+ // Install the first update.
+ await installUpdate(cards[0], "update-installed");
+ assertUpdateState({ card: cards[0], shown: false, expanded: false });
+
+ // The count goes down but the card stays.
+ is(availableCat.badgeCount, 2, "There are only 2 updates now");
+ is(
+ doc.querySelectorAll("addon-card").length,
+ 3,
+ "All 3 cards are still visible on the updates page"
+ );
+
+ // Install the other two updates.
+ await installUpdate(cards[1], "update-installed");
+ assertUpdateState({ card: cards[1], shown: false, expanded: false });
+ await installUpdate(cards[2], "update-installed");
+ assertUpdateState({ card: cards[2], shown: false, expanded: false });
+
+ // The count goes down but the card stays.
+ is(availableCat.badgeCount, 0, "There are no more updates");
+ is(
+ doc.querySelectorAll("addon-card").length,
+ 3,
+ "All 3 cards are still visible on the updates page"
+ );
+
+ // Enable global add-on updates again.
+ AddonManager.autoUpdateDefault = true;
+
+ await closeView(win);
+ await Promise.all(addons.map(addon => addon.unload()));
+});
+
+add_task(async function testUpdatesShownOnLoad() {
+ let id = "has-update@mochi.test";
+ let addon = await setupExtensionWithUpdate(id);
+
+ // Find the update for our addon.
+ AddonManager.autoUpdateDefault = false;
+ await findUpdatesForAddonId(id);
+
+ let win = await loadInitialView("extension");
+ let categoryUtils = new CategoryUtilities(win.managerWindow);
+ let updatesButton = categoryUtils.get("available-updates");
+
+ ok(!updatesButton.hidden, "The updates button is shown");
+ is(updatesButton.badgeCount, 1, "There is an update");
+
+ let loaded = waitForViewLoad(win);
+ updatesButton.click();
+ await loaded;
+
+ let cards = win.document.querySelectorAll("addon-card");
+
+ is(cards.length, 1, "There is one update card");
+
+ let card = cards[0];
+ is(card.addon.id, id, "The update is for the expected add-on");
+
+ await installUpdate(card, "update-installed");
+
+ ok(!updatesButton.hidden, "The updates button is still shown");
+ is(updatesButton.badgeCount, 0, "There are no more updates");
+
+ info("Check that the updates section is hidden when re-opened");
+ await closeView(win);
+ win = await loadInitialView("extension");
+ categoryUtils = new CategoryUtilities(win.managerWindow);
+ updatesButton = categoryUtils.get("available-updates");
+
+ ok(updatesButton.hidden, "Available updates is hidden");
+ is(updatesButton.badgeCount, 0, "There are no updates");
+
+ AddonManager.autoUpdateDefault = true;
+ await closeView(win);
+ await addon.unload();
+});
+
+add_task(async function testPromptOnBackgroundUpdateCheck() {
+ const id = "test-prompt-on-background-check@mochi.test";
+ const extension = await setupExtensionWithUpdate(id);
+
+ AddonManager.autoUpdateDefault = false;
+
+ const addon = await AddonManager.getAddonByID(id);
+ await AddonTestUtils.promiseFindAddonUpdates(
+ addon,
+ AddonManager.UPDATE_WHEN_PERIODIC_UPDATE
+ );
+ let win = await loadInitialView("extension");
+
+ let card = getAddonCard(win, id);
+
+ const promisePromptInfo = promisePermissionPrompt(id);
+ await installUpdate(card, "update-installed");
+ const promptInfo = await promisePromptInfo;
+ ok(promptInfo, "Got a permission prompt as expected");
+
+ AddonManager.autoUpdateDefault = true;
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testNoUpdateAvailableOnUnrelatedAddonCards() {
+ let idNoUpdate = "no-update@mochi.test";
+
+ let extensionNoUpdate = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+ manifest: {
+ name: "TestAddonNoUpdate",
+ applications: { gecko: { id: idNoUpdate } },
+ },
+ });
+ await extensionNoUpdate.startup();
+
+ let win = await loadInitialView("extension");
+
+ let cardNoUpdate = getAddonCard(win, idNoUpdate);
+ ok(cardNoUpdate, `Got AddonCard for ${idNoUpdate}`);
+
+ // Assert that there is not an update badge
+ assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false });
+
+ // Trigger a onNewInstall event by install another unrelated addon.
+ const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+ let install = await AddonManager.getInstallForURL(XPI_URL);
+ await AddonManager.installAddonFromAOM(
+ gBrowser.selectedBrowser,
+ win.document.documentURIObject,
+ install
+ );
+
+ // Cancel the install used to trigger the onNewInstall install event.
+ await install.cancel();
+ // Assert that the previously installed addon isn't marked with the
+ // update available badge after installing an unrelated addon.
+ assertUpdateState({ card: cardNoUpdate, shown: false, expanded: false });
+
+ await closeView(win);
+ await extensionNoUpdate.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js b/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js
new file mode 100644
index 0000000000..5d18f6f639
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_html_warning_messages.js
@@ -0,0 +1,355 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint max-len: ["error", 80] */
+
+"use strict";
+
+let gProvider;
+const {
+ STATE_BLOCKED,
+ STATE_OUTDATED,
+ STATE_SOFTBLOCKED,
+ STATE_VULNERABLE_NO_UPDATE,
+ STATE_VULNERABLE_UPDATE_AVAILABLE,
+} = Ci.nsIBlocklistService;
+
+const brandBundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+);
+const appName = brandBundle.GetStringFromName("brandShortName");
+const appVersion = Services.appinfo.version;
+const SUPPORT_URL = Services.urlFormatter.formatURL(
+ Services.prefs.getStringPref("app.support.baseURL")
+);
+
+add_task(async function setup() {
+ gProvider = new MockProvider();
+});
+
+async function checkMessageState(id, addonType, expected) {
+ async function checkAddonCard() {
+ let card = doc.querySelector(`addon-card[addon-id="${id}"]`);
+ let messageBar = card.querySelector(".addon-card-message");
+
+ if (!expected) {
+ ok(messageBar.hidden, "message is hidden");
+ } else {
+ let { linkText, linkUrl, text, type } = expected;
+
+ ok(!messageBar.hidden, "message is visible");
+ is(messageBar.getAttribute("type"), type, "message has the right type");
+ is(
+ messageBar.querySelector("span").textContent,
+ text,
+ "message has the right text"
+ );
+
+ let link = messageBar.querySelector("button");
+ if (linkUrl) {
+ ok(!link.hidden, "link is visible");
+ is(link.textContent, linkText, "link text is correct");
+ let newTab = BrowserTestUtils.waitForNewTab(gBrowser, linkUrl);
+ link.click();
+ BrowserTestUtils.removeTab(await newTab);
+ } else {
+ ok(link.hidden, "link is hidden");
+ }
+ }
+
+ return card;
+ }
+
+ let win = await loadInitialView(addonType);
+ let doc = win.document;
+
+ // Check the list view.
+ ok(doc.querySelector("addon-list"), "this is a list view");
+ let card = await checkAddonCard();
+
+ // Load the detail view.
+ let loaded = waitForViewLoad(win);
+ card.querySelector('[action="expand"]').click();
+ await loaded;
+
+ // Check the detail view.
+ ok(!doc.querySelector("addon-list"), "this isn't a list view");
+ await checkAddonCard();
+
+ await closeView(win);
+}
+
+add_task(async function testNoMessageExtension() {
+ let id = "no-message@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { applications: { gecko: { id } } },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ await checkMessageState(id, "extension", null);
+
+ await extension.unload();
+});
+
+add_task(async function testNoMessageLangpack() {
+ let id = "no-message@mochi.test";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ name: "Signed Langpack",
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ type: "locale",
+ },
+ ]);
+
+ await checkMessageState(id, "locale", null);
+});
+
+add_task(async function testBlocked() {
+ let id = "blocked@mochi.test";
+ let linkUrl = "https://example.com/addon-blocked";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ blocklistState: STATE_BLOCKED,
+ blocklistURL: linkUrl,
+ id,
+ isActive: false,
+ name: "Blocked",
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkText: "More Information",
+ linkUrl,
+ text: "Blocked has been disabled due to security or stability issues.",
+ type: "error",
+ });
+});
+
+add_task(async function testUnsignedDisabled() {
+ // This pref being disabled will cause the `specialpowers` addon to be
+ // uninstalled, which can cause a number of test failures due to features no
+ // longer working correctly.
+ // In order to avoid those issues, this code manually disables the pref, and
+ // ensures that `SpecialPowers` is fully re-enabled at the end of the test.
+ const sigPref = "xpinstall.signatures.required";
+ Services.prefs.setBoolPref(sigPref, true);
+
+ let id = "unsigned@mochi.test";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ name: "Unsigned",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkText: "More Information",
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text:
+ "Unsigned could not be verified for use in " +
+ appName +
+ " and has been disabled.",
+ type: "error",
+ });
+
+ // Ensure that `SpecialPowers` is fully re-initialized at the end of this
+ // test. This requires removing the existing binding so that it's
+ // re-registered, re-enabling unsigned extensions, and then waiting for the
+ // actor to be registered and ready.
+ delete window.SpecialPowers;
+ Services.prefs.setBoolPref(sigPref, false);
+ await TestUtils.waitForCondition(() => {
+ try {
+ return !!windowGlobalChild.getActor("SpecialPowers");
+ } catch (e) {
+ return false;
+ }
+ }, "wait for SpecialPowers to be reloaded");
+ ok(window.SpecialPowers, "SpecialPowers should be re-defined");
+});
+
+add_task(async function testUnsignedLangpackDisabled() {
+ let id = "unsigned-langpack@mochi.test";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ name: "Unsigned",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ type: "locale",
+ },
+ ]);
+ await checkMessageState(id, "locale", {
+ linkText: "More Information",
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text:
+ "Unsigned could not be verified for use in " +
+ appName +
+ " and has been disabled.",
+ type: "error",
+ });
+});
+
+add_task(async function testIncompatible() {
+ let id = "incompatible@mochi.test";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ id,
+ isActive: false,
+ isCompatible: false,
+ name: "Incompatible",
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ text:
+ "Incompatible is incompatible with " + appName + " " + appVersion + ".",
+ type: "warning",
+ });
+});
+
+add_task(async function testUnsignedEnabled() {
+ let id = "unsigned-allowed@mochi.test";
+ gProvider.createAddons([
+ {
+ id,
+ name: "Unsigned",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkText: "More Information",
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text:
+ "Unsigned could not be verified for use in " +
+ appName +
+ ". Proceed with caution.",
+ type: "warning",
+ });
+});
+
+add_task(async function testUnsignedLangpackEnabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.langpacks.signatures.required", false]],
+ });
+
+ let id = "unsigned-allowed-langpack@mochi.test";
+ gProvider.createAddons([
+ {
+ id,
+ name: "Unsigned Langpack",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ type: "locale",
+ },
+ ]);
+ await checkMessageState(id, "locale", {
+ linkText: "More Information",
+ linkUrl: SUPPORT_URL + "unsigned-addons",
+ text:
+ "Unsigned Langpack could not be verified for use in " +
+ appName +
+ ". Proceed with caution.",
+ type: "warning",
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testSoftBlocked() {
+ let id = "softblocked@mochi.test";
+ let linkUrl = "https://example.com/addon-blocked";
+ gProvider.createAddons([
+ {
+ appDisabled: true,
+ blocklistState: STATE_SOFTBLOCKED,
+ blocklistURL: linkUrl,
+ id,
+ isActive: false,
+ name: "Soft Blocked",
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkText: "More Information",
+ linkUrl,
+ text: "Soft Blocked is known to cause security or stability issues.",
+ type: "warning",
+ });
+});
+
+add_task(async function testOutdated() {
+ let id = "outdated@mochi.test";
+ let linkUrl = "https://example.com/addon-blocked";
+ gProvider.createAddons([
+ {
+ blocklistState: STATE_OUTDATED,
+ blocklistURL: linkUrl,
+ id,
+ name: "Outdated",
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkText: "Update Now",
+ linkUrl,
+ text: "An important update is available for Outdated.",
+ type: "warning",
+ });
+});
+
+add_task(async function testVulnerableUpdate() {
+ let id = "vulnerable-update@mochi.test";
+ let linkUrl = "https://example.com/addon-blocked";
+ gProvider.createAddons([
+ {
+ blocklistState: STATE_VULNERABLE_UPDATE_AVAILABLE,
+ blocklistURL: linkUrl,
+ id,
+ name: "Vulnerable Update",
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkText: "Update Now",
+ linkUrl,
+ text: "Vulnerable Update is known to be vulnerable and should be updated.",
+ type: "error",
+ });
+});
+
+add_task(async function testVulnerableNoUpdate() {
+ let id = "vulnerable-no-update@mochi.test";
+ let linkUrl = "https://example.com/addon-blocked";
+ gProvider.createAddons([
+ {
+ blocklistState: STATE_VULNERABLE_NO_UPDATE,
+ blocklistURL: linkUrl,
+ id,
+ name: "Vulnerable No Update",
+ },
+ ]);
+ await checkMessageState(id, "extension", {
+ linkText: "More Information",
+ linkUrl,
+ text: "Vulnerable No Update is known to be vulnerable. Use with caution.",
+ type: "error",
+ });
+});
+
+add_task(async function testPluginInstalling() {
+ let id = "plugin-installing@mochi.test";
+ gProvider.createAddons([
+ {
+ id,
+ isActive: true,
+ isGMPlugin: true,
+ isInstalled: false,
+ name: "Plugin Installing",
+ type: "plugin",
+ },
+ ]);
+ await checkMessageState(id, "plugin", {
+ text: "Plugin Installing will be installed shortly.",
+ type: "warning",
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_installssl.js b/toolkit/mozapps/extensions/test/browser/browser_installssl.js
new file mode 100644
index 0000000000..0d0272e503
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_installssl.js
@@ -0,0 +1,378 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const xpi = RELATIVE_DIR + "addons/browser_installssl.xpi";
+const redirect = RELATIVE_DIR + "redirect.sjs?";
+const SUCCESS = 0;
+const NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE;
+
+const HTTP = "http://example.com/";
+const HTTPS = "https://example.com/";
+const NOCERT = "https://nocert.example.com/";
+const SELFSIGNED = "https://self-signed.example.com/";
+const UNTRUSTED = "https://untrusted.example.com/";
+const EXPIRED = "https://expired.example.com/";
+
+const PREF_INSTALL_REQUIREBUILTINCERTS =
+ "extensions.install.requireBuiltInCerts";
+
+var gTests = [];
+var gStart = 0;
+var gLast = 0;
+var gPendingInstall = null;
+
+function test() {
+ gStart = Date.now();
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function() {
+ var cos = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+ );
+ cos.clearValidityOverride("nocert.example.com", -1);
+ cos.clearValidityOverride("self-signed.example.com", -1);
+ cos.clearValidityOverride("untrusted.example.com", -1);
+ cos.clearValidityOverride("expired.example.com", -1);
+
+ if (gPendingInstall) {
+ gTests = [];
+ ok(
+ false,
+ "Timed out in the middle of downloading " +
+ gPendingInstall.sourceURI.spec
+ );
+ try {
+ gPendingInstall.cancel();
+ } catch (e) {}
+ }
+ });
+
+ run_next_test();
+}
+
+function end_test() {
+ info("All tests completed in " + (Date.now() - gStart) + "ms");
+ finish();
+}
+
+function add_install_test(mainURL, redirectURL, expectedStatus) {
+ gTests.push([mainURL, redirectURL, expectedStatus]);
+}
+
+function run_install_tests(callback) {
+ async function run_next_install_test() {
+ if (!gTests.length) {
+ callback();
+ return;
+ }
+ gLast = Date.now();
+
+ let [mainURL, redirectURL, expectedStatus] = gTests.shift();
+ if (redirectURL) {
+ var url = mainURL + redirect + redirectURL + xpi;
+ var message =
+ "Should have seen the right result for an install redirected from " +
+ mainURL +
+ " to " +
+ redirectURL;
+ } else {
+ url = mainURL + xpi;
+ message =
+ "Should have seen the right result for an install from " + mainURL;
+ }
+
+ let install = await AddonManager.getInstallForURL(url);
+ gPendingInstall = install;
+ install.addListener({
+ onDownloadEnded(install) {
+ is(SUCCESS, expectedStatus, message);
+ info("Install test ran in " + (Date.now() - gLast) + "ms");
+ // Don't proceed with the install
+ install.cancel();
+ gPendingInstall = null;
+ run_next_install_test();
+ return false;
+ },
+
+ onDownloadFailed(install) {
+ is(install.error, expectedStatus, message);
+ info("Install test ran in " + (Date.now() - gLast) + "ms");
+ gPendingInstall = null;
+ run_next_install_test();
+ },
+ });
+ install.install();
+ }
+
+ run_next_install_test();
+}
+
+// Runs tests with built-in certificates required, no certificate exceptions
+// and no hashes
+add_test(async function test_builtin_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, true]],
+ });
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, NETWORK_FAILURE);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTP, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, NETWORK_FAILURE);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates, no certificate
+// exceptions and no hashes
+add_test(async function test_builtin_not_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, SUCCESS);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTP, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, SUCCESS);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Set up overrides for the next test.
+add_test(() => {
+ addCertOverrides().then(run_next_test);
+});
+
+// Runs tests with built-in certificates required, all certificate exceptions
+// and no hashes
+add_test(async function test_builtin_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, true]],
+ });
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, NETWORK_FAILURE);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, SUCCESS);
+ add_install_test(HTTP, SELFSIGNED, SUCCESS);
+ add_install_test(HTTP, UNTRUSTED, SUCCESS);
+ add_install_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, NETWORK_FAILURE);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates, all certificate
+// exceptions and no hashes
+add_test(async function test_builtin_not_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_INSTALL_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, SUCCESS);
+ add_install_test(NOCERT, null, SUCCESS);
+ add_install_test(SELFSIGNED, null, SUCCESS);
+ add_install_test(UNTRUSTED, null, SUCCESS);
+ add_install_test(EXPIRED, null, SUCCESS);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, SUCCESS);
+ add_install_test(HTTP, SELFSIGNED, SUCCESS);
+ add_install_test(HTTP, UNTRUSTED, SUCCESS);
+ add_install_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, SUCCESS);
+ add_install_test(HTTPS, NOCERT, SUCCESS);
+ add_install_test(HTTPS, SELFSIGNED, SUCCESS);
+ add_install_test(HTTPS, UNTRUSTED, SUCCESS);
+ add_install_test(HTTPS, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, SUCCESS);
+ add_install_test(NOCERT, NOCERT, SUCCESS);
+ add_install_test(NOCERT, SELFSIGNED, SUCCESS);
+ add_install_test(NOCERT, UNTRUSTED, SUCCESS);
+ add_install_test(NOCERT, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, SUCCESS);
+ add_install_test(SELFSIGNED, NOCERT, SUCCESS);
+ add_install_test(SELFSIGNED, SELFSIGNED, SUCCESS);
+ add_install_test(SELFSIGNED, UNTRUSTED, SUCCESS);
+ add_install_test(SELFSIGNED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, SUCCESS);
+ add_install_test(UNTRUSTED, NOCERT, SUCCESS);
+ add_install_test(UNTRUSTED, SELFSIGNED, SUCCESS);
+ add_install_test(UNTRUSTED, UNTRUSTED, SUCCESS);
+ add_install_test(UNTRUSTED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, SUCCESS);
+ add_install_test(EXPIRED, NOCERT, SUCCESS);
+ add_install_test(EXPIRED, SELFSIGNED, SUCCESS);
+ add_install_test(EXPIRED, UNTRUSTED, SUCCESS);
+ add_install_test(EXPIRED, EXPIRED, SUCCESS);
+
+ run_install_tests(run_next_test);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js
new file mode 100644
index 0000000000..8914a8c70d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_installtrigger_install.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+
+const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+const XPI_ADDON_ID = "amosigned-xpi@tests.mozilla.org";
+
+AddonTestUtils.initMochitest(this);
+
+add_task(async function testInstallAfterHistoryPushState() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+
+ PermissionTestUtils.add(
+ "https://example.com/",
+ "install",
+ Services.perms.ALLOW_ACTION
+ );
+
+ registerCleanupFunction(async () => {
+ PermissionTestUtils.remove("https://example.com", "install");
+ await SpecialPowers.popPrefEnv();
+ });
+
+ await BrowserTestUtils.withNewTab(SECURE_TESTROOT, async browser => {
+ let installPromptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ let promptPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ XPI_ADDON_ID
+ );
+
+ await SpecialPowers.spawn(
+ browser,
+ [SECURE_TESTROOT, XPI_URL],
+ (secureTestRoot, xpiUrl) => {
+ // `sourceURL` should match the exact location, even after a location
+ // update using the history API. In this case, we update the URL with
+ // query parameters and expect `sourceURL` to contain those parameters.
+ content.history.pushState(
+ {}, // state
+ "", // title
+ `${secureTestRoot}?some=query&par=am`
+ );
+
+ content.InstallTrigger.install({ URL: xpiUrl });
+ }
+ );
+
+ await Promise.all([installPromptPromise, promptPromise]);
+
+ let addon = await promiseAddonByID(XPI_ADDON_ID);
+
+ registerCleanupFunction(async () => {
+ await addon.uninstall();
+ });
+
+ // Check that the expected installTelemetryInfo has been stored in the
+ // addon details.
+ AddonTestUtils.checkInstallInfo(addon, {
+ method: "installTrigger",
+ source: "test-host",
+ sourceURL:
+ "https://example.com/browser/toolkit/mozapps/extensions/test/browser/?some=query&par=am",
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js b/toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
new file mode 100644
index 0000000000..6bcd675ade
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_interaction_telemetry.js
@@ -0,0 +1,531 @@
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm",
+ {}
+);
+
+AddonTestUtils.initMochitest(this);
+
+let gManagerWindow;
+let gCategoryUtilities;
+
+const TELEMETRY_METHODS = ["action", "link", "view"];
+const addonId = "extension@mochi.test";
+
+registerCleanupFunction(() => {
+ // AddonTestUtils with open_manager cause this reference to be maintained and creates a leak.
+ gManagerWindow = null;
+});
+
+add_task(function setupPromptService() {
+ let promptService = mockPromptService();
+ promptService._response = 0; // Accept dialogs.
+});
+
+async function installTheme() {
+ let id = "theme@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id } },
+ manifest_version: 2,
+ name: "atheme",
+ description: "wow. such theme.",
+ author: "Pixel Pusher",
+ version: "1",
+ theme: {},
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ return extension;
+}
+
+async function installExtension(manifest = {}) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id: addonId } },
+ manifest_version: 2,
+ name: "extension",
+ description: "wow. such extension.",
+ author: "Code Pusher",
+ version: "1",
+ chrome_url_overrides: { newtab: "new.html" },
+ options_ui: { page: "options.html", open_in_tab: true },
+ browser_action: { default_popup: "action.html" },
+ page_action: { default_popup: "action.html" },
+ ...manifest,
+ },
+ files: {
+ "new.html": "yo
",
+ "options.html": "options
",
+ "action.html": "do something
",
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+ return extension;
+}
+
+function getAddonCard(doc, id) {
+ return doc.querySelector(`addon-card[addon-id="${id}"]`);
+}
+
+function openDetailView(doc, id) {
+ let card = getAddonCard(doc, id);
+ card.querySelector('[action="expand"]').click();
+}
+
+async function enableAndDisable(doc, row) {
+ let toggleButton = row.querySelector('[action="toggle-disabled"]');
+ let disabled = BrowserTestUtils.waitForEvent(row, "update");
+ toggleButton.click();
+ await disabled;
+ let enabled = BrowserTestUtils.waitForEvent(row, "update");
+ toggleButton.click();
+ await enabled;
+}
+
+async function removeAddonAndUndo(doc, row) {
+ let id = row.addon.id;
+ let started = AddonTestUtils.promiseWebExtensionStartup(id);
+ let removed = BrowserTestUtils.waitForEvent(row, "remove");
+ row.querySelector('[action="remove"]').click();
+ await removed;
+
+ let undoBanner = doc.querySelector(`message-bar[addon-id="${row.addon.id}"]`);
+ undoBanner.querySelector('[action="undo"]').click();
+ await TestUtils.waitForCondition(() => getAddonCard(doc, row.addon.id));
+ await started;
+}
+
+async function openPrefs(doc, row) {
+ row.querySelector('[action="preferences"]').click();
+}
+
+function changeAutoUpdates(doc) {
+ let row = doc.querySelector(".addon-detail-row-updates");
+ let checked = row.querySelector(":checked");
+ is(checked.value, "1", "Use default is selected");
+ row.querySelector('[value="0"]').click();
+ row.querySelector('[action="update-check"]').click();
+ row.querySelector('[value="2"]').click();
+ row.querySelector('[value="1"]').click();
+}
+
+function clickLinks(doc) {
+ let links = ["author", "homepage", "rating"];
+ for (let linkType of links) {
+ doc.querySelector(`.addon-detail-row-${linkType} a`).click();
+ }
+}
+
+async function init(startPage) {
+ gManagerWindow = await open_manager(null);
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ // When about:addons initially loads it will load the last view that
+ // was open. If that's different than startPage, then clear the events
+ // so that we can reliably test them.
+ if (gCategoryUtilities.selectedCategory != startPage) {
+ Services.telemetry.clearEvents();
+ }
+
+ await gCategoryUtilities.openType(startPage);
+
+ return gManagerWindow.document.getElementById("html-view-browser")
+ .contentDocument;
+}
+
+/* Test functions start here. */
+
+add_task(async function setup() {
+ // Clear out any telemetry data that existed before this file is run.
+ Services.telemetry.clearEvents();
+});
+
+add_task(async function testBasicViewTelemetry() {
+ let addons = await Promise.all([installTheme(), installExtension()]);
+ let doc = await init("discover");
+
+ await gCategoryUtilities.openType("theme");
+ openDetailView(doc, "theme@mochi.test");
+ await wait_for_view_load(gManagerWindow);
+
+ await gCategoryUtilities.openType("extension");
+ openDetailView(doc, "extension@mochi.test");
+ await wait_for_view_load(gManagerWindow);
+
+ assertAboutAddonsTelemetryEvents(
+ [
+ ["addonsManager", "view", "aboutAddons", "discover"],
+ ["addonsManager", "view", "aboutAddons", "list", { type: "theme" }],
+ [
+ "addonsManager",
+ "view",
+ "aboutAddons",
+ "detail",
+ { type: "theme", addonId: "theme@mochi.test" },
+ ],
+ ["addonsManager", "view", "aboutAddons", "list", { type: "extension" }],
+ [
+ "addonsManager",
+ "view",
+ "aboutAddons",
+ "detail",
+ { type: "extension", addonId: "extension@mochi.test" },
+ ],
+ ],
+ { methods: ["view"] }
+ );
+
+ await close_manager(gManagerWindow);
+ await Promise.all(addons.map(addon => addon.unload()));
+});
+
+add_task(async function testExtensionEvents() {
+ let addon = await installExtension();
+ let type = "extension";
+ let doc = await init("extension");
+
+ // Check/clear the current telemetry.
+ assertAboutAddonsTelemetryEvents(
+ [["addonsManager", "view", "aboutAddons", "list", { type: "extension" }]],
+ { methods: ["view"] }
+ );
+
+ let row = getAddonCard(doc, addonId);
+
+ // Check disable/enable.
+ await enableAndDisable(doc, row);
+ assertAboutAddonsTelemetryEvents(
+ [
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "disable", addonId, type, view: "list" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "enable", addonId, type, view: "list" },
+ ],
+ ],
+ { methods: ["action"] }
+ );
+
+ // Check remove/undo.
+ await removeAddonAndUndo(doc, row);
+ let uninstallValue = "accepted";
+ assertAboutAddonsTelemetryEvents(
+ [
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ uninstallValue,
+ { action: "uninstall", addonId, type, view: "list" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "undo", addonId, type, view: "list" },
+ ],
+ ],
+ { methods: ["action"] }
+ );
+
+ // Open the preferences page.
+ let waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ // Find the row again since it was modified on uninstall.
+ row = getAddonCard(doc, addonId);
+ await openPrefs(doc, row);
+ BrowserTestUtils.removeTab(await waitForNewTab);
+ assertAboutAddonsTelemetryEvents(
+ [
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "external",
+ { action: "preferences", type, addonId, view: "list" },
+ ],
+ ],
+ { methods: ["action"] }
+ );
+
+ // Go to the detail view.
+ openDetailView(doc, addonId);
+ await wait_for_view_load(gManagerWindow);
+ assertAboutAddonsTelemetryEvents(
+ [["addonsManager", "view", "aboutAddons", "detail", { type, addonId }]],
+ { methods: ["view"] }
+ );
+
+ // Check updates.
+ changeAutoUpdates(doc);
+ assertAboutAddonsTelemetryEvents(
+ [
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "",
+ { action: "setAddonUpdate", type, addonId, view: "detail" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "checkForUpdate", type, addonId, view: "detail" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "enabled",
+ { action: "setAddonUpdate", type, addonId, view: "detail" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "default",
+ { action: "setAddonUpdate", type, addonId, view: "detail" },
+ ],
+ ],
+ { methods: ["action"] }
+ );
+
+ // These links don't actually have a URL, so they don't open a tab. They're only
+ // shown when there is a URL though.
+ clickLinks(doc);
+
+ // The support button will open a new tab.
+ waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ doc.getElementById("help-button").click();
+ BrowserTestUtils.removeTab(await waitForNewTab);
+
+ // Check that the preferences button includes the view.
+ waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ row = getAddonCard(doc, addonId);
+ await openPrefs(doc, row);
+ BrowserTestUtils.removeTab(await waitForNewTab);
+
+ assertAboutAddonsTelemetryEvents(
+ [
+ ["addonsManager", "link", "aboutAddons", "author", { view: "detail" }],
+ ["addonsManager", "link", "aboutAddons", "homepage", { view: "detail" }],
+ ["addonsManager", "link", "aboutAddons", "rating", { view: "detail" }],
+ ["addonsManager", "link", "aboutAddons", "support", { view: "detail" }],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "external",
+ { action: "preferences", type, addonId, view: "detail" },
+ ],
+ ],
+ { methods: ["action", "link"] }
+ );
+
+ // Update the preferences and check that inline changes.
+ await gCategoryUtilities.openType("extension");
+ let upgraded = await installExtension({
+ options_ui: { page: "options.html" },
+ version: "2",
+ });
+ row = getAddonCard(doc, addonId);
+ await openPrefs(doc, row);
+ await wait_for_view_load(gManagerWindow);
+
+ assertAboutAddonsTelemetryEvents(
+ [
+ ["addonsManager", "view", "aboutAddons", "list", { type }],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "inline",
+ { action: "preferences", type, addonId, view: "list" },
+ ],
+ [
+ "addonsManager",
+ "view",
+ "aboutAddons",
+ "detail",
+ { type: "extension", addonId: "extension@mochi.test" },
+ ],
+ ],
+ { methods: ["action", "view"] }
+ );
+
+ await close_manager(gManagerWindow);
+ await addon.unload();
+ await upgraded.unload();
+});
+
+add_task(async function testGeneralActions() {
+ let win = await loadInitialView("extension");
+ info("Loaded");
+
+ let doc = win.document;
+ let pageOptionsButton = doc.querySelector('[action="page-options"]');
+ let menu = doc.querySelector("#page-options panel-list");
+ let checkForUpdates = menu.querySelector('[action="check-for-updates"]');
+ let recentUpdates = menu.querySelector('[action="view-recent-updates"]');
+ let updatePolicy = menu.querySelector('[action="set-update-automatically"]');
+ let resetUpdatePolicy = menu.querySelector('[action="reset-update-states"]');
+ let debugAddons = menu.querySelector('[action="debug-addons"]');
+ let manageShortcuts = menu.querySelector('[action="manage-shortcuts"]');
+
+ async function clickInGearMenu(item) {
+ info(`Opening menu to click ${item.getAttribute("action")}`);
+ let shown = BrowserTestUtils.waitForEvent(menu, "shown");
+ // This should perform a click on the button to ensure that works. Other
+ // tests might just open the menu directly, or click items when it's closed.
+ EventUtils.synthesizeMouseAtCenter(pageOptionsButton, {}, win);
+ await shown;
+ info(`Clicking ${item.getAttribute("action")}`);
+ item.click();
+ }
+
+ await clickInGearMenu(checkForUpdates);
+ let recentUpdatesLoaded = waitForViewLoad(win);
+ await clickInGearMenu(recentUpdates);
+ await recentUpdatesLoaded;
+ await clickInGearMenu(updatePolicy);
+ await clickInGearMenu(updatePolicy);
+ await clickInGearMenu(resetUpdatePolicy);
+
+ // Check shortcuts view.
+ let shortcutsLoaded = waitForViewLoad(win);
+ await clickInGearMenu(manageShortcuts);
+ await shortcutsLoaded;
+ await clickInGearMenu(checkForUpdates);
+
+ let waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ await clickInGearMenu(debugAddons);
+ info("Waiting for about:debugging tab");
+ let tab = await waitForNewTab;
+ BrowserTestUtils.removeTab(tab);
+
+ waitForNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ let searchBox = doc.querySelector("search-addons").input.inputField;
+ searchBox.value = "something";
+ searchBox.focus();
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ info("Waiting for AMO search tab");
+ tab = await waitForNewTab;
+ BrowserTestUtils.removeTab(tab);
+
+ assertAboutAddonsTelemetryEvents(
+ [
+ ["addonsManager", "view", "aboutAddons", "list", { type: "extension" }],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "checkForUpdates", view: "list" },
+ ],
+ ["addonsManager", "view", "aboutAddons", "updates", { type: "recent" }],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "default,enabled",
+ { action: "setUpdatePolicy", view: "updates" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "enabled",
+ { action: "setUpdatePolicy", view: "updates" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "resetUpdatePolicy", view: "updates" },
+ ],
+ ["addonsManager", "view", "aboutAddons", "shortcuts"],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "checkForUpdates", view: "shortcuts" },
+ ],
+ [
+ "addonsManager",
+ "link",
+ "aboutAddons",
+ "about:debugging",
+ { view: "shortcuts" },
+ ],
+ [
+ "addonsManager",
+ "link",
+ "aboutAddons",
+ "search",
+ { view: "shortcuts", type: "shortcuts" },
+ ],
+ ],
+ { methods: TELEMETRY_METHODS }
+ );
+
+ await closeView(win);
+
+ assertAboutAddonsTelemetryEvents([]);
+});
+
+add_task(async function testPreferencesLink() {
+ assertAboutAddonsTelemetryEvents([]);
+
+ let doc = await init("theme");
+
+ // Open the about:preferences page from about:addons.
+ let waitForNewTab = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:preferences",
+ true
+ );
+ doc.getElementById("preferencesButton").click();
+ let tab = await waitForNewTab;
+ let getAddonsButton = () =>
+ tab.linkedBrowser.contentDocument.getElementById("addonsButton");
+
+ // Open the about:addons page from about:preferences.
+ getAddonsButton().click();
+
+ // Close the about:preferences tab.
+ BrowserTestUtils.removeTab(tab);
+
+ TelemetryTestUtils.assertEvents(
+ [
+ ["addonsManager", "view", "aboutAddons", "list", { type: "theme" }],
+ [
+ "addonsManager",
+ "link",
+ "aboutAddons",
+ "about:preferences",
+ { view: "list" },
+ ],
+ ["addonsManager", "link", "aboutPreferences", "about:addons"],
+ ],
+ {
+ category: "addonsManager",
+ method: /^(link|view)$/,
+ }
+ );
+
+ await close_manager(gManagerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js
new file mode 100644
index 0000000000..61630c89af
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts.js
@@ -0,0 +1,309 @@
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+
+function extensionShortcutsReady(id) {
+ let extension = WebExtensionPolicy.getByID(id).extension;
+ return BrowserTestUtils.waitForCondition(() => {
+ return extension.shortcuts.keysetsMap.has(window);
+ }, "Wait for add-on keyset to be registered");
+}
+
+async function loadShortcutsView() {
+ // Load the theme view initially so we can verify that the category is switched
+ // to "extension" when the shortcuts view is loaded.
+ let win = await loadInitialView("theme");
+ let categoryUtils = new CategoryUtilities(win.managerWindow);
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/theme",
+ "The theme category is selected"
+ );
+
+ let shortcutsLink = win.document.querySelector(
+ '#page-options [action="manage-shortcuts"]'
+ );
+ ok(!shortcutsLink.hidden, "The shortcuts link is visible");
+
+ let loaded = waitForViewLoad(win);
+ shortcutsLink.click();
+ await loaded;
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ "addons://list/extension",
+ "The extension category is now selected"
+ );
+
+ return win;
+}
+
+add_task(async function testUpdatingCommands() {
+ let commands = {
+ commandZero: {},
+ commandOne: {
+ suggested_key: { default: "Shift+Alt+7" },
+ },
+ commandTwo: {
+ description: "Command Two!",
+ suggested_key: { default: "Alt+4" },
+ },
+ _execute_browser_action: {
+ suggested_key: { default: "Shift+Alt+9" },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands,
+ browser_action: { default_popup: "popup.html" },
+ },
+ background() {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.sendMessage("oncommand", commandName);
+ });
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extensionShortcutsReady(extension.id);
+
+ async function checkShortcut(name, key, modifiers) {
+ EventUtils.synthesizeKey(key, modifiers);
+ let message = await extension.awaitMessage("oncommand");
+ is(
+ message,
+ name,
+ `Expected onCommand listener to fire with the correct name: ${name}`
+ );
+ }
+
+ // Check that the original shortcuts work.
+ await checkShortcut("commandOne", "7", { shiftKey: true, altKey: true });
+ await checkShortcut("commandTwo", "4", { altKey: true });
+
+ let win = await loadShortcutsView();
+ let doc = win.document;
+
+ let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
+ ok(card, `There is a card for the extension`);
+
+ let inputs = card.querySelectorAll(".shortcut-input");
+ is(
+ inputs.length,
+ Object.keys(commands).length,
+ "There is an input for each command"
+ );
+
+ let nameOrder = Array.from(inputs).map(input => input.getAttribute("name"));
+ Assert.deepEqual(
+ nameOrder,
+ ["commandOne", "commandTwo", "_execute_browser_action", "commandZero"],
+ "commandZero should be last since it is unset"
+ );
+
+ let count = 1;
+ for (let input of inputs) {
+ // Change the shortcut.
+ input.focus();
+ EventUtils.synthesizeKey("8", { shiftKey: true, altKey: true });
+ count++;
+
+ // Wait for the shortcut attribute to change.
+ await BrowserTestUtils.waitForCondition(
+ () => input.getAttribute("shortcut") == "Alt+Shift+8",
+ "Wait for shortcut to update to Alt+Shift+8"
+ );
+
+ // Check that the change worked (but skip if browserAction).
+ if (input.getAttribute("name") != "_execute_browser_action") {
+ await checkShortcut(input.getAttribute("name"), "8", {
+ shiftKey: true,
+ altKey: true,
+ });
+ }
+
+ // Change it again so it doesn't conflict with the next command.
+ input.focus();
+ EventUtils.synthesizeKey(count.toString(), {
+ shiftKey: true,
+ altKey: true,
+ });
+ await BrowserTestUtils.waitForCondition(
+ () => input.getAttribute("shortcut") == `Alt+Shift+${count}`,
+ `Wait for shortcut to update to Alt+Shift+${count}`
+ );
+ }
+
+ // Check that errors can be shown.
+ let input = inputs[0];
+ let error = doc.querySelector(".error-message");
+ let label = error.querySelector(".error-message-label");
+ is(error.style.visibility, "hidden", "The error is initially hidden");
+
+ // Try a shortcut with only shift for a modifier.
+ input.focus();
+ EventUtils.synthesizeKey("J", { shiftKey: true });
+ let possibleErrors = ["shortcuts-modifier-mac", "shortcuts-modifier-other"];
+ ok(possibleErrors.includes(label.dataset.l10nId), `The message is set`);
+ is(error.style.visibility, "visible", "The error is shown");
+
+ // Escape should clear the focus and hide the error.
+ is(doc.activeElement, input, "The input is focused");
+ EventUtils.synthesizeKey("Escape", {});
+ ok(doc.activeElement != input, "The input is no longer focused");
+ is(error.style.visibility, "hidden", "The error is hidden");
+
+ // Check if assigning already assigned shortcut is prevented.
+ input.focus();
+ EventUtils.synthesizeKey("2", { shiftKey: true, altKey: true });
+ is(label.dataset.l10nId, "shortcuts-exists", `The message is set`);
+ is(error.style.visibility, "visible", "The error is shown");
+
+ // Check the label uses the description first, and has a default for the special cases.
+ function checkLabel(name, value) {
+ let input = doc.querySelector(`.shortcut-input[name="${name}"]`);
+ let label = input.previousElementSibling;
+ if (label.dataset.l10nId) {
+ is(label.dataset.l10nId, value, "The l10n-id is set");
+ } else {
+ is(label.textContent, value, "The textContent is set");
+ }
+ }
+ checkLabel("commandOne", "commandOne");
+ checkLabel("commandTwo", "Command Two!");
+ checkLabel("_execute_browser_action", "shortcuts-browserAction2");
+
+ await closeView(win);
+ await extension.unload();
+});
+
+async function startExtensionWithCommands(numCommands) {
+ let commands = {};
+
+ for (let i = 0; i < numCommands; i++) {
+ commands[`command-${i}`] = {};
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands,
+ },
+ background() {
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await extensionShortcutsReady(extension.id);
+
+ return extension;
+}
+
+add_task(async function testExpanding() {
+ const numCommands = 7;
+ const visibleCommands = 5;
+
+ let extension = await startExtensionWithCommands(numCommands);
+
+ let win = await loadShortcutsView();
+ let doc = win.document;
+
+ let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
+ ok(!card.hasAttribute("expanded"), "The card is not expanded");
+
+ let shortcutRows = card.querySelectorAll(".shortcut-row");
+ is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`);
+
+ function assertCollapsedVisibility() {
+ for (let i = 0; i < shortcutRows.length; i++) {
+ let row = shortcutRows[i];
+ if (i < visibleCommands) {
+ ok(
+ getComputedStyle(row).display != "none",
+ `The first ${visibleCommands} rows are visible`
+ );
+ } else {
+ is(getComputedStyle(row).display, "none", "The other rows are hidden");
+ }
+ }
+ }
+
+ // Check the visibility of the rows.
+ assertCollapsedVisibility();
+
+ let expandButton = card.querySelector(".expand-button");
+ ok(expandButton, "There is an expand button");
+ let l10nAttrs = doc.l10n.getAttributes(expandButton);
+ is(l10nAttrs.id, "shortcuts-card-expand-button", "The expand text is shown");
+ is(
+ l10nAttrs.args.numberToShow,
+ numCommands - visibleCommands,
+ "The number to be shown is set on the expand button"
+ );
+
+ // Expand the card.
+ expandButton.click();
+
+ is(card.getAttribute("expanded"), "true", "The card is now expanded");
+
+ for (let row of shortcutRows) {
+ ok(getComputedStyle(row).display != "none", "All the rows are visible");
+ }
+
+ // The collapse text is now shown.
+ l10nAttrs = doc.l10n.getAttributes(expandButton);
+ is(
+ l10nAttrs.id,
+ "shortcuts-card-collapse-button",
+ "The colapse text is shown"
+ );
+
+ // Collapse the card.
+ expandButton.click();
+
+ ok(!card.hasAttribute("expanded"), "The card is now collapsed again");
+
+ assertCollapsedVisibility({ collapsed: true });
+
+ await closeView(win);
+ await extension.unload();
+});
+
+add_task(async function testOneExtraCommandIsNotCollapsed() {
+ const numCommands = 6;
+ let extension = await startExtensionWithCommands(numCommands);
+
+ let win = await loadShortcutsView();
+ let doc = win.document;
+
+ // The card is not expanded, since it doesn't collapse.
+ let card = doc.querySelector(`.card[addon-id="${extension.id}"]`);
+ ok(!card.hasAttribute("expanded"), "The card is not expanded");
+
+ // Each shortcut has a row.
+ let shortcutRows = card.querySelectorAll(".shortcut-row");
+ is(shortcutRows.length, numCommands, `There are ${numCommands} shortcuts`);
+
+ // There's no expand button, since it can't be expanded.
+ let expandButton = card.querySelector(".expand-button");
+ ok(!expandButton, "There is no expand button");
+
+ // All of the rows are visible, to avoid a "Show 1 More" button.
+ for (let row of shortcutRows) {
+ ok(getComputedStyle(row).display != "none", "All the rows are visible");
+ }
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js
new file mode 100644
index 0000000000..d88a9de446
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_hidden.js
@@ -0,0 +1,199 @@
+/* eslint max-len: ["error", 80] */
+"use strict";
+
+async function loadShortcutsView() {
+ let managerWin = await open_manager(null);
+ managerWin.gViewController.loadView("addons://shortcuts/shortcuts");
+ await wait_for_view_load(managerWin);
+ return managerWin.document.getElementById("html-view-browser")
+ .contentDocument;
+}
+
+async function closeShortcutsView(doc) {
+ let managerWin = doc.defaultView.parent;
+ await close_manager(managerWin);
+}
+
+async function registerAndStartExtension(mockProvider, ext) {
+ // Shortcuts are registered when an extension is started, so we need to load
+ // and start an extension.
+ let extension = ExtensionTestUtils.loadExtension(ext);
+ await extension.startup();
+
+ // Extensions only appear in the add-on manager when they are registered with
+ // the add-on manager, e.g. by passing "useAddonManager" to `loadExtension`.
+ // "useAddonManager" can however not be used, because the resulting add-ons
+ // are unsigned, and only add-ons with privileged signatures can be hidden.
+ mockProvider.createAddons([
+ {
+ id: extension.id,
+ name: ext.manifest.name,
+ type: "extension",
+ version: "1",
+ // We use MockProvider because the "hidden" property cannot
+ // be set when "useAddonManager" is passed to loadExtension.
+ hidden: ext.manifest.hidden,
+ isSystem: ext.isSystem,
+ },
+ ]);
+ return extension;
+}
+
+function getShortcutCard(doc, extension) {
+ return doc.querySelector(`.shortcut[addon-id="${extension.id}"]`);
+}
+
+function getShortcutByName(doc, extension, name) {
+ let card = getShortcutCard(doc, extension);
+ return card && card.querySelector(`.shortcut-input[name="${name}"]`);
+}
+
+function getNoShortcutListItem(doc, extension) {
+ let { id } = extension;
+ let li = doc.querySelector(`.shortcuts-no-commands-list [addon-id="${id}"]`);
+ return li && li.textContent;
+}
+
+add_task(async function extension_with_shortcuts() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "shortcut addon",
+ commands: {
+ theShortcut: {},
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let doc = await loadShortcutsView();
+
+ ok(
+ getShortcutByName(doc, extension, "theShortcut"),
+ "Extension with shortcuts should have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, extension),
+ null,
+ "Extension with shortcuts should not be listed"
+ );
+
+ await closeShortcutsView(doc);
+ await extension.unload();
+});
+
+add_task(async function extension_without_shortcuts() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "no shortcut addon",
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ let doc = await loadShortcutsView();
+
+ is(
+ getShortcutCard(doc, extension),
+ null,
+ "Extension without shortcuts should not have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, extension),
+ "no shortcut addon",
+ "The add-on's name is set in the list"
+ );
+
+ await closeShortcutsView(doc);
+ await extension.unload();
+});
+
+// Hidden add-ons without shortcuts should be hidden,
+// but their card should be shown if there is a shortcut.
+add_task(async function hidden_extension() {
+ let mockProvider = new MockProvider();
+ let hiddenExt1 = await registerAndStartExtension(mockProvider, {
+ manifest: {
+ name: "hidden with shortcuts",
+ hidden: true,
+ commands: {
+ hiddenShortcut: {},
+ },
+ },
+ });
+ let hiddenExt2 = await registerAndStartExtension(mockProvider, {
+ manifest: {
+ name: "hidden without shortcuts",
+ hidden: true,
+ },
+ });
+
+ let doc = await loadShortcutsView();
+
+ ok(
+ getShortcutByName(doc, hiddenExt1, "hiddenShortcut"),
+ "Hidden extension with shortcuts should have a card"
+ );
+
+ is(
+ getShortcutCard(doc, hiddenExt2),
+ null,
+ "Hidden extension without shortcuts should not have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, hiddenExt2),
+ null,
+ "Hidden extension without shortcuts should not be listed"
+ );
+
+ await closeShortcutsView(doc);
+ await hiddenExt1.unload();
+ await hiddenExt2.unload();
+
+ mockProvider.unregister();
+});
+
+add_task(async function system_addons_and_shortcuts() {
+ let mockProvider = new MockProvider();
+ let systemExt1 = await registerAndStartExtension(mockProvider, {
+ isSystem: true,
+ manifest: {
+ name: "system with shortcuts",
+ // In practice, all XPIStateLocations with isSystem=true also have
+ // isBuiltin=true, which implies that hidden=true as well.
+ hidden: true,
+ commands: {
+ systemShortcut: {},
+ },
+ },
+ });
+ let systemExt2 = await registerAndStartExtension(mockProvider, {
+ isSystem: true,
+ manifest: {
+ name: "system without shortcuts",
+ hidden: true,
+ },
+ });
+
+ let doc = await loadShortcutsView();
+
+ ok(
+ getShortcutByName(doc, systemExt1, "systemShortcut"),
+ "System add-on with shortcut should have a card"
+ );
+
+ is(
+ getShortcutCard(doc, systemExt2),
+ null,
+ "System add-on without shortcut should not have a card"
+ );
+ is(
+ getNoShortcutListItem(doc, systemExt2),
+ null,
+ "System add-on without shortcuts should not be listed"
+ );
+
+ await closeShortcutsView(doc);
+ await systemExt1.unload();
+ await systemExt2.unload();
+
+ mockProvider.unregister();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js
new file mode 100644
index 0000000000..bd6ebcc0f4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_manage_shortcuts_remove.js
@@ -0,0 +1,173 @@
+"use strict";
+
+async function loadShortcutsView() {
+ let managerWin = await open_manager(null);
+ managerWin.gViewController.loadView("addons://shortcuts/shortcuts");
+ await wait_for_view_load(managerWin);
+ return managerWin.document.getElementById("html-view-browser")
+ .contentDocument;
+}
+
+async function closeShortcutsView(doc) {
+ let managerWin = doc.defaultView.parent;
+ await close_manager(managerWin);
+}
+
+function getShortcutCard(doc, extension) {
+ return doc.querySelector(`.shortcut[addon-id="${extension.id}"]`);
+}
+
+function getShortcutByName(doc, extension, name) {
+ let card = getShortcutCard(doc, extension);
+ return card && card.querySelector(`.shortcut-input[name="${name}"]`);
+}
+
+async function waitForShortcutSet(input, expected) {
+ let doc = input.ownerDocument;
+ await BrowserTestUtils.waitForCondition(
+ () => input.getAttribute("shortcut") == expected,
+ `Shortcut should be set to ${JSON.stringify(expected)}`
+ );
+ ok(doc.activeElement != input, "The input is no longer focused");
+ checkHasRemoveButton(input, expected !== "");
+}
+
+function removeButtonForInput(input) {
+ let removeButton = input.parentNode.querySelector(".shortcut-remove-button");
+ ok(removeButton, "has remove button");
+ return removeButton;
+}
+
+function checkHasRemoveButton(input, expected) {
+ let removeButton = removeButtonForInput(input);
+ let visibility = input.ownerGlobal.getComputedStyle(removeButton).visibility;
+ if (expected) {
+ is(visibility, "visible", "Remove button should be visible");
+ } else {
+ is(visibility, "hidden", "Remove button should be hidden");
+ }
+}
+
+add_task(async function test_remove_shortcut() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ commandEmpty: {},
+ commandOne: {
+ suggested_key: { default: "Shift+Alt+1" },
+ },
+ commandTwo: {
+ suggested_key: { default: "Shift+Alt+2" },
+ },
+ },
+ },
+ background() {
+ browser.commands.onCommand.addListener(commandName => {
+ browser.test.sendMessage("oncommand", commandName);
+ });
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ let doc = await loadShortcutsView();
+
+ let input = getShortcutByName(doc, extension, "commandOne");
+
+ checkHasRemoveButton(input, true);
+
+ // First: Verify that Shift-Del is not valid, but doesn't do anything.
+ input.focus();
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ let errorElem = doc.querySelector(".error-message");
+ is(errorElem.style.visibility, "visible", "Expected error message");
+ let errorId = doc.l10n.getAttributes(
+ errorElem.querySelector(".error-message-label")
+ ).id;
+ if (AppConstants.platform == "macosx") {
+ is(errorId, "shortcuts-modifier-mac", "Shift-Del is not a valid shortcut");
+ } else {
+ is(errorId, "shortcuts-modifier-other", "Shift-Del isn't a valid shortcut");
+ }
+ checkHasRemoveButton(input, true);
+
+ // Now, verify that the original shortcut still works.
+ EventUtils.synthesizeKey("KEY_Escape");
+ ok(doc.activeElement != input, "The input is no longer focused");
+ is(errorElem.style.visibility, "hidden", "The error is hidden");
+
+ EventUtils.synthesizeKey("1", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("oncommand");
+
+ // Alt-Shift-Del is a valid shortcut.
+ input.focus();
+ EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true });
+ await waitForShortcutSet(input, "Alt+Shift+Delete");
+ EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("oncommand");
+
+ // Del without modifiers should clear the shortcut.
+ input.focus();
+ EventUtils.synthesizeKey("KEY_Delete");
+ await waitForShortcutSet(input, "");
+ // Trigger the shortcuts that were originally associated with commandOne,
+ // and then trigger commandTwo. The extension should only see commandTwo.
+ EventUtils.synthesizeKey("1", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("KEY_Delete", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true });
+ is(
+ await extension.awaitMessage("oncommand"),
+ "commandTwo",
+ "commandOne should be disabled, commandTwo should still be enabled"
+ );
+
+ // Set a shortcut where the default was not set.
+ let inputEmpty = getShortcutByName(doc, extension, "commandEmpty");
+ is(inputEmpty.getAttribute("shortcut"), "", "Empty shortcut by default");
+ checkHasRemoveButton(input, false);
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ await waitForShortcutSet(inputEmpty, "Alt+Shift+3");
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("oncommand");
+ // Clear shortcut.
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("KEY_Delete");
+ await waitForShortcutSet(inputEmpty, "");
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true });
+ is(
+ await extension.awaitMessage("oncommand"),
+ "commandTwo",
+ "commandEmpty should be disabled, commandTwo should still be enabled"
+ );
+
+ // Now verify that the Backspace button does the same thing as Delete.
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ await waitForShortcutSet(inputEmpty, "Alt+Shift+3");
+ inputEmpty.focus();
+ EventUtils.synthesizeKey("KEY_Backspace");
+ await waitForShortcutSet(input, "");
+ EventUtils.synthesizeKey("3", { altKey: true, shiftKey: true });
+ EventUtils.synthesizeKey("2", { altKey: true, shiftKey: true });
+ is(
+ await extension.awaitMessage("oncommand"),
+ "commandTwo",
+ "commandEmpty should be disabled again by Backspace"
+ );
+
+ // Check that the remove button works as expected.
+ let inputTwo = getShortcutByName(doc, extension, "commandTwo");
+ is(inputTwo.getAttribute("shortcut"), "Shift+Alt+2", "initial shortcut");
+ checkHasRemoveButton(inputTwo, true);
+ removeButtonForInput(inputTwo).click();
+ is(inputTwo.getAttribute("shortcut"), "", "cleared shortcut");
+ checkHasRemoveButton(inputTwo, false);
+ ok(doc.activeElement != inputTwo, "input of removed shortcut is not focused");
+
+ await closeShortcutsView(doc);
+
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js b/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js
new file mode 100644
index 0000000000..5433ddcc77
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_menu_button_accessibility.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function testOpenMenu(btn, method) {
+ let shown = BrowserTestUtils.waitForEvent(btn.ownerGlobal, "shown", true);
+ await method();
+ await shown;
+ is(btn.getAttribute("aria-expanded"), "true", "expanded when open");
+}
+
+async function testCloseMenu(btn, method) {
+ let hidden = BrowserTestUtils.waitForEvent(btn.ownerGlobal, "hidden", true);
+ await method();
+ await hidden;
+ is(btn.getAttribute("aria-expanded"), "false", "not expanded when closed");
+}
+
+async function testButton(btn) {
+ let win = btn.ownerGlobal;
+
+ is(btn.getAttribute("aria-haspopup"), "menu", "it has a menu");
+ is(btn.getAttribute("aria-expanded"), "false", "not expanded");
+
+ info("Test open/close with mouse");
+ await testOpenMenu(btn, () => {
+ EventUtils.synthesizeMouseAtCenter(btn, {}, win);
+ });
+ await testCloseMenu(btn, () => {
+ let spacer = win.document.querySelector(".main-heading .spacer");
+ EventUtils.synthesizeMouseAtCenter(spacer, {}, win);
+ });
+
+ info("Test open/close with keyboard");
+ await testOpenMenu(btn, async () => {
+ btn.focus();
+ EventUtils.synthesizeKey(" ", {}, win);
+ });
+ await testCloseMenu(btn, () => {
+ EventUtils.synthesizeKey("Escape", {}, win);
+ });
+}
+
+add_task(async function testPageOptionsMenuButton() {
+ let win = await loadInitialView("extension");
+
+ await testButton(
+ win.document.querySelector(".page-options-menu .more-options-button")
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testCardMoreOptionsButton() {
+ let id = "more-options-button@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let card = getAddonCard(win, id);
+
+ info("Check list page");
+ await testButton(card.querySelector(".more-options-button"));
+
+ let viewLoaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(card, {}, win);
+ await viewLoaded;
+
+ info("Check detail page");
+ card = getAddonCard(win, id);
+ await testButton(card.querySelector(".more-options-button"));
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js b/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js
new file mode 100644
index 0000000000..e049cbd618
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_page_accessibility.js
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testPageTitle() {
+ let win = await loadInitialView("extension");
+ let title = win.document.querySelector("title");
+ is(
+ win.document.l10n.getAttributes(title).id,
+ "addons-page-title",
+ "The page title is set"
+ );
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js
new file mode 100644
index 0000000000..f2c6d372a5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_page_options_install_addon.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests bug 567127 - Add install button to the add-ons manager
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+async function checkInstallConfirmation(...names) {
+ let notificationCount = 0;
+ let observer = {
+ observe(aSubject, aTopic, aData) {
+ var installInfo = aSubject.wrappedJSObject;
+ isnot(
+ installInfo.browser,
+ null,
+ "Notification should have non-null browser"
+ );
+ Assert.deepEqual(
+ installInfo.installs[0].installTelemetryInfo,
+ {
+ source: "about:addons",
+ method: "install-from-file",
+ },
+ "Got the expected installTelemetryInfo"
+ );
+ notificationCount++;
+ },
+ };
+ Services.obs.addObserver(observer, "addon-install-started");
+
+ let results = [];
+
+ let promise = promisePopupNotificationShown("addon-webext-permissions");
+ for (let i = 0; i < names.length; i++) {
+ let panel = await promise;
+ let name = panel.getAttribute("name");
+ results.push(name);
+
+ info(`Saw install for ${name}`);
+ if (results.length < names.length) {
+ info(
+ `Waiting for installs for ${names.filter(n => !results.includes(n))}`
+ );
+
+ promise = promisePopupNotificationShown("addon-webext-permissions");
+ }
+ panel.secondaryButton.click();
+ }
+
+ Assert.deepEqual(results.sort(), names.sort(), "Got expected installs");
+
+ is(
+ notificationCount,
+ names.length,
+ `Saw ${names.length} addon-install-started notification`
+ );
+ Services.obs.removeObserver(observer, "addon-install-started");
+}
+
+add_task(async function test_install_from_file() {
+ let win = await loadInitialView("extension");
+
+ var filePaths = [
+ get_addon_file_url("browser_dragdrop1.xpi"),
+ get_addon_file_url("browser_dragdrop2.xpi"),
+ ];
+ for (let uri of filePaths) {
+ ok(uri.file != null, `Should have file for ${uri.spec}`);
+ ok(uri.file instanceof Ci.nsIFile, `Should have nsIFile for ${uri.spec}`);
+ }
+ MockFilePicker.setFiles(filePaths.map(aPath => aPath.file));
+
+ // Set handler that executes the core test after the window opens,
+ // and resolves the promise when the window closes
+ let pInstallURIClosed = checkInstallConfirmation(
+ "Drag Drop test 1",
+ "Drag Drop test 2"
+ );
+
+ win.document
+ .querySelector('#page-options [action="install-from-file"]')
+ .click();
+
+ await pInstallURIClosed;
+
+ MockFilePicker.cleanup();
+ await closeView(win);
+});
+
+add_task(async function test_install_disabled() {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let pageOptionsMenu = doc.querySelector("addon-page-options panel-list");
+
+ function openPageOptions() {
+ let opened = BrowserTestUtils.waitForEvent(pageOptionsMenu, "shown");
+ pageOptionsMenu.open = true;
+ return opened;
+ }
+
+ function closePageOptions() {
+ let closed = BrowserTestUtils.waitForEvent(pageOptionsMenu, "hidden");
+ pageOptionsMenu.open = false;
+ return closed;
+ }
+
+ await openPageOptions();
+ let installButton = doc.querySelector('[action="install-from-file"]');
+ ok(!installButton.hidden, "The install button is shown");
+ await closePageOptions();
+
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_XPI_ENABLED, false]] });
+
+ await openPageOptions();
+ ok(installButton.hidden, "The install button is now hidden");
+ await closePageOptions();
+
+ await SpecialPowers.popPrefEnv();
+
+ await openPageOptions();
+ ok(!installButton.hidden, "The install button is shown again");
+ await closePageOptions();
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js b/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js
new file mode 100644
index 0000000000..4acc71af29
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_page_options_updates.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Make sure we don't accidentally start a background update while the prefs
+// are enabled.
+disableBackgroundUpdateTimer();
+registerCleanupFunction(() => {
+ enableBackgroundUpdateTimer();
+});
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm",
+ {}
+);
+
+const PREF_UPDATE_ENABLED = "extensions.update.enabled";
+const PREF_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
+
+add_task(async function testUpdateAutomaticallyButton() {
+ Services.telemetry.clearEvents();
+ SpecialPowers.pushPrefEnv({
+ set: [
+ [PREF_UPDATE_ENABLED, true],
+ [PREF_AUTOUPDATE_DEFAULT, true],
+ ],
+ });
+
+ let win = await loadInitialView("extension");
+
+ let toggleAutomaticButton = win.document.querySelector(
+ '#page-options [action="set-update-automatically"]'
+ );
+
+ info("Verify the checked state reflects the update state");
+ ok(toggleAutomaticButton.checked, "Automatic updates button is checked");
+
+ AddonManager.autoUpdateDefault = false;
+ ok(!toggleAutomaticButton.checked, "Automatic updates button is unchecked");
+
+ AddonManager.autoUpdateDefault = true;
+ ok(toggleAutomaticButton.checked, "Automatic updates button is re-checked");
+
+ info("Verify that clicking the button changes the update state");
+ ok(AddonManager.autoUpdateDefault, "Auto updates are default");
+ ok(AddonManager.updateEnabled, "Updates are enabled");
+
+ toggleAutomaticButton.click();
+ ok(!AddonManager.autoUpdateDefault, "Auto updates are disabled");
+ ok(AddonManager.updateEnabled, "Updates are enabled");
+
+ toggleAutomaticButton.click();
+ ok(AddonManager.autoUpdateDefault, "Auto updates are enabled again");
+ ok(AddonManager.updateEnabled, "Updates are enabled");
+
+ assertAboutAddonsTelemetryEvents(
+ [
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "enabled",
+ { action: "setUpdatePolicy", view: "list" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "default,enabled",
+ { action: "setUpdatePolicy", view: "list" },
+ ],
+ ],
+ { methods: ["action"] }
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testResetUpdateStates() {
+ let id = "update-state@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id } },
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ let win = await loadInitialView("extension");
+ let resetStateButton = win.document.querySelector(
+ '#page-options [action="reset-update-states"]'
+ );
+
+ info("Changing add-on update state");
+ let addon = await AddonManager.getAddonByID(id);
+
+ let setAddonUpdateState = async updateState => {
+ let changed = AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ addon.applyBackgroundUpdates = updateState;
+ await changed;
+ let addonState = addon.applyBackgroundUpdates;
+ is(addonState, updateState, `Add-on updates are ${updateState}`);
+ };
+
+ await setAddonUpdateState(AddonManager.AUTOUPDATE_DISABLE);
+
+ let propertyChanged = AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ resetStateButton.click();
+ await propertyChanged;
+ is(
+ addon.applyBackgroundUpdates,
+ AddonManager.AUTOUPDATE_DEFAULT,
+ "Add-on is reset to default updates"
+ );
+
+ await setAddonUpdateState(AddonManager.AUTOUPDATE_ENABLE);
+
+ propertyChanged = AddonTestUtils.promiseAddonEvent("onPropertyChanged");
+ resetStateButton.click();
+ await propertyChanged;
+ is(
+ addon.applyBackgroundUpdates,
+ AddonManager.AUTOUPDATE_DEFAULT,
+ "Add-on is reset to default updates again"
+ );
+
+ info("Check the label on the button as the global state changes");
+ is(
+ win.document.l10n.getAttributes(resetStateButton).id,
+ "addon-updates-reset-updates-to-automatic",
+ "The reset button label says it resets to automatic"
+ );
+
+ info("Disable auto updating globally");
+ AddonManager.autoUpdateDefault = false;
+
+ is(
+ win.document.l10n.getAttributes(resetStateButton).id,
+ "addon-updates-reset-updates-to-manual",
+ "The reset button label says it resets to manual"
+ );
+
+ assertAboutAddonsTelemetryEvents(
+ [
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "resetUpdatePolicy", view: "list" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ null,
+ { action: "resetUpdatePolicy", view: "list" },
+ ],
+ ],
+ { methods: ["action"] }
+ );
+
+ await closeView(win);
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_panel_item_accesskey.js b/toolkit/mozapps/extensions/test/browser/browser_panel_item_accesskey.js
new file mode 100644
index 0000000000..72e35f8dee
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_panel_item_accesskey.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testPanelItemWithAccesskey() {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let panelList = doc.createElement("panel-list");
+ let items = [
+ { textContent: "First Item", accessKey: "F" },
+ { textContent: "Second Item", accessKey: "S" },
+ { textContent: "Third Item" },
+ ];
+
+ let panelItems = items.map(details => {
+ let item = doc.createElement("panel-item");
+ for (let [attr, value] of Object.entries(details)) {
+ item[attr] = value;
+ }
+ panelList.appendChild(item);
+ return item;
+ });
+
+ doc.body.appendChild(panelList);
+
+ function assertAccessKeys(items, keys, { checkLabels = false } = {}) {
+ is(items.length, keys.length, "Got the same number of items and keys");
+ for (let i = 0; i < items.length; i++) {
+ is(items[i].accessKey, keys[i], `Item ${i} has the right key`);
+ if (checkLabels) {
+ let label = items[i].shadowRoot.querySelector("label");
+ is(label.accessKey, keys[i] || null, `Label ${i} has the right key`);
+ }
+ }
+ }
+
+ info("Accesskeys should be removed when closed");
+ assertAccessKeys(panelItems, ["", "", ""]);
+
+ info("Accesskeys should be set when open");
+ let panelShown = BrowserTestUtils.waitForEvent(panelList, "shown");
+ panelList.show();
+ await panelShown;
+ assertAccessKeys(panelItems, ["F", "S", ""], { checkLabels: true });
+
+ info("Changing accesskeys should happen right away");
+ panelItems[1].accessKey = "c";
+ panelItems[2].accessKey = "T";
+ assertAccessKeys(panelItems, ["F", "c", "T"], { checkLabels: true });
+
+ info("Accesskeys are removed again on hide");
+ let panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
+ panelList.hide();
+ await panelHidden;
+ assertAccessKeys(panelItems, ["", "", ""]);
+
+ info("Accesskeys are set again on show");
+ panelItems[0].removeAttribute("accesskey");
+ panelShown = BrowserTestUtils.waitForEvent(panelList, "shown");
+ panelList.show();
+ await panelShown;
+ assertAccessKeys(panelItems, ["", "c", "T"], { checkLabels: true });
+
+ info("Check that accesskeys can be used without the modifier when open");
+ let secondClickCount = 0;
+ let thirdClickCount = 0;
+ panelItems[1].addEventListener("click", () => secondClickCount++);
+ panelItems[2].addEventListener("click", () => thirdClickCount++);
+
+ // Make sure the focus is in the window.
+ panelItems[0].focus();
+
+ panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
+ EventUtils.synthesizeKey("c", {}, win);
+ await panelHidden;
+
+ is(secondClickCount, 1, "The accesskey worked unmodified");
+ is(thirdClickCount, 0, "The other listener wasn't fired");
+
+ EventUtils.synthesizeKey("c", {}, win);
+ EventUtils.synthesizeKey("t", {}, win);
+
+ is(secondClickCount, 1, "The key doesn't trigger when closed");
+ is(thirdClickCount, 0, "The key doesn't trigger when closed");
+
+ panelShown = BrowserTestUtils.waitForEvent(panelList, "shown");
+ panelList.show();
+ await panelShown;
+
+ panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
+ EventUtils.synthesizeKey("t", {}, win);
+ await panelHidden;
+
+ is(secondClickCount, 1, "The other listener wasn't fired");
+ is(thirdClickCount, 1, "The accesskey worked unmodified");
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_panel_list_accessibility.js b/toolkit/mozapps/extensions/test/browser/browser_panel_list_accessibility.js
new file mode 100644
index 0000000000..186d431a6d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_panel_list_accessibility.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function setupPanel(win) {
+ let doc = win.document;
+ // Clear out the other elements so only our test content is on the page.
+ doc.body.textContent = "";
+ let panelList = doc.createElement("panel-list");
+ let items = ["one", "two", "three"];
+ let panelItems = items.map(item => {
+ let panelItem = doc.createElement("panel-item");
+ panelItem.textContent = item;
+ panelList.append(panelItem);
+ return panelItem;
+ });
+
+ let anchorButton = doc.createElement("button");
+ anchorButton.addEventListener("click", e => panelList.toggle(e));
+
+ // Insert the button at the top of the page so it's in view.
+ doc.body.prepend(anchorButton, panelList);
+
+ return { anchorButton, panelList, panelItems };
+}
+
+add_task(async function testItemFocusOnOpen() {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+
+ let { anchorButton, panelList, panelItems } = setupPanel(win);
+
+ ok(doc.activeElement, "There is an active element");
+ ok(!doc.activeElement.closest("panel-list"), "Focus isn't in the list");
+
+ let shown = BrowserTestUtils.waitForEvent(panelList, "shown");
+ EventUtils.synthesizeMouseAtCenter(anchorButton, {}, win);
+ await shown;
+
+ is(doc.activeElement, panelItems[0], "The first item is focused");
+
+ let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
+ EventUtils.synthesizeKey("Escape", {}, win);
+ await hidden;
+
+ is(doc.activeElement, anchorButton, "The anchor is focused again on close");
+
+ await closeView(win);
+});
+
+add_task(async function testAriaAttributes() {
+ let win = await loadInitialView("extension");
+
+ let { panelList, panelItems } = setupPanel(win);
+
+ is(panelList.getAttribute("role"), "menu", "The panel is a menu");
+
+ is(panelItems.length, 3, "There are 3 items");
+ Assert.deepEqual(
+ panelItems.map(panelItem => panelItem.button.getAttribute("role")),
+ new Array(panelItems.length).fill("menuitem"),
+ "All of the items have a menuitem button"
+ );
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js b/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js
new file mode 100644
index 0000000000..34ea1b6ff6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_permission_prompt.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/*
+ * Test Permission Popup for Sideloaded Extensions.
+ */
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+const ADDON_ID = "addon1@test.mozilla.org";
+const CUSTOM_THEME_ID = "theme1@test.mozilla.org";
+const DEFAULT_THEME_ID = "default-theme@mozilla.org";
+
+AddonTestUtils.initMochitest(this);
+
+function assertDisabledSideloadedExtensionElement(managerWindow, addonElement) {
+ const doc = addonElement.ownerDocument;
+ const toggleDisabled = addonElement.querySelector(
+ '[action="toggle-disabled"]'
+ );
+ is(
+ doc.l10n.getAttributes(toggleDisabled).id,
+ "extension-enable-addon-button-label",
+ "Addon toggle-disabled action has the enable label"
+ );
+ ok(!toggleDisabled.checked, "toggle-disable isn't checked");
+}
+
+function assertEnabledSideloadedExtensionElement(managerWindow, addonElement) {
+ const doc = addonElement.ownerDocument;
+ const toggleDisabled = addonElement.querySelector(
+ '[action="toggle-disabled"]'
+ );
+ is(
+ doc.l10n.getAttributes(toggleDisabled).id,
+ "extension-enable-addon-button-label",
+ "Addon toggle-disabled action has the enable label"
+ );
+ ok(!toggleDisabled.checked, "toggle-disable isn't checked");
+}
+
+function clickEnableExtension(managerWindow, addonElement) {
+ addonElement.querySelector('[action="toggle-disabled"]').click();
+}
+
+// Test for bug 1647931
+// Install a theme, enable it and then enable the default theme again
+add_task(async function test_theme_enable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["xpinstall.signatures.required", false],
+ ["extensions.autoDisableScopes", 15],
+ ["extensions.ui.ignoreUnsigned", true],
+ ],
+ });
+
+ let theme = {
+ manifest: {
+ applications: { gecko: { id: CUSTOM_THEME_ID } },
+ name: "Theme 1",
+ theme: {
+ colors: {
+ frame: "#000000",
+ tab_background_text: "#ffffff",
+ },
+ },
+ },
+ };
+
+ let xpi = AddonTestUtils.createTempWebExtensionFile(theme);
+ await AddonTestUtils.manuallyInstall(xpi);
+
+ let changePromise = new Promise(resolve =>
+ ExtensionsUI.once("change", resolve)
+ );
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // enable fresh installed theme
+ let manager = await open_manager("addons://list/theme");
+ let customTheme = get_addon_element(manager, CUSTOM_THEME_ID);
+ clickEnableExtension(manager, customTheme);
+
+ // enable default theme again
+ let defaultTheme = get_addon_element(manager, DEFAULT_THEME_ID);
+ clickEnableExtension(manager, defaultTheme);
+
+ let addon = await AddonManager.getAddonByID(CUSTOM_THEME_ID);
+ await close_manager(manager);
+ await addon.uninstall();
+});
+
+// Loading extension by sideloading method
+add_task(async function test_sideloaded_extension_permissions_prompt() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["xpinstall.signatures.required", false],
+ ["extensions.autoDisableScopes", 15],
+ ["extensions.ui.ignoreUnsigned", true],
+ ],
+ });
+
+ let options = {
+ manifest: {
+ applications: { gecko: { id: ADDON_ID } },
+ name: "Test 1",
+ permissions: ["history", "https://*/*"],
+ icons: { "64": "foo-icon.png" },
+ },
+ };
+
+ let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+ await AddonTestUtils.manuallyInstall(xpi);
+
+ let changePromise = new Promise(resolve =>
+ ExtensionsUI.once("change", resolve)
+ );
+ ExtensionsUI._checkForSideloaded();
+ await changePromise;
+
+ // Test click event on permission cancel option.
+ let manager = await open_manager("addons://list/extension");
+ let addon = get_addon_element(manager, ADDON_ID);
+
+ Assert.notEqual(addon, null, "Found sideloaded addon in about:addons");
+
+ assertDisabledSideloadedExtensionElement(manager, addon);
+
+ let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ clickEnableExtension(manager, addon);
+ let panel = await popupPromise;
+
+ ok(PopupNotifications.isPanelOpen, "Permission popup should be visible");
+ panel.secondaryButton.click();
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "Permission popup should be closed / closing"
+ );
+
+ addon = await AddonManager.getAddonByID(ADDON_ID);
+ ok(
+ !addon.seen,
+ "Seen flag should remain false after permissions are refused"
+ );
+
+ // Test click event on permission accept option.
+ addon = get_addon_element(manager, ADDON_ID);
+ Assert.notEqual(addon, null, "Found sideloaded addon in about:addons");
+
+ assertEnabledSideloadedExtensionElement(manager, addon);
+
+ popupPromise = promisePopupNotificationShown("addon-webext-permissions");
+ clickEnableExtension(manager, addon);
+ panel = await popupPromise;
+
+ ok(PopupNotifications.isPanelOpen, "Permission popup should be visible");
+
+ let notificationPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ ADDON_ID
+ );
+
+ panel.button.click();
+ ok(
+ !PopupNotifications.isPanelOpen,
+ "Permission popup should be closed / closing"
+ );
+ await notificationPromise;
+
+ addon = await AddonManager.getAddonByID(ADDON_ID);
+ ok(addon.seen, "Seen flag should be true after permissions are accepted");
+
+ ok(!PopupNotifications.isPanelOpen, "Permission popup should not be visible");
+
+ await close_manager(manager);
+ await addon.uninstall();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_reinstall.js b/toolkit/mozapps/extensions/test/browser/browser_reinstall.js
new file mode 100644
index 0000000000..8025502cf4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_reinstall.js
@@ -0,0 +1,278 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that upgrading bootstrapped add-ons behaves correctly while the
+// manager is open
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const ID = "reinstall@tests.mozilla.org";
+const testIdSuffix = "@tests.mozilla.org";
+
+let gManagerWindow, xpi1, xpi2;
+
+function htmlDoc() {
+ return gManagerWindow.document.getElementById("html-view-browser")
+ .contentDocument;
+}
+
+function get_list_item_count() {
+ return htmlDoc().querySelectorAll(`addon-card[addon-id$="${testIdSuffix}"]`)
+ .length;
+}
+
+function removeItem(item) {
+ let button = item.querySelector('[action="remove"]');
+ button.click();
+}
+
+function hasPendingMessage(item, msg) {
+ let messageBar = htmlDoc().querySelector(
+ `message-bar[addon-id="${item.addon.id}"`
+ );
+ is_element_visible(messageBar, msg);
+}
+
+async function install_addon(xpi) {
+ let install = await AddonManager.getInstallForFile(
+ xpi,
+ "application/x-xpinstall"
+ );
+ return install.install();
+}
+
+async function check_addon(aAddon, aVersion) {
+ is(get_list_item_count(), 1, "Should be one item in the list");
+ is(aAddon.version, aVersion, "Add-on should have the right version");
+
+ let item = get_addon_element(gManagerWindow, ID);
+ ok(!!item, "Should see the add-on in the list");
+
+ // Force XBL to apply
+ item.clientTop;
+
+ let { version } = await get_tooltip_info(item, gManagerWindow);
+ is(version, aVersion, "Version should be correct");
+
+ const l10nAttrs = item.ownerDocument.l10n.getAttributes(item.addonNameEl);
+ if (aAddon.userDisabled) {
+ Assert.deepEqual(
+ l10nAttrs,
+ { id: "addon-name-disabled", args: { name: aAddon.name } },
+ "localized addon name is marked as disabled"
+ );
+ } else {
+ Assert.deepEqual(
+ l10nAttrs,
+ { id: null, args: null },
+ "localized addon name is not marked as disabled"
+ );
+ }
+}
+
+async function wait_for_addon_item_added(addonId) {
+ await BrowserTestUtils.waitForEvent(
+ htmlDoc().querySelector("addon-list"),
+ "add"
+ );
+ const item = get_addon_element(gManagerWindow, addonId);
+ ok(item, `Found addon card for ${addonId}`);
+}
+
+async function wait_for_addon_item_removed(addonId) {
+ await BrowserTestUtils.waitForEvent(
+ htmlDoc().querySelector("addon-list"),
+ "remove"
+ );
+ const item = get_addon_element(gManagerWindow, addonId);
+ ok(!item, `There shouldn't be an addon card for ${addonId}`);
+}
+
+function wait_for_addon_item_updated(addonId) {
+ return BrowserTestUtils.waitForEvent(
+ get_addon_element(gManagerWindow, addonId),
+ "update"
+ );
+}
+
+// Install version 1 then upgrade to version 2 with the manager open
+async function test_upgrade_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let addon = await promiseAddonByID(ID);
+ await check_addon(addon, "1.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ let promiseItemUpdated = wait_for_addon_item_updated(ID);
+ await install_addon(xpi2);
+ await promiseItemUpdated;
+
+ addon = await promiseAddonByID(ID);
+ await check_addon(addon, "2.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+// Install version 1 mark it as disabled then upgrade to version 2 with the
+// manager open
+async function test_upgrade_disabled_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let promiseItemUpdated = wait_for_addon_item_updated(ID);
+ let addon = await promiseAddonByID(ID);
+ await addon.disable();
+ await promiseItemUpdated;
+
+ await check_addon(addon, "1.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ promiseItemUpdated = wait_for_addon_item_updated(ID);
+ await install_addon(xpi2);
+ await promiseItemUpdated;
+
+ addon = await promiseAddonByID(ID);
+ await check_addon(addon, "2.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+// Install version 1 click the remove button and then upgrade to version 2 with
+// the manager open
+async function test_upgrade_pending_uninstall_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let addon = await promiseAddonByID(ID);
+ await check_addon(addon, "1.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ let item = get_addon_element(gManagerWindow, ID);
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ removeItem(item);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ await promiseItemRemoved;
+
+ ok(
+ !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "Add-on should be pending uninstall"
+ );
+ hasPendingMessage(item, "Pending message should be visible");
+
+ promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi2);
+ await promiseItemAdded;
+
+ addon = await promiseAddonByID(ID);
+ await check_addon(addon, "2.0");
+ ok(!addon.userDisabled, "Add-on should not be disabled");
+
+ promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+// Install version 1, disable it, click the remove button and then upgrade to
+// version 2 with the manager open
+async function test_upgrade_pending_uninstall_disabled_v1_to_v2() {
+ let promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi1);
+ await promiseItemAdded;
+
+ let promiseItemUpdated = wait_for_addon_item_updated(ID);
+ let addon = await promiseAddonByID(ID);
+ await addon.disable();
+ await promiseItemUpdated;
+
+ await check_addon(addon, "1.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ let item = get_addon_element(gManagerWindow, ID);
+
+ let promiseItemRemoved = wait_for_addon_item_removed(ID);
+ removeItem(item);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ await promiseItemRemoved;
+ ok(
+ !!(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "Add-on should be pending uninstall"
+ );
+ hasPendingMessage(item, "Pending message should be visible");
+
+ promiseItemAdded = wait_for_addon_item_added(ID);
+ await install_addon(xpi2);
+ addon = await promiseAddonByID(ID);
+
+ await promiseItemAdded;
+ await check_addon(addon, "2.0");
+ ok(addon.userDisabled, "Add-on should be disabled");
+
+ promiseItemRemoved = wait_for_addon_item_removed(ID);
+ await addon.uninstall();
+ await promiseItemRemoved;
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+}
+
+add_task(async function setup() {
+ xpi1 = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ applications: { gecko: { id: ID } },
+ },
+ });
+
+ xpi2 = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "2.0",
+ applications: { gecko: { id: ID } },
+ },
+ });
+
+ // Accept all prompts.
+ mockPromptService()._response = 0;
+});
+
+add_task(async function test_upgrades() {
+ // Close existing about:addons tab if a test failure has
+ // prevented it from being closed.
+ if (gManagerWindow) {
+ await close_manager(gManagerWindow);
+ }
+
+ gManagerWindow = await open_manager("addons://list/extension");
+
+ await test_upgrade_v1_to_v2();
+ await test_upgrade_disabled_v1_to_v2();
+ await test_upgrade_pending_uninstall_v1_to_v2();
+ await test_upgrade_pending_uninstall_disabled_v1_to_v2();
+
+ await close_manager(gManagerWindow);
+ gManagerWindow = null;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_search_bar_focus.js b/toolkit/mozapps/extensions/test/browser/browser_search_bar_focus.js
new file mode 100644
index 0000000000..80f9c9b907
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_search_bar_focus.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 570760 - Make ctrl-f and / focus the search box in the add-ons manager
+
+function testKeys(win, searchBox) {
+ let doc = win.document;
+ doc.firstElementChild.focus();
+ isnot(doc.activeElement, searchBox, "Search box is not focused");
+ EventUtils.synthesizeKey("f", { accelKey: true }, win);
+ is(
+ searchBox.ownerDocument.activeElement,
+ searchBox,
+ "ctrl-f focuses search box"
+ );
+
+ searchBox.blur();
+
+ doc.firstElementChild.focus();
+ isnot(doc.activeElement, searchBox, "Search box is not focused");
+ EventUtils.synthesizeKey("/", {}, win);
+ is(searchBox.ownerDocument.activeElement, searchBox, "/ focuses search box");
+
+ searchBox.blur();
+}
+
+// Get a stack frame with the expected browser type.
+const testHtmlKeys = (...args) => testKeys(...args);
+const testXulKeys = (...args) => testKeys(...args);
+
+add_task(async function testSearchBarKeyboardAccess() {
+ let win = await loadInitialView("extension");
+
+ let doc = win.document;
+ let searchBox = doc.querySelector("search-addons").input;
+
+ testHtmlKeys(win, searchBox);
+ testXulKeys(win.managerWindow, searchBox);
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js
new file mode 100644
index 0000000000..175a2f4f3e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_shortcuts_duplicate_check.js
@@ -0,0 +1,151 @@
+"use strict";
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromiseTestUtils.jsm"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+
+async function loadShortcutsView() {
+ let win = await loadInitialView("extension");
+
+ // There should be a manage shortcuts link.
+ let shortcutsLink = win.document.querySelector('[action="manage-shortcuts"]');
+
+ // Open the shortcuts view.
+ let loaded = waitForViewLoad(win);
+ shortcutsLink.click();
+ await loaded;
+
+ return win;
+}
+
+add_task(async function testDuplicateShortcutsWarnings() {
+ let duplicateCommands = {
+ commandOne: {
+ suggested_key: { default: "Shift+Alt+1" },
+ },
+ commandTwo: {
+ description: "Command Two!",
+ suggested_key: { default: "Shift+Alt+2" },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: duplicateCommands,
+ name: "Extension 1",
+ },
+ background() {
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ commands: {
+ ...duplicateCommands,
+ commandThree: {
+ description: "Command Three!",
+ suggested_key: { default: "Shift+Alt+3" },
+ },
+ },
+ name: "Extension 2",
+ },
+ background() {
+ browser.test.sendMessage("ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension2.startup();
+ await extension2.awaitMessage("ready");
+
+ let win = await loadShortcutsView();
+ let doc = win.document;
+
+ let warningBars = doc.querySelectorAll("message-bar");
+ // Ensure warning messages are shown for each duplicate shorctut.
+ is(
+ warningBars.length,
+ Object.keys(duplicateCommands).length,
+ "There is a warning message bar for each duplicate shortcut"
+ );
+
+ // Ensure warning messages are correct with correct shortcuts.
+ let count = 1;
+ for (let warning of warningBars) {
+ let warningMsg = warning.querySelector("span");
+ let l10nAttrs = doc.l10n.getAttributes(warningMsg);
+ is(
+ l10nAttrs.id,
+ "shortcuts-duplicate-warning-message",
+ "Warning message is shown"
+ );
+ Assert.deepEqual(
+ l10nAttrs.args,
+ { shortcut: `Shift+Alt+${count}` },
+ "Warning message shortcut is correct"
+ );
+ count++;
+ }
+
+ ["Shift+Alt+1", "Shift+Alt+2"].forEach((shortcut, index) => {
+ // Ensure warning messages are correct with correct shortcuts.
+ let warning = warningBars[index];
+ let warningMsg = warning.querySelector("span");
+ let l10nAttrs = doc.l10n.getAttributes(warningMsg);
+ is(
+ l10nAttrs.id,
+ "shortcuts-duplicate-warning-message",
+ "Warning message is shown"
+ );
+ Assert.deepEqual(
+ l10nAttrs.args,
+ { shortcut },
+ "Warning message shortcut is correct"
+ );
+
+ // Check if all inputs have warning style.
+ let inputs = doc.querySelectorAll(`input[shortcut="${shortcut}"]`);
+ for (let input of inputs) {
+ // Check if warning error message is shown on focus.
+ input.focus();
+ let error = doc.querySelector(".error-message");
+ let label = error.querySelector(".error-message-label");
+ is(error.style.visibility, "visible", "The error element is shown");
+ is(
+ error.getAttribute("type"),
+ "warning",
+ "Duplicate shortcut has warning class"
+ );
+ is(
+ label.dataset.l10nId,
+ "shortcuts-duplicate",
+ "Correct error message is shown"
+ );
+
+ // On keypress events wrning class should be removed.
+ EventUtils.synthesizeKey("A");
+ ok(
+ !error.classList.contains("warning"),
+ "Error element should not have warning class"
+ );
+
+ input.blur();
+ is(
+ error.style.visibility,
+ "hidden",
+ "The error element is hidden on blur"
+ );
+ }
+ });
+
+ await closeView(win);
+ await extension.unload();
+ await extension2.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js
new file mode 100644
index 0000000000..e4dd7b8595
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_categories.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const THEME_ID = "default-theme@mozilla.org";
+
+function assertViewHas(win, selector, msg) {
+ ok(win.document.querySelector(selector), msg);
+}
+function assertListView(win, type) {
+ assertViewHas(win, `addon-list[type="${type}"]`, `On ${type} list`);
+}
+
+add_task(async function testClickingSidebarEntriesChangesView() {
+ let win = await loadInitialView("extension");
+ let doc = win.document;
+ let themeCategory = doc.querySelector("#categories > [name=theme]");
+ let extensionCategory = doc.querySelector("#categories > [name=extension]");
+
+ assertListView(win, "extension");
+
+ let loaded = waitForViewLoad(win);
+ themeCategory.click();
+ await loaded;
+
+ assertListView(win, "theme");
+
+ loaded = waitForViewLoad(win);
+ getAddonCard(win, THEME_ID).click();
+ await loaded;
+
+ ok(!doc.querySelector("addon-list"), "No more addon-list");
+ assertViewHas(
+ win,
+ `addon-card[addon-id="${THEME_ID}"][expanded]`,
+ "Detail view now"
+ );
+
+ loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(themeCategory, {}, win);
+ await loaded;
+
+ assertListView(win, "theme");
+
+ loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(extensionCategory, {}, win);
+ await loaded;
+
+ assertListView(win, "extension");
+
+ await closeView(win);
+});
+
+add_task(async function testClickingSidebarPaddingNoChange() {
+ let win = await loadInitialView("theme");
+ let { managerWindow } = win;
+ let categoryUtils = new CategoryUtilities(managerWindow);
+ let themeCategory = categoryUtils.get("theme");
+
+ let loadDetailView = async () => {
+ let loaded = waitForViewLoad(win);
+ getAddonCard(win, THEME_ID).click();
+ await loaded;
+
+ is(
+ managerWindow.gViewController.currentViewId,
+ `addons://detail/${THEME_ID}`,
+ "The detail view loaded"
+ );
+ };
+
+ // Confirm that clicking the button directly works.
+ await loadDetailView();
+ let loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(themeCategory, {}, win);
+ await loaded;
+ is(
+ managerWindow.gViewController.currentViewId,
+ `addons://list/theme`,
+ "The detail view loaded"
+ );
+
+ // Confirm that clicking on the padding beside it does nothing.
+ await loadDetailView();
+ EventUtils.synthesizeMouse(themeCategory, -5, -5, {}, win);
+ ok(!managerWindow.gViewController.isLoading, "No view is loading");
+
+ await closeView(win);
+});
+
+add_task(async function testKeyboardUsage() {
+ let win = await loadInitialView("extension");
+ let categories = win.document.getElementById("categories");
+ let extensionCategory = categories.getButtonByName("extension");
+ let themeCategory = categories.getButtonByName("theme");
+ let pluginCategory = categories.getButtonByName("plugin");
+
+ let waitForAnimationFrame = () =>
+ new Promise(resolve => win.requestAnimationFrame(resolve));
+ let sendKey = (key, e = {}) => {
+ EventUtils.synthesizeKey(key, e, win);
+ return waitForAnimationFrame();
+ };
+ let sendTabKey = e => sendKey("VK_TAB", e);
+ let isFocusInCategories = () =>
+ categories.contains(win.document.activeElement);
+
+ ok(!isFocusInCategories(), "Focus is not in the category list");
+
+ // Tab into the HTML browser.
+ await sendTabKey();
+ // Tab to the first focusable element.
+ await sendTabKey();
+
+ ok(isFocusInCategories(), "Focus is in the category list");
+ is(
+ win.document.activeElement,
+ extensionCategory,
+ "The extension button is focused"
+ );
+
+ // Tab out of the categories list.
+ await sendTabKey();
+ ok(!isFocusInCategories(), "Focus is out of the category list");
+
+ // Tab back into the list.
+ await sendTabKey({ shiftKey: true });
+ is(win.document.activeElement, extensionCategory, "Back on Extensions");
+
+ // We're on the extension list.
+ assertListView(win, "extension");
+
+ // Switch to theme list.
+ let loaded = waitForViewLoad(win);
+ await sendKey("VK_DOWN");
+ is(win.document.activeElement, themeCategory, "Themes is focused");
+ await loaded;
+
+ assertListView(win, "theme");
+
+ loaded = waitForViewLoad(win);
+ await sendKey("VK_DOWN");
+ is(win.document.activeElement, pluginCategory, "Plugins is focused");
+ await loaded;
+
+ assertListView(win, "plugin");
+
+ await sendKey("VK_DOWN");
+ is(win.document.activeElement, pluginCategory, "Plugins is still focused");
+ ok(!win.managerWindow.gViewController.isLoading, "No view is loading");
+
+ loaded = waitForViewLoad(win);
+ await sendKey("VK_UP");
+ await loaded;
+ loaded = waitForViewLoad(win);
+ await sendKey("VK_UP");
+ await loaded;
+ is(win.document.activeElement, extensionCategory, "Extensions is focused");
+ assertListView(win, "extension");
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js
new file mode 100644
index 0000000000..db483e2a44
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_hidden_categories.js
@@ -0,0 +1,214 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the visible delay in showing the "Language" category occurs
+// very minimally
+
+let gProvider;
+let gInstall;
+let gInstallProperties = [
+ {
+ name: "Locale Category Test",
+ type: "locale",
+ },
+];
+
+function installLocale() {
+ return new Promise(resolve => {
+ gInstall = gProvider.createInstalls(gInstallProperties)[0];
+ gInstall.addTestListener({
+ onInstallEnded(aInstall) {
+ gInstall.removeTestListener(this);
+ resolve();
+ },
+ });
+ gInstall.install();
+ });
+}
+
+async function checkCategory(win, category, { expectHidden, expectSelected }) {
+ await win.customElements.whenDefined("categories-box");
+
+ let categoriesBox = win.document.getElementById("categories");
+ await categoriesBox.promiseRendered;
+
+ let button = categoriesBox.getButtonByName(category);
+ is(
+ button.hidden,
+ expectHidden,
+ `${category} button is ${expectHidden ? "" : "not "}hidden`
+ );
+ if (expectSelected !== undefined) {
+ is(
+ button.selected,
+ expectSelected,
+ `${category} button is ${expectSelected ? "" : "not "}selected`
+ );
+ }
+}
+
+add_task(async function setup() {
+ gProvider = new MockProvider();
+});
+
+add_task(async function testLocalesHiddenByDefault() {
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", { expectHidden: true });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", { expectHidden: true });
+
+ await installLocale();
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesShownWhenInstalled() {
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesHiddenWhenUninstalled() {
+ gInstall.cancel();
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", { expectHidden: true });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesHiddenWithoutDelay() {
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", { expectHidden: true });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", { expectHidden: true });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesShownAfterDelay() {
+ await installLocale();
+
+ gProvider.blockQueryResponses();
+
+ let viewLoaded = loadInitialView("extension", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", { expectHidden: true });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: false,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesShownIfPreviousView() {
+ gProvider.blockQueryResponses();
+
+ // Passing "locale" will set the last view to locales and open the view.
+ let viewLoaded = loadInitialView("locale", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: true,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: true,
+ });
+
+ await closeView(win);
+});
+
+add_task(async function testLocalesHiddenIfPreviousViewAndNoLocales() {
+ gInstall.cancel();
+ gProvider.blockQueryResponses();
+
+ // Passing "locale" will set the last view to locales and open the view.
+ let viewLoaded = loadInitialView("locale", {
+ async loadCallback(win) {
+ await checkCategory(win, "locale", {
+ expectHidden: false,
+ expectSelected: true,
+ });
+ gProvider.unblockQueryResponses();
+ },
+ });
+ let win = await viewLoaded;
+
+ let categoryUtils = new CategoryUtilities(win.managerWindow);
+
+ await TestUtils.waitForCondition(
+ () => categoryUtils.selectedCategory != "locale"
+ );
+
+ await checkCategory(win, "locale", {
+ expectHidden: true,
+ expectSelected: false,
+ });
+
+ is(
+ categoryUtils.getSelectedViewId(),
+ win.managerWindow.gViewController.defaultViewId,
+ "default view is selected"
+ );
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js b/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js
new file mode 100644
index 0000000000..886f6684dc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sidebar_restore_category.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that the selected category is persisted across loads of the manager
+
+add_task(async function testCategoryRestore() {
+ let win = await loadInitialView("extension");
+ let utils = new CategoryUtilities(win.managerWindow);
+
+ // Open the plugins category
+ await utils.openType("plugin");
+
+ // Re-open the manager
+ await closeView(win);
+ win = await loadInitialView();
+ utils = new CategoryUtilities(win.managerWindow);
+
+ is(
+ utils.selectedCategory,
+ "plugin",
+ "Should have shown the plugins category"
+ );
+
+ // Open the extensions category
+ await utils.openType("extension");
+
+ // Re-open the manager
+ await closeView(win);
+ win = await loadInitialView();
+ utils = new CategoryUtilities(win.managerWindow);
+
+ is(
+ utils.selectedCategory,
+ "extension",
+ "Should have shown the extensions category"
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testInvalidAddonType() {
+ let win = await loadInitialView("invalid");
+
+ let categoryUtils = new CategoryUtilities(win.managerWindow);
+ is(
+ categoryUtils.getSelectedViewId(),
+ win.managerWindow.gViewController.defaultViewId,
+ "default view is selected"
+ );
+ is(
+ win.managerWindow.gViewController.currentViewId,
+ win.managerWindow.gViewController.defaultViewId,
+ "default view is shown"
+ );
+
+ await closeView(win);
+});
+
+add_task(async function testInvalidViewId() {
+ let win = await loadInitialView("addons://invalid/view");
+
+ let categoryUtils = new CategoryUtilities(win.managerWindow);
+ is(
+ categoryUtils.getSelectedViewId(),
+ win.managerWindow.gViewController.defaultViewId,
+ "default view is selected"
+ );
+ is(
+ win.managerWindow.gViewController.currentViewId,
+ win.managerWindow.gViewController.defaultViewId,
+ "default view is shown"
+ );
+
+ await closeView(win);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js
new file mode 100644
index 0000000000..f8e3293b82
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that we throw if a test created with add_task()
+// calls run_next_test
+
+add_task(async function run_next_throws() {
+ let err = null;
+ try {
+ run_next_test();
+ } catch (e) {
+ err = e;
+ info("run_next_test threw " + err);
+ }
+ ok(err, "run_next_test() should throw an error inside an add_task test");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updateid.js b/toolkit/mozapps/extensions/test/browser/browser_updateid.js
new file mode 100644
index 0000000000..87e92b92d3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updateid.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that updates that change an add-on's ID show up correctly in the UI
+
+var gProvider;
+var gManagerWindow;
+var gCategoryUtilities;
+
+function getName(item) {
+ return item.addonNameEl.textContent;
+}
+
+async function getUpdateButton(item) {
+ let button = item.querySelector('[action="install-update"]');
+ let panel = button.closest("panel-list");
+ let shown = BrowserTestUtils.waitForEvent(panel, "shown");
+ let moreOptionsButton = item.querySelector('[action="more-options"]');
+ EventUtils.synthesizeMouseAtCenter(moreOptionsButton, {}, item.ownerGlobal);
+ await shown;
+ return button;
+}
+
+add_task(async function test_updateid() {
+ // Close the existing about:addons tab and unrestier the existing MockProvider
+ // instance if a previous failed test has not been able to clear them.
+ if (gManagerWindow) {
+ await close_manager(gManagerWindow);
+ }
+ if (gProvider) {
+ gProvider.unregister();
+ }
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([
+ {
+ id: "addon1@tests.mozilla.org",
+ name: "manually updating addon",
+ version: "1.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE,
+ },
+ ]);
+
+ gManagerWindow = await open_manager("addons://list/extension");
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ await gCategoryUtilities.openType("extension");
+
+ gProvider.createInstalls([
+ {
+ name: "updated add-on",
+ existingAddon: gProvider.addons[0],
+ version: "2.0",
+ },
+ ]);
+ var newAddon = new MockAddon("addon2@tests.mozilla.org");
+ newAddon.name = "updated add-on";
+ newAddon.version = "2.0";
+ newAddon.pendingOperations = AddonManager.PENDING_INSTALL;
+ gProvider.installs[0]._addonToInstall = newAddon;
+
+ var item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ is(
+ getName(item),
+ "manually updating addon",
+ "Should show the old name in the list"
+ );
+ const { name, version } = await get_tooltip_info(item, gManagerWindow);
+ is(
+ name,
+ "manually updating addon",
+ "Should show the old name in the tooltip"
+ );
+ is(version, "1.0", "Should still show the old version in the tooltip");
+
+ var update = await getUpdateButton(item);
+ is_element_visible(update, "Update button should be visible");
+
+ item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+ is(item, null, "Should not show the new version in the list");
+
+ await close_manager(gManagerWindow);
+ gManagerWindow = null;
+ gProvider.unregister();
+ gProvider = null;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.js b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js
new file mode 100644
index 0000000000..31901e053b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js
@@ -0,0 +1,392 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var tempScope = {};
+ChromeUtils.import(
+ "resource://gre/modules/addons/AddonUpdateChecker.jsm",
+ tempScope
+);
+var AddonUpdateChecker = tempScope.AddonUpdateChecker;
+
+const updatejson = RELATIVE_DIR + "browser_updatessl.json";
+const redirect = RELATIVE_DIR + "redirect.sjs?";
+const SUCCESS = 0;
+const DOWNLOAD_ERROR = AddonManager.ERROR_DOWNLOAD_ERROR;
+
+const HTTP = "http://example.com/";
+const HTTPS = "https://example.com/";
+const NOCERT = "https://nocert.example.com/";
+const SELFSIGNED = "https://self-signed.example.com/";
+const UNTRUSTED = "https://untrusted.example.com/";
+const EXPIRED = "https://expired.example.com/";
+
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+
+var gTests = [];
+var gStart = 0;
+var gLast = 0;
+
+var HTTPObserver = {
+ observeActivity(
+ aChannel,
+ aType,
+ aSubtype,
+ aTimestamp,
+ aSizeData,
+ aStringData
+ ) {
+ aChannel.QueryInterface(Ci.nsIChannel);
+
+ dump(
+ "*** HTTP Activity 0x" +
+ aType.toString(16) +
+ " 0x" +
+ aSubtype.toString(16) +
+ " " +
+ aChannel.URI.spec +
+ "\n"
+ );
+ },
+};
+
+function test() {
+ gStart = Date.now();
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ let observerService = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+ observerService.addObserver(HTTPObserver);
+
+ registerCleanupFunction(function() {
+ observerService.removeObserver(HTTPObserver);
+ });
+
+ run_next_test();
+}
+
+function end_test() {
+ var cos = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+ );
+ cos.clearValidityOverride("nocert.example.com", -1);
+ cos.clearValidityOverride("self-signed.example.com", -1);
+ cos.clearValidityOverride("untrusted.example.com", -1);
+ cos.clearValidityOverride("expired.example.com", -1);
+
+ info("All tests completed in " + (Date.now() - gStart) + "ms");
+ finish();
+}
+
+function add_update_test(mainURL, redirectURL, expectedStatus) {
+ gTests.push([mainURL, redirectURL, expectedStatus]);
+}
+
+function run_update_tests(callback) {
+ function run_next_update_test() {
+ if (!gTests.length) {
+ callback();
+ return;
+ }
+ gLast = Date.now();
+
+ let [mainURL, redirectURL, expectedStatus] = gTests.shift();
+ if (redirectURL) {
+ var url = mainURL + redirect + redirectURL + updatejson;
+ var message =
+ "Should have seen the right result for an update check redirected from " +
+ mainURL +
+ " to " +
+ redirectURL;
+ } else {
+ url = mainURL + updatejson;
+ message =
+ "Should have seen the right result for an update check from " + mainURL;
+ }
+
+ AddonUpdateChecker.checkForUpdates("addon1@tests.mozilla.org", url, {
+ onUpdateCheckComplete(updates) {
+ is(updates.length, 1, "Should be the right number of results");
+ is(SUCCESS, expectedStatus, message);
+ info("Update test ran in " + (Date.now() - gLast) + "ms");
+ run_next_update_test();
+ },
+
+ onUpdateCheckError(status) {
+ is(status, expectedStatus, message);
+ info("Update test ran in " + (Date.now() - gLast) + "ms");
+ run_next_update_test();
+ },
+ });
+ }
+
+ run_next_update_test();
+}
+
+// Runs tests with built-in certificates required and no certificate exceptions.
+add_test(async function test_builtin_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, true]],
+ });
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates and no certificate
+// exceptions.
+add_test(async function test_builtin_not_required() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, SUCCESS);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, SUCCESS);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Set up overrides for the next test.
+add_test(() => {
+ addCertOverrides().then(run_next_test);
+});
+
+// Runs tests with built-in certificates required and all certificate exceptions.
+add_test(async function test_builtin_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, true]],
+ });
+
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, SUCCESS);
+ add_update_test(HTTP, SELFSIGNED, SUCCESS);
+ add_update_test(HTTP, UNTRUSTED, SUCCESS);
+ add_update_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates and all certificate
+// exceptions.
+add_test(async function test_builtin_not_required_overrides() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_UPDATE_REQUIREBUILTINCERTS, false]],
+ });
+
+ // Tests that a simple update.json retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, SUCCESS);
+ add_update_test(NOCERT, null, SUCCESS);
+ add_update_test(SELFSIGNED, null, SUCCESS);
+ add_update_test(UNTRUSTED, null, SUCCESS);
+ add_update_test(EXPIRED, null, SUCCESS);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, SUCCESS);
+ add_update_test(HTTP, SELFSIGNED, SUCCESS);
+ add_update_test(HTTP, UNTRUSTED, SUCCESS);
+ add_update_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, SUCCESS);
+ add_update_test(HTTPS, NOCERT, SUCCESS);
+ add_update_test(HTTPS, SELFSIGNED, SUCCESS);
+ add_update_test(HTTPS, UNTRUSTED, SUCCESS);
+ add_update_test(HTTPS, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, SUCCESS);
+ add_update_test(NOCERT, NOCERT, SUCCESS);
+ add_update_test(NOCERT, SELFSIGNED, SUCCESS);
+ add_update_test(NOCERT, UNTRUSTED, SUCCESS);
+ add_update_test(NOCERT, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, SUCCESS);
+ add_update_test(SELFSIGNED, NOCERT, SUCCESS);
+ add_update_test(SELFSIGNED, SELFSIGNED, SUCCESS);
+ add_update_test(SELFSIGNED, UNTRUSTED, SUCCESS);
+ add_update_test(SELFSIGNED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, SUCCESS);
+ add_update_test(UNTRUSTED, NOCERT, SUCCESS);
+ add_update_test(UNTRUSTED, SELFSIGNED, SUCCESS);
+ add_update_test(UNTRUSTED, UNTRUSTED, SUCCESS);
+ add_update_test(UNTRUSTED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, SUCCESS);
+ add_update_test(EXPIRED, NOCERT, SUCCESS);
+ add_update_test(EXPIRED, SELFSIGNED, SUCCESS);
+ add_update_test(EXPIRED, UNTRUSTED, SUCCESS);
+ add_update_test(EXPIRED, EXPIRED, SUCCESS);
+
+ run_update_tests(run_next_test);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.json b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json
new file mode 100644
index 0000000000..223d1ef2d3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json
@@ -0,0 +1,17 @@
+{
+ "addons": {
+ "addon1@tests.mozilla.org": {
+ "updates": [
+ {
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0",
+ "advisory_max_version": "20"
+ }
+ },
+ "version": "2.0"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^
new file mode 100644
index 0000000000..2e4f8163bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.json^headers^
@@ -0,0 +1 @@
+Connection: close
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi.js b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
new file mode 100644
index 0000000000..57262a6fdc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+function testWithAPI(task) {
+ return async function() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, task);
+ };
+}
+
+let gProvider = new MockProvider();
+
+let addons = gProvider.createAddons([
+ {
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on 1",
+ version: "2.1",
+ description: "Short description",
+ type: "extension",
+ userDisabled: false,
+ isActive: true,
+ },
+ {
+ id: "addon2@tests.mozilla.org",
+ name: "Test add-on 2",
+ version: "5.3.7ab",
+ description: null,
+ type: "theme",
+ userDisabled: false,
+ isActive: false,
+ },
+ {
+ id: "addon3@tests.mozilla.org",
+ name: "Test add-on 3",
+ version: "1",
+ description: "Longer description",
+ type: "extension",
+ userDisabled: true,
+ isActive: false,
+ },
+ {
+ id: "addon4@tests.mozilla.org",
+ name: "Test add-on 4",
+ version: "1",
+ description: "Longer description",
+ type: "extension",
+ userDisabled: false,
+ isActive: true,
+ },
+]);
+
+addons[3].permissions &= ~AddonManager.PERM_CAN_UNINSTALL;
+
+function API_getAddonByID(browser, id) {
+ return SpecialPowers.spawn(browser, [id], async function(id) {
+ let addon = await content.navigator.mozAddonManager.getAddonByID(id);
+
+ // We can't send native objects back so clone its properties.
+ return JSON.parse(JSON.stringify(addon));
+ });
+}
+
+add_task(
+ testWithAPI(async function(browser) {
+ function compareObjects(web, real) {
+ for (let prop of Object.keys(web)) {
+ let webVal = web[prop];
+ let realVal = real[prop];
+
+ switch (prop) {
+ case "isEnabled":
+ realVal = !real.userDisabled;
+ break;
+
+ case "canUninstall":
+ realVal = Boolean(
+ real.permissions & AddonManager.PERM_CAN_UNINSTALL
+ );
+ break;
+ }
+
+ // null and undefined don't compare well so stringify them first
+ if (realVal === null || realVal === undefined) {
+ realVal = `${realVal}`;
+ webVal = `${webVal}`;
+ }
+
+ is(
+ webVal,
+ realVal,
+ `Property ${prop} should have the right value in add-on ${real.id}`
+ );
+ }
+ }
+
+ let [a1, a2, a3] = await promiseAddonsByIDs([
+ "addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ ]);
+ let w1 = await API_getAddonByID(browser, "addon1@tests.mozilla.org");
+ let w2 = await API_getAddonByID(browser, "addon2@tests.mozilla.org");
+ let w3 = await API_getAddonByID(browser, "addon3@tests.mozilla.org");
+
+ compareObjects(w1, a1);
+ compareObjects(w2, a2);
+ compareObjects(w3, a3);
+ })
+);
+
+add_task(
+ testWithAPI(async function(browser) {
+ async function check(value, message) {
+ let enabled = await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.mozAddonManager.permissionPromptsEnabled;
+ });
+ is(enabled, value, message);
+ }
+
+ const PERM = "extensions.webextPermissionPrompts";
+ if (!Services.prefs.getBoolPref(PERM, false)) {
+ await SpecialPowers.pushPrefEnv({ clear: [[PERM]] });
+ await check(
+ false,
+ `mozAddonManager.permissionPromptsEnabled is false when ${PERM} is unset`
+ );
+ }
+
+ await SpecialPowers.pushPrefEnv({ set: [[PERM, true]] });
+ await check(
+ true,
+ `mozAddonManager.permissionPromptsEnabled is true when ${PERM} is set`
+ );
+ })
+);
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js
new file mode 100644
index 0000000000..3ec5650dae
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_abuse_report.js
@@ -0,0 +1,370 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+loadTestSubscript("head_abuse_report.js");
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+const TELEMETRY_EVENTS_FILTERS = {
+ category: "addonsManager",
+ method: "report",
+};
+const REPORT_PROP_NAMES = [
+ "addon",
+ "addon_signature",
+ "reason",
+ "message",
+ "report_entry_point",
+];
+
+function getObjectProps(obj, propNames) {
+ const res = {};
+ for (const k of propNames) {
+ res[k] = obj[k];
+ }
+ return res;
+}
+
+async function assertSubmittedReport(expectedReportProps) {
+ let reportSubmitted;
+ const onReportSubmitted = AbuseReportTestUtils.promiseReportSubmitHandled(
+ ({ data, request, response }) => {
+ reportSubmitted = JSON.parse(data);
+ handleSubmitRequest({ request, response });
+ }
+ );
+
+ let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ let promiseWinClosed = waitClosedWindow();
+ let promisePanelUpdated = AbuseReportTestUtils.promiseReportUpdated(
+ panelEl,
+ "submit"
+ );
+ panelEl._form.elements.reason.value = expectedReportProps.reason;
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnNext);
+ await promisePanelUpdated;
+
+ panelEl._form.elements.message.value = expectedReportProps.message;
+ // Reset the timestamp of the last report between tests.
+ AbuseReporter._lastReportTimestamp = null;
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnSubmit);
+ await Promise.all([onReportSubmitted, promiseWinClosed]);
+
+ ok(!panelEl.ownerGlobal, "Report dialog window is closed");
+ Assert.deepEqual(
+ getObjectProps(reportSubmitted, REPORT_PROP_NAMES),
+ expectedReportProps,
+ "Got the expected report data submitted"
+ );
+}
+
+add_task(async function setup() {
+ await AbuseReportTestUtils.setup();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.abuseReport.amWebAPI.enabled", true],
+ ],
+ });
+});
+
+add_task(async function test_report_installed_addon_cancelled() {
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const extension = await installTestExtension(ADDON_ID);
+
+ let reportEnabled = await SpecialPowers.spawn(browser, [], () => {
+ return content.navigator.mozAddonManager.abuseReportPanelEnabled;
+ });
+
+ is(reportEnabled, true, "Expect abuseReportPanelEnabled to be true");
+
+ info("Test reportAbuse result on user cancelled report");
+
+ let promiseNewWindow = waitForNewWindow();
+ let promiseWebAPIResult = SpecialPowers.spawn(
+ browser,
+ [ADDON_ID],
+ addonId => content.navigator.mozAddonManager.reportAbuse(addonId)
+ );
+
+ let win = await promiseNewWindow;
+ is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
+
+ let panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ let promiseWinClosed = waitClosedWindow();
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel);
+ let reportResult = await promiseWebAPIResult;
+ is(
+ reportResult,
+ false,
+ "Expect reportAbuse to resolve to false on user cancelled report"
+ );
+ await promiseWinClosed;
+ ok(!panelEl.ownerGlobal, "Report dialog window is closed");
+
+ await extension.unload();
+ });
+
+ // Expect no telemetry events collected for user cancelled reports.
+ TelemetryTestUtils.assertEvents([], TELEMETRY_EVENTS_FILTERS);
+});
+
+add_task(async function test_report_installed_addon_submitted() {
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const extension = await installTestExtension(ADDON_ID);
+
+ let promiseNewWindow = waitForNewWindow();
+ let promiseWebAPIResult = SpecialPowers.spawn(browser, [ADDON_ID], id =>
+ content.navigator.mozAddonManager.reportAbuse(id)
+ );
+ let win = await promiseNewWindow;
+ is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
+
+ await assertSubmittedReport({
+ addon: ADDON_ID,
+ addon_signature: "missing",
+ message: "fake report message",
+ reason: "unwanted",
+ report_entry_point: "amo",
+ });
+
+ let reportResult = await promiseWebAPIResult;
+ is(
+ reportResult,
+ true,
+ "Expect reportAbuse to resolve to false on user cancelled report"
+ );
+
+ await extension.unload();
+ });
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "amo",
+ value: ADDON_ID,
+ extra: { addon_type: "extension" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+});
+
+add_task(async function test_report_unknown_not_installed_addon() {
+ const addonId = "unknown-addon@mochi.test";
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id =>
+ content.navigator.mozAddonManager.reportAbuse(id).catch(err => {
+ return { name: err.name, message: err.message };
+ })
+ );
+
+ await Assert.deepEqual(
+ await promiseWebAPIResult,
+ { name: "Error", message: "Error creating abuse report" },
+ "Got the expected rejected error on reporting unknown addon"
+ );
+
+ ok(!AbuseReportTestUtils.getReportDialog(), "No report dialog is open");
+ });
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "amo",
+ value: addonId,
+ extra: { error_type: "ERROR_AMODETAILS_NOTFOUND" },
+ },
+ {
+ object: "amo",
+ value: addonId,
+ extra: { error_type: "ERROR_ADDON_NOTFOUND" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+});
+
+add_task(async function test_report_not_installed_addon() {
+ const addonId = "not-installed-addon@mochi.test";
+ Services.telemetry.clearEvents();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const fakeAMODetails = {
+ name: "fake name",
+ current_version: { version: "1.0" },
+ type: "extension",
+ icon_url: "http://test.addons.org/asserts/fake-icon-url.png",
+ homepage: "http://fake.url/homepage",
+ authors: [{ name: "author1", url: "http://fake.url/author1" }],
+ is_recommended: false,
+ };
+
+ AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
+ registerCleanupFunction(() =>
+ AbuseReportTestUtils.amoAddonDetailsMap.clear()
+ );
+
+ let promiseNewWindow = waitForNewWindow();
+
+ let promiseWebAPIResult = SpecialPowers.spawn(browser, [addonId], id =>
+ content.navigator.mozAddonManager.reportAbuse(id)
+ );
+ let win = await promiseNewWindow;
+ is(win, AbuseReportTestUtils.getReportDialog(), "Got the report dialog");
+
+ await assertSubmittedReport({
+ addon: addonId,
+ addon_signature: "unknown",
+ message: "fake report message",
+ reason: "other",
+ report_entry_point: "amo",
+ });
+
+ let reportResult = await promiseWebAPIResult;
+ is(
+ reportResult,
+ true,
+ "Expect reportAbuse to resolve to true on submitted report"
+ );
+ });
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "amo",
+ value: addonId,
+ extra: { addon_type: "extension" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+});
+
+add_task(async function test_amo_report_on_report_already_inprogress() {
+ const extension = await installTestExtension(ADDON_ID);
+ const reportDialog = await AbuseReporter.openDialog(
+ ADDON_ID,
+ "menu",
+ gBrowser.selectedBrowser
+ );
+ await AbuseReportTestUtils.promiseReportDialogRendered();
+ ok(reportDialog.window, "Got an open report dialog");
+
+ let promiseWinClosed = waitClosedWindow();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const promiseAMOResult = SpecialPowers.spawn(browser, [ADDON_ID], id =>
+ content.navigator.mozAddonManager.reportAbuse(id)
+ );
+
+ await promiseWinClosed;
+ ok(reportDialog.window.closed, "previous report dialog should be closed");
+
+ is(
+ await reportDialog.promiseAMOResult,
+ undefined,
+ "old report cancelled after AMO called mozAddonManager.reportAbuse"
+ );
+
+ const panelEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ const { report } = AbuseReportTestUtils.getReportDialogParams();
+ Assert.deepEqual(
+ {
+ reportEntryPoint: report.reportEntryPoint,
+ addonId: report.addon.id,
+ },
+ {
+ reportEntryPoint: "amo",
+ addonId: ADDON_ID,
+ },
+ "Got the expected report from the opened report dialog"
+ );
+
+ promiseWinClosed = waitClosedWindow();
+ AbuseReportTestUtils.clickPanelButton(panelEl._btnCancel);
+ await promiseWinClosed;
+
+ is(
+ await promiseAMOResult,
+ false,
+ "AMO report request resolved to false on cancel button clicked"
+ );
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_reject_on_unsupported_addon_types() {
+ const addonId = "not-supported-addon-type@mochi.test";
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ const fakeAMODetails = {
+ name: "fake name",
+ current_version: { version: "1.0" },
+ type: "fake-unsupported-addon-type",
+ };
+
+ AbuseReportTestUtils.amoAddonDetailsMap.set(addonId, fakeAMODetails);
+ registerCleanupFunction(() =>
+ AbuseReportTestUtils.amoAddonDetailsMap.clear()
+ );
+
+ let webAPIResult = await SpecialPowers.spawn(browser, [addonId], id =>
+ content.navigator.mozAddonManager.reportAbuse(id).then(
+ res => ({ gotRejection: false, result: res }),
+ err => ({ gotRejection: true, message: err.message })
+ )
+ );
+
+ Assert.deepEqual(
+ webAPIResult,
+ { gotRejection: true, message: "Error creating abuse report" },
+ "Got the expected rejection from mozAddonManager.reportAbuse"
+ );
+ });
+});
+
+add_task(async function test_report_on_disabled_webapi() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.amWebAPI.enabled", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ let reportEnabled = await SpecialPowers.spawn(browser, [], () => {
+ return content.navigator.mozAddonManager.abuseReportPanelEnabled;
+ });
+
+ is(reportEnabled, false, "Expect abuseReportPanelEnabled to be false");
+
+ info("Test reportAbuse result on report webAPI disabled");
+
+ let promiseWebAPIResult = SpecialPowers.spawn(
+ browser,
+ ["an-addon@mochi.test"],
+ addonId =>
+ content.navigator.mozAddonManager.reportAbuse(addonId).catch(err => {
+ return { name: err.name, message: err.message };
+ })
+ );
+
+ Assert.deepEqual(
+ await promiseWebAPIResult,
+ { name: "Error", message: "amWebAPI reportAbuse not supported" },
+ "Got the expected rejected error"
+ );
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
new file mode 100644
index 0000000000..384ea890ed
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function check_frame_availability(browser) {
+ return check_availability(browser.browsingContext.children[0]);
+}
+
+function check_availability(browser) {
+ return SpecialPowers.spawn(browser, [], async function() {
+ return content.document.getElementById("result").textContent == "true";
+ });
+}
+
+// Test that initially the API isn't available in the test domain
+add_task(async function test_not_available() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that with testing on the API is available in the test domain
+add_task(async function test_available() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(available, "API should be available.");
+ }
+ );
+});
+
+// Test that the API is not available in a bad domain
+add_task(async function test_bad_domain() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT2}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that the API is only available in https sites
+add_task(async function test_not_available_http() {
+ await BrowserTestUtils.withNewTab(
+ `${TESTROOT}webapi_checkavailable.html`,
+ async function test_not_available(browser) {
+ let available = await check_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that the API is available when in a frame of the test domain
+add_task(async function test_available_framed() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT}webapi_checkframed.html`,
+ async function test_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(available, "API should be available.");
+ }
+ );
+});
+
+// Test that if the external frame is http then the inner frame doesn't have
+// the API
+add_task(async function test_not_available_http_framed() {
+ await BrowserTestUtils.withNewTab(
+ `${TESTROOT}webapi_checkframed.html`,
+ async function test_not_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that if the external frame is a bad domain then the inner frame doesn't
+// have the API
+add_task(async function test_not_available_framed() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT2}webapi_checkframed.html`,
+ async function test_not_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(!available, "API should not be available.");
+ }
+ );
+});
+
+// Test that a window navigated to a bad domain doesn't allow access to the API
+add_task(async function test_navigated_window() {
+ await BrowserTestUtils.withNewTab(
+ `${SECURE_TESTROOT2}webapi_checknavigatedwindow.html`,
+ async function test_available(browser) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ await SpecialPowers.spawn(browser, [], async function() {
+ await content.wrappedJSObject.openWindow();
+ });
+
+ // Should be a new tab open
+ let tab = await tabPromise;
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.getBrowserForTab(tab)
+ );
+
+ SpecialPowers.spawn(browser, [], async function() {
+ content.wrappedJSObject.navigate();
+ });
+
+ await loadPromise;
+
+ let available = await SpecialPowers.spawn(browser, [], async function() {
+ return content.wrappedJSObject.check();
+ });
+
+ ok(!available, "API should not be available.");
+
+ gBrowser.removeTab(tab);
+ }
+ );
+});
+
+// Check that if a page is embedded in a chrome content UI that it can still
+// access the API.
+add_task(async function test_chrome_frame() {
+ SpecialPowers.pushPrefEnv({
+ set: [["security.allow_unsafe_parent_loads", true]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ `${CHROMEROOT}webapi_checkchromeframe.xhtml`,
+ async function test_available(browser) {
+ let available = await check_frame_availability(browser);
+ ok(available, "API should be available.");
+ }
+ );
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
new file mode 100644
index 0000000000..996224a7f6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
@@ -0,0 +1,124 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+async function getListenerEvents(browser) {
+ let result = await SpecialPowers.spawn(browser, [], async function() {
+ return content.document.getElementById("result").textContent;
+ });
+
+ return result.split("\n").map(JSON.parse);
+}
+
+const RESTARTLESS_ID = "restartless@tests.mozilla.org";
+const INSTALL_ID = "install@tests.mozilla.org";
+const CANCEL_ID = "cancel@tests.mozilla.org";
+
+let provider = new MockProvider();
+provider.createAddons([
+ {
+ id: RESTARTLESS_ID,
+ name: "Restartless add-on",
+ operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+ },
+ {
+ id: CANCEL_ID,
+ name: "Add-on for uninstall cancel",
+ },
+]);
+
+// Test enable/disable events for restartless
+add_task(async function test_restartless() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function(browser) {
+ let addon = await promiseAddonByID(RESTARTLESS_ID);
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // disable it
+ await addon.disable();
+ is(addon.userDisabled, true, "addon was disabled successfully");
+
+ // re-enable it
+ await addon.enable();
+ is(addon.userDisabled, false, "addon was re-enabled successfuly");
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: RESTARTLESS_ID, event: "onDisabling" },
+ { id: RESTARTLESS_ID, event: "onDisabled" },
+ { id: RESTARTLESS_ID, event: "onEnabling" },
+ { id: RESTARTLESS_ID, event: "onEnabled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable/enable events");
+ });
+});
+
+// Test install events
+add_task(async function test_restartless() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function(browser) {
+ let addon = new MockAddon(
+ INSTALL_ID,
+ "installme",
+ null,
+ AddonManager.OP_NEED_RESTART_NONE
+ );
+ let install = new MockInstall(null, null, addon);
+
+ let installPromise = new Promise(resolve => {
+ install.addTestListener({
+ onInstallEnded: resolve,
+ });
+ });
+
+ provider.addInstall(install);
+ install.install();
+
+ await installPromise;
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: INSTALL_ID, event: "onInstalling" },
+ { id: INSTALL_ID, event: "onInstalled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected install events");
+ });
+});
+
+// Test uninstall
+add_task(async function test_uninstall() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function(browser) {
+ let addon = await promiseAddonByID(RESTARTLESS_ID);
+ isnot(addon, null, "Found add-on for uninstall");
+
+ addon.uninstall();
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: RESTARTLESS_ID, event: "onUninstalling" },
+ { id: RESTARTLESS_ID, event: "onUninstalled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected uninstall events");
+ });
+});
+
+// Test cancel of uninstall.
+add_task(async function test_cancel() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function(browser) {
+ let addon = await promiseAddonByID(CANCEL_ID);
+ isnot(addon, null, "Found add-on for cancelling uninstall");
+
+ addon.uninstall();
+
+ let events = await getListenerEvents(browser);
+ let expected = [{ id: CANCEL_ID, event: "onUninstalling" }];
+ Assert.deepEqual(events, expected, "Got expected uninstalling event");
+
+ addon.cancelUninstall();
+ events = await getListenerEvents(browser);
+ expected.push({ id: CANCEL_ID, event: "onOperationCancelled" });
+ Assert.deepEqual(events, expected, "Got expected cancel event");
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js
new file mode 100644
index 0000000000..0bfbe198cd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js
@@ -0,0 +1,63 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+async function getListenerEvents(browser) {
+ let result = await SpecialPowers.spawn(browser, [], async function() {
+ return content.document.getElementById("result").textContent;
+ });
+
+ return result.split("\n").map(JSON.parse);
+}
+
+const ID = "test@tests.mozilla.org";
+
+let provider = new MockProvider();
+provider.createAddons([
+ {
+ id: ID,
+ name: "Test add-on",
+ operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+ },
+]);
+
+// Test disable and enable from content
+add_task(async function() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function(browser) {
+ let addon = await promiseAddonByID(ID);
+ isnot(addon, null, "Test addon exists");
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // Disable the addon from content.
+ await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.mozAddonManager
+ .getAddonByID("test@tests.mozilla.org")
+ .then(addon => addon.setEnabled(false));
+ });
+
+ let events = await getListenerEvents(browser);
+ let expected = [
+ { id: ID, event: "onDisabling" },
+ { id: ID, event: "onDisabled" },
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable events");
+
+ // Enable the addon from content.
+ await SpecialPowers.spawn(browser, [], async function() {
+ return content.navigator.mozAddonManager
+ .getAddonByID("test@tests.mozilla.org")
+ .then(addon => addon.setEnabled(true));
+ });
+
+ events = await getListenerEvents(browser);
+ expected = expected.concat([
+ { id: ID, event: "onEnabling" },
+ { id: ID, event: "onEnabled" },
+ ]);
+ Assert.deepEqual(events, expected, "Got expected enable events");
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
new file mode 100644
index 0000000000..b068fccf00
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
@@ -0,0 +1,542 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const TESTPATH = "webapi_checkavailable.html";
+const TESTPAGE = `${SECURE_TESTROOT}${TESTPATH}`;
+const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+const XPI_ADDON_ID = "amosigned-xpi@tests.mozilla.org";
+
+const XPI_SHA =
+ "sha256:91121ed2c27f670f2307b9aebdd30979f147318c7fb9111c254c14ddbb84e4b0";
+
+const ID = "amosigned-xpi@tests.mozilla.org";
+// eh, would be good to just stat the real file instead of this...
+const XPI_LEN = 4287;
+
+AddonTestUtils.initMochitest(this);
+
+function waitForClear() {
+ const MSG = "WebAPICleanup";
+ return new Promise(resolve => {
+ let listener = {
+ receiveMessage(msg) {
+ if (msg.name == MSG) {
+ Services.mm.removeMessageListener(MSG, listener);
+ resolve();
+ }
+ },
+ };
+
+ Services.mm.addMessageListener(MSG, listener, true);
+ });
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.allowPrivateBrowsingByDefault", false],
+ ],
+ });
+ info("added preferences");
+});
+
+// Wrapper around a common task to run in the content process to test
+// the mozAddonManager API. Takes a URL for the XPI to install and an
+// array of steps, each of which can either be an action to take
+// (i.e., start or cancel the install) or an install event to wait for.
+// Steps that look for a specific event may also include a "props" property
+// with properties that the AddonInstall object is expected to have when
+// that event is triggered.
+async function testInstall(browser, args, steps, description) {
+ let success = await SpecialPowers.spawn(
+ browser,
+ [{ args, steps }],
+ async function(opts) {
+ let { args, steps } = opts;
+ let install = await content.navigator.mozAddonManager.createInstall(args);
+ if (!install) {
+ await Promise.reject(
+ "createInstall() did not return an install object"
+ );
+ }
+
+ // Check that the initial state of the AddonInstall is sane.
+ if (install.state != "STATE_AVAILABLE") {
+ await Promise.reject("new install should be in STATE_AVAILABLE");
+ }
+ if (install.error != null) {
+ await Promise.reject("new install should have null error");
+ }
+
+ const events = [
+ "onDownloadStarted",
+ "onDownloadProgress",
+ "onDownloadEnded",
+ "onDownloadCancelled",
+ "onDownloadFailed",
+ "onInstallStarted",
+ "onInstallEnded",
+ "onInstallCancelled",
+ "onInstallFailed",
+ ];
+ let eventWaiter = null;
+ let receivedEvents = [];
+ let prevEvent = null;
+ events.forEach(event => {
+ install.addEventListener(event, e => {
+ receivedEvents.push({
+ event,
+ state: install.state,
+ error: install.error,
+ progress: install.progress,
+ maxProgress: install.maxProgress,
+ });
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+ });
+
+ // Returns a promise that is resolved when the given event occurs
+ // or rejects if a different event comes first or if props is supplied
+ // and properties on the AddonInstall don't match those in props.
+ function expectEvent(event, props) {
+ return new Promise((resolve, reject) => {
+ function check() {
+ let received = receivedEvents.shift();
+ // Skip any repeated onDownloadProgress events.
+ while (
+ received &&
+ received.event == prevEvent &&
+ prevEvent == "onDownloadProgress"
+ ) {
+ received = receivedEvents.shift();
+ }
+ // Wait for more events if we skipped all there were.
+ if (!received) {
+ eventWaiter = () => {
+ eventWaiter = null;
+ check();
+ };
+ return;
+ }
+ prevEvent = received.event;
+ if (received.event != event) {
+ let err = new Error(
+ `expected ${event} but got ${received.event}`
+ );
+ reject(err);
+ }
+ if (props) {
+ for (let key of Object.keys(props)) {
+ if (received[key] != props[key]) {
+ throw new Error(
+ `AddonInstall property ${key} was ${received[key]} but expected ${props[key]}`
+ );
+ }
+ }
+ }
+ resolve();
+ }
+ check();
+ });
+ }
+
+ while (steps.length) {
+ let nextStep = steps.shift();
+ if (nextStep.action) {
+ if (nextStep.action == "install") {
+ try {
+ await install.install();
+ if (nextStep.expectError) {
+ throw new Error("Expected install to fail but it did not");
+ }
+ } catch (err) {
+ if (!nextStep.expectError) {
+ throw new Error("Install failed unexpectedly");
+ }
+ }
+ } else if (nextStep.action == "cancel") {
+ await install.cancel();
+ } else {
+ throw new Error(`unknown action ${nextStep.action}`);
+ }
+ } else {
+ await expectEvent(nextStep.event, nextStep.props);
+ }
+ }
+
+ return true;
+ }
+ );
+
+ is(success, true, description);
+}
+
+function makeInstallTest(task) {
+ return async function() {
+ // withNewTab() will close the test tab before returning, at which point
+ // the cleanup event will come from the content process. We need to see
+ // that event but don't want to race to install a listener for it after
+ // the tab is closed. So set up the listener now but don't yield the
+ // listening promise until below.
+ let clearPromise = waitForClear();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, task);
+
+ await clearPromise;
+ is(AddonManager.webAPI.installs.size, 0, "AddonInstall was cleaned up");
+ };
+}
+
+function makeRegularTest(options, what) {
+ return makeInstallTest(async function(browser) {
+ let steps = [
+ { action: "install" },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ {
+ event: "onDownloadProgress",
+ props: { maxProgress: XPI_LEN },
+ },
+ {
+ event: "onDownloadEnded",
+ props: {
+ state: "STATE_DOWNLOADED",
+ progress: XPI_LEN,
+ maxProgress: XPI_LEN,
+ },
+ },
+ {
+ event: "onInstallStarted",
+ props: { state: "STATE_INSTALLING" },
+ },
+ {
+ event: "onInstallEnded",
+ props: { state: "STATE_INSTALLED" },
+ },
+ ];
+
+ let installPromptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ let promptPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ options.addonId
+ );
+
+ await testInstall(browser, options, steps, what);
+
+ await installPromptPromise;
+
+ await promptPromise;
+
+ // Sanity check to ensure that the test in makeInstallTest() that
+ // installs.size == 0 means we actually did clean up.
+ ok(
+ AddonManager.webAPI.installs.size > 0,
+ "webAPI is tracking the AddonInstall"
+ );
+
+ let addon = await promiseAddonByID(ID);
+ isnot(addon, null, "Found the addon");
+
+ // Check that the expected installTelemetryInfo has been stored in the addon details.
+ AddonTestUtils.checkInstallInfo(addon, {
+ method: "amWebAPI",
+ source: "test-host",
+ sourceURL: /https:\/\/example.com\/.*\/webapi_checkavailable.html/,
+ });
+
+ await addon.uninstall();
+
+ addon = await promiseAddonByID(ID);
+ is(addon, null, "Addon was uninstalled");
+ });
+}
+
+let addonId = XPI_ADDON_ID;
+add_task(makeRegularTest({ url: XPI_URL, addonId }, "a basic install works"));
+add_task(
+ makeRegularTest(
+ { url: XPI_URL, addonId, hash: null },
+ "install with hash=null works"
+ )
+);
+add_task(
+ makeRegularTest(
+ { url: XPI_URL, addonId, hash: "" },
+ "install with empty string for hash works"
+ )
+);
+add_task(
+ makeRegularTest(
+ { url: XPI_URL, addonId, hash: XPI_SHA },
+ "install with hash works"
+ )
+);
+
+add_task(
+ makeInstallTest(async function(browser) {
+ let steps = [
+ { action: "cancel" },
+ {
+ event: "onDownloadCancelled",
+ props: {
+ state: "STATE_CANCELLED",
+ error: null,
+ },
+ },
+ ];
+
+ await testInstall(
+ browser,
+ { url: XPI_URL },
+ steps,
+ "canceling an install works"
+ );
+
+ let addons = await promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ ok(
+ AddonManager.webAPI.installs.size > 0,
+ "webAPI is tracking the AddonInstall"
+ );
+ })
+);
+
+add_task(
+ makeInstallTest(async function(browser) {
+ let steps = [
+ { action: "install", expectError: true },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ { event: "onDownloadProgress" },
+ {
+ event: "onDownloadFailed",
+ props: {
+ state: "STATE_DOWNLOAD_FAILED",
+ error: "ERROR_NETWORK_FAILURE",
+ },
+ },
+ ];
+
+ await testInstall(
+ browser,
+ { url: XPI_URL + "bogus" },
+ steps,
+ "install of a bad url fails"
+ );
+
+ let addons = await promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ ok(
+ AddonManager.webAPI.installs.size > 0,
+ "webAPI is tracking the AddonInstall"
+ );
+ })
+);
+
+add_task(
+ makeInstallTest(async function(browser) {
+ let steps = [
+ { action: "install", expectError: true },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ { event: "onDownloadProgress" },
+ {
+ event: "onDownloadFailed",
+ props: {
+ state: "STATE_DOWNLOAD_FAILED",
+ error: "ERROR_INCORRECT_HASH",
+ },
+ },
+ ];
+
+ await testInstall(
+ browser,
+ { url: XPI_URL, hash: "sha256:bogus" },
+ steps,
+ "install with bad hash fails"
+ );
+
+ let addons = await promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ ok(
+ AddonManager.webAPI.installs.size > 0,
+ "webAPI is tracking the AddonInstall"
+ );
+ })
+);
+
+add_task(async function test_permissions() {
+ function testBadUrl(url, pattern, successMessage) {
+ return BrowserTestUtils.withNewTab(TESTPAGE, async function(browser) {
+ let result = await SpecialPowers.spawn(
+ browser,
+ [{ url, pattern }],
+ function(opts) {
+ return new Promise(resolve => {
+ content.navigator.mozAddonManager
+ .createInstall({ url: opts.url })
+ .then(
+ () => {
+ resolve({
+ success: false,
+ message: "createInstall should not have succeeded",
+ });
+ },
+ err => {
+ if (err.message.match(new RegExp(opts.pattern))) {
+ resolve({ success: true });
+ }
+ resolve({
+ success: false,
+ message: `Wrong error message: ${err.message}`,
+ });
+ }
+ );
+ });
+ }
+ );
+ is(result.success, true, result.message || successMessage);
+ });
+ }
+
+ await testBadUrl(
+ "i am not a url",
+ "NS_ERROR_MALFORMED_URI",
+ "Installing from an unparseable URL fails"
+ );
+
+ await testBadUrl(
+ "https://addons.not-really-mozilla.org/impostor.xpi",
+ "not permitted",
+ "Installing from non-approved URL fails"
+ );
+});
+
+add_task(
+ makeInstallTest(async function(browser) {
+ let xpiURL = `${SECURE_TESTROOT}../xpinstall/incompatible.xpi`;
+ let id = "incompatible-xpi@tests.mozilla.org";
+
+ let steps = [
+ { action: "install", expectError: true },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ { event: "onDownloadProgress" },
+ { event: "onDownloadEnded" },
+ { event: "onDownloadCancelled" },
+ ];
+
+ await testInstall(
+ browser,
+ { url: xpiURL },
+ steps,
+ "install of an incompatible XPI fails"
+ );
+
+ let addons = await promiseAddonsByIDs([id]);
+ is(addons[0], null, "The addon was not installed");
+ })
+);
+
+add_task(
+ makeInstallTest(async function(browser) {
+ const options = { url: XPI_URL, addonId };
+ let steps = [
+ { action: "install" },
+ {
+ event: "onDownloadStarted",
+ props: { state: "STATE_DOWNLOADING" },
+ },
+ {
+ event: "onDownloadProgress",
+ props: { maxProgress: XPI_LEN },
+ },
+ {
+ event: "onDownloadEnded",
+ props: {
+ state: "STATE_DOWNLOADED",
+ progress: XPI_LEN,
+ maxProgress: XPI_LEN,
+ },
+ },
+ {
+ event: "onInstallStarted",
+ props: { state: "STATE_INSTALLING" },
+ },
+ {
+ event: "onInstallEnded",
+ props: { state: "STATE_INSTALLED" },
+ },
+ ];
+
+ await SpecialPowers.spawn(browser, [TESTPATH], testPath => {
+ // `sourceURL` should match the exact location, even after a location
+ // update using the history API. In this case, we update the URL with
+ // query parameters and expect `sourceURL` to contain those parameters.
+ content.history.pushState(
+ {}, // state
+ "", // title
+ `/${testPath}?some=query&par=am`
+ );
+ });
+
+ let installPromptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ let promptPromise = acceptAppMenuNotificationWhenShown(
+ "addon-installed",
+ options.addonId
+ );
+
+ await Promise.all([
+ testInstall(browser, options, steps, "install to check source URL"),
+ installPromptPromise,
+ promptPromise,
+ ]);
+
+ let addon = await promiseAddonByID(ID);
+
+ registerCleanupFunction(async () => {
+ await addon.uninstall();
+ });
+
+ // Check that the expected installTelemetryInfo has been stored in the
+ // addon details.
+ AddonTestUtils.checkInstallInfo(addon, {
+ method: "amWebAPI",
+ source: "test-host",
+ sourceURL:
+ "https://example.com/webapi_checkavailable.html?some=query&par=am",
+ });
+ })
+);
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js
new file mode 100644
index 0000000000..32c94d3cc3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install_disabled.js
@@ -0,0 +1,58 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+const XPI_URL = `${SECURE_TESTROOT}../xpinstall/amosigned.xpi`;
+
+function waitForClear() {
+ const MSG = "WebAPICleanup";
+ return new Promise(resolve => {
+ let listener = {
+ receiveMessage(msg) {
+ if (msg.name == MSG) {
+ Services.mm.removeMessageListener(MSG, listener);
+ resolve();
+ }
+ },
+ };
+
+ Services.mm.addMessageListener(MSG, listener, true);
+ });
+}
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["xpinstall.enabled", false],
+ ["extensions.install.requireBuiltInCerts", false],
+ ],
+ });
+ info("added preferences");
+});
+
+async function testInstall(browser, args) {
+ let success = await SpecialPowers.spawn(browser, [{ args }], async function(
+ opts
+ ) {
+ let { args } = opts;
+ let install;
+ try {
+ install = await content.navigator.mozAddonManager.createInstall(args);
+ } catch (e) {}
+ return !!install;
+ });
+ is(success, false, "Install was blocked");
+}
+
+add_task(async function() {
+ // withNewTab() will close the test tab before returning, at which point
+ // the cleanup event will come from the content process. We need to see
+ // that event but don't want to race to install a listener for it after
+ // the tab is closed. So set up the listener now but don't yield the
+ // listening promise until below.
+ let clearPromise = waitForClear();
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async function(browser) {
+ await testInstall(browser, { url: XPI_URL });
+ });
+
+ await clearPromise;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js
new file mode 100644
index 0000000000..948f96fb0a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_theme.js
@@ -0,0 +1,80 @@
+"use strict";
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+const URL = `${SECURE_TESTROOT}addons/browser_theme.xpi`;
+
+add_task(async function test_theme_install() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false],
+ ["extensions.allowPrivateBrowsingByDefault", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(TESTPAGE, async browser => {
+ let updates = [];
+ function observer(subject, topic, data) {
+ updates.push(JSON.stringify(subject.wrappedJSObject));
+ }
+ Services.obs.addObserver(observer, "lightweight-theme-styling-update");
+ registerCleanupFunction(() => {
+ Services.obs.removeObserver(observer, "lightweight-theme-styling-update");
+ });
+
+ let sawConfirm = false;
+ promisePopupNotificationShown("addon-install-confirmation").then(panel => {
+ sawConfirm = true;
+ panel.button.click();
+ });
+
+ let prompt1 = waitAppMenuNotificationShown(
+ "addon-installed",
+ "theme@tests.mozilla.org",
+ false
+ );
+ let installPromise = SpecialPowers.spawn(browser, [URL], async url => {
+ let install = await content.navigator.mozAddonManager.createInstall({
+ url,
+ });
+ return install.install();
+ });
+ await prompt1;
+
+ ok(sawConfirm, "Confirm notification was displayed before installation");
+
+ // Open a new window and test the app menu panel from there. This verifies the
+ // incognito checkbox as well as finishing install in this case.
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await waitAppMenuNotificationShown(
+ "addon-installed",
+ "theme@tests.mozilla.org",
+ true,
+ newWin
+ );
+ await installPromise;
+ ok(true, "Theme install completed");
+
+ await BrowserTestUtils.closeWindow(newWin);
+
+ Assert.equal(updates.length, 1, "Got a single theme update");
+ let parsed = JSON.parse(updates[0]);
+ ok(
+ parsed.theme.headerURL.endsWith("/testImage.png"),
+ "Theme update has the expected headerURL"
+ );
+ is(
+ parsed.theme.id,
+ "theme@tests.mozilla.org",
+ "Theme update includes the theme ID"
+ );
+ is(
+ parsed.theme.version,
+ "1.0",
+ "Theme update includes the theme's version"
+ );
+
+ let addon = await AddonManager.getAddonByID(parsed.theme.id);
+ await addon.uninstall();
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js
new file mode 100644
index 0000000000..887c30ead3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true]],
+ });
+});
+
+function testWithAPI(task) {
+ return async function() {
+ await BrowserTestUtils.withNewTab(TESTPAGE, task);
+ };
+}
+
+function API_uninstallByID(browser, id) {
+ return SpecialPowers.spawn(browser, [id], async function(id) {
+ let addon = await content.navigator.mozAddonManager.getAddonByID(id);
+
+ let result = await addon.uninstall();
+ return result;
+ });
+}
+
+add_task(
+ testWithAPI(async function(browser) {
+ const ID1 = "addon1@tests.mozilla.org";
+ const ID2 = "addon2@tests.mozilla.org";
+ const ID3 = "addon3@tests.mozilla.org";
+
+ let provider = new MockProvider();
+
+ provider.addAddon(new MockAddon(ID1, "Test add-on 1", "extension", 0));
+ provider.addAddon(new MockAddon(ID2, "Test add-on 2", "extension", 0));
+
+ let addon = new MockAddon(ID3, "Test add-on 3", "extension", 0);
+ addon.permissions &= ~AddonManager.PERM_CAN_UNINSTALL;
+ provider.addAddon(addon);
+
+ let [a1, a2, a3] = await promiseAddonsByIDs([ID1, ID2, ID3]);
+ isnot(a1, null, "addon1 is installed");
+ isnot(a2, null, "addon2 is installed");
+ isnot(a3, null, "addon3 is installed");
+
+ let result = await API_uninstallByID(browser, ID1);
+ is(result, true, "uninstall of addon1 succeeded");
+
+ [a1, a2, a3] = await promiseAddonsByIDs([ID1, ID2, ID3]);
+ is(a1, null, "addon1 is uninstalled");
+ isnot(a2, null, "addon2 is still installed");
+
+ result = await API_uninstallByID(browser, ID2);
+ is(result, true, "uninstall of addon2 succeeded");
+ [a2] = await promiseAddonsByIDs([ID2]);
+ is(a2, null, "addon2 is uninstalled");
+
+ await Assert.rejects(
+ API_uninstallByID(browser, ID3),
+ /Addon cannot be uninstalled/,
+ "Unable to uninstall addon"
+ );
+
+ // Cleanup addon3
+ a3.permissions |= AddonManager.PERM_CAN_UNINSTALL;
+ await a3.uninstall();
+ [a3] = await promiseAddonsByIDs([ID3]);
+ is(a3, null, "addon3 is uninstalled");
+ })
+);
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js b/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js
new file mode 100644
index 0000000000..43ea8224d0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_icon.js
@@ -0,0 +1,84 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function pngArrayBuffer(size) {
+ const canvas = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.height = canvas.width = size;
+ const ctx = canvas.getContext("2d");
+ ctx.fillStyle = "blue";
+ ctx.fillRect(0, 0, size, size);
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ const fileReader = new FileReader();
+ fileReader.onload = () => {
+ resolve(fileReader.result);
+ };
+ fileReader.readAsArrayBuffer(blob);
+ });
+ });
+}
+
+async function checkIconInView(view, name, findIcon) {
+ const manager = await open_manager(view);
+ const icon = findIcon(manager.document);
+ const size = Number(icon.src.match(/icon(\d+)\.png/)[1]);
+ is(
+ icon.clientWidth,
+ icon.clientHeight,
+ `The icon should be square in ${name}`
+ );
+ is(
+ size,
+ icon.clientWidth * window.devicePixelRatio,
+ `The correct icon size should have been chosen in ${name}`
+ );
+ await close_manager(manager);
+}
+
+add_task(async function test_addon_icon() {
+ // This test loads an extension with a variety of icon sizes, and checks that the
+ // fitting one is chosen. If this fails it's because you changed the icon size in
+ // about:addons but didn't update some AddonManager.getPreferredIconURL call.
+ const id = "@test-addon-icon";
+ const icons = {};
+ const files = {};
+ const file = await pngArrayBuffer(256);
+ for (let size = 1; size <= 256; ++size) {
+ let fileName = `icon${size}.png`;
+ icons[size] = fileName;
+ files[fileName] = file;
+ }
+ const extensionDefinition = {
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: { id },
+ },
+ icons,
+ },
+ files,
+ };
+
+ const extension = ExtensionTestUtils.loadExtension(extensionDefinition);
+ await extension.startup();
+
+ await checkIconInView("addons://list/extension", "list", doc => {
+ return get_addon_element(doc.defaultView, id).querySelector(".addon-icon");
+ });
+
+ await checkIconInView(
+ "addons://detail/" + encodeURIComponent(id),
+ "details",
+ doc => {
+ return get_addon_element(doc.defaultView, id).querySelector(
+ ".addon-icon"
+ );
+ }
+ );
+
+ await extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js
new file mode 100644
index 0000000000..48bd99a1eb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_incognito.js
@@ -0,0 +1,645 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+const { ExtensionPermissions } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionPermissions.jsm"
+);
+const { Management } = ChromeUtils.import(
+ "resource://gre/modules/Extension.jsm"
+);
+
+var gManagerWindow;
+
+AddonTestUtils.initMochitest(this);
+
+function get_test_items() {
+ var items = {};
+
+ for (let item of gManagerWindow
+ .getHtmlBrowser()
+ .contentDocument.querySelectorAll("addon-card")) {
+ items[item.getAttribute("addon-id")] = item;
+ }
+
+ return items;
+}
+
+function getHtmlElem(selector) {
+ return gManagerWindow
+ .getHtmlBrowser()
+ .contentDocument.querySelector(selector);
+}
+
+function getPrivateBrowsingBadge(card) {
+ return card.querySelector(".addon-badge-private-browsing-allowed");
+}
+
+function getPreferencesButtonAtListView(card) {
+ return card.querySelector("panel-item[action='preferences']");
+}
+
+function getPreferencesButtonAtDetailsView() {
+ return getHtmlElem("panel-item[action='preferences']");
+}
+
+function isInlineOptionsVisible() {
+ // The following button is used to open the inline options browser.
+ return !getHtmlElem(".tab-button[name='preferences']").hidden;
+}
+
+function getPrivateBrowsingValue() {
+ return getHtmlElem("input[type='radio'][name='private-browsing']:checked")
+ .value;
+}
+
+async function setPrivateBrowsingValue(value, id) {
+ let changePromise = new Promise(resolve => {
+ const listener = (type, { extensionId, added, removed }) => {
+ if (extensionId == id) {
+ // Let's make sure we received the right message
+ let { permissions } = value == "0" ? removed : added;
+ ok(permissions.includes("internal:privateBrowsingAllowed"));
+ resolve();
+ }
+ };
+ Management.once("change-permissions", listener);
+ });
+ let radio = getHtmlElem(
+ `input[type="radio"][name="private-browsing"][value="${value}"]`
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ radio,
+ { clickCount: 1 },
+ radio.ownerGlobal
+ );
+ // Let's make sure we wait until the change has peristed in the database
+ return changePromise;
+}
+
+// Check whether the private browsing inputs are visible in the details view.
+function checkIsModifiable(expected) {
+ if (expected) {
+ is_element_visible(
+ getHtmlElem(".addon-detail-row-private-browsing"),
+ "Private browsing should be visible"
+ );
+ } else {
+ is_element_hidden(
+ getHtmlElem(".addon-detail-row-private-browsing"),
+ "Private browsing should be hidden"
+ );
+ }
+ checkHelpRow(".addon-detail-row-private-browsing", expected);
+}
+
+// Check whether the details view shows that private browsing is forcibly disallowed.
+function checkIsDisallowed(expected) {
+ if (expected) {
+ is_element_visible(
+ getHtmlElem(".addon-detail-row-private-browsing-disallowed"),
+ "Private browsing should be disallowed"
+ );
+ } else {
+ is_element_hidden(
+ getHtmlElem(".addon-detail-row-private-browsing-disallowed"),
+ "Private browsing should not be disallowed"
+ );
+ }
+ checkHelpRow(".addon-detail-row-private-browsing-disallowed", expected);
+}
+
+// Check whether the details view shows that private browsing is forcibly allowed.
+function checkIsRequired(expected) {
+ if (expected) {
+ is_element_visible(
+ getHtmlElem(".addon-detail-row-private-browsing-required"),
+ "Private browsing should be required"
+ );
+ } else {
+ is_element_hidden(
+ getHtmlElem(".addon-detail-row-private-browsing-required"),
+ "Private browsing should not be required"
+ );
+ }
+ checkHelpRow(".addon-detail-row-private-browsing-required", expected);
+}
+
+function checkHelpRow(selector, expected) {
+ let helpRow = getHtmlElem(`${selector} + .addon-detail-help-row`);
+ if (expected) {
+ is_element_visible(helpRow, `Help row should be shown: ${selector}`);
+ is_element_visible(helpRow.querySelector("a"), "Expected learn more link");
+ } else {
+ is_element_hidden(helpRow, `Help row should be hidden: ${selector}`);
+ }
+}
+
+async function hasPrivateAllowed(id) {
+ let perms = await ExtensionPermissions.get(id);
+ return (
+ perms.permissions.length == 1 &&
+ perms.permissions[0] == "internal:privateBrowsingAllowed"
+ );
+}
+
+add_task(function clearInitialTelemetry() {
+ // Clear out any telemetry data that existed before this file is run.
+ Services.telemetry.clearEvents();
+});
+
+add_task(async function test_badge_and_toggle_incognito() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.allowPrivateBrowsingByDefault", false]],
+ });
+
+ let addons = new Map([
+ [
+ "@test-default",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: { id: "@test-default" },
+ },
+ },
+ },
+ ],
+ [
+ "@test-override",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: { id: "@test-override" },
+ },
+ },
+ incognitoOverride: "spanning",
+ },
+ ],
+ [
+ "@test-override-permanent",
+ {
+ useAddonManager: "permanent",
+ manifest: {
+ applications: {
+ gecko: { id: "@test-override-permanent" },
+ },
+ },
+ incognitoOverride: "spanning",
+ },
+ ],
+ [
+ "@test-not-allowed",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ applications: {
+ gecko: { id: "@test-not-allowed" },
+ },
+ incognito: "not_allowed",
+ },
+ },
+ ],
+ ]);
+ let extensions = [];
+ for (let definition of addons.values()) {
+ let extension = ExtensionTestUtils.loadExtension(definition);
+ extensions.push(extension);
+ await extension.startup();
+ }
+
+ gManagerWindow = await open_manager("addons://list/extension");
+ let items = get_test_items();
+ for (let [id, definition] of addons.entries()) {
+ ok(items[id], `${id} listed`);
+ let badge = getPrivateBrowsingBadge(items[id]);
+ if (definition.incognitoOverride == "spanning") {
+ is_element_visible(badge, `private browsing badge is visible`);
+ } else {
+ is_element_hidden(badge, `private browsing badge is hidden`);
+ }
+ }
+ await close_manager(gManagerWindow);
+
+ for (let [id, definition] of addons.entries()) {
+ gManagerWindow = await open_manager(
+ "addons://detail/" + encodeURIComponent(id)
+ );
+ ok(true, `==== ${id} detail opened`);
+ if (definition.manifest.incognito == "not_allowed") {
+ checkIsModifiable(false);
+ ok(!(await hasPrivateAllowed(id)), "Private browsing permission not set");
+ checkIsDisallowed(true);
+ } else {
+ // This assumes PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS, we test other options in a later test in this file.
+ checkIsModifiable(true);
+ if (definition.incognitoOverride == "spanning") {
+ is(getPrivateBrowsingValue(), "1", "Private browsing should be on");
+ ok(await hasPrivateAllowed(id), "Private browsing permission set");
+ await setPrivateBrowsingValue("0", id);
+ is(getPrivateBrowsingValue(), "0", "Private browsing should be off");
+ ok(
+ !(await hasPrivateAllowed(id)),
+ "Private browsing permission removed"
+ );
+ } else {
+ is(getPrivateBrowsingValue(), "0", "Private browsing should be off");
+ ok(
+ !(await hasPrivateAllowed(id)),
+ "Private browsing permission not set"
+ );
+ await setPrivateBrowsingValue("1", id);
+ is(getPrivateBrowsingValue(), "1", "Private browsing should be on");
+ ok(await hasPrivateAllowed(id), "Private browsing permission set");
+ }
+ }
+ await close_manager(gManagerWindow);
+ }
+
+ for (let extension of extensions) {
+ await extension.unload();
+ }
+
+ const expectedExtras = {
+ action: "privateBrowsingAllowed",
+ view: "detail",
+ type: "extension",
+ };
+
+ assertAboutAddonsTelemetryEvents(
+ [
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "on",
+ { ...expectedExtras, addonId: "@test-default" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "off",
+ { ...expectedExtras, addonId: "@test-override" },
+ ],
+ [
+ "addonsManager",
+ "action",
+ "aboutAddons",
+ "off",
+ { ...expectedExtras, addonId: "@test-override-permanent" },
+ ],
+ ],
+ { methods: ["action"] }
+ );
+});
+
+add_task(async function test_addon_preferences_button() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.allowPrivateBrowsingByDefault", false]],
+ });
+
+ let addons = new Map([
+ [
+ "test-inline-options@mozilla.com",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension with inline options",
+ applications: { gecko: { id: "test-inline-options@mozilla.com" } },
+ options_ui: { page: "options.html", open_in_tab: false },
+ },
+ },
+ ],
+ [
+ "test-newtab-options@mozilla.com",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension with options page in a new tab",
+ applications: { gecko: { id: "test-newtab-options@mozilla.com" } },
+ options_ui: { page: "options.html", open_in_tab: true },
+ },
+ },
+ ],
+ [
+ "test-not-allowed@mozilla.com",
+ {
+ useAddonManager: "temporary",
+ manifest: {
+ name: "Extension not allowed in PB windows",
+ incognito: "not_allowed",
+ applications: { gecko: { id: "test-not-allowed@mozilla.com" } },
+ options_ui: { page: "options.html", open_in_tab: true },
+ },
+ },
+ ],
+ ]);
+
+ async function runTest(openInPrivateWin) {
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ private: openInPrivateWin,
+ });
+
+ gManagerWindow = await open_manager(
+ "addons://list/extension",
+ undefined,
+ undefined,
+ undefined,
+ win
+ );
+
+ const checkPrefsVisibility = (id, hasInlinePrefs, expectVisible) => {
+ if (!hasInlinePrefs) {
+ const detailsPrefBtn = getPreferencesButtonAtDetailsView();
+ is(
+ !detailsPrefBtn.hidden,
+ expectVisible,
+ `The ${id} prefs button in the addon details has the expected visibility`
+ );
+ } else {
+ is(
+ isInlineOptionsVisible(),
+ expectVisible,
+ `The ${id} inline prefs in the addon details has the expected visibility`
+ );
+ }
+ };
+
+ const setAddonPrivateBrowsingAccess = async (id, allowPrivateBrowsing) => {
+ const cardUpdatedPromise = BrowserTestUtils.waitForEvent(
+ getHtmlElem("addon-card"),
+ "update"
+ );
+ is(
+ getPrivateBrowsingValue(),
+ allowPrivateBrowsing ? "0" : "1",
+ `Private browsing should be initially ${
+ allowPrivateBrowsing ? "off" : "on"
+ }`
+ );
+
+ // Get the DOM element we want to click on (to allow or disallow the
+ // addon on private browsing windows).
+ await setPrivateBrowsingValue(allowPrivateBrowsing ? "1" : "0", id);
+
+ info(`Waiting for details view of ${id} to be reloaded`);
+ await cardUpdatedPromise;
+
+ is(
+ getPrivateBrowsingValue(),
+ allowPrivateBrowsing ? "1" : "0",
+ `Private browsing should be initially ${
+ allowPrivateBrowsing ? "on" : "off"
+ }`
+ );
+
+ is(
+ await hasPrivateAllowed(id),
+ allowPrivateBrowsing,
+ `Private browsing permission ${
+ allowPrivateBrowsing ? "added" : "removed"
+ }`
+ );
+ let badge = getPrivateBrowsingBadge(getHtmlElem("addon-card"));
+ is(
+ !badge.hidden,
+ allowPrivateBrowsing,
+ `Expected private browsing badge at ${id}`
+ );
+ };
+
+ const extensions = [];
+ for (const definition of addons.values()) {
+ const extension = ExtensionTestUtils.loadExtension(definition);
+ extensions.push(extension);
+ await extension.startup();
+ }
+
+ const items = get_test_items();
+
+ for (const id of addons.keys()) {
+ // Check the preferences button in the addon list page.
+ is(
+ getPreferencesButtonAtListView(items[id]).hidden,
+ openInPrivateWin,
+ `The ${id} prefs button in the addon list has the expected visibility`
+ );
+ }
+
+ for (const [id, definition] of addons.entries()) {
+ // Check the preferences button or inline frame in the addon
+ // details page.
+ info(`Opening addon details for ${id}`);
+ const hasInlinePrefs = !definition.manifest.options_ui.open_in_tab;
+ const onceViewChanged = BrowserTestUtils.waitForEvent(
+ gManagerWindow,
+ "ViewChanged"
+ );
+ gManagerWindow.loadView(`addons://detail/${encodeURIComponent(id)}`);
+ await onceViewChanged;
+
+ checkPrefsVisibility(id, hasInlinePrefs, !openInPrivateWin);
+
+ // While testing in a private window, also check that the preferences
+ // are going to be visible when we toggle the PB access for the addon.
+ if (openInPrivateWin && definition.manifest.incognito !== "not_allowed") {
+ await setAddonPrivateBrowsingAccess(id, true);
+ checkPrefsVisibility(id, hasInlinePrefs, true);
+
+ await setAddonPrivateBrowsingAccess(id, false);
+ checkPrefsVisibility(id, hasInlinePrefs, false);
+ }
+ }
+
+ for (const extension of extensions) {
+ await extension.unload();
+ }
+
+ await close_manager(gManagerWindow);
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ // run tests in private and non-private windows.
+ await runTest(true);
+ await runTest(false);
+});
+
+add_task(async function test_addon_postinstall_incognito_hidden_checkbox() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.allowPrivateBrowsingByDefault", false],
+ ["extensions.langpacks.signatures.required", false],
+ ],
+ });
+
+ const TEST_ADDONS = [
+ {
+ manifest: {
+ name: "Extension incognito default opt-in",
+ applications: {
+ gecko: { id: "ext-incognito-default-opt-in@mozilla.com" },
+ },
+ },
+ },
+ {
+ manifest: {
+ name: "Extension incognito not_allowed",
+ applications: {
+ gecko: { id: "ext-incognito-not-allowed@mozilla.com" },
+ },
+ incognito: "not_allowed",
+ },
+ },
+ {
+ manifest: {
+ name: "Static Theme",
+ applications: { gecko: { id: "static-theme@mozilla.com" } },
+ theme: {
+ colors: {
+ frame: "#FFFFFF",
+ tab_background_text: "#000",
+ },
+ },
+ },
+ },
+ {
+ manifest: {
+ name: "Dictionary",
+ applications: { gecko: { id: "dictionary@mozilla.com" } },
+ dictionaries: {
+ und: "dictionaries/und.dic",
+ },
+ },
+ files: {
+ "dictionaries/und.dic": "",
+ "dictionaries/und.aff": "",
+ },
+ },
+ {
+ manifest: {
+ name: "Langpack",
+ applications: { gecko: { id: "langpack@mozilla.com" } },
+ langpack_id: "und",
+ languages: {
+ und: {
+ chrome_resources: {
+ global: "chrome/und/locale/und/global",
+ },
+ version: "20190326174300",
+ },
+ },
+ },
+ },
+ ];
+
+ for (let definition of TEST_ADDONS) {
+ let { id } = definition.manifest.applications.gecko;
+ info(
+ `Testing incognito checkbox visibility on ${id} post install notification`
+ );
+
+ const xpi = AddonTestUtils.createTempWebExtensionFile(definition);
+ let install = await AddonManager.getInstallForFile(xpi);
+
+ await Promise.all([
+ waitAppMenuNotificationShown("addon-installed", id, true),
+ install.install().then(() => {
+ Services.obs.notifyObservers(
+ {
+ addon: install.addon,
+ target: gBrowser.selectedBrowser,
+ },
+ "webextension-install-notify"
+ );
+ }),
+ ]);
+
+ const { addon } = install;
+ const { permissions } = addon;
+ const canChangePBAccess = Boolean(
+ permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
+ );
+
+ if (id === "ext-incognito-default-opt-in@mozilla.com") {
+ ok(
+ canChangePBAccess,
+ `${id} should have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission`
+ );
+ } else {
+ ok(
+ !canChangePBAccess,
+ `${id} should not have the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission`
+ );
+ }
+
+ // This tests the visibility of various private detail rows.
+ gManagerWindow = await open_manager(
+ "addons://detail/" + encodeURIComponent(id)
+ );
+ info(`addon ${id} detail opened`);
+ if (addon.type === "extension") {
+ checkIsModifiable(canChangePBAccess);
+ let required = addon.incognito === "spanning";
+ checkIsRequired(!canChangePBAccess && required);
+ checkIsDisallowed(!canChangePBAccess && !required);
+ } else {
+ checkIsModifiable(false);
+ checkIsRequired(false);
+ checkIsDisallowed(false);
+ }
+ await close_manager(gManagerWindow);
+
+ await addon.uninstall();
+ }
+
+ // It is not possible to create a privileged add-on and install it, so just
+ // simulate an installed privileged add-on and check the UI.
+ await test_incognito_of_privileged_addons();
+});
+
+// Checks that the private browsing flag of privileged add-ons cannot be modified.
+async function test_incognito_of_privileged_addons() {
+ // In mochitests it is not possible to create and install a privileged add-on
+ // or a system add-on, so create a mock provider that simulates privileged
+ // add-ons (which lack the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission).
+ let provider = new MockProvider();
+ provider.createAddons([
+ {
+ name: "default incognito",
+ id: "default-incognito@mock",
+ incognito: "spanning", // This is the default.
+ // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission.
+ permissions: 0,
+ },
+ {
+ name: "not_allowed incognito",
+ id: "not-allowed-incognito@mock",
+ incognito: "not_allowed",
+ // Anything without the PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS permission.
+ permissions: 0,
+ },
+ ]);
+
+ gManagerWindow = await open_manager(
+ "addons://detail/default-incognito%40mock"
+ );
+ checkIsModifiable(false);
+ checkIsRequired(true);
+ checkIsDisallowed(false);
+ await close_manager(gManagerWindow);
+
+ gManagerWindow = await open_manager(
+ "addons://detail/not-allowed-incognito%40mock"
+ );
+ checkIsModifiable(false);
+ checkIsRequired(false);
+ checkIsDisallowed(true);
+ await close_manager(gManagerWindow);
+
+ provider.unregister();
+}
diff --git a/toolkit/mozapps/extensions/test/browser/discovery/api_response.json b/toolkit/mozapps/extensions/test/browser/discovery/api_response.json
new file mode 100644
index 0000000000..ee130ef1a5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery/api_response.json
@@ -0,0 +1,769 @@
+{
+ "results" : [
+ {
+ "description_text" : "",
+ "addon" : {
+ "icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid" : "{e0d2e13b-2e07-49d5-9574-eb0227482320}",
+ "authors" : [
+ {
+ "id" : 7804538,
+ "name" : "Sondergaard",
+ "picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/7/7804/7804538.png?modified=1392125542",
+ "username" : "EatingStick",
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/7804538/"
+ }
+ ],
+ "previews" : [
+ {
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183758.png?modified=1555593109",
+ "image_size" : [
+ 680,
+ 92
+ ],
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183758.png?modified=1555593109",
+ "id" : 183758,
+ "thumbnail_size" : [
+ 473,
+ 64
+ ],
+ "caption" : null
+ },
+ {
+ "id" : 183768,
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183768.png?modified=1555593111",
+ "image_size" : [
+ 760,
+ 92
+ ],
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183768.png?modified=1555593111",
+ "caption" : null,
+ "thumbnail_size" : [
+ 529,
+ 64
+ ]
+ },
+ {
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183777.png?modified=1555593112",
+ "id" : 183777,
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183777.png?modified=1555593112",
+ "image_size" : [
+ 720,
+ 92
+ ],
+ "caption" : null,
+ "thumbnail_size" : [
+ 501,
+ 64
+ ]
+ }
+ ],
+ "name" : "Tigers Matter ** DON'T DELTE ME**",
+ "id" : 496012,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/tigers-matter/",
+ "type" : "statictheme",
+ "ratings" : {
+ "average" : 4.7636,
+ "text_count" : 55,
+ "count" : 55,
+ "bayesian_average" : 4.75672
+ },
+ "slug" : "tigers-matter",
+ "average_daily_users" : 1,
+ "current_version" : {
+ "compatibility" : {
+ "firefox" : {
+ "max" : "*",
+ "min" : "53.0"
+ },
+ "android" : {
+ "max" : "*",
+ "min" : "65.0"
+ }
+ },
+ "is_strict_compatibility_enabled" : false,
+ "id" : 1655900,
+ "files" : [
+ {
+ "is_restart_required" : false,
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/376561/tigers_matter_dont_delte_me-2.0-an+fx.xpi?src=",
+ "created" : "2019-04-18T13:11:48Z",
+ "size" : 86337,
+ "status" : "public",
+ "is_webextension" : true,
+ "is_mozilla_signed_extension" : false,
+ "permissions" : [],
+ "hash" : "sha256:ebeb6e4f40ceafbc4affc5bc9a182ed44ae410d71b8c5f9c547f8d45863e0c37",
+ "platform" : "all",
+ "id" : 376561
+ }
+ ]
+ }
+ },
+ "is_recommendation" : false
+ },
+ {
+ "is_recommendation" : false,
+ "addon" : {
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/awesome-screenshot-plus-/",
+ "type" : "extension",
+ "ratings" : {
+ "count" : 848,
+ "bayesian_average" : 3.87925,
+ "average" : 3.8797,
+ "text_count" : 842
+ },
+ "slug" : "awesome-screenshot-plus-",
+ "average_daily_users" : 1,
+ "current_version" : {
+ "is_strict_compatibility_enabled" : false,
+ "id" : 1532816,
+ "files" : [
+ {
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/253549/awesome_screenshot_plus-7-an+fx.xpi?src=",
+ "is_restart_required" : false,
+ "size" : 4196,
+ "created" : "2017-09-01T13:31:17Z",
+ "is_webextension" : true,
+ "status" : "public",
+ "is_mozilla_signed_extension" : false,
+ "permissions" : [],
+ "hash" : "sha256:4cd8e9b7e89f61e6855d01c73c5c05920c1e0e91f3ae0f45adbb4bd9919f59d7",
+ "platform" : "all",
+ "id" : 253549
+ }
+ ],
+ "compatibility" : {
+ "android" : {
+ "min" : "48.0",
+ "max" : "*"
+ },
+ "firefox" : {
+ "max" : "*",
+ "min" : "48.0"
+ }
+ }
+ },
+ "authors" : [
+ {
+ "username" : "diigo-inc",
+ "name" : "Diigo Inc.",
+ "picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/0/6/6724.png?modified=1554393597",
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/6724/",
+ "id" : 6724
+ }
+ ],
+ "icon_url" : "https://addons-dev-cdn.allizom.org/user-media/addon_icons/287/287841-64.png?modified=mcrushed",
+ "guid" : "jid0-GXjLLfbCoAx0LcltEdFrEkQdQPI@jetpack",
+ "previews" : [
+ {
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54638.png?modified=1543388383",
+ "id" : 54638,
+ "image_size" : [
+ 625,
+ 525
+ ],
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54638.png?modified=1543388383",
+ "caption" : "Capture and annotate a page",
+ "thumbnail_size" : [
+ 571,
+ 480
+ ]
+ },
+ {
+ "caption" : "Crop selected area",
+ "thumbnail_size" : [
+ 571,
+ 480
+ ],
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54639.png?modified=1543388385",
+ "image_size" : [
+ 625,
+ 525
+ ],
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54639.png?modified=1543388385",
+ "id" : 54639
+ },
+ {
+ "caption" : "Save as a local file or upload to get a sharable link",
+ "thumbnail_size" : [
+ 640,
+ 234
+ ],
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/full/54/54641.png?modified=1543388385",
+ "image_size" : [
+ 700,
+ 256
+ ],
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/previews/thumbs/54/54641.png?modified=1543388385",
+ "id" : 54641
+ }
+ ],
+ "name" : "Awesome Screenshot Plus - Capture, Annotate & More",
+ "id" : 287841
+ },
+ "description_text" : "Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines and text, blur sensitive info, one-click upload to share. And more! Capture the whole page or any portion, annotate it with rectangles, circles, arrows, lines"
+ },
+ {
+ "description_text" : "Help Admins in their daily work",
+ "addon" : {
+ "slug" : "amo-admin-assistant-test",
+ "average_daily_users" : 0,
+ "current_version" : {
+ "files" : [
+ {
+ "is_restart_required" : false,
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/255370/amo_admin_assistant-4.2-fx.xpi?src=",
+ "size" : 16016,
+ "created" : "2018-08-21T16:49:21Z",
+ "is_webextension" : true,
+ "status" : "public",
+ "is_mozilla_signed_extension" : false,
+ "permissions" : [
+ "tabs",
+ "https://addons-internal.prod.mozaws.net/*"
+ ],
+ "hash" : "sha256:cd28c841a6daf8a2e3c94b0773b373fec0213404b70074309326cfc75e6725d3",
+ "platform" : "all",
+ "id" : 255370
+ }
+ ],
+ "is_strict_compatibility_enabled" : false,
+ "id" : 1534709,
+ "compatibility" : {
+ "firefox" : {
+ "min" : "45.0",
+ "max" : "*"
+ }
+ }
+ },
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/amo-admin-assistant-test/",
+ "ratings" : {
+ "bayesian_average" : 0,
+ "count" : 0,
+ "text_count" : 0,
+ "average" : 0
+ },
+ "type" : "extension",
+ "id" : 496168,
+ "guid" : "aaa-test-icon@xulforge.com",
+ "icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "authors" : [
+ {
+ "id" : 4230,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/4230/",
+ "username" : "jorge-villalobos",
+ "name" : "Jorge Villalobos",
+ "picture_url" : null
+ }
+ ],
+ "previews" : [],
+ "name" : "AMO Admin Assistant Test"
+ },
+ "is_recommendation" : false
+ },
+ {
+ "addon" : {
+ "authors" : [
+ {
+ "name" : "LexaDev",
+ "picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10640/10640485.png?modified=1554812253",
+ "username" : "LexaSV",
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/10640485/",
+ "id" : 10640485
+ }
+ ],
+ "icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid" : "{f9b9cdd3-91ae-476e-9c21-d5ecfce9889f}",
+ "previews" : [
+ {
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183694.png?modified=1555593096",
+ "image_size" : [
+ 680,
+ 92
+ ],
+ "id" : 183694,
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183694.png?modified=1555593096",
+ "thumbnail_size" : [
+ 473,
+ 64
+ ],
+ "caption" : null
+ },
+ {
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183699.png?modified=1555593097",
+ "id" : 183699,
+ "image_size" : [
+ 760,
+ 92
+ ],
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183699.png?modified=1555593097",
+ "caption" : null,
+ "thumbnail_size" : [
+ 529,
+ 64
+ ]
+ },
+ {
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/183/183703.png?modified=1555593098",
+ "image_size" : [
+ 720,
+ 92
+ ],
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/183/183703.png?modified=1555593098",
+ "id" : 183703,
+ "caption" : null,
+ "thumbnail_size" : [
+ 501,
+ 64
+ ]
+ }
+ ],
+ "name" : "iarba",
+ "id" : 495969,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/iarba/",
+ "ratings" : {
+ "bayesian_average" : 4.86128,
+ "count" : 10,
+ "text_count" : 10,
+ "average" : 4.9
+ },
+ "type" : "statictheme",
+ "slug" : "iarba",
+ "current_version" : {
+ "files" : [
+ {
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/376535/iarba-2.0-an+fx.xpi?src=",
+ "is_restart_required" : false,
+ "size" : 895804,
+ "created" : "2019-04-18T13:11:35Z",
+ "is_mozilla_signed_extension" : false,
+ "status" : "public",
+ "is_webextension" : true,
+ "id" : 376535,
+ "permissions" : [],
+ "platform" : "all",
+ "hash" : "sha256:d7ecbdfa8ba56c5d08129c867e68b02ffc8c6000a7f7f85d85d2a558045babfa"
+ }
+ ],
+ "is_strict_compatibility_enabled" : false,
+ "id" : 1655874,
+ "compatibility" : {
+ "android" : {
+ "min" : "65.0",
+ "max" : "*"
+ },
+ "firefox" : {
+ "min" : "53.0",
+ "max" : "*"
+ }
+ }
+ },
+ "average_daily_users" : 1
+ },
+ "description_text" : "",
+ "is_recommendation" : false
+ },
+ {
+ "description_text" : "Get international weather forecasts",
+ "addon" : {
+ "id" : 502855,
+ "authors" : [
+ {
+ "id" : 10641527,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/10641527/",
+ "name" : "Amoga-dev",
+ "picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641527.png?modified=1555333028",
+ "username" : "Amoga_dev_REST"
+ }
+ ],
+ "icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid" : "forecastfox@s3_fix_version",
+ "previews" : [],
+ "name" : "Forecastfox (fix version)",
+ "slug" : "forecastfox-fix-version",
+ "current_version" : {
+ "id" : 1541667,
+ "is_strict_compatibility_enabled" : false,
+ "files" : [
+ {
+ "permissions" : [
+ "activeTab",
+ "tabs",
+ "background",
+ "storage",
+ "webRequest",
+ "webRequestBlocking",
+ "",
+ "http://www.s3blog.org/geolocation.html*",
+ "https://embed.windy.com/embed2.html*"
+ ],
+ "platform" : "all",
+ "hash" : "sha256:89e4de4ce86005c57b0197f671e86936aaf843ebd5751caae02cad4991ccbf0a",
+ "id" : 262328,
+ "is_webextension" : true,
+ "status" : "public",
+ "is_mozilla_signed_extension" : false,
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/262328/forecastfox_fix_version-4.20-an+fx.xpi?src=",
+ "is_restart_required" : false,
+ "created" : "2019-01-16T07:54:26Z",
+ "size" : 1331686
+ }
+ ],
+ "compatibility" : {
+ "android" : {
+ "min" : "51.0",
+ "max" : "*"
+ },
+ "firefox" : {
+ "min" : "51.0",
+ "max" : "*"
+ }
+ }
+ },
+ "average_daily_users" : 0,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/forecastfox-fix-version/",
+ "type" : "extension",
+ "ratings" : {
+ "count" : 0,
+ "bayesian_average" : 0,
+ "average" : 0,
+ "text_count" : 0
+ }
+ },
+ "is_recommendation" : false
+ },
+ {
+ "description_text" : "A test extension from webext-generator.",
+ "addon" : {
+ "name" : "tabby cat",
+ "previews" : [],
+ "guid" : "{1ed4b641-bac7-4492-b304-6ddc01f538ae}",
+ "icon_url" : "https://addons-dev-cdn.allizom.org/user-media/addon_icons/502/502774-64.png?modified=f289a992",
+ "authors" : [
+ {
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/10641572/",
+ "username" : "AdminUserTestDev1",
+ "picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641572.png?modified=1555675110",
+ "name" : "úþÿ Ψ Φ ֎",
+ "id" : 10641572
+ }
+ ],
+ "id" : 502774,
+ "ratings" : {
+ "bayesian_average" : 0,
+ "count" : 0,
+ "text_count" : 0,
+ "average" : 0
+ },
+ "type" : "extension",
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/tabby-catextension/",
+ "current_version" : {
+ "compatibility" : {
+ "firefox" : {
+ "max" : "*",
+ "min" : "48.0"
+ },
+ "android" : {
+ "max" : "*",
+ "min" : "48.0"
+ }
+ },
+ "is_strict_compatibility_enabled" : false,
+ "id" : 1541570,
+ "files" : [
+ {
+ "created" : "2018-12-04T09:54:24Z",
+ "size" : 4374,
+ "is_restart_required" : false,
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/262231/tabby_cat-1.0-an+fx.xpi?src=",
+ "is_mozilla_signed_extension" : false,
+ "status" : "public",
+ "is_webextension" : true,
+ "id" : 262231,
+ "hash" : "sha256:f12c8a8b71e7d4c48e38db6b6a374ca8dcde42d6cb13fb1f2a8299bb51116615",
+ "platform" : "all",
+ "permissions" : []
+ }
+ ]
+ },
+ "average_daily_users" : 1,
+ "slug" : "tabby-catextension"
+ },
+ "is_recommendation" : false
+ },
+ {
+ "addon" : {
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/the-moon-cat/",
+ "ratings" : {
+ "average" : 4.8182,
+ "text_count" : 11,
+ "count" : 11,
+ "bayesian_average" : 4.78325
+ },
+ "type" : "statictheme",
+ "slug" : "the-moon-cat",
+ "average_daily_users" : 2,
+ "current_version" : {
+ "files" : [
+ {
+ "is_mozilla_signed_extension" : false,
+ "status" : "public",
+ "is_webextension" : true,
+ "id" : 262333,
+ "permissions" : [],
+ "hash" : "sha256:d159190add69c739b0fe07b19ad3ff48045c5ded502a8df0f892b8feb645c5ae",
+ "platform" : "all",
+ "is_restart_required" : false,
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/262333/the_moon_cat-1.0-an+fx.xpi?src=",
+ "size" : 102889,
+ "created" : "2019-01-16T08:31:21Z"
+ }
+ ],
+ "is_strict_compatibility_enabled" : false,
+ "id" : 1541672,
+ "compatibility" : {
+ "firefox" : {
+ "max" : "*",
+ "min" : "53.0"
+ },
+ "android" : {
+ "min" : "65.0",
+ "max" : "*"
+ }
+ }
+ },
+ "icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "authors" : [
+ {
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/5822165/",
+ "username" : "Rallara",
+ "name" : "Rallara",
+ "picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/5/5822/5822165.png?modified=1391855104",
+ "id" : 5822165
+ }
+ ],
+ "guid" : "{db4f6548-da04-43fb-a03e-249bf70ef5a1}",
+ "previews" : [
+ {
+ "thumbnail_size" : [
+ 473,
+ 64
+ ],
+ "caption" : null,
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14307.png?modified=1547627485",
+ "image_size" : [
+ 680,
+ 92
+ ],
+ "id" : 14307,
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14307.png?modified=1547627485"
+ },
+ {
+ "thumbnail_size" : [
+ 529,
+ 64
+ ],
+ "caption" : null,
+ "id" : 14308,
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14308.png?modified=1547627486",
+ "image_size" : [
+ 760,
+ 92
+ ],
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14308.png?modified=1547627486"
+ },
+ {
+ "thumbnail_size" : [
+ 501,
+ 64
+ ],
+ "caption" : null,
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14309.png?modified=1547627487",
+ "image_size" : [
+ 720,
+ 92
+ ],
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14309.png?modified=1547627487",
+ "id" : 14309
+ }
+ ],
+ "name" : "the Moon Cat",
+ "id" : 502859
+ },
+ "description_text" : "",
+ "is_recommendation" : false
+ },
+ {
+ "is_recommendation" : false,
+ "addon" : {
+ "icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid" : "{2e5ff8c8-32fe-46d0-9fc8-6b8986621f3c}",
+ "authors" : [
+ {
+ "id" : 10641570,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/10641570/",
+ "name" : "BobsDisplayName",
+ "picture_url" : "https://addons-dev-cdn.allizom.org/user-media/userpics/10/10641/10641570.png?modified=1536063975",
+ "username" : "BobsUserName"
+ }
+ ],
+ "previews" : [],
+ "name" : "SI",
+ "id" : 495710,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/search_by_image/",
+ "ratings" : {
+ "average" : 3.8333,
+ "text_count" : 5,
+ "count" : 6,
+ "bayesian_average" : 3.77144
+ },
+ "type" : "extension",
+ "slug" : "search_by_image",
+ "current_version" : {
+ "files" : [
+ {
+ "id" : 262271,
+ "permissions" : [
+ "contextMenus",
+ "storage",
+ "tabs",
+ "activeTab",
+ "notifications",
+ "webRequest",
+ "webRequestBlocking",
+ "",
+ "http://*/*",
+ "https://*/*",
+ "ftp://*/*",
+ "file:///*"
+ ],
+ "platform" : "all",
+ "hash" : "sha256:f358b24d0b950f5acf035342dec64c99ee2e22a5cf369e7c787ebb00013127a8",
+ "is_mozilla_signed_extension" : false,
+ "is_webextension" : true,
+ "status" : "public",
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/262271/search_by_image_reverse_image_search-1.12.6-fx.xpi?src=",
+ "is_restart_required" : false,
+ "size" : 372225,
+ "created" : "2018-12-14T13:48:23Z"
+ }
+ ],
+ "id" : 1541610,
+ "is_strict_compatibility_enabled" : false,
+ "compatibility" : {
+ "firefox" : {
+ "min" : "57.0",
+ "max" : "*"
+ }
+ }
+ },
+ "average_daily_users" : 374
+ },
+ "description_text" : "AAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGGAAAGGGG"
+ },
+ {
+ "addon" : {
+ "icon_url" : "https://addons-dev-cdn.allizom.org/static/img/addon-icons/default-64.png",
+ "guid" : "{f5e7a6ee-ebe0-4add-8f75-b5e4015feca1}",
+ "authors" : [
+ {
+ "id" : 8733220,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/user/8733220/",
+ "username" : "michellet-2",
+ "name" : "michellet",
+ "picture_url" : null
+ }
+ ],
+ "previews" : [
+ {
+ "caption" : null,
+ "thumbnail_size" : [
+ 473,
+ 64
+ ],
+ "id" : 14304,
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14304.png?modified=1547627480",
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14304.png?modified=1547627480",
+ "image_size" : [
+ 680,
+ 92
+ ]
+ },
+ {
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14305.png?modified=1547627481",
+ "image_size" : [
+ 760,
+ 92
+ ],
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14305.png?modified=1547627481",
+ "id" : 14305,
+ "thumbnail_size" : [
+ 529,
+ 64
+ ],
+ "caption" : null
+ },
+ {
+ "caption" : null,
+ "thumbnail_size" : [
+ 501,
+ 64
+ ],
+ "thumbnail_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/thumbs/14/14306.png?modified=1547627482",
+ "id" : 14306,
+ "image_size" : [
+ 720,
+ 92
+ ],
+ "image_url" : "https://addons-dev-cdn.allizom.org/user-media/version-previews/full/14/14306.png?modified=1547627482"
+ }
+ ],
+ "name" : "Purple Sparkles",
+ "id" : 502858,
+ "url" : "https://addons-dev.allizom.org/en-US/firefox/addon/purple-sparkles/",
+ "type" : "statictheme",
+ "ratings" : {
+ "count" : 4,
+ "bayesian_average" : 4.1476,
+ "average" : 4.25,
+ "text_count" : 3
+ },
+ "slug" : "purple-sparkles",
+ "average_daily_users" : 445,
+ "current_version" : {
+ "compatibility" : {
+ "firefox" : {
+ "min" : "53.0",
+ "max" : "*"
+ },
+ "android" : {
+ "max" : "*",
+ "min" : "65.0"
+ }
+ },
+ "id" : 1541671,
+ "is_strict_compatibility_enabled" : false,
+ "files" : [
+ {
+ "created" : "2019-01-16T08:31:18Z",
+ "size" : 237348,
+ "url" : "https://addons-dev.allizom.org/firefox/downloads/file/262332/purple_sparkles-1.0-an+fx.xpi?src=",
+ "is_restart_required" : false,
+ "is_mozilla_signed_extension" : false,
+ "is_webextension" : true,
+ "status" : "public",
+ "id" : 262332,
+ "hash" : "sha256:5a3d311b7c1be2ee32446dbcf1422c5d7c786c5a237aa3d4e2939074ab50ad30",
+ "platform" : "all",
+ "permissions" : []
+ }
+ ]
+ }
+ },
+ "description_text" : "",
+ "is_recommendation" : false
+ }
+ ],
+ "count" : 9
+}
diff --git a/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json b/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json
new file mode 100644
index 0000000000..330e8fe313
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery/api_response_empty.json
@@ -0,0 +1 @@
+{"results": []}
diff --git a/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png b/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png
new file mode 100644
index 0000000000..862d1dd10c
Binary files /dev/null and b/toolkit/mozapps/extensions/test/browser/discovery/small-1x1.png differ
diff --git a/toolkit/mozapps/extensions/test/browser/head.js b/toolkit/mozapps/extensions/test/browser/head.js
new file mode 100644
index 0000000000..cc008aff24
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -0,0 +1,1809 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* globals end_test */
+
+/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */
+
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+const { TelemetryTestUtils } = ChromeUtils.import(
+ "resource://testing-common/TelemetryTestUtils.jsm"
+);
+
+var tmp = {};
+ChromeUtils.import("resource://gre/modules/AddonManager.jsm", tmp);
+ChromeUtils.import("resource://gre/modules/Log.jsm", tmp);
+var AddonManager = tmp.AddonManager;
+var AddonManagerPrivate = tmp.AddonManagerPrivate;
+var Log = tmp.Log;
+
+var pathParts = gTestPath.split("/");
+// Drop the test filename
+pathParts.splice(pathParts.length - 1, pathParts.length);
+
+const RELATIVE_DIR = pathParts.slice(4).join("/") + "/";
+
+const TESTROOT = "http://example.com/" + RELATIVE_DIR;
+const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR;
+const TESTROOT2 = "http://example.org/" + RELATIVE_DIR;
+const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR;
+const CHROMEROOT = pathParts.join("/") + "/";
+const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
+const PREF_XPI_ENABLED = "xpinstall.enabled";
+const PREF_UPDATEURL = "extensions.update.url";
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
+
+const MANAGER_URI = "about:addons";
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const PREF_STRICT_COMPAT = "extensions.strictCompatibility";
+
+var PREF_CHECK_COMPATIBILITY;
+(function() {
+ var channel = Services.prefs.getCharPref("app.update.channel", "default");
+ if (
+ channel != "aurora" &&
+ channel != "beta" &&
+ channel != "release" &&
+ channel != "esr"
+ ) {
+ var version = "nightly";
+ } else {
+ version = Services.appinfo.version.replace(
+ /^([^\.]+\.[0-9]+[a-z]*).*/gi,
+ "$1"
+ );
+ }
+ PREF_CHECK_COMPATIBILITY = "extensions.checkCompatibility." + version;
+})();
+
+var gPendingTests = [];
+var gTestsRun = 0;
+var gTestStart = null;
+
+var gRestorePrefs = [
+ { name: PREF_LOGGING_ENABLED },
+ { name: "extensions.webservice.discoverURL" },
+ { name: "extensions.update.url" },
+ { name: "extensions.update.background.url" },
+ { name: "extensions.update.enabled" },
+ { name: "extensions.update.autoUpdateDefault" },
+ { name: "extensions.getAddons.get.url" },
+ { name: "extensions.getAddons.getWithPerformance.url" },
+ { name: "extensions.getAddons.cache.enabled" },
+ { name: "devtools.chrome.enabled" },
+ { name: PREF_STRICT_COMPAT },
+ { name: PREF_CHECK_COMPATIBILITY },
+];
+
+for (let pref of gRestorePrefs) {
+ if (!Services.prefs.prefHasUserValue(pref.name)) {
+ pref.type = "clear";
+ continue;
+ }
+ pref.type = Services.prefs.getPrefType(pref.name);
+ if (pref.type == Services.prefs.PREF_BOOL) {
+ pref.value = Services.prefs.getBoolPref(pref.name);
+ } else if (pref.type == Services.prefs.PREF_INT) {
+ pref.value = Services.prefs.getIntPref(pref.name);
+ } else if (pref.type == Services.prefs.PREF_STRING) {
+ pref.value = Services.prefs.getCharPref(pref.name);
+ }
+}
+
+// Turn logging on for all tests
+Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
+
+function promiseFocus(window) {
+ return new Promise(resolve => waitForFocus(resolve, window));
+}
+
+// Helper to register test failures and close windows if any are left open
+function checkOpenWindows(aWindowID) {
+ let found = false;
+ for (let win of Services.wm.getEnumerator(aWindowID)) {
+ if (!win.closed) {
+ found = true;
+ win.close();
+ }
+ }
+ if (found) {
+ ok(false, "Found unexpected " + aWindowID + " window still open");
+ }
+}
+
+// Tools to disable and re-enable the background update and blocklist timers
+// so that tests can protect themselves from unwanted timer events.
+var gCatMan = Services.catMan;
+// Default value from toolkit/mozapps/extensions/extensions.manifest, but disable*UpdateTimer()
+// records the actual value so we can put it back in enable*UpdateTimer()
+var backgroundUpdateConfig =
+ "@mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400";
+
+var UTIMER = "update-timer";
+var AMANAGER = "addonManager";
+var BLOCKLIST = "nsBlocklistService";
+
+function disableBackgroundUpdateTimer() {
+ info("Disabling " + UTIMER + " " + AMANAGER);
+ backgroundUpdateConfig = gCatMan.getCategoryEntry(UTIMER, AMANAGER);
+ gCatMan.deleteCategoryEntry(UTIMER, AMANAGER, true);
+}
+
+function enableBackgroundUpdateTimer() {
+ info("Enabling " + UTIMER + " " + AMANAGER);
+ gCatMan.addCategoryEntry(
+ UTIMER,
+ AMANAGER,
+ backgroundUpdateConfig,
+ false,
+ true
+ );
+}
+
+registerCleanupFunction(function() {
+ // Restore prefs
+ for (let pref of gRestorePrefs) {
+ if (pref.type == "clear") {
+ Services.prefs.clearUserPref(pref.name);
+ } else if (pref.type == Services.prefs.PREF_BOOL) {
+ Services.prefs.setBoolPref(pref.name, pref.value);
+ } else if (pref.type == Services.prefs.PREF_INT) {
+ Services.prefs.setIntPref(pref.name, pref.value);
+ } else if (pref.type == Services.prefs.PREF_STRING) {
+ Services.prefs.setCharPref(pref.name, pref.value);
+ }
+ }
+
+ // Throw an error if the add-ons manager window is open anywhere
+ checkOpenWindows("Addons:Manager");
+ checkOpenWindows("Addons:Compatibility");
+ checkOpenWindows("Addons:Install");
+
+ return AddonManager.getAllInstalls().then(aInstalls => {
+ for (let install of aInstalls) {
+ if (install instanceof MockInstall) {
+ continue;
+ }
+
+ ok(
+ false,
+ "Should not have seen an install of " +
+ install.sourceURI.spec +
+ " in state " +
+ install.state
+ );
+ install.cancel();
+ }
+ });
+});
+
+function log_exceptions(aCallback, ...aArgs) {
+ try {
+ return aCallback.apply(null, aArgs);
+ } catch (e) {
+ info("Exception thrown: " + e);
+ throw e;
+ }
+}
+
+function log_callback(aPromise, aCallback) {
+ aPromise.then(aCallback).catch(e => info("Exception thrown: " + e));
+ return aPromise;
+}
+
+function add_test(test) {
+ gPendingTests.push(test);
+}
+
+function run_next_test() {
+ // Make sure we're not calling run_next_test from inside an add_task() test
+ // We're inside the browser_test.js 'testScope' here
+ if (this.__tasks) {
+ throw new Error(
+ "run_next_test() called from an add_task() test function. " +
+ "run_next_test() should not be called from inside add_task() " +
+ "under any circumstances!"
+ );
+ }
+ if (gTestsRun > 0) {
+ info("Test " + gTestsRun + " took " + (Date.now() - gTestStart) + "ms");
+ }
+
+ if (!gPendingTests.length) {
+ executeSoon(end_test);
+ return;
+ }
+
+ gTestsRun++;
+ var test = gPendingTests.shift();
+ if (test.name) {
+ info("Running test " + gTestsRun + " (" + test.name + ")");
+ } else {
+ info("Running test " + gTestsRun);
+ }
+
+ gTestStart = Date.now();
+ executeSoon(() => log_exceptions(test));
+}
+
+var get_tooltip_info = async function(addonEl, managerWindow) {
+ // Extract from title attribute.
+ const { addon } = addonEl;
+ const name = addon.name;
+
+ let nameWithVersion = addonEl.addonNameEl.title;
+ if (addonEl.addon.userDisabled) {
+ // TODO - Bug 1558077: Currently Fluent is clearing the addon title
+ // when the addon is disabled, fixing it requires changes to the
+ // HTML about:addons localized strings, and then remove this
+ // workaround.
+ nameWithVersion = `${name} ${addon.version}`;
+ }
+
+ return {
+ name,
+ version: nameWithVersion.substring(name.length + 1),
+ };
+};
+
+function get_addon_file_url(aFilename) {
+ try {
+ var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
+ Ci.nsIChromeRegistry
+ );
+ var fileurl = cr.convertChromeURL(
+ makeURI(CHROMEROOT + "addons/" + aFilename)
+ );
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+ } catch (ex) {
+ var jar = getJar(CHROMEROOT + "addons/" + aFilename);
+ var tmpDir = extractJarToTmp(jar);
+ tmpDir.append(aFilename);
+
+ return Services.io.newFileURI(tmpDir).QueryInterface(Ci.nsIFileURL);
+ }
+}
+
+function check_all_in_list(aManager, aIds, aIgnoreExtras) {
+ var doc = aManager.document;
+ var list = doc.getElementById("addon-list");
+
+ var inlist = [];
+ var node = list.firstChild;
+ while (node) {
+ if (node.value) {
+ inlist.push(node.value);
+ }
+ node = node.nextSibling;
+ }
+
+ for (let id of aIds) {
+ if (!inlist.includes(id)) {
+ ok(false, "Should find " + id + " in the list");
+ }
+ }
+
+ if (aIgnoreExtras) {
+ return;
+ }
+
+ for (let inlistItem of inlist) {
+ if (!aIds.includes(inlistItem)) {
+ ok(false, "Shouldn't have seen " + inlistItem + " in the list");
+ }
+ }
+}
+
+function get_addon_element(aManager, aId) {
+ const win = aManager.getHtmlBrowser().contentWindow;
+ return getAddonCard(win, aId);
+}
+
+function getAddonCard(win, id) {
+ return win.document.querySelector(`addon-card[addon-id="${id}"]`);
+}
+
+function wait_for_view_load(
+ aManagerWindow,
+ aCallback,
+ aForceWait,
+ aLongerTimeout
+) {
+ let p = new Promise(resolve => {
+ requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
+
+ if (!aForceWait && !aManagerWindow.gViewController.isLoading) {
+ resolve(aManagerWindow);
+ return;
+ }
+
+ aManagerWindow.document.addEventListener(
+ "ViewChanged",
+ function() {
+ resolve(aManagerWindow);
+ },
+ { once: true }
+ );
+ });
+
+ return log_callback(p, aCallback);
+}
+
+function wait_for_manager_load(aManagerWindow, aCallback) {
+ let p = new Promise(resolve => {
+ if (!aManagerWindow.gIsInitializing) {
+ resolve(aManagerWindow);
+ return;
+ }
+
+ info("Waiting for initialization");
+ aManagerWindow.document.addEventListener(
+ "Initialized",
+ function() {
+ resolve(aManagerWindow);
+ },
+ { once: true }
+ );
+ });
+
+ return log_callback(p, aCallback);
+}
+
+function open_manager(
+ aView,
+ aCallback,
+ aLoadCallback,
+ aLongerTimeout,
+ aWin = window
+) {
+ let p = new Promise((resolve, reject) => {
+ async function setup_manager(aManagerWindow) {
+ if (aLoadCallback) {
+ log_exceptions(aLoadCallback, aManagerWindow);
+ }
+
+ if (aView) {
+ aManagerWindow.loadView(aView);
+ }
+
+ ok(aManagerWindow != null, "Should have an add-ons manager window");
+ is(
+ aManagerWindow.location.href,
+ MANAGER_URI,
+ "Should be displaying the correct UI"
+ );
+
+ await promiseFocus(aManagerWindow);
+ info("window has focus, waiting for manager load");
+ await wait_for_manager_load(aManagerWindow);
+ info("Manager waiting for view load");
+ await wait_for_view_load(aManagerWindow, null, null, aLongerTimeout);
+ resolve(aManagerWindow);
+ }
+
+ info("Loading manager window in tab");
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(observer, aTopic);
+ if (aSubject.location.href != MANAGER_URI) {
+ info("Ignoring load event for " + aSubject.location.href);
+ return;
+ }
+ setup_manager(aSubject);
+ }, "EM-loaded");
+
+ aWin.gBrowser.selectedTab = BrowserTestUtils.addTab(aWin.gBrowser);
+ aWin.switchToTabHavingURI(MANAGER_URI, true, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ });
+
+ // The promise resolves with the manager window, so it is passed to the callback
+ return log_callback(p, aCallback);
+}
+
+function close_manager(aManagerWindow, aCallback, aLongerTimeout) {
+ let p = new Promise((resolve, reject) => {
+ requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
+
+ ok(
+ aManagerWindow != null,
+ "Should have an add-ons manager window to close"
+ );
+ is(
+ aManagerWindow.location.href,
+ MANAGER_URI,
+ "Should be closing window with correct URI"
+ );
+
+ aManagerWindow.addEventListener("unload", function listener() {
+ try {
+ dump("Manager window unload handler\n");
+ this.removeEventListener("unload", listener);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ });
+ });
+
+ info("Telling manager window to close");
+ aManagerWindow.close();
+ info("Manager window close() call returned");
+
+ return log_callback(p, aCallback);
+}
+
+function restart_manager(aManagerWindow, aView, aCallback, aLoadCallback) {
+ if (!aManagerWindow) {
+ return open_manager(aView, aCallback, aLoadCallback);
+ }
+
+ return close_manager(aManagerWindow).then(() =>
+ open_manager(aView, aCallback, aLoadCallback)
+ );
+}
+
+function wait_for_window_open(aCallback) {
+ let p = new Promise(resolve => {
+ Services.wm.addListener({
+ onOpenWindow(aXulWin) {
+ Services.wm.removeListener(this);
+
+ let domwindow = aXulWin.docShell.domWindow;
+ domwindow.addEventListener(
+ "load",
+ function() {
+ executeSoon(function() {
+ resolve(domwindow);
+ });
+ },
+ { once: true }
+ );
+ },
+
+ onCloseWindow(aWindow) {},
+ });
+ });
+
+ return log_callback(p, aCallback);
+}
+
+function get_string(aName, ...aArgs) {
+ var bundle = Services.strings.createBundle(
+ "chrome://mozapps/locale/extensions/extensions.properties"
+ );
+ if (!aArgs.length) {
+ return bundle.GetStringFromName(aName);
+ }
+ return bundle.formatStringFromName(aName, aArgs);
+}
+
+function formatDate(aDate) {
+ const dtOptions = { year: "numeric", month: "long", day: "numeric" };
+ return aDate.toLocaleDateString(undefined, dtOptions);
+}
+
+function is_hidden(aElement) {
+ var style = aElement.ownerGlobal.getComputedStyle(aElement);
+ if (style.display == "none") {
+ return true;
+ }
+ if (style.visibility != "visible") {
+ return true;
+ }
+
+ // Hiding a parent element will hide all its children
+ if (aElement.parentNode != aElement.ownerDocument) {
+ return is_hidden(aElement.parentNode);
+ }
+
+ return false;
+}
+
+function is_element_visible(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(!is_hidden(aElement), aMsg || aElement + " should be visible");
+}
+
+function is_element_hidden(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(aElement), aMsg || aElement + " should be hidden");
+}
+
+function promiseAddonByID(aId) {
+ return AddonManager.getAddonByID(aId);
+}
+
+function promiseAddonsByIDs(aIDs) {
+ return AddonManager.getAddonsByIDs(aIDs);
+}
+/**
+ * Install an add-on and call a callback when complete.
+ *
+ * The callback will receive the Addon for the installed add-on.
+ */
+async function install_addon(path, cb, pathPrefix = TESTROOT) {
+ let install = await AddonManager.getInstallForURL(pathPrefix + path);
+ let p = new Promise((resolve, reject) => {
+ install.addListener({
+ onInstallEnded: () => resolve(install.addon),
+ });
+
+ install.install();
+ });
+
+ return log_callback(p, cb);
+}
+
+function CategoryUtilities(aManagerWindow) {
+ this.window = aManagerWindow.getHtmlBrowser().contentWindow;
+ this.managerWindow = aManagerWindow;
+
+ this.window.addEventListener("unload", () => (this.window = null), {
+ once: true,
+ });
+ this.managerWindow.addEventListener(
+ "unload",
+ () => (this.managerWindow = null),
+ { once: true }
+ );
+}
+
+CategoryUtilities.prototype = {
+ window: null,
+ managerWindow: null,
+
+ get _categoriesBox() {
+ return this.window.document.querySelector("categories-box");
+ },
+
+ getSelectedViewId() {
+ let selectedItem = this._categoriesBox.querySelector("[selected]");
+ isnot(selectedItem, null, "A category should be selected");
+ return selectedItem.getAttribute("viewid");
+ },
+
+ get selectedCategory() {
+ isnot(
+ this.window,
+ null,
+ "Should not get selected category when manager window is not loaded"
+ );
+ let viewId = this.getSelectedViewId();
+ let view = this.managerWindow.gViewController.parseViewId(viewId);
+ return view.type == "list" ? view.param : view.type;
+ },
+
+ get(categoryType) {
+ isnot(
+ this.window,
+ null,
+ "Should not get category when manager window is not loaded"
+ );
+
+ let button = this._categoriesBox.querySelector(`[name="${categoryType}"]`);
+ if (button) {
+ return button;
+ }
+
+ ok(false, "Should have found a category with type " + categoryType);
+ return null;
+ },
+
+ isVisible(categoryButton) {
+ isnot(
+ this.window,
+ null,
+ "Should not check visible state when manager window is not loaded"
+ );
+
+ // There are some tests checking this before the categories have loaded.
+ if (!categoryButton) {
+ return false;
+ }
+
+ if (categoryButton.disabled || categoryButton.hidden) {
+ return false;
+ }
+
+ return !is_hidden(categoryButton);
+ },
+
+ isTypeVisible(categoryType) {
+ return this.isVisible(this.get(categoryType));
+ },
+
+ open(categoryButton) {
+ isnot(
+ this.window,
+ null,
+ "Should not open category when manager window is not loaded"
+ );
+ ok(
+ this.isVisible(categoryButton),
+ "Category should be visible if attempting to open it"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(categoryButton, {}, this.window);
+
+ // Use wait_for_view_load until all open_manager calls are gone.
+ return wait_for_view_load(this.managerWindow);
+ },
+
+ openType(categoryType) {
+ return this.open(this.get(categoryType));
+ },
+};
+
+// Returns a promise that will resolve when the certificate error override has been added, or reject
+// if there is some failure.
+function addCertOverride(host, bits) {
+ return new Promise((resolve, reject) => {
+ let req = new XMLHttpRequest();
+ req.open("GET", "https://" + host + "/");
+ req.onload = reject;
+ req.onerror = () => {
+ if (req.channel && req.channel.securityInfo) {
+ let securityInfo = req.channel.securityInfo.QueryInterface(
+ Ci.nsITransportSecurityInfo
+ );
+ if (securityInfo.serverCert) {
+ let cos = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+ );
+ cos.rememberValidityOverride(
+ host,
+ -1,
+ securityInfo.serverCert,
+ bits,
+ false
+ );
+ resolve();
+ return;
+ }
+ }
+ reject();
+ };
+ req.send(null);
+ });
+}
+
+// Returns a promise that will resolve when the necessary certificate overrides have been added.
+function addCertOverrides() {
+ return Promise.all([
+ addCertOverride(
+ "nocert.example.com",
+ Ci.nsICertOverrideService.ERROR_MISMATCH
+ ),
+ addCertOverride(
+ "self-signed.example.com",
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED
+ ),
+ addCertOverride(
+ "untrusted.example.com",
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED
+ ),
+ addCertOverride(
+ "expired.example.com",
+ Ci.nsICertOverrideService.ERROR_TIME
+ ),
+ ]);
+}
+
+/** *** Mock Provider *****/
+
+function MockProvider() {
+ this.addons = [];
+ this.installs = [];
+ this.types = [
+ {
+ id: "extension",
+ name: "Extensions",
+ uiPriority: 4000,
+ flags:
+ AddonManager.TYPE_UI_VIEW_LIST |
+ AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL,
+ },
+ ];
+
+ var self = this;
+ registerCleanupFunction(function() {
+ if (self.started) {
+ self.unregister();
+ }
+ });
+
+ this.register();
+}
+
+MockProvider.prototype = {
+ addons: null,
+ installs: null,
+ started: null,
+ types: null,
+ queryDelayPromise: Promise.resolve(),
+
+ blockQueryResponses() {
+ this.queryDelayPromise = new Promise(resolve => {
+ this._unblockQueries = resolve;
+ });
+ },
+
+ unblockQueryResponses() {
+ if (this._unblockQueries) {
+ this._unblockQueries();
+ this._unblockQueries = null;
+ } else {
+ throw new Error("Queries are not blocked");
+ }
+ },
+
+ /** *** Utility functions *****/
+
+ /**
+ * Register this provider with the AddonManager
+ */
+ register: function MP_register() {
+ info("Registering mock add-on provider");
+ AddonManagerPrivate.registerProvider(this, this.types);
+ },
+
+ /**
+ * Unregister this provider with the AddonManager
+ */
+ unregister: function MP_unregister() {
+ info("Unregistering mock add-on provider");
+ AddonManagerPrivate.unregisterProvider(this);
+ },
+
+ /**
+ * Adds an add-on to the list of add-ons that this provider exposes to the
+ * AddonManager, dispatching appropriate events in the process.
+ *
+ * @param aAddon
+ * The add-on to add
+ */
+ addAddon: function MP_addAddon(aAddon) {
+ var oldAddons = this.addons.filter(aOldAddon => aOldAddon.id == aAddon.id);
+ var oldAddon = oldAddons.length ? oldAddons[0] : null;
+
+ this.addons = this.addons.filter(aOldAddon => aOldAddon.id != aAddon.id);
+
+ this.addons.push(aAddon);
+ aAddon._provider = this;
+
+ if (!this.started) {
+ return;
+ }
+
+ let requiresRestart =
+ (aAddon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL) !=
+ 0;
+ AddonManagerPrivate.callInstallListeners(
+ "onExternalInstall",
+ null,
+ aAddon,
+ oldAddon,
+ requiresRestart
+ );
+ },
+
+ /**
+ * Removes an add-on from the list of add-ons that this provider exposes to
+ * the AddonManager, dispatching the onUninstalled event in the process.
+ *
+ * @param aAddon
+ * The add-on to add
+ */
+ removeAddon: function MP_removeAddon(aAddon) {
+ var pos = this.addons.indexOf(aAddon);
+ if (pos == -1) {
+ ok(
+ false,
+ "Tried to remove an add-on that wasn't registered with the mock provider"
+ );
+ return;
+ }
+
+ this.addons.splice(pos, 1);
+
+ if (!this.started) {
+ return;
+ }
+
+ AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon);
+ },
+
+ /**
+ * Adds an add-on install to the list of installs that this provider exposes
+ * to the AddonManager, dispatching appropriate events in the process.
+ *
+ * @param aInstall
+ * The add-on install to add
+ */
+ addInstall: function MP_addInstall(aInstall) {
+ this.installs.push(aInstall);
+ aInstall._provider = this;
+
+ if (!this.started) {
+ return;
+ }
+
+ aInstall.callListeners("onNewInstall");
+ },
+
+ removeInstall: function MP_removeInstall(aInstall) {
+ var pos = this.installs.indexOf(aInstall);
+ if (pos == -1) {
+ ok(
+ false,
+ "Tried to remove an install that wasn't registered with the mock provider"
+ );
+ return;
+ }
+
+ this.installs.splice(pos, 1);
+ },
+
+ /**
+ * Creates a set of mock add-on objects and adds them to the list of add-ons
+ * managed by this provider.
+ *
+ * @param aAddonProperties
+ * An array of objects containing properties describing the add-ons
+ * @return Array of the new MockAddons
+ */
+ createAddons: function MP_createAddons(aAddonProperties) {
+ var newAddons = [];
+ for (let addonProp of aAddonProperties) {
+ let addon = new MockAddon(addonProp.id);
+ for (let prop in addonProp) {
+ if (prop == "id") {
+ continue;
+ }
+ if (prop == "applyBackgroundUpdates") {
+ addon._applyBackgroundUpdates = addonProp[prop];
+ } else if (prop == "appDisabled") {
+ addon._appDisabled = addonProp[prop];
+ } else if (prop == "userDisabled") {
+ addon.setUserDisabled(addonProp[prop]);
+ } else {
+ addon[prop] = addonProp[prop];
+ }
+ }
+ if (!addon.optionsType && !!addon.optionsURL) {
+ addon.optionsType = AddonManager.OPTIONS_TYPE_DIALOG;
+ }
+
+ // Make sure the active state matches the passed in properties
+ addon.isActive = addon.shouldBeActive;
+
+ this.addAddon(addon);
+ newAddons.push(addon);
+ }
+
+ return newAddons;
+ },
+
+ /**
+ * Creates a set of mock add-on install objects and adds them to the list
+ * of installs managed by this provider.
+ *
+ * @param aInstallProperties
+ * An array of objects containing properties describing the installs
+ * @return Array of the new MockInstalls
+ */
+ createInstalls: function MP_createInstalls(aInstallProperties) {
+ var newInstalls = [];
+ for (let installProp of aInstallProperties) {
+ let install = new MockInstall(
+ installProp.name || null,
+ installProp.type || null,
+ null
+ );
+ for (let prop in installProp) {
+ switch (prop) {
+ case "name":
+ case "type":
+ break;
+ case "sourceURI":
+ install[prop] = NetUtil.newURI(installProp[prop]);
+ break;
+ default:
+ install[prop] = installProp[prop];
+ }
+ }
+ this.addInstall(install);
+ newInstalls.push(install);
+ }
+
+ return newInstalls;
+ },
+
+ /** *** AddonProvider implementation *****/
+
+ /**
+ * Called to initialize the provider.
+ */
+ startup: function MP_startup() {
+ this.started = true;
+ },
+
+ /**
+ * Called when the provider should shutdown.
+ */
+ shutdown: function MP_shutdown() {
+ this.started = false;
+ },
+
+ /**
+ * Called to get an Addon with a particular ID.
+ *
+ * @param aId
+ * The ID of the add-on to retrieve
+ */
+ async getAddonByID(aId) {
+ await this.queryDelayPromise;
+
+ for (let addon of this.addons) {
+ if (addon.id == aId) {
+ return addon;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Called to get Addons of a particular type.
+ *
+ * @param aTypes
+ * An array of types to fetch. Can be null to get all types.
+ */
+ async getAddonsByTypes(aTypes) {
+ await this.queryDelayPromise;
+
+ var addons = this.addons.filter(function(aAddon) {
+ if (aTypes && !!aTypes.length && !aTypes.includes(aAddon.type)) {
+ return false;
+ }
+ return true;
+ });
+ return addons;
+ },
+
+ /**
+ * Called to get the current AddonInstalls, optionally restricting by type.
+ *
+ * @param aTypes
+ * An array of types or null to get all types
+ */
+ async getInstallsByTypes(aTypes) {
+ await this.queryDelayPromise;
+
+ var installs = this.installs.filter(function(aInstall) {
+ // Appear to have actually removed cancelled installs from the provider
+ if (aInstall.state == AddonManager.STATE_CANCELLED) {
+ return false;
+ }
+
+ if (aTypes && !!aTypes.length && !aTypes.includes(aInstall.type)) {
+ return false;
+ }
+
+ return true;
+ });
+ return installs;
+ },
+
+ /**
+ * Called when a new add-on has been enabled when only one add-on of that type
+ * can be enabled.
+ *
+ * @param aId
+ * The ID of the newly enabled add-on
+ * @param aType
+ * The type of the newly enabled add-on
+ * @param aPendingRestart
+ * true if the newly enabled add-on will only become enabled after a
+ * restart
+ */
+ addonChanged: function MP_addonChanged(aId, aType, aPendingRestart) {
+ // Not implemented
+ },
+
+ /**
+ * Update the appDisabled property for all add-ons.
+ */
+ updateAddonAppDisabledStates: function MP_updateAddonAppDisabledStates() {
+ // Not needed
+ },
+
+ /**
+ * Called to get an AddonInstall to download and install an add-on from a URL.
+ *
+ * @param {string} aUrl
+ * The URL to be installed
+ * @param {object} aOptions
+ * Options for the install
+ */
+ getInstallForURL: function MP_getInstallForURL(aUrl, aOptions) {
+ // Not yet implemented
+ },
+
+ /**
+ * Called to get an AddonInstall to install an add-on from a local file.
+ *
+ * @param aFile
+ * The file to be installed
+ */
+ getInstallForFile: function MP_getInstallForFile(aFile) {
+ // Not yet implemented
+ },
+
+ /**
+ * Called to test whether installing add-ons is enabled.
+ *
+ * @return true if installing is enabled
+ */
+ isInstallEnabled: function MP_isInstallEnabled() {
+ return false;
+ },
+
+ /**
+ * Called to test whether this provider supports installing a particular
+ * mimetype.
+ *
+ * @param aMimetype
+ * The mimetype to check for
+ * @return true if the mimetype is supported
+ */
+ supportsMimetype: function MP_supportsMimetype(aMimetype) {
+ return false;
+ },
+
+ /**
+ * Called to test whether installing add-ons from a URI is allowed.
+ *
+ * @param aUri
+ * The URI being installed from
+ * @return true if installing is allowed
+ */
+ isInstallAllowed: function MP_isInstallAllowed(aUri) {
+ return false;
+ },
+};
+
+/** *** Mock Addon object for the Mock Provider *****/
+
+function MockAddon(aId, aName, aType, aOperationsRequiringRestart) {
+ // Only set required attributes.
+ this.id = aId || "";
+ this.name = aName || "";
+ this.type = aType || "extension";
+ this.version = "";
+ this.isCompatible = true;
+ this.providesUpdatesSecurely = true;
+ this.blocklistState = 0;
+ this._appDisabled = false;
+ this._userDisabled = false;
+ this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ this.scope = AddonManager.SCOPE_PROFILE;
+ this.isActive = true;
+ this.creator = "";
+ this.pendingOperations = 0;
+ this._permissions =
+ AddonManager.PERM_CAN_UNINSTALL |
+ AddonManager.PERM_CAN_ENABLE |
+ AddonManager.PERM_CAN_DISABLE |
+ AddonManager.PERM_CAN_UPGRADE |
+ AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS;
+ this.operationsRequiringRestart =
+ aOperationsRequiringRestart != undefined
+ ? aOperationsRequiringRestart
+ : AddonManager.OP_NEEDS_RESTART_INSTALL |
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL |
+ AddonManager.OP_NEEDS_RESTART_ENABLE |
+ AddonManager.OP_NEEDS_RESTART_DISABLE;
+}
+
+MockAddon.prototype = {
+ get isCorrectlySigned() {
+ if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
+ return true;
+ }
+ return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
+ },
+
+ get shouldBeActive() {
+ return (
+ !this.appDisabled &&
+ !this._userDisabled &&
+ !(this.pendingOperations & AddonManager.PENDING_UNINSTALL)
+ );
+ },
+
+ get appDisabled() {
+ return this._appDisabled;
+ },
+
+ set appDisabled(val) {
+ if (val == this._appDisabled) {
+ return val;
+ }
+
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [
+ "appDisabled",
+ ]);
+
+ var currentActive = this.shouldBeActive;
+ this._appDisabled = val;
+ var newActive = this.shouldBeActive;
+ this._updateActiveState(currentActive, newActive);
+
+ return val;
+ },
+
+ get userDisabled() {
+ return this._userDisabled;
+ },
+
+ set userDisabled(val) {
+ throw new Error("No. Bad.");
+ },
+
+ setUserDisabled(val) {
+ if (val == this._userDisabled) {
+ return;
+ }
+
+ var currentActive = this.shouldBeActive;
+ this._userDisabled = val;
+ var newActive = this.shouldBeActive;
+ this._updateActiveState(currentActive, newActive);
+ },
+
+ async enable() {
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ this.setUserDisabled(false);
+ },
+ async disable() {
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ this.setUserDisabled(true);
+ },
+
+ get permissions() {
+ let permissions = this._permissions;
+ if (this.appDisabled || !this._userDisabled) {
+ permissions &= ~AddonManager.PERM_CAN_ENABLE;
+ }
+ if (this.appDisabled || this._userDisabled) {
+ permissions &= ~AddonManager.PERM_CAN_DISABLE;
+ }
+ return permissions;
+ },
+
+ set permissions(val) {
+ return (this._permissions = val);
+ },
+
+ get applyBackgroundUpdates() {
+ return this._applyBackgroundUpdates;
+ },
+
+ set applyBackgroundUpdates(val) {
+ if (
+ val != AddonManager.AUTOUPDATE_DEFAULT &&
+ val != AddonManager.AUTOUPDATE_DISABLE &&
+ val != AddonManager.AUTOUPDATE_ENABLE
+ ) {
+ ok(false, "addon.applyBackgroundUpdates set to an invalid value: " + val);
+ }
+ this._applyBackgroundUpdates = val;
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, [
+ "applyBackgroundUpdates",
+ ]);
+ },
+
+ isCompatibleWith(aAppVersion, aPlatformVersion) {
+ return true;
+ },
+
+ findUpdates(aListener, aReason, aAppVersion, aPlatformVersion) {
+ // Tests can implement this if they need to
+ },
+
+ async getBlocklistURL() {
+ return this.blocklistURL;
+ },
+
+ uninstall(aAlwaysAllowUndo = false) {
+ if (
+ this.operationsRequiringRestart &
+ AddonManager.OP_NEED_RESTART_UNINSTALL &&
+ this.pendingOperations & AddonManager.PENDING_UNINSTALL
+ ) {
+ throw Components.Exception("Add-on is already pending uninstall");
+ }
+
+ var needsRestart =
+ aAlwaysAllowUndo ||
+ !!(
+ this.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL
+ );
+ this.pendingOperations |= AddonManager.PENDING_UNINSTALL;
+ AddonManagerPrivate.callAddonListeners(
+ "onUninstalling",
+ this,
+ needsRestart
+ );
+ if (!needsRestart) {
+ this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+ this._provider.removeAddon(this);
+ } else if (
+ !(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE)
+ ) {
+ this.isActive = false;
+ }
+ },
+
+ cancelUninstall() {
+ if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL)) {
+ throw Components.Exception("Add-on is not pending uninstall");
+ }
+
+ this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+ this.isActive = this.shouldBeActive;
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
+ },
+
+ markAsSeen() {
+ this.seen = true;
+ },
+
+ _updateActiveState(currentActive, newActive) {
+ if (currentActive == newActive) {
+ return;
+ }
+
+ if (newActive == this.isActive) {
+ this.pendingOperations -= newActive
+ ? AddonManager.PENDING_DISABLE
+ : AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
+ } else if (newActive) {
+ let needsRestart = !!(
+ this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE
+ );
+ this.pendingOperations |= AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onEnabling", this, needsRestart);
+ if (!needsRestart) {
+ this.isActive = newActive;
+ this.pendingOperations -= AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onEnabled", this);
+ }
+ } else {
+ let needsRestart = !!(
+ this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE
+ );
+ this.pendingOperations |= AddonManager.PENDING_DISABLE;
+ AddonManagerPrivate.callAddonListeners("onDisabling", this, needsRestart);
+ if (!needsRestart) {
+ this.isActive = newActive;
+ this.pendingOperations -= AddonManager.PENDING_DISABLE;
+ AddonManagerPrivate.callAddonListeners("onDisabled", this);
+ }
+ }
+ },
+};
+
+/** *** Mock AddonInstall object for the Mock Provider *****/
+
+function MockInstall(aName, aType, aAddonToInstall) {
+ this.name = aName || "";
+ // Don't expose type until download completed
+ this._type = aType || "extension";
+ this.type = null;
+ this.version = "1.0";
+ this.iconURL = "";
+ this.infoURL = "";
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.error = 0;
+ this.sourceURI = null;
+ this.file = null;
+ this.progress = 0;
+ this.maxProgress = -1;
+ this.certificate = null;
+ this.certName = "";
+ this.existingAddon = null;
+ this.addon = null;
+ this._addonToInstall = aAddonToInstall;
+ this.listeners = [];
+
+ // Another type of install listener for tests that want to check the results
+ // of code run from standard install listeners
+ this.testListeners = [];
+}
+
+MockInstall.prototype = {
+ install() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.state = AddonManager.STATE_DOWNLOADING;
+ if (!this.callListeners("onDownloadStarted")) {
+ this.state = AddonManager.STATE_CANCELLED;
+ this.callListeners("onDownloadCancelled");
+ return;
+ }
+
+ this.type = this._type;
+
+ // Adding addon to MockProvider to be implemented when needed
+ if (this._addonToInstall) {
+ this.addon = this._addonToInstall;
+ } else {
+ this.addon = new MockAddon("", this.name, this.type);
+ this.addon.version = this.version;
+ this.addon.pendingOperations = AddonManager.PENDING_INSTALL;
+ }
+ this.addon.install = this;
+ if (this.existingAddon) {
+ if (!this.addon.id) {
+ this.addon.id = this.existingAddon.id;
+ }
+ this.existingAddon.pendingUpgrade = this.addon;
+ this.existingAddon.pendingOperations |= AddonManager.PENDING_UPGRADE;
+ }
+
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.callListeners("onDownloadEnded");
+ // fall through
+ case AddonManager.STATE_DOWNLOADED:
+ this.state = AddonManager.STATE_INSTALLING;
+ if (!this.callListeners("onInstallStarted")) {
+ this.state = AddonManager.STATE_CANCELLED;
+ this.callListeners("onInstallCancelled");
+ return;
+ }
+
+ let needsRestart =
+ this.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL;
+ AddonManagerPrivate.callAddonListeners(
+ "onInstalling",
+ this.addon,
+ needsRestart
+ );
+ if (!needsRestart) {
+ AddonManagerPrivate.callAddonListeners("onInstalled", this.addon);
+ }
+
+ this.state = AddonManager.STATE_INSTALLED;
+ this.callListeners("onInstallEnded");
+ break;
+ case AddonManager.STATE_DOWNLOADING:
+ case AddonManager.STATE_CHECKING_UPDATE:
+ case AddonManager.STATE_INSTALLING:
+ // Installation is already running
+ return;
+ default:
+ ok(false, "Cannot start installing when state = " + this.state);
+ }
+ },
+
+ cancel() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.state = AddonManager.STATE_CANCELLED;
+ break;
+ case AddonManager.STATE_INSTALLED:
+ this.state = AddonManager.STATE_CANCELLED;
+ this._provider.removeInstall(this);
+ this.callListeners("onInstallCancelled");
+ break;
+ default:
+ // Handling cancelling when downloading to be implemented when needed
+ ok(false, "Cannot cancel when state = " + this.state);
+ }
+ },
+
+ addListener(aListener) {
+ if (!this.listeners.some(i => i == aListener)) {
+ this.listeners.push(aListener);
+ }
+ },
+
+ removeListener(aListener) {
+ this.listeners = this.listeners.filter(i => i != aListener);
+ },
+
+ addTestListener(aListener) {
+ if (!this.testListeners.some(i => i == aListener)) {
+ this.testListeners.push(aListener);
+ }
+ },
+
+ removeTestListener(aListener) {
+ this.testListeners = this.testListeners.filter(i => i != aListener);
+ },
+
+ callListeners(aMethod) {
+ var result = AddonManagerPrivate.callInstallListeners(
+ aMethod,
+ this.listeners,
+ this,
+ this.addon
+ );
+
+ // Call test listeners after standard listeners to remove race condition
+ // between standard and test listeners
+ for (let listener of this.testListeners) {
+ try {
+ if (aMethod in listener) {
+ if (listener[aMethod](this, this.addon) === false) {
+ result = false;
+ }
+ }
+ } catch (e) {
+ ok(false, "Test listener threw exception: " + e);
+ }
+ }
+
+ return result;
+ },
+};
+
+function waitForCondition(condition, nextTest, errorMsg) {
+ let tries = 0;
+ let interval = setInterval(function() {
+ if (tries >= 30) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ let moveOn = function() {
+ clearInterval(interval);
+ nextTest();
+ };
+}
+
+function getTestPluginTag() {
+ let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ let tags = ph.getPluginTags();
+
+ // Find the test plugin
+ for (let i = 0; i < tags.length; i++) {
+ if (tags[i].name == "Test Plug-in") {
+ return tags[i];
+ }
+ }
+ ok(false, "Unable to find plugin");
+ return null;
+}
+
+// Wait for and then acknowledge (by pressing the primary button) the
+// given notification.
+function promiseNotification(id = "addon-webext-permissions") {
+ if (
+ !Services.prefs.getBoolPref("extensions.webextPermissionPrompts", false)
+ ) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = PopupNotifications.getNotification(id);
+ if (notification) {
+ PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+ PopupNotifications.panel.firstElementChild.button.click();
+ resolve();
+ }
+ }
+ PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ * The name of the notification to wait for.
+ *
+ * @returns {Promise}
+ * Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name = "addon-webext-permissions") {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = PopupNotifications.getNotification(name);
+ if (!notification) {
+ return;
+ }
+
+ ok(notification, `${name} notification shown`);
+ ok(PopupNotifications.isPanelOpen, "notification panel open");
+
+ PopupNotifications.panel.removeEventListener("popupshown", popupshown);
+ resolve(PopupNotifications.panel.firstChild);
+ }
+ PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function waitAppMenuNotificationShown(
+ id,
+ addonId,
+ accept = false,
+ win = window
+) {
+ const { AppMenuNotifications } = ChromeUtils.import(
+ "resource://gre/modules/AppMenuNotifications.jsm"
+ );
+ return new Promise(resolve => {
+ let { document, PanelUI } = win;
+
+ async function popupshown() {
+ let notification = AppMenuNotifications.activeNotification;
+ if (!notification) {
+ return;
+ }
+
+ is(notification.id, id, `${id} notification shown`);
+ ok(PanelUI.isNotificationPanelOpen, "notification panel open");
+
+ PanelUI.notificationPanel.removeEventListener("popupshown", popupshown);
+
+ if (id == "addon-installed" && addonId) {
+ let addon = await AddonManager.getAddonByID(addonId);
+ if (!addon) {
+ ok(false, `Addon with id "${addonId}" not found`);
+ }
+ let hidden = !(
+ addon.permissions &
+ AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
+ );
+ let checkbox = document.getElementById("addon-incognito-checkbox");
+ is(checkbox.hidden, hidden, "checkbox visibility is correct");
+ }
+ if (accept) {
+ let popupnotificationID = PanelUI._getPopupId(notification);
+ let popupnotification = document.getElementById(popupnotificationID);
+ popupnotification.button.click();
+ }
+
+ resolve();
+ }
+ // If it's already open just run the test.
+ let notification = AppMenuNotifications.activeNotification;
+ if (notification && PanelUI.isNotificationPanelOpen) {
+ popupshown();
+ return;
+ }
+ PanelUI.notificationPanel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function acceptAppMenuNotificationWhenShown(id, addonId) {
+ return waitAppMenuNotificationShown(id, addonId, true);
+}
+
+const ABOUT_ADDONS_METHODS = new Set(["action", "view", "link"]);
+function assertAboutAddonsTelemetryEvents(events, filters = {}) {
+ TelemetryTestUtils.assertEvents(events, {
+ category: "addonsManager",
+ method: actual =>
+ filters.methods
+ ? filters.methods.includes(actual)
+ : ABOUT_ADDONS_METHODS.has(actual),
+ object: "aboutAddons",
+ });
+}
+
+/* HTML view helpers */
+async function loadInitialView(type, opts) {
+ if (type) {
+ // Force the first page load to be the view we want.
+ let viewId;
+ if (type.startsWith("addons://")) {
+ viewId = type;
+ } else {
+ viewId =
+ type == "discover" ? "addons://discover/" : `addons://list/${type}`;
+ }
+ Services.prefs.setCharPref(PREF_UI_LASTCATEGORY, viewId);
+ }
+
+ let loadCallback;
+ let loadCallbackDone = Promise.resolve();
+
+ if (opts && opts.loadCallback) {
+ // Make sure the HTML browser is loaded and pass its window to the callback
+ // function instead of the XUL window.
+ loadCallback = managerWindow => {
+ loadCallbackDone = managerWindow
+ .promiseHtmlBrowserLoaded()
+ .then(async browser => {
+ let win = browser.contentWindow;
+ win.managerWindow = managerWindow;
+ // Wait for the test code to finish running before proceeding.
+ await opts.loadCallback(win);
+ });
+ };
+ }
+ let managerWindow = await open_manager(null, null, loadCallback);
+
+ let browser = managerWindow.document.getElementById("html-view-browser");
+ let win = browser.contentWindow;
+ if (!opts || !opts.withAnimations) {
+ win.document.body.setAttribute("skip-animations", "");
+ }
+ win.managerWindow = managerWindow;
+
+ // Let any load callback code to run before the rest of the test continues.
+ await loadCallbackDone;
+
+ return win;
+}
+
+function waitForViewLoad(win) {
+ return wait_for_view_load(win.managerWindow, undefined, true);
+}
+
+function closeView(win) {
+ return close_manager(win.managerWindow);
+}
+
+function switchView(win, type) {
+ return new CategoryUtilities(win.managerWindow).openType(type);
+}
+
+function isCategoryVisible(win, type) {
+ return new CategoryUtilities(win.managerWindow).isTypeVisible(type);
+}
+
+function mockPromptService() {
+ let { prompt } = Services;
+ let promptService = {
+ // The prompt returns 1 for cancelled and 0 for accepted.
+ _response: 1,
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx: () => promptService._response,
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+ return promptService;
+}
+
+function assertHasPendingUninstalls(addonList, expectedPendingUninstallsCount) {
+ const pendingUninstalls = addonList.querySelector(
+ "message-bar-stack.pending-uninstall"
+ );
+ ok(pendingUninstalls, "Got a pending-uninstall message-bar-stack");
+ is(
+ pendingUninstalls.childElementCount,
+ expectedPendingUninstallsCount,
+ "Got a message bar in the pending-uninstall message-bar-stack"
+ );
+}
+
+function assertHasPendingUninstallAddon(addonList, addon) {
+ const pendingUninstalls = addonList.querySelector(
+ "message-bar-stack.pending-uninstall"
+ );
+ const addonPendingUninstall = addonList.getPendingUninstallBar(addon);
+ ok(
+ addonPendingUninstall,
+ "Got expected message-bar for the pending uninstall test extension"
+ );
+ is(
+ addonPendingUninstall.parentNode,
+ pendingUninstalls,
+ "pending uninstall bar should be part of the message-bar-stack"
+ );
+ is(
+ addonPendingUninstall.getAttribute("addon-id"),
+ addon.id,
+ "Got expected addon-id attribute on the pending uninstall message-bar"
+ );
+}
+
+async function testUndoPendingUninstall(addonList, addon) {
+ const addonPendingUninstall = addonList.getPendingUninstallBar(addon);
+ const undoButton = addonPendingUninstall.querySelector("button[action=undo]");
+ ok(undoButton, "Got undo action button in the pending uninstall message-bar");
+
+ info(
+ "Clicking the pending uninstall undo button and wait for addon card rendered"
+ );
+ const updated = BrowserTestUtils.waitForEvent(addonList, "add");
+ undoButton.click();
+ await updated;
+
+ ok(
+ addon && !(addon.pendingOperations & AddonManager.PENDING_UNINSTALL),
+ "The addon pending uninstall cancelled"
+ );
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
+
+function cleanupPendingNotifications() {
+ const { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+ );
+ info("Cleanup any pending notification before exiting the test");
+ const keys = ChromeUtils.nondeterministicGetWeakSetKeys(
+ ExtensionsUI.pendingNotifications
+ );
+ if (keys) {
+ keys.forEach(key => ExtensionsUI.pendingNotifications.delete(key));
+ }
+}
+
+function promisePermissionPrompt(addonId) {
+ return BrowserUtils.promiseObserved(
+ "webextension-permission-prompt",
+ subject => {
+ const { info } = subject.wrappedJSObject || {};
+ return !addonId || (info.addon && info.addon.id === addonId);
+ }
+ ).then(({ subject }) => {
+ return subject.wrappedJSObject.info;
+ });
+}
+
+async function handlePermissionPrompt({
+ addonId,
+ reject = false,
+ assertIcon = true,
+} = {}) {
+ const info = await promisePermissionPrompt(addonId);
+ // Assert that info.addon and info.icon are defined as expected.
+ is(
+ info.addon && info.addon.id,
+ addonId,
+ "Got the AddonWrapper in the permission prompt info"
+ );
+
+ if (assertIcon) {
+ ok(info.icon != null, "Got an addon icon in the permission prompt info");
+ }
+
+ if (reject) {
+ info.reject();
+ } else {
+ info.resolve();
+ }
+}
+
+async function switchToDetailView({ id, win }) {
+ let card = getAddonCard(win, id);
+ ok(card, `Addon card found for ${id}`);
+ ok(!card.querySelector("addon-details"), "The card doesn't have details");
+ let loaded = waitForViewLoad(win);
+ EventUtils.synthesizeMouseAtCenter(card, { clickCount: 1 }, win);
+ await loaded;
+ card = getAddonCard(win, id);
+ ok(card.querySelector("addon-details"), "The card does have details");
+ return card;
+}
diff --git a/toolkit/mozapps/extensions/test/browser/head_abuse_report.js b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js
new file mode 100644
index 0000000000..9ff81f39cd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/head_abuse_report.js
@@ -0,0 +1,541 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* eslint max-len: ["error", 80] */
+
+/* exported installTestExtension, addCommonAbuseReportTestTasks,
+ * createPromptConfirmEx, DEFAULT_BUILTIN_THEME_ID,
+ * gManagerWindow, handleSubmitRequest, makeWidgetId,
+ * waitForNewWindow, waitClosedWindow, AbuseReporter,
+ * AbuseReporterTestUtils, AddonTestUtils
+ */
+
+/* global MockProvider, loadInitialView, closeView */
+
+const { AbuseReporter } = ChromeUtils.import(
+ "resource://gre/modules/AbuseReporter.jsm"
+);
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+const { ExtensionCommon } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionCommon.jsm"
+);
+
+const { makeWidgetId } = ExtensionCommon;
+
+const ADDON_ID = "test-extension-to-report@mochi.test";
+const REPORT_ENTRY_POINT = "menu";
+const BASE_TEST_MANIFEST = {
+ name: "Fake extension to report",
+ author: "Fake author",
+ homepage_url: "https://fake.extension.url/",
+ applications: { gecko: { id: ADDON_ID } },
+ icons: {
+ 32: "test-icon.png",
+ },
+};
+const DEFAULT_BUILTIN_THEME_ID = "default-theme@mozilla.org";
+const EXT_DICTIONARY_ADDON_ID = "fake-dictionary@mochi.test";
+const EXT_LANGPACK_ADDON_ID = "fake-langpack@mochi.test";
+const EXT_WITH_PRIVILEGED_URL_ID = "ext-with-privileged-url@mochi.test";
+const EXT_SYSTEM_ADDON_ID = "test-system-addon@mochi.test";
+const EXT_UNSUPPORTED_TYPE_ADDON_ID = "report-unsupported-type@mochi.test";
+const THEME_NO_UNINSTALL_ID = "theme-without-perm-can-uninstall@mochi.test";
+
+let gHtmlAboutAddonsWindow;
+let gManagerWindow;
+
+AddonTestUtils.initMochitest(this);
+
+async function openAboutAddons(type = "extension") {
+ const win = await loadInitialView(type);
+ gHtmlAboutAddonsWindow = win;
+ gManagerWindow = win.managerWindow;
+}
+
+async function closeAboutAddons() {
+ if (gHtmlAboutAddonsWindow) {
+ await closeView(gHtmlAboutAddonsWindow);
+ gHtmlAboutAddonsWindow = null;
+ gManagerWindow = null;
+ }
+}
+
+function waitForNewWindow() {
+ return new Promise(resolve => {
+ let listener = win => {
+ Services.obs.removeObserver(listener, "toplevel-window-ready");
+ resolve(win);
+ };
+
+ Services.obs.addObserver(listener, "toplevel-window-ready");
+ });
+}
+
+function waitClosedWindow(win) {
+ return new Promise((resolve, reject) => {
+ function onWindowClosed() {
+ if (win && !win.closed) {
+ // If a specific window reference has been passed, then check
+ // that the window is closed before resolving the promise.
+ return;
+ }
+ Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+ resolve();
+ }
+ Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
+ });
+}
+
+async function installTestExtension(
+ id = ADDON_ID,
+ type = "extension",
+ manifest = {}
+) {
+ const additionalProps =
+ type === "theme"
+ ? {
+ theme: {
+ colors: {
+ frame: "#a14040",
+ tab_background_text: "#fac96e",
+ },
+ },
+ }
+ : {};
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ ...BASE_TEST_MANIFEST,
+ ...additionalProps,
+ ...manifest,
+ applications: { gecko: { id } },
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ return extension;
+}
+
+function handleSubmitRequest({ request, response }) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.write("{}");
+}
+
+const AbuseReportTestUtils = {
+ _mockProvider: null,
+ _mockServer: null,
+ _abuseRequestHandlers: [],
+
+ // Mock addon details API endpoint.
+ amoAddonDetailsMap: new Map(),
+
+ // Setup the test environment by setting the expected prefs and
+ // initializing MockProvider and the mock AMO server.
+ async setup() {
+ // Enable html about:addons and the abuse reporting and
+ // set the api endpoints url to the mock service.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.abuseReport.enabled", true],
+ ["extensions.abuseReport.url", "http://test.addons.org/api/report/"],
+ [
+ "extensions.abuseReport.amoDetailsURL",
+ "http://test.addons.org/api/addons/addon/",
+ ],
+ ],
+ });
+
+ this._setupMockProvider();
+ this._setupMockServer();
+ },
+
+ // Returns the currently open abuse report dialog window (if any).
+ getReportDialog() {
+ return Services.ww.getWindowByName("addons-abuse-report-dialog", null);
+ },
+
+ // Returns the parameters related to the report dialog (if any).
+ getReportDialogParams() {
+ const win = this.getReportDialog();
+ return win && win.arguments[0] && win.arguments[0].wrappedJSObject;
+ },
+
+ // Returns a reference to the addon-abuse-report element from the currently
+ // open abuse report.
+ getReportPanel() {
+ const win = this.getReportDialog();
+ ok(win, "Got an abuse report dialog open");
+ return win && win.document.querySelector("addon-abuse-report");
+ },
+
+ // Returns the list of abuse report reasons.
+ getReasons(abuseReportEl) {
+ return Object.keys(abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS);
+ },
+
+ // Returns the info related to a given abuse report reason.
+ getReasonInfo(abuseReportEl, reason) {
+ return abuseReportEl.ownerGlobal.ABUSE_REPORT_REASONS[reason];
+ },
+
+ async promiseReportOpened({ addonId, reportEntryPoint, managerWindow }) {
+ let abuseReportEl;
+
+ if (!this.getReportDialog()) {
+ info("Wait for the report dialog window");
+ const dialog = await waitForNewWindow();
+ is(dialog, this.getReportDialog(), "Report dialog opened");
+ }
+
+ info("Wait for the abuse report panel render");
+ abuseReportEl = await AbuseReportTestUtils.promiseReportDialogRendered();
+
+ ok(abuseReportEl, "Got an abuse report panel");
+ is(
+ abuseReportEl.addon && abuseReportEl.addon.id,
+ addonId,
+ "Abuse Report panel rendered for the expected addonId"
+ );
+ is(
+ abuseReportEl._report && abuseReportEl._report.reportEntryPoint,
+ reportEntryPoint,
+ "Abuse Report panel rendered for the expected reportEntryPoint"
+ );
+
+ return abuseReportEl;
+ },
+
+ // Return a promise resolved when the currently open report panel
+ // is closed.
+ // Also asserts that a specific report panel element has been closed,
+ // if one has been provided through the optional panel parameter.
+ async promiseReportClosed(panel) {
+ const win = panel ? panel.ownerGlobal : this.getReportDialog();
+ if (!win || win.closed) {
+ throw Error("Expected report dialog not found or already closed");
+ }
+
+ await waitClosedWindow(win);
+ // Assert that the panel has been closed (if the caller has passed it).
+ if (panel) {
+ ok(!panel.ownerGlobal, "abuse report dialog closed");
+ }
+ },
+
+ // Returns a promise resolved when the report panel has been rendered
+ // (rejects is there is no dialog currently open).
+ async promiseReportDialogRendered() {
+ const params = this.getReportDialogParams();
+ if (!params) {
+ throw new Error("abuse report dialog not found");
+ }
+ return params.promiseReportPanel;
+ },
+
+ // Given a `requestHandler` function, an HTTP server handler function
+ // to use to handle a report submit request received by the mock AMO server),
+ // returns a promise resolved when the mock AMO server has received and
+ // handled the report submit request.
+ async promiseReportSubmitHandled(requestHandler) {
+ if (typeof requestHandler !== "function") {
+ throw new Error("requestHandler should be a function");
+ }
+ return new Promise((resolve, reject) => {
+ this._abuseRequestHandlers.unshift({ resolve, reject, requestHandler });
+ });
+ },
+
+ // Return a promise resolved to the abuse report panel element,
+ // once its rendering is completed.
+ // If abuseReportEl is undefined, it looks for the currently opened
+ // report panel.
+ async promiseReportRendered(abuseReportEl) {
+ let el = abuseReportEl;
+
+ if (!el) {
+ const win = this.getReportDialog();
+ if (!win) {
+ await waitForNewWindow();
+ }
+
+ el = await this.promiseReportDialogRendered();
+ ok(el, "Got an abuse report panel");
+ }
+
+ return el._radioCheckedReason
+ ? el
+ : BrowserTestUtils.waitForEvent(
+ el,
+ "abuse-report:updated",
+ "Wait the abuse report panel to be rendered"
+ ).then(() => el);
+ },
+
+ // A promise resolved when the given abuse report panel element
+ // has been rendered. If a panel name ("reasons" or "submit") is
+ // passed as a second parameter, it also asserts that the panel is
+ // updated to the expected view mode.
+ async promiseReportUpdated(abuseReportEl, panel) {
+ const evt = await BrowserTestUtils.waitForEvent(
+ abuseReportEl,
+ "abuse-report:updated",
+ "Wait abuse report panel update"
+ );
+
+ if (panel) {
+ is(evt.detail.panel, panel, `Got a "${panel}" update event`);
+
+ const el = abuseReportEl;
+ switch (evt.detail.panel) {
+ case "reasons":
+ ok(!el._reasonsPanel.hidden, "Reasons panel should be visible");
+ ok(el._submitPanel.hidden, "Submit panel should be hidden");
+ break;
+ case "submit":
+ ok(el._reasonsPanel.hidden, "Reasons panel should be hidden");
+ ok(!el._submitPanel.hidden, "Submit panel should be visible");
+ break;
+ }
+ }
+ },
+
+ // Returns a promise resolved once the expected number of abuse report
+ // message bars have been created.
+ promiseMessageBars(expectedMessageBarCount) {
+ return new Promise(resolve => {
+ const details = [];
+ function listener(evt) {
+ details.push(evt.detail);
+ if (details.length >= expectedMessageBarCount) {
+ cleanup();
+ resolve(details);
+ }
+ }
+ function cleanup() {
+ if (gHtmlAboutAddonsWindow) {
+ gHtmlAboutAddonsWindow.document.removeEventListener(
+ "abuse-report:new-message-bar",
+ listener
+ );
+ }
+ }
+ gHtmlAboutAddonsWindow.document.addEventListener(
+ "abuse-report:new-message-bar",
+ listener
+ );
+ });
+ },
+
+ // Assert that the report action is hidden on the addon card
+ // for the given about:addons windows and extension id.
+ async assertReportActionHidden(gManagerWindow, extId) {
+ await gManagerWindow.promiseHtmlBrowserLoaded();
+ const { contentDocument: doc } = gManagerWindow.getHtmlBrowser();
+
+ let addonCard = doc.querySelector(
+ `addon-list addon-card[addon-id="${extId}"]`
+ );
+ ok(addonCard, `Got the addon-card for the ${extId} test extension`);
+
+ let reportButton = addonCard.querySelector("[action=report]");
+ ok(reportButton, `Got the report action for ${extId}`);
+ ok(reportButton.hidden, `${extId} report action should be hidden`);
+ },
+
+ // Assert that the report panel is hidden (or closed if the report
+ // panel is opened in its own dialog window).
+ async assertReportPanelHidden() {
+ const win = this.getReportDialog();
+ ok(!win, "Abuse Report dialog should be initially hidden");
+ },
+
+ createMockAddons(mockProviderAddons) {
+ this._mockProvider.createAddons(mockProviderAddons);
+ },
+
+ async clickPanelButton(buttonEl, { label = undefined } = {}) {
+ info(`Clicking the '${buttonEl.textContent.trim() || label}' button`);
+ // NOTE: ideally this should synthesize the mouse event,
+ // we call the click method to prevent intermittent timeouts
+ // due to the mouse event not received by the target element.
+ buttonEl.click();
+ },
+
+ triggerNewReport(addonId, reportEntryPoint) {
+ gHtmlAboutAddonsWindow.openAbuseReport({ addonId, reportEntryPoint });
+ },
+
+ triggerSubmit(reason, message) {
+ const reportEl = this.getReportDialog().document.querySelector(
+ "addon-abuse-report"
+ );
+ reportEl._form.elements.message.value = message;
+ reportEl._form.elements.reason.value = reason;
+ reportEl.submit();
+ },
+
+ async openReport(addonId, reportEntryPoint = REPORT_ENTRY_POINT) {
+ // Close the current about:addons window if it has been leaved open from
+ // a previous test case failure.
+ if (gHtmlAboutAddonsWindow) {
+ await closeAboutAddons();
+ }
+
+ await openAboutAddons();
+
+ let promiseReportPanel = waitForNewWindow().then(() =>
+ this.promiseReportDialogRendered()
+ );
+
+ this.triggerNewReport(addonId, reportEntryPoint);
+
+ const panelEl = await promiseReportPanel;
+ await this.promiseReportRendered(panelEl);
+ is(panelEl.addonId, addonId, `Got Abuse Report panel for ${addonId}`);
+
+ return panelEl;
+ },
+
+ async closeReportPanel(panelEl) {
+ const onceReportClosed = AbuseReportTestUtils.promiseReportClosed(panelEl);
+
+ info("Cancel report and wait the dialog to be closed");
+ panelEl.dispatchEvent(new CustomEvent("abuse-report:cancel"));
+
+ await onceReportClosed;
+ },
+
+ // Internal helper methods.
+
+ _setupMockProvider() {
+ this._mockProvider = new MockProvider();
+ this._mockProvider.createAddons([
+ {
+ id: THEME_NO_UNINSTALL_ID,
+ name: "This theme cannot be uninstalled",
+ version: "1.1",
+ creator: { name: "Theme creator", url: "http://example.com/creator" },
+ type: "theme",
+ permissions: 0,
+ },
+ {
+ id: EXT_WITH_PRIVILEGED_URL_ID,
+ name: "This extension has an unexpected privileged creator URL",
+ version: "1.1",
+ creator: { name: "creator", url: "about:config" },
+ type: "extension",
+ },
+ {
+ id: EXT_SYSTEM_ADDON_ID,
+ name: "This is a system addon",
+ version: "1.1",
+ creator: { name: "creator", url: "http://example.com/creator" },
+ type: "extension",
+ isSystem: true,
+ },
+ {
+ id: EXT_UNSUPPORTED_TYPE_ADDON_ID,
+ name: "This is a fake unsupported addon type",
+ version: "1.1",
+ type: "unsupported_addon_type",
+ },
+ {
+ id: EXT_LANGPACK_ADDON_ID,
+ name: "This is a fake langpack",
+ version: "1.1",
+ type: "locale",
+ },
+ {
+ id: EXT_DICTIONARY_ADDON_ID,
+ name: "This is a fake dictionary",
+ version: "1.1",
+ type: "dictionary",
+ },
+ ]);
+ },
+
+ _setupMockServer() {
+ if (this._mockServer) {
+ return;
+ }
+
+ // Init test report api server.
+ const server = AddonTestUtils.createHttpServer({
+ hosts: ["test.addons.org"],
+ });
+ this._mockServer = server;
+
+ server.registerPathHandler("/api/report/", (request, response) => {
+ const stream = request.bodyInputStream;
+ const buffer = NetUtil.readInputStream(stream, stream.available());
+ const data = new TextDecoder().decode(buffer);
+ const promisedHandler = this._abuseRequestHandlers.pop();
+ if (promisedHandler) {
+ const { requestHandler, resolve, reject } = promisedHandler;
+ try {
+ requestHandler({ data, request, response });
+ resolve();
+ } catch (err) {
+ ok(false, `Unexpected requestHandler error ${err} ${err.stack}\n`);
+ reject(err);
+ }
+ } else {
+ ok(false, `Unexpected request: ${request.path} ${data}`);
+ }
+ });
+
+ server.registerPrefixHandler("/api/addons/addon/", (request, response) => {
+ const addonId = request.path.split("/").pop();
+ if (!this.amoAddonDetailsMap.has(addonId)) {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ response.write(JSON.stringify({ detail: "Not found." }));
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "Success");
+ response.write(JSON.stringify(this.amoAddonDetailsMap.get(addonId)));
+ }
+ });
+ server.registerPathHandler(
+ "/assets/fake-icon-url.png",
+ (request, response) => {
+ response.setStatusLine(request.httpVersion, 200, "Success");
+ response.write("");
+ response.finish();
+ }
+ );
+ },
+};
+
+function createPromptConfirmEx({
+ remove = false,
+ report = false,
+ expectCheckboxHidden = false,
+} = {}) {
+ return (...args) => {
+ const checkboxState = args.pop();
+ const checkboxMessage = args.pop();
+ is(
+ checkboxState && checkboxState.value,
+ false,
+ "checkboxState should be initially false"
+ );
+ if (expectCheckboxHidden) {
+ ok(
+ !checkboxMessage,
+ "Should not have a checkboxMessage in promptService.confirmEx call"
+ );
+ } else {
+ ok(
+ checkboxMessage,
+ "Got a checkboxMessage in promptService.confirmEx call"
+ );
+ }
+
+ // Report checkbox selected.
+ checkboxState.value = report;
+
+ // Remove accepted.
+ return remove ? 0 : 1;
+ };
+}
diff --git a/toolkit/mozapps/extensions/test/browser/moz.build b/toolkit/mozapps/extensions/test/browser/moz.build
new file mode 100644
index 0000000000..09ada90ad1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser.ini",
+]
+
+addons = [
+ "browser_dragdrop1",
+ "browser_dragdrop2",
+ "browser_dragdrop_incompat",
+ "browser_installssl",
+ "browser_theme",
+ "options_signed",
+]
+
+output_dir = (
+ OBJDIR_FILES._tests.testing.mochitest.browser.toolkit.mozapps.extensions.test.browser.addons
+)
+
+for addon in addons:
+ for file_type in ["xpi", "zip"]:
+ indir = "addons/%s" % addon
+ path = "%s.%s" % (indir, file_type)
+
+ GeneratedFile(path, script="../create_xpi.py", inputs=[indir])
+
+ output_dir += ["!%s" % path]
diff --git a/toolkit/mozapps/extensions/test/browser/plugin_test.html b/toolkit/mozapps/extensions/test/browser/plugin_test.html
new file mode 100644
index 0000000000..0709eda066
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/plugin_test.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/browser/redirect.sjs b/toolkit/mozapps/extensions/test/browser/redirect.sjs
new file mode 100644
index 0000000000..8f9d1c08af
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/redirect.sjs
@@ -0,0 +1,5 @@
+function handleRequest(request, response) {
+ dump("*** Received redirect for " + request.queryString + "\n");
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", request.queryString, false);
+}
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
new file mode 100644
index 0000000000..383d2a0986
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
new file mode 100644
index 0000000000..141f09cc61
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml
new file mode 100644
index 0000000000..6e3ba328ec
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xhtml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
new file mode 100644
index 0000000000..1467699789
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
@@ -0,0 +1,7 @@
+
+
+
+
+