From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../BackgroundTask_defaultagent.sys.mjs | 456 + toolkit/mozapps/defaultagent/Cache.cpp | 594 ++ toolkit/mozapps/defaultagent/Cache.h | 189 + toolkit/mozapps/defaultagent/DefaultAgent.cpp | 491 ++ toolkit/mozapps/defaultagent/DefaultAgent.h | 28 + toolkit/mozapps/defaultagent/DefaultBrowser.cpp | 240 + toolkit/mozapps/defaultagent/DefaultBrowser.h | 39 + toolkit/mozapps/defaultagent/DefaultPDF.cpp | 151 + toolkit/mozapps/defaultagent/DefaultPDF.h | 34 + toolkit/mozapps/defaultagent/EventLog.cpp | 11 + toolkit/mozapps/defaultagent/EventLog.h | 24 + toolkit/mozapps/defaultagent/Notification.cpp | 709 ++ toolkit/mozapps/defaultagent/Notification.h | 60 + toolkit/mozapps/defaultagent/Policy.cpp | 162 + toolkit/mozapps/defaultagent/Policy.h | 17 + toolkit/mozapps/defaultagent/Registry.cpp | 330 + toolkit/mozapps/defaultagent/Registry.h | 100 + toolkit/mozapps/defaultagent/ScheduledTask.cpp | 328 + toolkit/mozapps/defaultagent/ScheduledTask.h | 23 + .../mozapps/defaultagent/ScheduledTaskRemove.cpp | 126 + toolkit/mozapps/defaultagent/ScheduledTaskRemove.h | 37 + toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp | 347 + toolkit/mozapps/defaultagent/SetDefaultBrowser.h | 66 + toolkit/mozapps/defaultagent/Telemetry.cpp | 585 ++ toolkit/mozapps/defaultagent/Telemetry.h | 24 + toolkit/mozapps/defaultagent/UtfConvert.cpp | 59 + toolkit/mozapps/defaultagent/UtfConvert.h | 24 + toolkit/mozapps/defaultagent/WindowsMutex.cpp | 103 + toolkit/mozapps/defaultagent/WindowsMutex.h | 45 + toolkit/mozapps/defaultagent/common.cpp | 85 + toolkit/mozapps/defaultagent/common.h | 29 + toolkit/mozapps/defaultagent/components.conf | 21 + toolkit/mozapps/defaultagent/defaultagent.ini | 9 + toolkit/mozapps/defaultagent/docs/index.rst | 49 + toolkit/mozapps/defaultagent/metrics.yaml | 208 + toolkit/mozapps/defaultagent/module.ver | 1 + toolkit/mozapps/defaultagent/moz.build | 113 + toolkit/mozapps/defaultagent/nsIDefaultAgent.idl | 167 + toolkit/mozapps/defaultagent/nsIWindowsMutex.idl | 62 + toolkit/mozapps/defaultagent/pings.yaml | 42 + toolkit/mozapps/defaultagent/proxy/Makefile.in | 16 + .../proxy/default-browser-agent.exe.manifest | 31 + toolkit/mozapps/defaultagent/proxy/main.cpp | 118 + toolkit/mozapps/defaultagent/proxy/moz.build | 68 + .../mozapps/defaultagent/tests/gtest/CacheTest.cpp | 301 + .../tests/gtest/SetDefaultBrowserTest.cpp | 55 + toolkit/mozapps/defaultagent/tests/gtest/moz.build | 33 + .../tests/xpcshell/test_windows_mutex.js | 144 + .../defaultagent/tests/xpcshell/xpcshell.toml | 4 + toolkit/mozapps/downloads/DownloadLastDir.sys.mjs | 254 + toolkit/mozapps/downloads/DownloadUtils.sys.mjs | 616 ++ toolkit/mozapps/downloads/HelperAppDlg.sys.mjs | 1349 +++ toolkit/mozapps/downloads/components.conf | 14 + .../downloads/content/unknownContentType.xhtml | 104 + toolkit/mozapps/downloads/jar.mn | 7 + toolkit/mozapps/downloads/moz.build | 22 + .../mozapps/downloads/tests/browser/browser.toml | 33 + .../tests/browser/browser_save_wrongextension.js | 98 + .../browser/browser_unknownContentType_blob.js | 118 + .../browser_unknownContentType_delayedbutton.js | 96 + .../browser_unknownContentType_dialog_layout.js | 103 + .../browser_unknownContentType_extension.js | 58 + .../browser/browser_unknownContentType_policy.js | 68 + .../mozapps/downloads/tests/browser/example.jnlp | 0 .../downloads/tests/browser/example.jnlp^headers^ | 1 + toolkit/mozapps/downloads/tests/browser/head.js | 17 + .../downloads/tests/browser/unknownContentType.EXE | 0 .../tests/browser/unknownContentType.EXE^headers^ | 1 + .../unknownContentType_dialog_layout_data.pif | 1 + ...nownContentType_dialog_layout_data.pif^headers^ | 1 + .../unknownContentType_dialog_layout_data.txt | 1 + ...nownContentType_dialog_layout_data.txt^headers^ | 2 + toolkit/mozapps/downloads/tests/moz.build | 8 + .../mozapps/downloads/tests/unit/head_downloads.js | 5 + .../downloads/tests/unit/test_DownloadUtils.js | 398 + .../downloads/tests/unit/test_lowMinutes.js | 56 + .../tests/unit/test_syncedDownloadUtils.js | 28 + .../tests/unit/test_unspecified_arguments.js | 33 + toolkit/mozapps/downloads/tests/unit/xpcshell.toml | 10 + toolkit/mozapps/extensions/.eslintrc.js | 36 + toolkit/mozapps/extensions/AbuseReporter.sys.mjs | 703 ++ toolkit/mozapps/extensions/AddonContentPolicy.cpp | 371 + toolkit/mozapps/extensions/AddonContentPolicy.h | 18 + toolkit/mozapps/extensions/AddonManager.sys.mjs | 5538 ++++++++++++ .../extensions/AddonManagerStartup-inlines.h | 229 + toolkit/mozapps/extensions/AddonManagerStartup.cpp | 884 ++ toolkit/mozapps/extensions/AddonManagerStartup.h | 59 + toolkit/mozapps/extensions/AddonManagerWebAPI.cpp | 168 + toolkit/mozapps/extensions/AddonManagerWebAPI.h | 32 + toolkit/mozapps/extensions/Blocklist.sys.mjs | 1490 ++++ .../extensions/LightweightThemeManager.sys.mjs | 32 + .../mozapps/extensions/amContentHandler.sys.mjs | 103 + .../mozapps/extensions/amIAddonManagerStartup.idl | 82 + toolkit/mozapps/extensions/amIWebInstallPrompt.idl | 32 + .../mozapps/extensions/amInstallTrigger.sys.mjs | 272 + toolkit/mozapps/extensions/amManager.sys.mjs | 359 + toolkit/mozapps/extensions/amWebAPI.sys.mjs | 289 + toolkit/mozapps/extensions/components.conf | 45 + .../extensions/content/OpenH264-license.txt | 59 + toolkit/mozapps/extensions/content/aboutaddons.css | 759 ++ .../mozapps/extensions/content/aboutaddons.html | 825 ++ toolkit/mozapps/extensions/content/aboutaddons.js | 4231 ++++++++++ .../extensions/content/aboutaddonsCommon.js | 275 + .../extensions/content/abuse-report-frame.html | 213 + .../extensions/content/abuse-report-panel.css | 181 + .../extensions/content/abuse-report-panel.js | 873 ++ .../mozapps/extensions/content/abuse-reports.js | 376 + .../content/drag-drop-addon-installer.js | 81 + toolkit/mozapps/extensions/content/shortcuts.css | 138 + toolkit/mozapps/extensions/content/shortcuts.js | 658 ++ .../mozapps/extensions/content/view-controller.js | 204 + toolkit/mozapps/extensions/default-theme/icon.svg | 7 + .../mozapps/extensions/default-theme/manifest.json | 95 + .../mozapps/extensions/default-theme/preview.svg | 46 + .../docs/AMRemoteSettings-JSONSchema.json | 83 + .../extensions/docs/AMRemoteSettings-UISchema.json | 10 + .../extensions/docs/AMRemoteSettings-overview.rst | 177 + .../mozapps/extensions/docs/AMRemoteSettings.rst | 5 + toolkit/mozapps/extensions/docs/AddonManager.rst | 4 + toolkit/mozapps/extensions/docs/SystemAddons.rst | 275 + toolkit/mozapps/extensions/docs/index.rst | 21 + toolkit/mozapps/extensions/extensions.manifest | 9 + toolkit/mozapps/extensions/gen_built_in_addons.py | 99 + .../extensions/internal/AddonRepository.sys.mjs | 1257 +++ .../extensions/internal/AddonSettings.sys.mjs | 138 + .../extensions/internal/AddonTestUtils.sys.mjs | 1876 +++++ .../extensions/internal/AddonUpdateChecker.sys.mjs | 643 ++ .../extensions/internal/GMPProvider.sys.mjs | 934 +++ .../internal/ProductAddonChecker.sys.mjs | 601 ++ .../internal/SitePermsAddonProvider.sys.mjs | 661 ++ .../extensions/internal/XPIDatabase.sys.mjs | 3832 +++++++++ .../mozapps/extensions/internal/XPIExports.sys.mjs | 36 + .../mozapps/extensions/internal/XPIInstall.sys.mjs | 4897 +++++++++++ .../extensions/internal/XPIProvider.sys.mjs | 3377 ++++++++ .../extensions/internal/crypto-utils.sys.mjs | 57 + toolkit/mozapps/extensions/internal/moz.build | 29 + .../internal/siteperms-addon-utils.sys.mjs | 72 + toolkit/mozapps/extensions/jar.mn | 25 + toolkit/mozapps/extensions/metrics.yaml | 527 ++ 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.toml | 193 + .../browser/browser_AMBrowserExtensionsImport.js | 220 + .../test/browser/browser_about_debugging_link.js | 129 + .../test/browser/browser_addon_list_reordering.js | 204 + .../test/browser/browser_amo_abuse_report.js | 85 + .../extensions/test/browser/browser_bug572561.js | 96 + .../browser/browser_checkAddonCompatibility.js | 36 + .../browser/browser_colorwaybuiltins_migration.js | 265 + .../extensions/test/browser/browser_dragdrop.js | 270 + .../browser/browser_file_xpi_no_process_switch.js | 122 + .../test/browser/browser_globalwarnings.js | 176 + .../extensions/test/browser/browser_gmpProvider.js | 406 + .../test/browser/browser_history_navigation.js | 623 ++ .../test/browser/browser_html_abuse_report.js | 1093 +++ .../browser/browser_html_abuse_report_dialog.js | 185 + .../browser/browser_html_detail_permissions.js | 827 ++ .../test/browser/browser_html_detail_view.js | 1675 ++++ .../test/browser/browser_html_discover_view.js | 668 ++ .../browser/browser_html_discover_view_clientid.js | 219 + .../browser/browser_html_discover_view_prefs.js | 83 + .../test/browser/browser_html_list_view.js | 1063 +++ .../browser_html_list_view_recommendations.js | 293 + .../test/browser/browser_html_message_bar.js | 185 + .../test/browser/browser_html_options_ui.js | 651 ++ .../test/browser/browser_html_options_ui_in_tab.js | 136 + .../test/browser/browser_html_pending_updates.js | 311 + .../test/browser/browser_html_recent_updates.js | 180 + .../test/browser/browser_html_recommendations.js | 165 + .../browser/browser_html_scroll_restoration.js | 229 + .../browser/browser_html_sitepermission_addons.js | 178 + .../test/browser/browser_html_updates.js | 750 ++ .../test/browser/browser_html_warning_messages.js | 290 + .../extensions/test/browser/browser_installssl.js | 378 + .../test/browser/browser_installtrigger_install.js | 362 + .../test/browser/browser_local_install.js | 245 + .../test/browser/browser_manage_shortcuts.js | 331 + .../browser/browser_manage_shortcuts_hidden.js | 198 + .../browser/browser_manage_shortcuts_remove.js | 180 + .../browser/browser_menu_button_accessibility.js | 93 + .../test/browser/browser_page_accessibility.js | 15 + .../browser/browser_page_options_install_addon.js | 128 + .../test/browser/browser_page_options_updates.js | 124 + .../test/browser/browser_permission_prompt.js | 178 + .../extensions/test/browser/browser_reinstall.js | 277 + .../browser/browser_shortcuts_duplicate_check.js | 262 + .../test/browser/browser_sidebar_categories.js | 166 + .../browser/browser_sidebar_hidden_categories.js | 214 + .../browser/browser_sidebar_restore_category.js | 76 + .../test/browser/browser_subframe_install.js | 234 + .../test/browser/browser_task_next_test.js | 17 + .../extensions/test/browser/browser_updateid.js | 87 + .../extensions/test/browser/browser_updatessl.js | 389 + .../extensions/test/browser/browser_updatessl.json | 17 + .../test/browser/browser_updatessl.json^headers^ | 1 + .../test/browser/browser_verify_l10n_strings.js | 62 + .../extensions/test/browser/browser_webapi.js | 125 + .../test/browser/browser_webapi_abuse_report.js | 375 + .../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 | 652 ++ .../browser/browser_webapi_install_disabled.js | 60 + .../test/browser/browser_webapi_theme.js | 79 + .../test/browser/browser_webapi_uninstall.js | 72 + .../extensions/test/browser/browser_webext_icon.js | 82 + .../test/browser/browser_webext_incognito.js | 593 ++ .../test/browser/discovery/api_response.json | 679 ++ .../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 | 1714 ++++ .../extensions/test/browser/head_abuse_report.js | 615 ++ .../mozapps/extensions/test/browser/head_disco.js | 125 + toolkit/mozapps/extensions/test/browser/moz.build | 31 + .../mozapps/extensions/test/browser/redirect.sjs | 5 + .../mozapps/extensions/test/browser/sandboxed.html | 11 + .../test/browser/sandboxed.html^headers^ | 1 + .../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.toml | 3 + .../extensions/test/mochitest/file_empty.html | 2 + .../extensions/test/mochitest/mochitest.toml | 6 + .../mochitest/test_blocklist_gfx_initialized.html | 31 + .../extensions/test/mochitest/test_bug887098.html | 70 + .../test/mochitest/test_default_theme.html | 37 + toolkit/mozapps/extensions/test/moz.build | 20 + .../mozapps/extensions/test/xpcshell/.eslintrc.js | 24 + .../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 + .../data/productaddons/content_signing_aus_ee.pem | 15 + .../content_signing_aus_ee.pem.certspec | 5 + .../data/productaddons/content_signing_int.pem | 18 + .../productaddons/content_signing_int.pem.certspec | 4 + .../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 + .../data/test_AddonRepository_getMappedAddons.json | 25 + ...test_AddonRepository_getMappedAddons_empty.json | 8 + .../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 | 332 + .../xpcshell/data/test_bug449027_app-plugins.json | 332 + .../test/xpcshell/data/test_bug449027_app.xml | 333 + .../data/test_bug449027_toolkit-extensions.json | 189 + .../data/test_bug449027_toolkit-plugins.json | 189 + .../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 | 12 + .../data/test_delay_updates_complete_legacy.json | 18 + .../xpcshell/data/test_delay_updates_defer.json | 12 + .../data/test_delay_updates_defer_legacy.json | 18 + .../xpcshell/data/test_delay_updates_ignore.json | 12 + .../data/test_delay_updates_ignore_legacy.json | 18 + .../xpcshell/data/test_delay_updates_staged.json | 32 + .../test/xpcshell/data/test_gfxBlacklist.json | 377 + .../xpcshell/data/test_gfxBlacklist_AllOS.json | 581 ++ .../xpcshell/data/test_gfxBlacklist_OSVersion.json | 20 + .../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 + .../xpcshell/data/test_trash_directory.worker.js | 40 + .../extensions/test/xpcshell/data/test_update.json | 120 + .../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 | 1223 +++ .../test/xpcshell/head_amremotesettings.js | 31 + .../extensions/test/xpcshell/head_cert_handling.js | 33 + .../extensions/test/xpcshell/head_compat.js | 47 + .../extensions/test/xpcshell/head_sideload.js | 76 + .../extensions/test/xpcshell/head_system_addons.js | 486 ++ .../extensions/test/xpcshell/head_unpack.js | 3 + .../extensions/test/xpcshell/rs-blocklist/head.js | 57 + .../rs-blocklist/test_android_blocklist_dump.js | 84 + .../rs-blocklist/test_blocklist_addonBlockURL.js | 56 + .../rs-blocklist/test_blocklist_appversion.js | 293 + .../rs-blocklist/test_blocklist_clients.js | 225 + .../xpcshell/rs-blocklist/test_blocklist_gfx.js | 113 + .../test_blocklist_metadata_filters.js | 116 + .../xpcshell/rs-blocklist/test_blocklist_mlbf.js | 290 + .../rs-blocklist/test_blocklist_mlbf_dump.js | 155 + .../rs-blocklist/test_blocklist_mlbf_fetch.js | 231 + .../rs-blocklist/test_blocklist_mlbf_stashes.js | 219 + .../rs-blocklist/test_blocklist_mlbf_telemetry.js | 188 + .../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 | 225 + .../rs-blocklist/test_blocklist_severities.js | 504 ++ .../test_blocklist_statechange_telemetry.js | 411 + .../test_blocklist_targetapp_filter.js | 392 + .../rs-blocklist/test_blocklist_telemetry.js | 138 + .../xpcshell/rs-blocklist/test_blocklistchange.js | 1389 +++ .../rs-blocklist/test_blocklistchange_v2.js | 13 + .../rs-blocklist/test_gfxBlacklist_Device.js | 73 + .../rs-blocklist/test_gfxBlacklist_DriverNew.js | 67 + .../test_gfxBlacklist_Equal_DriverNew.js | 112 + .../test_gfxBlacklist_Equal_DriverOld.js | 68 + .../rs-blocklist/test_gfxBlacklist_Equal_OK.js | 68 + .../test_gfxBlacklist_GTE_DriverOld.js | 68 + .../rs-blocklist/test_gfxBlacklist_GTE_OK.js | 70 + .../test_gfxBlacklist_No_Comparison.js | 69 + .../xpcshell/rs-blocklist/test_gfxBlacklist_OK.js | 69 + .../xpcshell/rs-blocklist/test_gfxBlacklist_OS.js | 68 + .../test_gfxBlacklist_OSVersion_match.js | 70 + ...fxBlacklist_OSVersion_mismatch_DriverVersion.js | 70 + ...st_gfxBlacklist_OSVersion_mismatch_OSVersion.js | 71 + .../rs-blocklist/test_gfxBlacklist_Vendor.js | 68 + .../rs-blocklist/test_gfxBlacklist_Version.js | 190 + .../rs-blocklist/test_gfxBlacklist_prefs.js | 124 + .../test/xpcshell/rs-blocklist/test_softblocked.js | 61 + .../test/xpcshell/rs-blocklist/xpcshell.toml | 102 + .../xpcshell/test_AMBrowserExtensionsImport.js | 500 ++ .../extensions/test/xpcshell/test_AbuseReporter.js | 908 ++ .../test/xpcshell/test_AddonRepository.js | 488 ++ .../test_AddonRepository_appIsShuttingDown.js | 82 + .../test/xpcshell/test_AddonRepository_cache.js | 728 ++ .../xpcshell/test_AddonRepository_cache_locale.js | 217 + .../xpcshell/test_AddonRepository_langpacks.js | 135 + .../test/xpcshell/test_AddonRepository_paging.js | 91 + .../test/xpcshell/test_ProductAddonChecker.js | 310 + .../test_ProductAddonChecker_signatures.js | 201 + .../test_QuarantinedDomains_AMRemoteSettings.js | 217 + .../test_QuarantinedDomains_AddonWrapper.js | 207 + .../extensions/test/xpcshell/test_XPIStates.js | 133 + .../extensions/test/xpcshell/test_XPIcancel.js | 70 + .../extensions/test/xpcshell/test_addonStartup.js | 93 + .../test_addon_manager_telemetry_events.js | 1049 +++ .../test/xpcshell/test_amo_stats_telemetry.js | 102 + .../extensions/test/xpcshell/test_aom_startup.js | 189 + .../extensions/test/xpcshell/test_bad_json.js | 41 + .../extensions/test/xpcshell/test_badschema.js | 237 + .../extensions/test/xpcshell/test_bug587088.js | 194 + .../test/xpcshell/test_builtin_location.js | 149 + .../extensions/test/xpcshell/test_cacheflush.js | 86 + .../extensions/test/xpcshell/test_childprocess.js | 25 + .../test_colorways_builtin_theme_upgrades.js | 582 ++ .../extensions/test/xpcshell/test_cookies.js | 102 + .../extensions/test/xpcshell/test_corrupt.js | 216 + .../test/xpcshell/test_crash_annotation_quoting.js | 25 + .../extensions/test/xpcshell/test_db_path.js | 64 + .../xpcshell/test_delay_update_webextension.js | 556 ++ .../extensions/test/xpcshell/test_dependencies.js | 140 + .../test/xpcshell/test_dictionary_webextension.js | 263 + .../extensions/test/xpcshell/test_distribution.js | 115 + .../test/xpcshell/test_distribution_langpack.js | 112 + .../test/xpcshell/test_embedderDisabled.js | 124 + .../mozapps/extensions/test/xpcshell/test_error.js | 75 + .../test/xpcshell/test_ext_management.js | 223 + .../extensions/test/xpcshell/test_filepointer.js | 327 + .../extensions/test/xpcshell/test_general.js | 49 + .../test/xpcshell/test_getInstallSourceFromHost.js | 47 + .../extensions/test/xpcshell/test_gmpProvider.js | 477 ++ .../extensions/test/xpcshell/test_harness.js | 13 + .../extensions/test/xpcshell/test_hidden.js | 251 + .../extensions/test/xpcshell/test_install.js | 1063 +++ .../test/xpcshell/test_installOrigins.js | 549 ++ .../test/xpcshell/test_install_cancel.js | 92 + .../test/xpcshell/test_install_file_change.js | 180 + .../extensions/test/xpcshell/test_install_icons.js | 62 + .../xpcshell/test_installtrigger_deprecation.js | 346 + .../test/xpcshell/test_installtrigger_schemes.js | 75 + .../extensions/test/xpcshell/test_isDebuggable.js | 21 + .../extensions/test/xpcshell/test_isReady.js | 71 + .../xpcshell/test_loadManifest_isPrivileged.js | 233 + .../extensions/test/xpcshell/test_locale.js | 103 + .../test/xpcshell/test_moved_extension_metadata.js | 186 + .../extensions/test/xpcshell/test_no_addons.js | 83 + .../test/xpcshell/test_nodisable_hidden.js | 100 + .../xpcshell/test_onPropertyChanged_appDisabled.js | 52 + .../extensions/test/xpcshell/test_permissions.js | 199 + .../test/xpcshell/test_permissions_prefs.js | 99 + .../test/xpcshell/test_pref_properties.js | 221 + .../test/xpcshell/test_provider_markSafe.js | 43 + .../test/xpcshell/test_provider_shutdown.js | 96 + .../test_provider_unsafe_access_shutdown.js | 65 + .../test_provider_unsafe_access_startup.js | 59 + .../extensions/test/xpcshell/test_proxies.js | 235 + .../test/xpcshell/test_recommendations.js | 707 ++ .../test/xpcshell/test_registerchrome.js | 88 + .../extensions/test/xpcshell/test_registry.js | 160 + .../test/xpcshell/test_reinstall_disabled_addon.js | 213 + .../extensions/test/xpcshell/test_reload.js | 188 + .../test/xpcshell/test_remote_pref_telemetry.js | 48 + .../extensions/test/xpcshell/test_safemode.js | 90 + .../extensions/test/xpcshell/test_schema_change.js | 157 + .../mozapps/extensions/test/xpcshell/test_seen.js | 277 + .../extensions/test/xpcshell/test_shutdown.js | 131 + .../test/xpcshell/test_shutdown_barriers.js | 215 + .../test/xpcshell/test_shutdown_early.js | 62 + .../test/xpcshell/test_sideload_scopes.js | 188 + .../extensions/test/xpcshell/test_sideloads.js | 117 + .../test/xpcshell/test_sideloads_after_rebuild.js | 149 + .../extensions/test/xpcshell/test_signed_inject.js | 429 + .../test/xpcshell/test_signed_install.js | 337 + .../test/xpcshell/test_signed_langpack.js | 67 + .../extensions/test/xpcshell/test_signed_long.js | 23 + .../test/xpcshell/test_signed_updatepref.js | 130 + .../extensions/test/xpcshell/test_signed_verify.js | 109 + .../test/xpcshell/test_sitePermsAddonProvider.js | 967 +++ .../extensions/test/xpcshell/test_startup.js | 648 ++ .../test/xpcshell/test_startup_enable.js | 47 + .../test/xpcshell/test_startup_isPrivileged.js | 58 + .../extensions/test/xpcshell/test_startup_scan.js | 125 + .../test/xpcshell/test_strictcompatibility.js | 156 + .../extensions/test/xpcshell/test_syncGUID.js | 113 + .../test/xpcshell/test_system_allowed.js | 55 + .../test/xpcshell/test_system_delay_update.js | 486 ++ .../test/xpcshell/test_system_profile_location.js | 204 + .../test/xpcshell/test_system_repository.js | 69 + .../extensions/test/xpcshell/test_system_reset.js | 539 ++ .../test/xpcshell/test_system_update_blank.js | 118 + .../xpcshell/test_system_update_checkSizeHash.js | 182 + .../test/xpcshell/test_system_update_custom.js | 492 ++ .../test/xpcshell/test_system_update_empty.js | 142 + .../test_system_update_enterprisepolicy.js | 78 + .../test/xpcshell/test_system_update_fail.js | 186 + .../test_system_update_installTelemetryInfo.js | 95 + .../test/xpcshell/test_system_update_newset.js | 166 + .../xpcshell/test_system_update_overlapping.js | 181 + .../xpcshell/test_system_update_uninstall_check.js | 57 + .../test/xpcshell/test_system_update_upgrades.js | 166 + .../test/xpcshell/test_system_upgrades.js | 417 + .../test/xpcshell/test_systemaddomstartupprefs.js | 56 + .../extensions/test/xpcshell/test_temporary.js | 765 ++ .../test/xpcshell/test_trash_directory.js | 47 + .../mozapps/extensions/test/xpcshell/test_types.js | 117 + .../extensions/test/xpcshell/test_undouninstall.js | 584 ++ .../extensions/test/xpcshell/test_update.js | 834 ++ .../extensions/test/xpcshell/test_updateCancel.js | 139 + .../test/xpcshell/test_update_addontype.js | 75 + .../test/xpcshell/test_update_compatmode.js | 112 + .../test/xpcshell/test_update_ignorecompat.js | 116 + .../test/xpcshell/test_update_isPrivileged.js | 181 + .../xpcshell/test_update_noSystemAddonUpdate.js | 43 + .../test/xpcshell/test_update_strictcompat.js | 216 + .../extensions/test/xpcshell/test_update_theme.js | 121 + .../test/xpcshell/test_update_webextensions.js | 209 + .../extensions/test/xpcshell/test_updatecheck.js | 167 + .../test/xpcshell/test_updatecheck_errors.js | 52 + .../test/xpcshell/test_updatecheck_json.js | 423 + .../extensions/test/xpcshell/test_updateid.js | 82 + .../extensions/test/xpcshell/test_updateversion.js | 101 + .../extensions/test/xpcshell/test_upgrade.js | 199 + .../test/xpcshell/test_upgrade_incompatible.js | 73 + .../extensions/test/xpcshell/test_webextension.js | 676 ++ .../test/xpcshell/test_webextension_events.js | 94 + .../test/xpcshell/test_webextension_icons.js | 212 + .../test/xpcshell/test_webextension_install.js | 696 ++ .../test_webextension_install_syntax_error.js | 42 + .../test/xpcshell/test_webextension_langpack.js | 669 ++ .../test/xpcshell/test_webextension_paths.js | 47 + .../test/xpcshell/test_webextension_theme.js | 365 + .../extensions/test/xpcshell/xpcshell-unpack.toml | 15 + .../mozapps/extensions/test/xpcshell/xpcshell.toml | 362 + .../extensions/test/xpinstall/amosigned.xpi | Bin 0 -> 4287 bytes .../extensions/test/xpinstall/authRedirect.sjs | 21 + .../mozapps/extensions/test/xpinstall/browser.toml | 175 + .../test/xpinstall/browser_amosigned_trigger.js | 86 + .../xpinstall/browser_amosigned_trigger_iframe.js | 77 + .../test/xpinstall/browser_amosigned_url.js | 63 + .../extensions/test/xpinstall/browser_auth.js | 68 + .../extensions/test/xpinstall/browser_auth2.js | 73 + .../extensions/test/xpinstall/browser_auth3.js | 72 + .../extensions/test/xpinstall/browser_auth4.js | 71 + .../extensions/test/xpinstall/browser_badargs.js | 49 + .../extensions/test/xpinstall/browser_badargs2.js | 55 + .../extensions/test/xpinstall/browser_badhash.js | 46 + .../test/xpinstall/browser_badhashtype.js | 46 + .../xpinstall/browser_block_fullscreen_prompt.js | 129 + .../extensions/test/xpinstall/browser_bug540558.js | 31 + .../extensions/test/xpinstall/browser_bug611242.js | 34 + .../extensions/test/xpinstall/browser_bug638292.js | 51 + .../extensions/test/xpinstall/browser_bug645699.js | 69 + .../xpinstall/browser_bug645699_postDownload.js | 55 + .../extensions/test/xpinstall/browser_bug672485.js | 63 + .../test/xpinstall/browser_containers.js | 116 + .../extensions/test/xpinstall/browser_cookies.js | 42 + .../extensions/test/xpinstall/browser_cookies2.js | 64 + .../extensions/test/xpinstall/browser_cookies3.js | 68 + .../extensions/test/xpinstall/browser_cookies4.js | 68 + .../extensions/test/xpinstall/browser_corrupt.js | 53 + .../extensions/test/xpinstall/browser_datauri.js | 80 + .../test/xpinstall/browser_doorhanger_installs.js | 1545 ++++ .../extensions/test/xpinstall/browser_empty.js | 39 + .../extensions/test/xpinstall/browser_enabled.js | 103 + .../extensions/test/xpinstall/browser_hash.js | 47 + .../extensions/test/xpinstall/browser_hash2.js | 47 + .../extensions/test/xpinstall/browser_httphash.js | 55 + .../extensions/test/xpinstall/browser_httphash2.js | 52 + .../extensions/test/xpinstall/browser_httphash3.js | 52 + .../extensions/test/xpinstall/browser_httphash4.js | 49 + .../extensions/test/xpinstall/browser_httphash5.js | 53 + .../extensions/test/xpinstall/browser_httphash6.js | 107 + .../test/xpinstall/browser_installchrome.js | 36 + .../extensions/test/xpinstall/browser_localfile.js | 42 + .../test/xpinstall/browser_localfile2.js | 61 + .../test/xpinstall/browser_localfile3.js | 42 + .../test/xpinstall/browser_localfile4.js | 55 + .../xpinstall/browser_localfile4_postDownload.js | 54 + .../extensions/test/xpinstall/browser_newwindow.js | 89 + .../extensions/test/xpinstall/browser_offline.js | 82 + .../test/xpinstall/browser_privatebrowsing.js | 133 + .../extensions/test/xpinstall/browser_relative.js | 67 + .../xpinstall/browser_required_useractivation.js | 156 + .../test/xpinstall/browser_signed_url.js | 32 + .../test/xpinstall/browser_softwareupdate.js | 36 + .../test/xpinstall/browser_trigger_redirect.js | 48 + .../test/xpinstall/browser_unsigned_trigger.js | 68 + .../xpinstall/browser_unsigned_trigger_iframe.js | 77 + .../xpinstall/browser_unsigned_trigger_xorigin.js | 58 + .../test/xpinstall/browser_unsigned_url.js | 43 + .../extensions/test/xpinstall/bug540558.html | 24 + .../extensions/test/xpinstall/bug638292.html | 17 + .../extensions/test/xpinstall/bug645699.html | 32 + .../extensions/test/xpinstall/cookieRedirect.sjs | 23 + .../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 | 14 + toolkit/mozapps/extensions/test/xpinstall/head.js | 568 ++ .../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 | 39 + .../extensions/test/xpinstall/restartless.xpi | Bin 0 -> 4447 bytes .../extensions/test/xpinstall/slowinstall.sjs | 103 + .../test/xpinstall/startsoftwareupdate.html | 21 + .../extensions/test/xpinstall/triggerredirect.html | 37 + .../mozapps/extensions/test/xpinstall/unsigned.xpi | Bin 0 -> 312 bytes .../extensions/test/xpinstall/unsigned_mv3.xpi | Bin 0 -> 316 bytes .../test/xpinstall/webmidi_permission.xpi | Bin 0 -> 7533 bytes .../handling/ContentDispatchChooser.sys.mjs | 465 + toolkit/mozapps/handling/components.conf | 14 + toolkit/mozapps/handling/content/appChooser.js | 369 + toolkit/mozapps/handling/content/appChooser.xhtml | 73 + toolkit/mozapps/handling/content/handler.css | 59 + .../mozapps/handling/content/permissionDialog.js | 223 + .../handling/content/permissionDialog.xhtml | 56 + toolkit/mozapps/handling/jar.mn | 11 + toolkit/mozapps/handling/metrics.yaml | 37 + toolkit/mozapps/handling/moz.build | 18 + toolkit/mozapps/installer/find-dupes.py | 148 + toolkit/mozapps/installer/informulate.py | 128 + toolkit/mozapps/installer/js-compare-ast.js | 31 + toolkit/mozapps/installer/l10n-repack.py | 80 + .../mozapps/installer/linux/rpm/mozilla.desktop | 21 + toolkit/mozapps/installer/linux/rpm/mozilla.spec | 116 + toolkit/mozapps/installer/moz.build | 8 + toolkit/mozapps/installer/package-name.mk | 135 + toolkit/mozapps/installer/packager.mk | 236 + toolkit/mozapps/installer/packager.py | 295 + toolkit/mozapps/installer/strip.py | 25 + toolkit/mozapps/installer/unify.py | 77 + toolkit/mozapps/installer/unpack.py | 25 + toolkit/mozapps/installer/upload-files.mk | 434 + toolkit/mozapps/installer/windows/nsis/common.nsh | 8842 ++++++++++++++++++++ .../installer/windows/nsis/locale-fonts.nsh | 675 ++ .../mozapps/installer/windows/nsis/locale-rtl.nlf | 12 + toolkit/mozapps/installer/windows/nsis/locale.nlf | 12 + toolkit/mozapps/installer/windows/nsis/locales.nsi | 23 + toolkit/mozapps/installer/windows/nsis/makensis.mk | 133 + .../mozapps/installer/windows/nsis/overrides.nsh | 610 ++ .../installer/windows/nsis/preprocess-locale.py | 379 + toolkit/mozapps/installer/windows/nsis/setup.ico | Bin 0 -> 25214 bytes .../notificationserver/NotificationCallback.cpp | 276 + .../notificationserver/NotificationCallback.h | 73 + .../notificationserver/NotificationComServer.cpp | 132 + .../notificationserver/NotificationFactory.cpp | 33 + .../notificationserver/NotificationFactory.h | 31 + toolkit/mozapps/notificationserver/moz.build | 34 + .../notificationserver/notificationserver.def | 6 + toolkit/mozapps/preferences/changemp.js | 220 + toolkit/mozapps/preferences/changemp.xhtml | 96 + toolkit/mozapps/preferences/fontbuilder.js | 120 + toolkit/mozapps/preferences/jar.mn | 11 + toolkit/mozapps/preferences/moz.build | 10 + toolkit/mozapps/preferences/removemp.js | 52 + toolkit/mozapps/preferences/removemp.xhtml | 54 + toolkit/mozapps/update/AppUpdater.sys.mjs | 880 ++ .../update/BackgroundTask_backgroundupdate.sys.mjs | 470 ++ toolkit/mozapps/update/BackgroundUpdate.sys.mjs | 1045 +++ toolkit/mozapps/update/UpdateListener.sys.mjs | 524 ++ toolkit/mozapps/update/UpdateLog.sys.mjs | 206 + toolkit/mozapps/update/UpdateService.sys.mjs | 7241 ++++++++++++++++ toolkit/mozapps/update/UpdateServiceStub.sys.mjs | 388 + toolkit/mozapps/update/UpdateTelemetry.sys.mjs | 652 ++ toolkit/mozapps/update/common/certificatecheck.cpp | 241 + toolkit/mozapps/update/common/certificatecheck.h | 22 + toolkit/mozapps/update/common/commonupdatedir.cpp | 723 ++ toolkit/mozapps/update/common/commonupdatedir.h | 39 + toolkit/mozapps/update/common/moz.build | 76 + toolkit/mozapps/update/common/pathhash.cpp | 128 + toolkit/mozapps/update/common/pathhash.h | 21 + toolkit/mozapps/update/common/readstrings.cpp | 396 + toolkit/mozapps/update/common/readstrings.h | 91 + .../mozapps/update/common/registrycertificates.cpp | 148 + .../mozapps/update/common/registrycertificates.h | 14 + toolkit/mozapps/update/common/uachelper.cpp | 186 + toolkit/mozapps/update/common/uachelper.h | 24 + toolkit/mozapps/update/common/updatecommon.cpp | 470 ++ toolkit/mozapps/update/common/updatecommon.h | 43 + toolkit/mozapps/update/common/updatedefines.h | 164 + toolkit/mozapps/update/common/updatehelper.cpp | 763 ++ toolkit/mozapps/update/common/updatehelper.h | 39 + toolkit/mozapps/update/common/updatererrors.h | 130 + toolkit/mozapps/update/common/updateutils_win.cpp | 166 + toolkit/mozapps/update/common/updateutils_win.h | 47 + toolkit/mozapps/update/components.conf | 38 + toolkit/mozapps/update/content/history.js | 96 + toolkit/mozapps/update/content/history.xhtml | 44 + toolkit/mozapps/update/content/updateElevation.js | 138 + .../mozapps/update/content/updateElevation.xhtml | 80 + toolkit/mozapps/update/docs/BackgroundUpdates.rst | 221 + .../update/docs/MaintenanceServiceTests.rst | 103 + .../update/docs/SettingUpAnUpdateServer.rst | 223 + toolkit/mozapps/update/docs/index.rst | 10 + toolkit/mozapps/update/jar.mn | 10 + toolkit/mozapps/update/metrics.yaml | 440 + toolkit/mozapps/update/moz.build | 59 + toolkit/mozapps/update/nsIUpdateService.idl | 828 ++ toolkit/mozapps/update/nsUpdateService.manifest | 1 + toolkit/mozapps/update/pings.yaml | 35 + toolkit/mozapps/update/tests/Makefile.in | 13 + toolkit/mozapps/update/tests/TestAUSHelper.cpp | 462 + .../mozapps/update/tests/TestAUSReadStrings.cpp | 210 + .../mozapps/update/tests/TestAUSReadStrings1.ini | 47 + .../mozapps/update/tests/TestAUSReadStrings2.ini | 39 + .../mozapps/update/tests/TestAUSReadStrings3.ini | 39 + .../mozapps/update/tests/TestAUSReadStrings4.ini | 5 + .../mozapps/update/tests/browser/browser.bits.toml | 135 + toolkit/mozapps/update/tests/browser/browser.toml | 203 + ...browser_aboutDialog_AppUpdater_stop_checking.js | 32 + ...tDialog_AppUpdater_stop_download_and_install.js | 41 + ..._aboutDialog_AppUpdater_stop_download_failed.js | 43 + ...wser_aboutDialog_AppUpdater_stop_downloading.js | 47 + ...r_aboutDialog_AppUpdater_stop_internal_error.js | 45 + ...rowser_aboutDialog_AppUpdater_stop_no_update.js | 29 + ...boutDialog_AppUpdater_stop_ready_for_restart.js | 27 + .../browser_aboutDialog_AppUpdater_stop_staging.js | 64 + .../browser_aboutDialog_AppUpdater_stop_swap.js | 64 + .../browser/browser_aboutDialog_bc_downloaded.js | 17 + .../browser_aboutDialog_bc_downloaded_staged.js | 28 + .../browser_aboutDialog_bc_downloaded_staging.js | 55 + ...ser_aboutDialog_bc_downloaded_stagingFailure.js | 31 + .../browser/browser_aboutDialog_bc_downloading.js | 76 + .../browser_aboutDialog_bc_downloading_notify.js | 67 + .../browser_aboutDialog_bc_downloading_staging.js | 68 + .../browser/browser_aboutDialog_bc_multiUpdate.js | 52 + .../browser_aboutDialog_fc_apply_blocked.js | 94 + .../browser_aboutDialog_fc_check_cantApply.js | 24 + .../browser_aboutDialog_fc_check_malformedXML.js | 22 + .../browser_aboutDialog_fc_check_noUpdate.js | 22 + .../browser_aboutDialog_fc_check_otherInstance.js | 19 + .../browser_aboutDialog_fc_check_unsupported.js | 22 + .../browser/browser_aboutDialog_fc_downloadAuto.js | 62 + .../browser_aboutDialog_fc_downloadAuto_staging.js | 46 + .../browser_aboutDialog_fc_downloadOptIn.js | 44 + ...browser_aboutDialog_fc_downloadOptIn_staging.js | 52 + .../browser_aboutDialog_fc_network_failure.js | 18 + .../browser_aboutDialog_fc_network_offline.js | 31 + ...browser_aboutDialog_fc_patch_completeBadSize.js | 36 + .../browser_aboutDialog_fc_patch_partialBadSize.js | 36 + ...aboutDialog_fc_patch_partialBadSize_complete.js | 38 + ...alog_fc_patch_partialBadSize_completeBadSize.js | 53 + .../browser/browser_aboutDialog_internalError.js | 35 + .../browser_aboutPrefs_backgroundUpdateSetting.js | 172 + .../browser/browser_aboutPrefs_bc_downloaded.js | 17 + .../browser_aboutPrefs_bc_downloaded_staged.js | 28 + .../browser_aboutPrefs_bc_downloaded_staging.js | 59 + ...wser_aboutPrefs_bc_downloaded_stagingFailure.js | 29 + .../browser/browser_aboutPrefs_bc_downloading.js | 63 + .../browser_aboutPrefs_bc_downloading_staging.js | 72 + .../browser/browser_aboutPrefs_bc_multiUpdate.js | 52 + .../browser/browser_aboutPrefs_fc_apply_blocked.js | 96 + .../browser_aboutPrefs_fc_check_cantApply.js | 24 + .../browser_aboutPrefs_fc_check_malformedXML.js | 22 + .../browser_aboutPrefs_fc_check_noUpdate.js | 22 + .../browser_aboutPrefs_fc_check_otherInstance.js | 19 + .../browser_aboutPrefs_fc_check_unsupported.js | 22 + .../browser/browser_aboutPrefs_fc_downloadAuto.js | 37 + .../browser_aboutPrefs_fc_downloadAuto_staging.js | 46 + .../browser/browser_aboutPrefs_fc_downloadOptIn.js | 44 + .../browser_aboutPrefs_fc_downloadOptIn_staging.js | 52 + .../browser_aboutPrefs_fc_network_failure.js | 17 + .../browser_aboutPrefs_fc_network_offline.js | 31 + .../browser_aboutPrefs_fc_patch_completeBadSize.js | 36 + .../browser_aboutPrefs_fc_patch_partialBadSize.js | 36 + ..._aboutPrefs_fc_patch_partialBadSize_complete.js | 38 + ...refs_fc_patch_partialBadSize_completeBadSize.js | 53 + .../browser/browser_aboutPrefs_internalError.js | 35 + .../tests/browser/browser_aboutPrefs_settings.js | 151 + .../browser_doorhanger_bc_check_cantApply.js | 18 + .../browser_doorhanger_bc_check_malformedXML.js | 26 + .../browser_doorhanger_bc_check_unsupported.js | 94 + .../browser_doorhanger_bc_downloadAutoFailures.js | 35 + ...ser_doorhanger_bc_downloadAutoFailures_bgWin.js | 80 + .../browser/browser_doorhanger_bc_downloadOptIn.js | 57 + .../browser_doorhanger_bc_downloadOptIn_bgWin.js | 63 + .../browser_doorhanger_bc_downloadOptIn_staging.js | 29 + .../browser/browser_doorhanger_bc_downloaded.js | 18 + ...browser_doorhanger_bc_downloaded_disableBITS.js | 31 + .../browser_doorhanger_bc_downloaded_staged.js | 22 + .../browser/browser_doorhanger_bc_multiUpdate.js | 93 + ...ser_doorhanger_bc_multiUpdate_promptWaitTime.js | 89 + .../browser_doorhanger_bc_patch_completeBadSize.js | 33 + .../browser_doorhanger_bc_patch_partialBadSize.js | 33 + ..._doorhanger_bc_patch_partialBadSize_complete.js | 18 + ...nger_bc_patch_partialBadSize_completeBadSize.js | 33 + ...ser_doorhanger_sp_patch_completeApplyFailure.js | 23 + ...wser_doorhanger_sp_patch_partialApplyFailure.js | 22 + ...hanger_sp_patch_partialApplyFailure_complete.js | 22 + ...sp_patch_partialApplyFailure_completeBadSize.js | 32 + ...p_patch_partialApplyFailure_complete_staging.js | 28 + .../tests/browser/browser_elevationDialog.js | 139 + .../browser_memory_allocation_error_fallback.js | 81 + ...rowser_telemetry_updatePing_downloaded_ready.js | 69 + .../browser_telemetry_updatePing_staged_ready.js | 73 + .../mozapps/update/tests/browser/downloadPage.html | 13 + toolkit/mozapps/update/tests/browser/head.js | 1353 +++ .../browser/manual_app_update_only/browser.toml | 24 + .../browser_aboutDialog_fc_autoUpdateFalse.js | 43 + .../browser_aboutDialog_fc_autoUpdateTrue.js | 43 + .../browser_aboutPrefs_fc_autoUpdateFalse.js | 55 + .../browser_aboutPrefs_fc_autoUpdateTrue.js | 55 + .../browser_noBackgroundUpdate.js | 17 + .../config_manual_app_update_only.json | 5 + .../tests/browser/manual_app_update_only/head.js | 9 + .../mozapps/update/tests/browser/testConstants.js | 7 + toolkit/mozapps/update/tests/data/app_update.sjs | 251 + toolkit/mozapps/update/tests/data/complete.exe | Bin 0 -> 79872 bytes toolkit/mozapps/update/tests/data/complete.mar | Bin 0 -> 86612 bytes toolkit/mozapps/update/tests/data/complete.png | Bin 0 -> 878 bytes .../update/tests/data/complete_log_success_mac | 332 + .../update/tests/data/complete_log_success_win | 320 + toolkit/mozapps/update/tests/data/complete_mac.mar | Bin 0 -> 87129 bytes .../mozapps/update/tests/data/complete_precomplete | 18 + .../update/tests/data/complete_precomplete_mac | 21 + .../update/tests/data/complete_removed-files | 41 + .../update/tests/data/complete_removed-files_mac | 41 + .../update/tests/data/complete_update_manifest | 59 + toolkit/mozapps/update/tests/data/old_version.mar | Bin 0 -> 709 bytes toolkit/mozapps/update/tests/data/partial.exe | Bin 0 -> 79872 bytes toolkit/mozapps/update/tests/data/partial.mar | Bin 0 -> 9872 bytes toolkit/mozapps/update/tests/data/partial.png | Bin 0 -> 776 bytes .../update/tests/data/partial_log_failure_mac | 192 + .../update/tests/data/partial_log_failure_win | 192 + .../update/tests/data/partial_log_success_mac | 279 + .../update/tests/data/partial_log_success_win | 279 + toolkit/mozapps/update/tests/data/partial_mac.mar | Bin 0 -> 10361 bytes .../mozapps/update/tests/data/partial_precomplete | 19 + .../update/tests/data/partial_precomplete_mac | 22 + .../update/tests/data/partial_removed-files | 41 + .../update/tests/data/partial_removed-files_mac | 41 + .../update/tests/data/partial_update_manifest | 63 + .../mozapps/update/tests/data/replace_log_success | 6 + toolkit/mozapps/update/tests/data/shared.js | 933 +++ .../mozapps/update/tests/data/sharedUpdateXML.js | 417 + toolkit/mozapps/update/tests/data/simple.mar | Bin 0 -> 1419 bytes .../update/tests/data/syncManagerTestChild.js | 55 + .../update/tests/data/xpcshellConstantsPP.js | 29 + .../mozapps/update/tests/data/xpcshellUtilsAUS.js | 4881 +++++++++++ .../mozapps/update/tests/diff_base_service.bash | 116 + .../update/tests/marionette/marionette.toml | 5 + .../marionette/test_no_window_update_restart.py | 255 + toolkit/mozapps/update/tests/moz.build | 118 + .../update/tests/unit_aus_update/ausReadStrings.js | 33 + .../backgroundUpdateTaskInternalUpdater.js | 85 + .../canCheckForAndCanApplyUpdates.js | 62 + .../cleanupDownloadingForDifferentChannel.js | 60 + .../cleanupDownloadingForOlderAppVersion.js | 58 + .../cleanupDownloadingForSameVersionAndBuildID.js | 59 + .../cleanupDownloadingIncorrectStatus.js | 57 + .../cleanupPendingVersionFileIncorrectStatus.js | 57 + .../tests/unit_aus_update/cleanupSuccessLogMove.js | 77 + .../unit_aus_update/cleanupSuccessLogsFIFO.js | 226 + .../disableBackgroundUpdatesBackgroundTask.js | 48 + .../disableBackgroundUpdatesNonBackgroundTask.js | 41 + .../downloadInterruptedNoRecovery.js | 23 + .../unit_aus_update/downloadInterruptedOffline.js | 21 + .../unit_aus_update/downloadInterruptedRecovery.js | 26 + .../downloadResumeForSameAppVersion.js | 38 + ...ensureExperimentToRolloutTransitionPerformed.js | 111 + .../update/tests/unit_aus_update/head_update.js | 8 + .../tests/unit_aus_update/languagePackUpdates.js | 291 + .../update/tests/unit_aus_update/multiUpdate.js | 398 + .../onlyDownloadUpdatesThisSession.js | 69 + .../tests/unit_aus_update/perInstallationPrefs.js | 238 + .../tests/unit_aus_update/remoteUpdateXML.js | 327 + .../update/tests/unit_aus_update/testConstants.js | 8 + .../tests/unit_aus_update/updateAutoPrefMigrate.js | 74 + .../tests/unit_aus_update/updateCheckCombine.js | 38 + .../unit_aus_update/updateDirectoryMigrate.js | 246 + .../tests/unit_aus_update/updateManagerXML.js | 593 ++ .../tests/unit_aus_update/updateSyncManager.js | 105 + .../tests/unit_aus_update/urlConstruction.js | 26 + .../unit_aus_update/verifyChannelPrefsFile.js | 38 + .../update/tests/unit_aus_update/xpcshell.toml | 89 + .../update/tests/unit_background_update/head.js | 58 + .../test_backgroundupdate_exitcodes.js | 80 + .../test_backgroundupdate_glean.js | 275 + .../test_backgroundupdate_reason.js | 66 + .../test_backgroundupdate_reason_schedule.js | 136 + .../test_backgroundupdate_reason_update.js | 321 + .../tests/unit_background_update/xpcshell.toml | 24 + .../update/tests/unit_base_updater/head_update.js | 7 + ...invalidArgCallbackFileNotInInstallDirFailure.js | 32 + .../invalidArgCallbackFilePathTooLongFailure.js | 39 + .../invalidArgInstallDirPathTooLongFailure.js | 53 + .../invalidArgInstallDirPathTraversalFailure.js | 50 + ...idArgInstallWorkingDirPathNotSameFailure_win.js | 45 + .../invalidArgPatchDirPathTraversalFailure.js | 32 + ...invalidArgStageDirNotInInstallDirFailure_win.js | 45 + .../invalidArgWorkingDirPathLocalUNCFailure_win.js | 45 + .../invalidArgWorkingDirPathRelativeFailure.js | 44 + .../marAppApplyDirLockedStageFailure_win.js | 29 + ...marAppApplyUpdateAppBinInUseStageSuccess_win.js | 50 + .../marAppApplyUpdateSkippedWriteAccess_win.js | 74 + .../marAppApplyUpdateStageOldVersionFailure.js | 64 + .../marAppApplyUpdateStageSuccess.js | 49 + .../unit_base_updater/marAppApplyUpdateSuccess.js | 47 + .../marAppInUseBackgroundTaskFailure_win.js | 51 + .../marAppInUseStageFailureComplete_win.js | 37 + .../marAppInUseStageSuccessComplete_unix.js | 68 + .../marAppInUseSuccessComplete.js | 26 + .../marCallbackAppStageSuccessComplete_win.js | 30 + .../marCallbackAppStageSuccessPartial_win.js | 30 + .../marCallbackAppSuccessComplete_win.js | 24 + .../marCallbackAppSuccessPartial_win.js | 24 + .../unit_base_updater/marCallbackUmask_unix.js | 42 + .../tests/unit_base_updater/marFailurePartial.js | 39 + .../marFileInUseStageFailureComplete_win.js | 40 + .../marFileInUseStageFailurePartial_win.js | 40 + .../marFileInUseSuccessComplete_win.js | 29 + .../marFileInUseSuccessPartial_win.js | 29 + .../marFileLockedFailureComplete_win.js | 27 + .../marFileLockedFailurePartial_win.js | 26 + .../marFileLockedStageFailureComplete_win.js | 34 + .../marFileLockedStageFailurePartial_win.js | 33 + .../unit_base_updater/marMissingUpdateSettings.js | 42 + .../marMissingUpdateSettingsStage.js | 35 + .../marPIDPersistsSuccessComplete_win.js | 25 + .../marRMRFDirFileInUseStageFailureComplete_win.js | 43 + .../marRMRFDirFileInUseStageFailurePartial_win.js | 41 + .../marRMRFDirFileInUseSuccessComplete_win.js | 31 + .../marRMRFDirFileInUseSuccessPartial_win.js | 29 + .../unit_base_updater/marStageFailurePartial.js | 31 + .../unit_base_updater/marStageSuccessComplete.js | 71 + .../unit_base_updater/marStageSuccessPartial.js | 35 + .../tests/unit_base_updater/marSuccessComplete.js | 26 + .../tests/unit_base_updater/marSuccessPartial.js | 29 + .../marSuccessPartialWhileBackgroundTaskRunning.js | 121 + .../tests/unit_base_updater/marVersionDowngrade.js | 41 + .../tests/unit_base_updater/marWrongChannel.js | 43 + .../unit_base_updater/marWrongChannelStage.js | 36 + .../update/tests/unit_base_updater/xpcshell.toml | 181 + .../tests/unit_service_updater/bootstrapSvc.js | 23 + .../unit_service_updater/checkUpdaterSigSvc.js | 53 + .../unit_service_updater/fallbackOnSvcFailure.js | 38 + .../tests/unit_service_updater/head_update.js | 7 + .../invalidArgInstallDirPathTooLongFailureSvc.js | 53 + .../invalidArgInstallDirPathTraversalFailureSvc.js | 50 + ...rgInstallWorkingDirPathNotSameFailureSvc_win.js | 45 + .../invalidArgPatchDirPathSuffixFailureSvc.js | 27 + .../invalidArgPatchDirPathTraversalFailureSvc.js | 32 + ...alidArgStageDirNotInInstallDirFailureSvc_win.js | 45 + ...validArgWorkingDirPathLocalUNCFailureSvc_win.js | 45 + .../invalidArgWorkingDirPathRelativeFailureSvc.js | 44 + .../marAppApplyDirLockedStageFailureSvc_win.js | 29 + ...AppApplyUpdateAppBinInUseStageSuccessSvc_win.js | 50 + .../marAppApplyUpdateStageSuccessSvc.js | 49 + .../marAppApplyUpdateSuccessSvc.js | 47 + .../marAppInUseBackgroundTaskFailureSvc_win.js | 51 + .../marAppInUseStageFailureCompleteSvc_win.js | 37 + .../marAppInUseSuccessCompleteSvc.js | 26 + .../marCallbackAppStageSuccessCompleteSvc_win.js | 30 + .../marCallbackAppStageSuccessPartialSvc_win.js | 30 + .../marCallbackAppSuccessCompleteSvc_win.js | 24 + .../marCallbackAppSuccessPartialSvc_win.js | 24 + .../unit_service_updater/marFailurePartialSvc.js | 39 + .../marFileInUseStageFailureCompleteSvc_win.js | 40 + .../marFileInUseStageFailurePartialSvc_win.js | 40 + .../marFileInUseSuccessCompleteSvc_win.js | 29 + .../marFileInUseSuccessPartialSvc_win.js | 29 + .../marFileLockedFailureCompleteSvc_win.js | 27 + .../marFileLockedFailurePartialSvc_win.js | 26 + .../marFileLockedStageFailureCompleteSvc_win.js | 34 + .../marFileLockedStageFailurePartialSvc_win.js | 33 + ...rRMRFDirFileInUseStageFailureCompleteSvc_win.js | 43 + ...arRMRFDirFileInUseStageFailurePartialSvc_win.js | 41 + .../marRMRFDirFileInUseSuccessCompleteSvc_win.js | 31 + .../marRMRFDirFileInUseSuccessPartialSvc_win.js | 29 + .../marStageFailurePartialSvc.js | 31 + .../marStageSuccessCompleteSvc.js | 71 + .../marStageSuccessPartialSvc.js | 35 + .../unit_service_updater/marSuccessCompleteSvc.js | 26 + .../unit_service_updater/marSuccessPartialSvc.js | 29 + .../tests/unit_service_updater/xpcshell.toml | 134 + toolkit/mozapps/update/updater/Launchd.plist | 10 + toolkit/mozapps/update/updater/Makefile.in | 28 + toolkit/mozapps/update/updater/TsanOptions.cpp | 23 + toolkit/mozapps/update/updater/archivereader.cpp | 348 + toolkit/mozapps/update/updater/archivereader.h | 46 + toolkit/mozapps/update/updater/autograph_stage.pem | 14 + toolkit/mozapps/update/updater/bspatch/LICENSE | 23 + toolkit/mozapps/update/updater/bspatch/bspatch.cpp | 216 + toolkit/mozapps/update/updater/bspatch/bspatch.h | 93 + toolkit/mozapps/update/updater/bspatch/moz.build | 22 + toolkit/mozapps/update/updater/bspatch/moz.yaml | 30 + toolkit/mozapps/update/updater/crctable.h | 71 + toolkit/mozapps/update/updater/dep1.der | Bin 0 -> 1215 bytes toolkit/mozapps/update/updater/dep2.der | Bin 0 -> 1215 bytes toolkit/mozapps/update/updater/gen_cert_header.py | 27 + toolkit/mozapps/update/updater/launchchild_osx.mm | 519 ++ toolkit/mozapps/update/updater/loaddlls.cpp | 84 + .../update/updater/macbuild/Contents/Info.plist.in | 40 + .../Resources/English.lproj/InfoPlist.strings.in | 8 + .../English.lproj/MainMenu.nib/classes.nib | 19 + .../Resources/English.lproj/MainMenu.nib/info.nib | 22 + .../macbuild/Contents/Resources/updater.icns | Bin 0 -> 55969 bytes toolkit/mozapps/update/updater/module.ver | 1 + toolkit/mozapps/update/updater/moz.build | 78 + .../updater/nightly_aurora_level3_primary.der | Bin 0 -> 1225 bytes .../updater/nightly_aurora_level3_secondary.der | Bin 0 -> 1225 bytes toolkit/mozapps/update/updater/progressui.h | 40 + toolkit/mozapps/update/updater/progressui_gtk.cpp | 121 + toolkit/mozapps/update/updater/progressui_null.cpp | 15 + toolkit/mozapps/update/updater/progressui_osx.mm | 137 + toolkit/mozapps/update/updater/progressui_win.cpp | 302 + toolkit/mozapps/update/updater/release_primary.der | Bin 0 -> 1225 bytes .../mozapps/update/updater/release_secondary.der | Bin 0 -> 1225 bytes toolkit/mozapps/update/updater/resource.h | 29 + .../mozapps/update/updater/updater-common.build | 142 + .../mozapps/update/updater/updater-dep/moz.build | 13 + .../update/updater/updater-xpcshell/Makefile.in | 48 + .../update/updater/updater-xpcshell/moz.build | 13 + toolkit/mozapps/update/updater/updater.cpp | 4909 +++++++++++ .../update/updater/updater.exe.comctl32.manifest | 43 + .../mozapps/update/updater/updater.exe.manifest | 31 + toolkit/mozapps/update/updater/updater.ico | Bin 0 -> 92854 bytes toolkit/mozapps/update/updater/updater.png | Bin 0 -> 2153 bytes toolkit/mozapps/update/updater/updater.rc | 137 + .../mozapps/update/updater/xpcshellCertificate.der | Bin 0 -> 1189 bytes 1012 files changed, 187649 insertions(+) create mode 100644 toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs create mode 100644 toolkit/mozapps/defaultagent/Cache.cpp create mode 100644 toolkit/mozapps/defaultagent/Cache.h create mode 100644 toolkit/mozapps/defaultagent/DefaultAgent.cpp create mode 100644 toolkit/mozapps/defaultagent/DefaultAgent.h create mode 100644 toolkit/mozapps/defaultagent/DefaultBrowser.cpp create mode 100644 toolkit/mozapps/defaultagent/DefaultBrowser.h create mode 100644 toolkit/mozapps/defaultagent/DefaultPDF.cpp create mode 100644 toolkit/mozapps/defaultagent/DefaultPDF.h create mode 100644 toolkit/mozapps/defaultagent/EventLog.cpp create mode 100644 toolkit/mozapps/defaultagent/EventLog.h create mode 100644 toolkit/mozapps/defaultagent/Notification.cpp create mode 100644 toolkit/mozapps/defaultagent/Notification.h create mode 100644 toolkit/mozapps/defaultagent/Policy.cpp create mode 100644 toolkit/mozapps/defaultagent/Policy.h create mode 100644 toolkit/mozapps/defaultagent/Registry.cpp create mode 100644 toolkit/mozapps/defaultagent/Registry.h create mode 100644 toolkit/mozapps/defaultagent/ScheduledTask.cpp create mode 100644 toolkit/mozapps/defaultagent/ScheduledTask.h create mode 100644 toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp create mode 100644 toolkit/mozapps/defaultagent/ScheduledTaskRemove.h create mode 100644 toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp create mode 100644 toolkit/mozapps/defaultagent/SetDefaultBrowser.h create mode 100644 toolkit/mozapps/defaultagent/Telemetry.cpp create mode 100644 toolkit/mozapps/defaultagent/Telemetry.h create mode 100644 toolkit/mozapps/defaultagent/UtfConvert.cpp create mode 100644 toolkit/mozapps/defaultagent/UtfConvert.h create mode 100644 toolkit/mozapps/defaultagent/WindowsMutex.cpp create mode 100644 toolkit/mozapps/defaultagent/WindowsMutex.h create mode 100644 toolkit/mozapps/defaultagent/common.cpp create mode 100644 toolkit/mozapps/defaultagent/common.h create mode 100644 toolkit/mozapps/defaultagent/components.conf create mode 100644 toolkit/mozapps/defaultagent/defaultagent.ini create mode 100644 toolkit/mozapps/defaultagent/docs/index.rst create mode 100644 toolkit/mozapps/defaultagent/metrics.yaml create mode 100644 toolkit/mozapps/defaultagent/module.ver create mode 100644 toolkit/mozapps/defaultagent/moz.build create mode 100644 toolkit/mozapps/defaultagent/nsIDefaultAgent.idl create mode 100644 toolkit/mozapps/defaultagent/nsIWindowsMutex.idl create mode 100644 toolkit/mozapps/defaultagent/pings.yaml create mode 100644 toolkit/mozapps/defaultagent/proxy/Makefile.in create mode 100644 toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest create mode 100644 toolkit/mozapps/defaultagent/proxy/main.cpp create mode 100644 toolkit/mozapps/defaultagent/proxy/moz.build create mode 100644 toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp create mode 100644 toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp create mode 100644 toolkit/mozapps/defaultagent/tests/gtest/moz.build create mode 100644 toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js create mode 100644 toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml create mode 100644 toolkit/mozapps/downloads/DownloadLastDir.sys.mjs create mode 100644 toolkit/mozapps/downloads/DownloadUtils.sys.mjs create mode 100644 toolkit/mozapps/downloads/HelperAppDlg.sys.mjs create mode 100644 toolkit/mozapps/downloads/components.conf create mode 100644 toolkit/mozapps/downloads/content/unknownContentType.xhtml create mode 100644 toolkit/mozapps/downloads/jar.mn create mode 100644 toolkit/mozapps/downloads/moz.build create mode 100644 toolkit/mozapps/downloads/tests/browser/browser.toml create mode 100644 toolkit/mozapps/downloads/tests/browser/browser_save_wrongextension.js create mode 100644 toolkit/mozapps/downloads/tests/browser/browser_unknownContentType_blob.js create mode 100644 toolkit/mozapps/downloads/tests/browser/browser_unknownContentType_delayedbutton.js create mode 100644 toolkit/mozapps/downloads/tests/browser/browser_unknownContentType_dialog_layout.js create mode 100644 toolkit/mozapps/downloads/tests/browser/browser_unknownContentType_extension.js create mode 100644 toolkit/mozapps/downloads/tests/browser/browser_unknownContentType_policy.js create mode 100644 toolkit/mozapps/downloads/tests/browser/example.jnlp create mode 100644 toolkit/mozapps/downloads/tests/browser/example.jnlp^headers^ create mode 100644 toolkit/mozapps/downloads/tests/browser/head.js create mode 100644 toolkit/mozapps/downloads/tests/browser/unknownContentType.EXE create mode 100644 toolkit/mozapps/downloads/tests/browser/unknownContentType.EXE^headers^ create mode 100644 toolkit/mozapps/downloads/tests/browser/unknownContentType_dialog_layout_data.pif create mode 100644 toolkit/mozapps/downloads/tests/browser/unknownContentType_dialog_layout_data.pif^headers^ create mode 100644 toolkit/mozapps/downloads/tests/browser/unknownContentType_dialog_layout_data.txt create mode 100644 toolkit/mozapps/downloads/tests/browser/unknownContentType_dialog_layout_data.txt^headers^ create mode 100644 toolkit/mozapps/downloads/tests/moz.build create mode 100644 toolkit/mozapps/downloads/tests/unit/head_downloads.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js create mode 100644 toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js create mode 100644 toolkit/mozapps/downloads/tests/unit/xpcshell.toml create mode 100644 toolkit/mozapps/extensions/.eslintrc.js create mode 100644 toolkit/mozapps/extensions/AbuseReporter.sys.mjs create mode 100644 toolkit/mozapps/extensions/AddonContentPolicy.cpp create mode 100644 toolkit/mozapps/extensions/AddonContentPolicy.h create mode 100644 toolkit/mozapps/extensions/AddonManager.sys.mjs 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.sys.mjs create mode 100644 toolkit/mozapps/extensions/LightweightThemeManager.sys.mjs create mode 100644 toolkit/mozapps/extensions/amContentHandler.sys.mjs create mode 100644 toolkit/mozapps/extensions/amIAddonManagerStartup.idl create mode 100644 toolkit/mozapps/extensions/amIWebInstallPrompt.idl create mode 100644 toolkit/mozapps/extensions/amInstallTrigger.sys.mjs create mode 100644 toolkit/mozapps/extensions/amManager.sys.mjs create mode 100644 toolkit/mozapps/extensions/amWebAPI.sys.mjs 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/drag-drop-addon-installer.js create mode 100644 toolkit/mozapps/extensions/content/shortcuts.css create mode 100644 toolkit/mozapps/extensions/content/shortcuts.js create mode 100644 toolkit/mozapps/extensions/content/view-controller.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/default-theme/preview.svg create mode 100644 toolkit/mozapps/extensions/docs/AMRemoteSettings-JSONSchema.json create mode 100644 toolkit/mozapps/extensions/docs/AMRemoteSettings-UISchema.json create mode 100644 toolkit/mozapps/extensions/docs/AMRemoteSettings-overview.rst create mode 100644 toolkit/mozapps/extensions/docs/AMRemoteSettings.rst 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.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/AddonSettings.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/AddonTestUtils.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/AddonUpdateChecker.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/GMPProvider.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/ProductAddonChecker.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/SitePermsAddonProvider.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/XPIDatabase.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/XPIExports.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/XPIInstall.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/XPIProvider.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/crypto-utils.sys.mjs create mode 100644 toolkit/mozapps/extensions/internal/moz.build create mode 100644 toolkit/mozapps/extensions/internal/siteperms-addon-utils.sys.mjs create mode 100644 toolkit/mozapps/extensions/jar.mn create mode 100644 toolkit/mozapps/extensions/metrics.yaml 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.toml create mode 100644 toolkit/mozapps/extensions/test/browser/browser_AMBrowserExtensionsImport.js 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_amo_abuse_report.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_colorwaybuiltins_migration.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_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_sitepermission_addons.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_local_install.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_permission_prompt.js create mode 100644 toolkit/mozapps/extensions/test/browser/browser_reinstall.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_subframe_install.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_verify_l10n_strings.js 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/head_disco.js create mode 100644 toolkit/mozapps/extensions/test/browser/moz.build create mode 100644 toolkit/mozapps/extensions/test/browser/redirect.sjs create mode 100644 toolkit/mozapps/extensions/test/browser/sandboxed.html create mode 100644 toolkit/mozapps/extensions/test/browser/sandboxed.html^headers^ 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.toml create mode 100644 toolkit/mozapps/extensions/test/mochitest/file_empty.html create mode 100644 toolkit/mozapps/extensions/test/mochitest/mochitest.toml create mode 100644 toolkit/mozapps/extensions/test/mochitest/test_blocklist_gfx_initialized.html 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/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/content_signing_aus_ee.pem create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_aus_ee.pem.certspec create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/productaddons/content_signing_int.pem.certspec 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_AddonRepository_getMappedAddons.json create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getMappedAddons_empty.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_delay_updates_staged.json create mode 100644 toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.json create mode 100644 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_trash_directory.worker.js 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_amremotesettings.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/head_cert_handling.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_android_blocklist_dump.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_statechange_telemetry.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.toml create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AMBrowserExtensionsImport.js 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_appIsShuttingDown.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache_locale.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_ProductAddonChecker_signatures.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AMRemoteSettings.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_QuarantinedDomains_AddonWrapper.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_colorways_builtin_theme_upgrades.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_distribution_langpack.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_installOrigins.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_install_cancel.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_install_file_change.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_deprecation.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_installtrigger_schemes.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_loadManifest_isPrivileged.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_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_remote_pref_telemetry.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_sitePermsAddonProvider.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_isPrivileged.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_installTelemetryInfo.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_addontype.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_isPrivileged.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_updateversion.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.toml create mode 100644 toolkit/mozapps/extensions/test/xpcshell/xpcshell.toml 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.toml 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_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_required_useractivation.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/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 create mode 100644 toolkit/mozapps/extensions/test/xpinstall/unsigned_mv3.xpi create mode 100644 toolkit/mozapps/extensions/test/xpinstall/webmidi_permission.xpi create mode 100644 toolkit/mozapps/handling/ContentDispatchChooser.sys.mjs create mode 100644 toolkit/mozapps/handling/components.conf create mode 100644 toolkit/mozapps/handling/content/appChooser.js create mode 100644 toolkit/mozapps/handling/content/appChooser.xhtml create mode 100644 toolkit/mozapps/handling/content/handler.css create mode 100644 toolkit/mozapps/handling/content/permissionDialog.js create mode 100644 toolkit/mozapps/handling/content/permissionDialog.xhtml create mode 100644 toolkit/mozapps/handling/jar.mn create mode 100644 toolkit/mozapps/handling/metrics.yaml create mode 100644 toolkit/mozapps/handling/moz.build create mode 100644 toolkit/mozapps/installer/find-dupes.py create mode 100644 toolkit/mozapps/installer/informulate.py create mode 100644 toolkit/mozapps/installer/js-compare-ast.js create mode 100644 toolkit/mozapps/installer/l10n-repack.py create mode 100644 toolkit/mozapps/installer/linux/rpm/mozilla.desktop create mode 100644 toolkit/mozapps/installer/linux/rpm/mozilla.spec create mode 100644 toolkit/mozapps/installer/moz.build create mode 100644 toolkit/mozapps/installer/package-name.mk create mode 100644 toolkit/mozapps/installer/packager.mk create mode 100644 toolkit/mozapps/installer/packager.py create mode 100644 toolkit/mozapps/installer/strip.py create mode 100644 toolkit/mozapps/installer/unify.py create mode 100644 toolkit/mozapps/installer/unpack.py create mode 100644 toolkit/mozapps/installer/upload-files.mk create mode 100755 toolkit/mozapps/installer/windows/nsis/common.nsh create mode 100644 toolkit/mozapps/installer/windows/nsis/locale-fonts.nsh create mode 100644 toolkit/mozapps/installer/windows/nsis/locale-rtl.nlf create mode 100644 toolkit/mozapps/installer/windows/nsis/locale.nlf create mode 100755 toolkit/mozapps/installer/windows/nsis/locales.nsi create mode 100755 toolkit/mozapps/installer/windows/nsis/makensis.mk create mode 100755 toolkit/mozapps/installer/windows/nsis/overrides.nsh create mode 100644 toolkit/mozapps/installer/windows/nsis/preprocess-locale.py create mode 100644 toolkit/mozapps/installer/windows/nsis/setup.ico create mode 100644 toolkit/mozapps/notificationserver/NotificationCallback.cpp create mode 100644 toolkit/mozapps/notificationserver/NotificationCallback.h create mode 100644 toolkit/mozapps/notificationserver/NotificationComServer.cpp create mode 100644 toolkit/mozapps/notificationserver/NotificationFactory.cpp create mode 100644 toolkit/mozapps/notificationserver/NotificationFactory.h create mode 100644 toolkit/mozapps/notificationserver/moz.build create mode 100644 toolkit/mozapps/notificationserver/notificationserver.def create mode 100644 toolkit/mozapps/preferences/changemp.js create mode 100644 toolkit/mozapps/preferences/changemp.xhtml create mode 100644 toolkit/mozapps/preferences/fontbuilder.js create mode 100644 toolkit/mozapps/preferences/jar.mn create mode 100644 toolkit/mozapps/preferences/moz.build create mode 100644 toolkit/mozapps/preferences/removemp.js create mode 100644 toolkit/mozapps/preferences/removemp.xhtml create mode 100644 toolkit/mozapps/update/AppUpdater.sys.mjs create mode 100644 toolkit/mozapps/update/BackgroundTask_backgroundupdate.sys.mjs create mode 100644 toolkit/mozapps/update/BackgroundUpdate.sys.mjs create mode 100644 toolkit/mozapps/update/UpdateListener.sys.mjs create mode 100644 toolkit/mozapps/update/UpdateLog.sys.mjs create mode 100644 toolkit/mozapps/update/UpdateService.sys.mjs create mode 100644 toolkit/mozapps/update/UpdateServiceStub.sys.mjs create mode 100644 toolkit/mozapps/update/UpdateTelemetry.sys.mjs create mode 100644 toolkit/mozapps/update/common/certificatecheck.cpp create mode 100644 toolkit/mozapps/update/common/certificatecheck.h create mode 100644 toolkit/mozapps/update/common/commonupdatedir.cpp create mode 100644 toolkit/mozapps/update/common/commonupdatedir.h create mode 100644 toolkit/mozapps/update/common/moz.build create mode 100644 toolkit/mozapps/update/common/pathhash.cpp create mode 100644 toolkit/mozapps/update/common/pathhash.h create mode 100644 toolkit/mozapps/update/common/readstrings.cpp create mode 100644 toolkit/mozapps/update/common/readstrings.h create mode 100644 toolkit/mozapps/update/common/registrycertificates.cpp create mode 100644 toolkit/mozapps/update/common/registrycertificates.h create mode 100644 toolkit/mozapps/update/common/uachelper.cpp create mode 100644 toolkit/mozapps/update/common/uachelper.h create mode 100644 toolkit/mozapps/update/common/updatecommon.cpp create mode 100644 toolkit/mozapps/update/common/updatecommon.h create mode 100644 toolkit/mozapps/update/common/updatedefines.h create mode 100644 toolkit/mozapps/update/common/updatehelper.cpp create mode 100644 toolkit/mozapps/update/common/updatehelper.h create mode 100644 toolkit/mozapps/update/common/updatererrors.h create mode 100644 toolkit/mozapps/update/common/updateutils_win.cpp create mode 100644 toolkit/mozapps/update/common/updateutils_win.h create mode 100644 toolkit/mozapps/update/components.conf create mode 100644 toolkit/mozapps/update/content/history.js create mode 100644 toolkit/mozapps/update/content/history.xhtml create mode 100644 toolkit/mozapps/update/content/updateElevation.js create mode 100644 toolkit/mozapps/update/content/updateElevation.xhtml create mode 100644 toolkit/mozapps/update/docs/BackgroundUpdates.rst create mode 100644 toolkit/mozapps/update/docs/MaintenanceServiceTests.rst create mode 100644 toolkit/mozapps/update/docs/SettingUpAnUpdateServer.rst create mode 100644 toolkit/mozapps/update/docs/index.rst create mode 100644 toolkit/mozapps/update/jar.mn create mode 100644 toolkit/mozapps/update/metrics.yaml create mode 100644 toolkit/mozapps/update/moz.build create mode 100644 toolkit/mozapps/update/nsIUpdateService.idl create mode 100644 toolkit/mozapps/update/nsUpdateService.manifest create mode 100644 toolkit/mozapps/update/pings.yaml create mode 100644 toolkit/mozapps/update/tests/Makefile.in create mode 100644 toolkit/mozapps/update/tests/TestAUSHelper.cpp create mode 100644 toolkit/mozapps/update/tests/TestAUSReadStrings.cpp create mode 100644 toolkit/mozapps/update/tests/TestAUSReadStrings1.ini create mode 100644 toolkit/mozapps/update/tests/TestAUSReadStrings2.ini create mode 100644 toolkit/mozapps/update/tests/TestAUSReadStrings3.ini create mode 100644 toolkit/mozapps/update/tests/TestAUSReadStrings4.ini create mode 100644 toolkit/mozapps/update/tests/browser/browser.bits.toml create mode 100644 toolkit/mozapps/update/tests/browser/browser.toml create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_checking.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_and_install.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_download_failed.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_downloading.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_internal_error.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_no_update.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_ready_for_restart.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_AppUpdater_stop_swap.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staged.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_stagingFailure.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_notify.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloading_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_multiUpdate.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_apply_blocked.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_cantApply.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_malformedXML.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_noUpdate.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_otherInstance.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_unsupported.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadAuto_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_failure.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_network_offline.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_completeBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_complete.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_patch_partialBadSize_completeBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutDialog_internalError.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_backgroundUpdateSetting.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staged.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloaded_stagingFailure.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_downloading_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_bc_multiUpdate.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_apply_blocked.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_cantApply.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_malformedXML.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_noUpdate.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_otherInstance.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_check_unsupported.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadAuto_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_downloadOptIn_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_failure.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_network_offline.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_completeBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_complete.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_fc_patch_partialBadSize_completeBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_internalError.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_aboutPrefs_settings.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_cantApply.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_malformedXML.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_check_unsupported.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadAutoFailures_bgWin.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_bgWin.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloadOptIn_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_disableBITS.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_downloaded_staged.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_multiUpdate_promptWaitTime.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_completeBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_complete.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_bc_patch_partialBadSize_completeBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_completeApplyFailure.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_completeBadSize.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_doorhanger_sp_patch_partialApplyFailure_complete_staging.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_elevationDialog.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_memory_allocation_error_fallback.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_downloaded_ready.js create mode 100644 toolkit/mozapps/update/tests/browser/browser_telemetry_updatePing_staged_ready.js create mode 100644 toolkit/mozapps/update/tests/browser/downloadPage.html create mode 100644 toolkit/mozapps/update/tests/browser/head.js create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/browser.toml create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateFalse.js create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutDialog_fc_autoUpdateTrue.js create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateFalse.js create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_aboutPrefs_fc_autoUpdateTrue.js create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/browser_noBackgroundUpdate.js create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/config_manual_app_update_only.json create mode 100644 toolkit/mozapps/update/tests/browser/manual_app_update_only/head.js create mode 100644 toolkit/mozapps/update/tests/browser/testConstants.js create mode 100644 toolkit/mozapps/update/tests/data/app_update.sjs create mode 100644 toolkit/mozapps/update/tests/data/complete.exe create mode 100644 toolkit/mozapps/update/tests/data/complete.mar create mode 100644 toolkit/mozapps/update/tests/data/complete.png create mode 100644 toolkit/mozapps/update/tests/data/complete_log_success_mac create mode 100644 toolkit/mozapps/update/tests/data/complete_log_success_win create mode 100644 toolkit/mozapps/update/tests/data/complete_mac.mar create mode 100644 toolkit/mozapps/update/tests/data/complete_precomplete create mode 100644 toolkit/mozapps/update/tests/data/complete_precomplete_mac create mode 100644 toolkit/mozapps/update/tests/data/complete_removed-files create mode 100644 toolkit/mozapps/update/tests/data/complete_removed-files_mac create mode 100644 toolkit/mozapps/update/tests/data/complete_update_manifest create mode 100644 toolkit/mozapps/update/tests/data/old_version.mar create mode 100644 toolkit/mozapps/update/tests/data/partial.exe create mode 100644 toolkit/mozapps/update/tests/data/partial.mar create mode 100644 toolkit/mozapps/update/tests/data/partial.png create mode 100644 toolkit/mozapps/update/tests/data/partial_log_failure_mac create mode 100644 toolkit/mozapps/update/tests/data/partial_log_failure_win create mode 100644 toolkit/mozapps/update/tests/data/partial_log_success_mac create mode 100644 toolkit/mozapps/update/tests/data/partial_log_success_win create mode 100644 toolkit/mozapps/update/tests/data/partial_mac.mar create mode 100644 toolkit/mozapps/update/tests/data/partial_precomplete create mode 100644 toolkit/mozapps/update/tests/data/partial_precomplete_mac create mode 100644 toolkit/mozapps/update/tests/data/partial_removed-files create mode 100644 toolkit/mozapps/update/tests/data/partial_removed-files_mac create mode 100644 toolkit/mozapps/update/tests/data/partial_update_manifest create mode 100644 toolkit/mozapps/update/tests/data/replace_log_success create mode 100644 toolkit/mozapps/update/tests/data/shared.js create mode 100644 toolkit/mozapps/update/tests/data/sharedUpdateXML.js create mode 100644 toolkit/mozapps/update/tests/data/simple.mar create mode 100644 toolkit/mozapps/update/tests/data/syncManagerTestChild.js create mode 100644 toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js create mode 100644 toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js create mode 100644 toolkit/mozapps/update/tests/diff_base_service.bash create mode 100644 toolkit/mozapps/update/tests/marionette/marionette.toml create mode 100644 toolkit/mozapps/update/tests/marionette/test_no_window_update_restart.py create mode 100644 toolkit/mozapps/update/tests/moz.build create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/ausReadStrings.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/backgroundUpdateTaskInternalUpdater.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesBackgroundTask.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/disableBackgroundUpdatesNonBackgroundTask.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedNoRecovery.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedOffline.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/ensureExperimentToRolloutTransitionPerformed.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/head_update.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/languagePackUpdates.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/multiUpdate.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/onlyDownloadUpdatesThisSession.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/perInstallationPrefs.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/testConstants.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/updateAutoPrefMigrate.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/updateCheckCombine.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/updateDirectoryMigrate.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/updateSyncManager.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/verifyChannelPrefsFile.js create mode 100644 toolkit/mozapps/update/tests/unit_aus_update/xpcshell.toml create mode 100644 toolkit/mozapps/update/tests/unit_background_update/head.js create mode 100644 toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_exitcodes.js create mode 100644 toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_glean.js create mode 100644 toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason.js create mode 100644 toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_schedule.js create mode 100644 toolkit/mozapps/update/tests/unit_background_update/test_backgroundupdate_reason_update.js create mode 100644 toolkit/mozapps/update/tests/unit_background_update/xpcshell.toml create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/head_update.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSkippedWriteAccess_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppInUseBackgroundTaskFailure_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marCallbackUmask_unix.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettings.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marMissingUpdateSettingsStage.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marPIDPersistsSuccessComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartialWhileBackgroundTaskRunning.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/marWrongChannelStage.js create mode 100644 toolkit/mozapps/update/tests/unit_base_updater/xpcshell.toml create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/fallbackOnSvcFailure.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/head_update.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathSuffixFailureSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marAppInUseBackgroundTaskFailureSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js create mode 100644 toolkit/mozapps/update/tests/unit_service_updater/xpcshell.toml create mode 100644 toolkit/mozapps/update/updater/Launchd.plist create mode 100644 toolkit/mozapps/update/updater/Makefile.in create mode 100644 toolkit/mozapps/update/updater/TsanOptions.cpp create mode 100644 toolkit/mozapps/update/updater/archivereader.cpp create mode 100644 toolkit/mozapps/update/updater/archivereader.h create mode 100644 toolkit/mozapps/update/updater/autograph_stage.pem create mode 100644 toolkit/mozapps/update/updater/bspatch/LICENSE create mode 100644 toolkit/mozapps/update/updater/bspatch/bspatch.cpp create mode 100644 toolkit/mozapps/update/updater/bspatch/bspatch.h create mode 100644 toolkit/mozapps/update/updater/bspatch/moz.build create mode 100644 toolkit/mozapps/update/updater/bspatch/moz.yaml create mode 100644 toolkit/mozapps/update/updater/crctable.h create mode 100644 toolkit/mozapps/update/updater/dep1.der create mode 100644 toolkit/mozapps/update/updater/dep2.der create mode 100644 toolkit/mozapps/update/updater/gen_cert_header.py create mode 100644 toolkit/mozapps/update/updater/launchchild_osx.mm create mode 100644 toolkit/mozapps/update/updater/loaddlls.cpp create mode 100644 toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in create mode 100644 toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in create mode 100644 toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib create mode 100644 toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib create mode 100644 toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns create mode 100644 toolkit/mozapps/update/updater/module.ver create mode 100644 toolkit/mozapps/update/updater/moz.build create mode 100644 toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der create mode 100644 toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der create mode 100644 toolkit/mozapps/update/updater/progressui.h create mode 100644 toolkit/mozapps/update/updater/progressui_gtk.cpp create mode 100644 toolkit/mozapps/update/updater/progressui_null.cpp create mode 100644 toolkit/mozapps/update/updater/progressui_osx.mm create mode 100644 toolkit/mozapps/update/updater/progressui_win.cpp create mode 100644 toolkit/mozapps/update/updater/release_primary.der create mode 100644 toolkit/mozapps/update/updater/release_secondary.der create mode 100644 toolkit/mozapps/update/updater/resource.h create mode 100644 toolkit/mozapps/update/updater/updater-common.build create mode 100644 toolkit/mozapps/update/updater/updater-dep/moz.build create mode 100644 toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in create mode 100644 toolkit/mozapps/update/updater/updater-xpcshell/moz.build create mode 100644 toolkit/mozapps/update/updater/updater.cpp create mode 100644 toolkit/mozapps/update/updater/updater.exe.comctl32.manifest create mode 100644 toolkit/mozapps/update/updater/updater.exe.manifest create mode 100644 toolkit/mozapps/update/updater/updater.ico create mode 100644 toolkit/mozapps/update/updater/updater.png create mode 100644 toolkit/mozapps/update/updater/updater.rc create mode 100644 toolkit/mozapps/update/updater/xpcshellCertificate.der (limited to 'toolkit/mozapps') diff --git a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs new file mode 100644 index 0000000000..c727a55997 --- /dev/null +++ b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs @@ -0,0 +1,456 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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/. */ + +import { EXIT_CODE as EXIT_CODE_BASE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs"; +import { AppConstants as AC } from "resource://gre/modules/AppConstants.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const EXIT_CODE = { + ...EXIT_CODE_BASE, + DISABLED_BY_POLICY: EXIT_CODE_BASE.LAST_RESERVED + 1, + INVALID_ARGUMENT: EXIT_CODE_BASE.LAST_RESERVED + 2, + MUTEX_NOT_LOCKABLE: EXIT_CODE_BASE.LAST_RESERVED + 3, +}; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", + BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit + ShellService: "resource:///modules/ShellService.sys.mjs", +}); +XPCOMUtils.defineLazyServiceGetters(lazy, { + AlertsService: ["@mozilla.org/alerts-service;1", "nsIAlertsService"], +}); +ChromeUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + let consoleOptions = { + maxLogLevel: "error", + maxLogLevelPref: "app.defaultagent.loglevel", + prefix: "DefaultAgent", + }; + return new ConsoleAPI(consoleOptions); +}); + +// Should be slightly longer than NOTIFICATION_WAIT_TIMEOUT_MS in +// Notification.cpp (divided by 1000 to convert millseconds to seconds) to not +// cause race between timeouts. Currently 12 hours + 5 additional minutes. +export const backgroundTaskTimeoutSec = 12 * 60 * 60 + 60 * 5; +const kNotificationTimeoutMs = 12 * 60 * 60 * 1000; + +const kNotificationShown = Object.freeze({ + notShown: "not-shown", + shown: "shown", + error: "error", +}); + +const kNotificationAction = Object.freeze({ + dismissedByTimeout: "dismissed-by-timeout", + dismissedByButton: "dismissed-by-button", + dismissedToActionCenter: "dismissed-to-action-center", + makeFirefoxDefaultButton: "make-firefox-default-button", + toastClicked: "toast-clicked", + noAction: "no-action", +}); + +// We expect to be given a command string in argv[1], perhaps followed by other +// arguments depending on the command. The valid commands are: +// register-task [unique-token] +// Create a Windows scheduled task that will launch this binary with the +// do-task command every 24 hours, starting from 24 hours after register-task +// is run. unique-token is required and should be some string that uniquely +// identifies this installation of the product; typically this will be the +// install path hash that's used for the update directory, the AppUserModelID, +// and other related purposes. +// update-task [unique-token] +// Update an existing task registration, without changing its schedule. This +// should be called during updates of the application, in case this program +// has been updated and any of the task parameters have changed. The unique +// token argument is required and should be the same one that was passed in +// when the task was registered. +// unregister-task [unique-token] +// Removes the previously created task. The unique token argument is required +// and should be the same one that was passed in when the task was registered. +// uninstall [unique-token] +// Removes the previously created task, and also removes all registry entries +// running the task may have created. The unique token argument is required +// and should be the same one that was passed in when the task was registered. +// do-task [app-user-model-id] +// Actually performs the default agent task, which currently means generating +// and sending our telemetry ping and possibly showing a notification to the +// user if their browser has switched from Firefox to Edge with Blink. +// set-default-browser-user-choice [app-user-model-id] [[.file1 ProgIDRoot1] +// ...] +// Set the default browser via the UserChoice registry keys. Additional +// optional file extensions to register can be specified as additional +// argument pairs: the first element is the file extension, the second element +// is the root of a ProgID, which will be suffixed with `-$AUMI`. +export async function runBackgroundTask(commandLine) { + Services.fog.initializeFOG( + undefined, + "firefox.desktop.background.defaultagent" + ); + + let defaultAgent = Cc["@mozilla.org/default-agent;1"].getService( + Ci.nsIDefaultAgent + ); + + let command = commandLine.getArgument(0); + + // The uninstall and unregister commands are allowed even if the policy + // disabling the task is set, so that uninstalls and updates always work. + // Similarly, debug commands are always allowed. + switch (command) { + case "uninstall": { + let token = commandLine.getArgument(1); + lazy.log.info(`Uninstalling for token "${token}"`); + defaultAgent.uninstall(token); + return EXIT_CODE.SUCCESS; + } + case "unregister-task": { + let token = commandLine.getArgument(1); + lazy.log.info(`Unregistering task for token "${token}"`); + defaultAgent.unregisterTask(token); + return EXIT_CODE.SUCCESS; + } + } + + // We check for disablement by policy because that's assumed to be static. + // But we don't check for disablement by remote settings so that + // `register-task` and `update-task` can proceed as part of the update + // cycle, waiting for remote (re-)enablement. + if (defaultAgent.agentDisabled()) { + lazy.log.warn("Default Agent disabled, exiting without running."); + return EXIT_CODE.DISABLED_BY_POLICY; + } + + switch (command) { + case "register-task": { + let token = commandLine.getArgument(1); + lazy.log.info(`Registering task for token "${token}"`); + defaultAgent.registerTask(token); + return EXIT_CODE.SUCCESS; + } + case "update-task": { + let token = commandLine.getArgument(1); + lazy.log.info(`Updating task for token "${token}"`); + defaultAgent.updateTask(token); + return EXIT_CODE.SUCCESS; + } + case "do-task": { + let aumid = commandLine.getArgument(1); + let force = commandLine.findFlag("force", true) != -1; + + lazy.log.info(`Running do-task with AUMID "${aumid}"`); + + let cppFallback = false; + try { + await lazy.BackgroundTasksUtils.enableNimbus(commandLine); + cppFallback = + lazy.NimbusFeatures.defaultAgent.getVariable("cppFallback"); + } catch (e) { + lazy.log.error(`Error enabling nimbus: ${e}`); + } + + try { + if (!cppFallback) { + lazy.log.info("Running JS do-task."); + await runWithRegistryLocked(async () => { + await doTask(defaultAgent, force); + }); + } else { + lazy.log.info("Running C++ do-task."); + defaultAgent.doTask(aumid, force); + } + } catch (e) { + if (e.message) { + lazy.log.error(e.message); + } + + if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + return EXIT_CODE.MUTEX_NOT_LOCKABLE; + } + + return EXIT_CODE.EXCEPTION; + } + + // Bug 1857333: We wait for arbitrary time for Glean to submit telemetry. + lazy.log.info("Pinged glean, waiting for submission."); + await new Promise(resolve => lazy.setTimeout(resolve, 5000)); + + return EXIT_CODE.SUCCESS; + } + } + + return EXIT_CODE.INVALID_ARGUMENT; +} + +// Throws if unable to lock mutex (therefore function isn't run). +async function runWithRegistryLocked(aMutexGuardedFunction) { + const kVendor = Services.appinfo.vendor || ""; + const kRegistryMutexName = `${kVendor}${AC.MOZ_APP_BASENAME}DefaultBrowserAgentRegistryMutex`; + let mutexFactory = Cc["@mozilla.org/windows-mutex-factory;1"].getService( + Ci.nsIWindowsMutexFactory + ); + + let mutex = mutexFactory.createMutex(kRegistryMutexName); + mutex.tryLock(kRegistryMutexName); + lazy.log.debug(`Locked named mutex: ${kRegistryMutexName}`); + try { + await aMutexGuardedFunction(); + } finally { + mutex.unlock(); + lazy.log.debug(`Unlocked named mutex: ${kRegistryMutexName}`); + } +} + +async function doTask(defaultAgent, force) { + if (!defaultAgent.appRanRecently() && !force) { + lazy.log.warn("Main app has not ran recently, exiting without running."); + throw new Error("App hasn't ran recently"); + } + + let browser = defaultAgent.getDefaultBrowser(); + lazy.log.debug(`Default browser: ${browser}`); + let previousBrowser = defaultAgent.getReplacePreviousDefaultBrowser(browser); + lazy.log.debug(`Previous browser: ${previousBrowser}`); + let defaultPdfHandler = defaultAgent.getDefaultPdfHandler(); + lazy.log.debug(`Default PDF Handler: ${defaultPdfHandler}`); + + let notificationTelemetry = { + shown: kNotificationShown.notShown, + action: kNotificationAction.noAction, + }; + if ((browser == "edge-chrome" && previousBrowser == "firefox") || force) { + lazy.log.info("Showing default browser intervention notification."); + + const alertName = "default_agent_intervention"; + let notification = showNotification(alertName); + let timeout = makeTimeout(alertName); + + notificationTelemetry = await Promise.race([notification, timeout]); + } + lazy.log.debug(`Notification telemetry: ${notificationTelemetry}`); + + if ( + notificationTelemetry.action == + kNotificationAction.makeFirefoxDefaultButton || + notificationTelemetry.action == kNotificationAction.toastClicked + ) { + await lazy.ShellService.setDefaultBrowser(false).catch(e => { + lazy.log.error(`setDefaultBrowser failed: ${e}`); + }); + } + + defaultAgent.sendPing( + browser, + previousBrowser, + defaultPdfHandler, + notificationTelemetry.shown, + notificationTelemetry.action + ); +} + +async function showNotification(name) { + let notificationTelemetry = { + shown: kNotificationShown.error, + action: kNotificationAction.noAction, + }; + + // Bug 1868714: We disable the notification server to defer on changes + // necessary for it to work with Background Tasks. + try { + lazy.log.debug("Disabling notification server."); + Services.prefs.setBoolPref( + "alerts.useSystemBackend.windows.notificationserver.enabled", + false + ); + + const l10n = new Localization([ + "branding/brand.ftl", + // Background tasks are only used in a context where browser refs are + // present; that it's in toolkit instead of browser is a historical + // artifact of the default agent having previously been a + // standalone application. + // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit + "browser/backgroundtasks/defaultagent.ftl", + ]); + let [title, body, yesButtonText, noButtonText] = await l10n.formatValues([ + { id: "default-browser-notification-header-text" }, + { id: "default-browser-notification-body-text" }, + { id: "default-browser-notification-yes-button-text" }, + { id: "default-browser-notification-no-button-text" }, + ]); + + let yesAction = "yes-action"; + let noAction = "no-action"; + + let alert = makeAlert({ + name, + title, + body, + actions: [ + { + action: yesAction, + title: yesButtonText, + }, + { + action: noAction, + title: noButtonText, + }, + ], + }); + + const { observer, shownPromise } = makeObserver({ yesAction, noAction }); + + lazy.AlertsService.showAlert(alert, observer); + + notificationTelemetry = await shownPromise.promise; + } catch (e) { + if (e.message) { + lazy.log.error(e.message); + } + } finally { + // Reset the pref so we can assume the default value in the future. + lazy.log.debug("Reenabling notification server."); + Services.prefs.clearUserPref( + "alerts.useSystemBackend.windows.notificationserver.enabled" + ); + } + + return notificationTelemetry; +} + +function makeAlert(options) { + let winalert = Cc["@mozilla.org/windows-alert-notification;1"].createInstance( + Ci.nsIWindowsAlertNotification + ); + winalert.handleActions = true; + winalert.imagePlacement = winalert.eIcon; + + let alert = winalert.QueryInterface(Ci.nsIAlertNotification); + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + alert.init( + options.name, + "chrome://branding/content/about-logo@2x.png", + options.title, + options.body, + true /* aTextClickable */, + null /* aCookie */, + null /* aDir */, + null /* aLang */, + null /* aData */, + systemPrincipal, + null /* aInPrivateBrowsing */, + true /* aRequireInteraction */ + ); + + alert.actions = options.actions; + + return alert; +} + +function makeObserver(actions) { + let shownPromise = Promise.withResolvers(); + + // We'll receive multiple callbacks which individually might indicate an + // interaction. Only log the first one to disambiguate and reduce noise. + let firstInteraction = true; + let logFirstInteraction = message => { + if (firstInteraction) { + lazy.log.debug(message); + firstInteraction = false; + } + }; + + let observer = (subject, topic, data) => { + switch (topic) { + case "alertactioncallback": + switch (data) { + case actions.yesAction: + logFirstInteraction( + 'Notification "yes" button clicked, setting default browser.' + ); + shownPromise.resolve({ + shown: kNotificationShown.shown, + action: kNotificationAction.makeFirefoxDefaultButton, + }); + break; + case actions.noAction: + logFirstInteraction("Notification dismissed by button."); + shownPromise.resolve({ + shown: kNotificationShown.shown, + action: kNotificationAction.dismissedByButton, + }); + break; + default: + lazy.log.error(`Unrecognized notification action ${data}`); + throw new Error(`Unexpected notification action received: ${data}`); + } + break; + case "alertclickcallback": + logFirstInteraction( + "Notification body clicked, setting default browser." + ); + shownPromise.resolve({ + shown: kNotificationShown.shown, + action: kNotificationAction.toastClicked, + }); + break; + case "alerterror": + lazy.log.error("Error showing notification."); + shownPromise.resolve({ + shown: kNotificationShown.error, + action: kNotificationAction.noAction, + }); + break; + case "alertfinished": + logFirstInteraction("Notification dismissed from action center."); + shownPromise.resolve({ + shown: kNotificationShown.shown, + action: kNotificationAction.dismissedToActionCenter, + }); + break; + } + }; + + return { observer, shownPromise }; +} + +function makeTimeout(alertName) { + return new Promise(resolve => { + // If the notification hasn't been activated or dismissed within 12 hours, + // stop waiting for it. + let timeoutMs = kNotificationTimeoutMs; + + // Allow overriding the notification timeout fron an environment variable. + const envTimeoutKey = "MOZ_NOTIFICATION_TIMEOUT_MS"; + if (Services.env.exists(envTimeoutKey)) { + let envTimeoutValue = Services.env.get(envTimeoutKey); + if (!isNaN(envTimeoutValue)) { + timeoutMs = Number(envTimeoutValue); + } else { + lazy.log.error( + `Environment variable ${envTimeoutKey}=${envTimeoutValue} is not a number.` + ); + } + } + lazy.log.info(`Registering notification timeout in ${timeoutMs}ms`); + + lazy.setTimeout(() => { + lazy.log.warn(`Notification timed out after ${timeoutMs}ms`); + + lazy.AlertsService.closeAlert(alertName); + + resolve({ + shown: kNotificationShown.shown, + action: kNotificationAction.dismissedByTimeout, + }); + }, timeoutMs); + }); +} diff --git a/toolkit/mozapps/defaultagent/Cache.cpp b/toolkit/mozapps/defaultagent/Cache.cpp new file mode 100644 index 0000000000..1a323e54d9 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Cache.cpp @@ -0,0 +1,594 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "Cache.h" + +#include + +#include "common.h" +#include "EventLog.h" +#include "mozilla/Unused.h" + +namespace mozilla::default_agent { + +// Cache entry version documentation: +// Version 1: +// The version number is written explicitly when version 1 cache entries are +// migrated, but in their original location there is no version key. +// Required Keys: +// CacheEntryVersion: +// NotificationType: +// NotificationShown: +// NotificationAction: +// Version 2: +// Required Keys: +// CacheEntryVersion: +// NotificationType: +// NotificationShown: +// NotificationAction: +// PrevNotificationAction: + +static std::wstring MakeVersionedRegSubKey(const wchar_t* baseKey) { + std::wstring key; + if (baseKey) { + key = baseKey; + } else { + key = Cache::kDefaultPingCacheRegKey; + } + key += L"\\version"; + key += std::to_wstring(Cache::kVersion); + return key; +} + +Cache::Cache(const wchar_t* cacheRegKey /* = nullptr */) + : mCacheRegKey(MakeVersionedRegSubKey(cacheRegKey)), + mInitializeResult(mozilla::Nothing()), + mCapacity(Cache::kDefaultCapacity), + mFront(0), + mSize(0) {} + +Cache::~Cache() {} + +VoidResult Cache::Init() { + if (mInitializeResult.isSome()) { + HRESULT hr = mInitializeResult.value(); + if (FAILED(hr)) { + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } else { + return mozilla::Ok(); + } + } + + VoidResult result = SetupCache(); + if (result.isErr()) { + HRESULT hr = result.inspectErr().AsHResult(); + mInitializeResult = mozilla::Some(hr); + return result; + } + + // At this point, the cache is ready to use, so mark the initialization as + // complete. This is important so that when we attempt migration, below, + // the migration's attempts to write to the cache don't try to initialize + // the cache again. + mInitializeResult = mozilla::Some(S_OK); + + // Ignore the result of the migration. If we failed to migrate, there may be + // some data loss. But that's better than failing to ever use the new cache + // just because there's something wrong with the old one. + mozilla::Unused << MaybeMigrateVersion1(); + + return mozilla::Ok(); +} + +// If the setting does not exist, the default value is written and returned. +DwordResult Cache::EnsureDwordSetting(const wchar_t* regName, + uint32_t defaultValue) { + MaybeDwordResult readResult = RegistryGetValueDword( + IsPrefixed::Unprefixed, regName, mCacheRegKey.c_str()); + if (readResult.isErr()) { + HRESULT hr = readResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to read setting \"%s\": %#X", regName, hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + mozilla::Maybe maybeValue = readResult.unwrap(); + if (maybeValue.isSome()) { + return maybeValue.value(); + } + + VoidResult writeResult = RegistrySetValueDword( + IsPrefixed::Unprefixed, regName, defaultValue, mCacheRegKey.c_str()); + if (writeResult.isErr()) { + HRESULT hr = writeResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to write setting \"%s\": %#X", regName, hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + return defaultValue; +} + +// This function does two things: +// 1. It creates and sets the registry values used by the cache, if they don't +// already exist. +// 2. If the the values already existed, it reads the settings of the cache +// into their member variables. +VoidResult Cache::SetupCache() { + DwordResult result = + EnsureDwordSetting(Cache::kCapacityRegName, Cache::kDefaultCapacity); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + mCapacity = std::min(result.unwrap(), Cache::kMaxCapacity); + + result = EnsureDwordSetting(Cache::kFrontRegName, 0); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + mFront = std::min(result.unwrap(), Cache::kMaxCapacity - 1); + + result = EnsureDwordSetting(Cache::kSizeRegName, 0); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + mSize = std::min(result.unwrap(), mCapacity); + + return mozilla::Ok(); +} + +static MaybeStringResult ReadVersion1CacheKey(const wchar_t* baseRegKeyName, + uint32_t index) { + std::wstring regName = Cache::kVersion1KeyPrefix; + regName += baseRegKeyName; + regName += std::to_wstring(index); + + MaybeStringResult result = + RegistryGetValueString(IsPrefixed::Unprefixed, regName.c_str()); + if (result.isErr()) { + HRESULT hr = result.inspectErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to read \"%s\": %#X", regName.c_str(), hr); + } + return result; +} + +static VoidResult DeleteVersion1CacheKey(const wchar_t* baseRegKeyName, + uint32_t index) { + std::wstring regName = Cache::kVersion1KeyPrefix; + regName += baseRegKeyName; + regName += std::to_wstring(index); + + VoidResult result = + RegistryDeleteValue(IsPrefixed::Unprefixed, regName.c_str()); + if (result.isErr()) { + HRESULT hr = result.inspectErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to delete \"%s\": %#X", regName.c_str(), hr); + } + return result; +} + +static VoidResult DeleteVersion1CacheEntry(uint32_t index) { + VoidResult typeResult = + DeleteVersion1CacheKey(Cache::kNotificationTypeKey, index); + VoidResult shownResult = + DeleteVersion1CacheKey(Cache::kNotificationShownKey, index); + VoidResult actionResult = + DeleteVersion1CacheKey(Cache::kNotificationActionKey, index); + + if (typeResult.isErr()) { + return typeResult; + } + if (shownResult.isErr()) { + return shownResult; + } + return actionResult; +} + +VoidResult Cache::MaybeMigrateVersion1() { + for (uint32_t index = 0; index < Cache::kVersion1MaxSize; ++index) { + MaybeStringResult typeResult = + ReadVersion1CacheKey(Cache::kNotificationTypeKey, index); + if (typeResult.isErr()) { + return mozilla::Err(typeResult.unwrapErr()); + } + MaybeString maybeType = typeResult.unwrap(); + + MaybeStringResult shownResult = + ReadVersion1CacheKey(Cache::kNotificationShownKey, index); + if (shownResult.isErr()) { + return mozilla::Err(shownResult.unwrapErr()); + } + MaybeString maybeShown = shownResult.unwrap(); + + MaybeStringResult actionResult = + ReadVersion1CacheKey(Cache::kNotificationActionKey, index); + if (actionResult.isErr()) { + return mozilla::Err(actionResult.unwrapErr()); + } + MaybeString maybeAction = actionResult.unwrap(); + + if (maybeType.isSome() && maybeShown.isSome() && maybeAction.isSome()) { + // If something goes wrong, we'd rather lose a little data than migrate + // over and over again. So delete the old entry before we add the new one. + VoidResult result = DeleteVersion1CacheEntry(index); + if (result.isErr()) { + return result; + } + + VersionedEntry entry = VersionedEntry{ + .entryVersion = 1, + .notificationType = maybeType.value(), + .notificationShown = maybeShown.value(), + .notificationAction = maybeAction.value(), + .prevNotificationAction = mozilla::Nothing(), + }; + result = VersionedEnqueue(entry); + if (result.isErr()) { + // We already deleted the version 1 cache entry. No real reason to abort + // now. May as well keep attempting to migrate. + LOG_ERROR_MESSAGE(L"Warning: Version 1 cache entry %u dropped: %#X", + index, result.unwrapErr().AsHResult()); + } + } else if (maybeType.isNothing() && maybeShown.isNothing() && + maybeAction.isNothing()) { + // Looks like we've reached the end of the version 1 cache. + break; + } else { + // This cache entry seems to be missing a key. Just drop it. + LOG_ERROR_MESSAGE( + L"Warning: Version 1 cache entry %u dropped due to missing keys", + index); + mozilla::Unused << DeleteVersion1CacheEntry(index); + } + } + return mozilla::Ok(); +} + +std::wstring Cache::MakeEntryRegKeyName(uint32_t index) { + std::wstring regName = mCacheRegKey; + regName += L'\\'; + regName += std::to_wstring(index); + return regName; +} + +VoidResult Cache::WriteEntryKeys(uint32_t index, const VersionedEntry& entry) { + std::wstring subKey = MakeEntryRegKeyName(index); + + VoidResult result = + RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kEntryVersionKey, + entry.entryVersion, subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write entry version to index %u: %#X", index, + result.inspectErr().AsHResult()); + return result; + } + + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kNotificationTypeKey, + entry.notificationType.c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X", + index, result.inspectErr().AsHResult()); + return result; + } + + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kNotificationShownKey, + entry.notificationShown.c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write notification shown to index %u: %#X", + index, result.inspectErr().AsHResult()); + return result; + } + + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kNotificationActionKey, + entry.notificationAction.c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to write notification type to index %u: %#X", + index, result.inspectErr().AsHResult()); + return result; + } + + if (entry.prevNotificationAction.isSome()) { + result = RegistrySetValueString( + IsPrefixed::Unprefixed, Cache::kPrevNotificationActionKey, + entry.prevNotificationAction.value().c_str(), subKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE( + L"Unable to write prev notification type to index %u: %#X", index, + result.inspectErr().AsHResult()); + return result; + } + } + + return mozilla::Ok(); +} + +// Returns success on an attempt to delete a non-existent entry. +VoidResult Cache::DeleteEntry(uint32_t index) { + std::wstring key = AGENT_REGKEY_NAME; + key += L'\\'; + key += MakeEntryRegKeyName(index); + // We could probably just delete they key here, rather than use this function, + // which deletes keys recursively. But this mechanism allows future entry + // versions to contain sub-keys without causing problems for older versions. + LSTATUS ls = RegDeleteTreeW(HKEY_CURRENT_USER, key.c_str()); + if (ls != ERROR_SUCCESS && ls != ERROR_FILE_NOT_FOUND) { + return mozilla::Err(mozilla::WindowsError::FromWin32Error(ls)); + } + return mozilla::Ok(); +} + +VoidResult Cache::SetFront(uint32_t newFront) { + VoidResult result = + RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kFrontRegName, + newFront, mCacheRegKey.c_str()); + if (result.isOk()) { + mFront = newFront; + } + return result; +} + +VoidResult Cache::SetSize(uint32_t newSize) { + VoidResult result = + RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kSizeRegName, + newSize, mCacheRegKey.c_str()); + if (result.isOk()) { + mSize = newSize; + } + return result; +} + +// The entry passed to this function MUST already be valid. This function does +// not do any validation internally. We must not, for example, pass an entry +// to it with a version of 2 and a prevNotificationAction of mozilla::Nothing() +// because a version 2 entry requires that key. +VoidResult Cache::VersionedEnqueue(const VersionedEntry& entry) { + VoidResult result = Init(); + if (result.isErr()) { + return result; + } + + if (mSize >= mCapacity) { + LOG_ERROR_MESSAGE(L"Attempted to add an entry to the cache, but it's full"); + return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS)); + } + + uint32_t index = (mFront + mSize) % mCapacity; + + // We really don't want to write to a location that has stale cache entry data + // already lying around. + result = DeleteEntry(index); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Unable to remove stale entry: %#X", + result.inspectErr().AsHResult()); + return result; + } + + result = WriteEntryKeys(index, entry); + if (result.isErr()) { + // We might have written a partial key. Attempt to clean up after ourself. + mozilla::Unused << DeleteEntry(index); + return result; + } + + result = SetSize(mSize + 1); + if (result.isErr()) { + // If we failed to write the size, the new entry was not added successfully. + // Attempt to clean up after ourself. + mozilla::Unused << DeleteEntry(index); + return result; + } + + return mozilla::Ok(); +} + +VoidResult Cache::Enqueue(const Cache::Entry& entry) { + Cache::VersionedEntry vEntry = Cache::VersionedEntry{ + .entryVersion = Cache::kEntryVersion, + .notificationType = entry.notificationType, + .notificationShown = entry.notificationShown, + .notificationAction = entry.notificationAction, + .prevNotificationAction = mozilla::Some(entry.prevNotificationAction), + }; + return VersionedEnqueue(vEntry); +} + +VoidResult Cache::DiscardFront() { + if (mSize < 1) { + LOG_ERROR_MESSAGE(L"Attempted to discard entry from an empty cache"); + return mozilla::Err(mozilla::WindowsError::FromHResult(E_BOUNDS)); + } + // It's not a huge deal if we can't delete this. Moving mFront will result in + // it being excluded from the cache anyways. We'll try to delete it again + // anyways if we try to write to this index again. + mozilla::Unused << DeleteEntry(mFront); + + VoidResult result = SetSize(mSize - 1); + // We don't really need to bother moving mFront to the next index if the cache + // is empty. + if (result.isErr() || mSize == 0) { + return result; + } + result = SetFront((mFront + 1) % mCapacity); + if (result.isErr()) { + // If we failed to set the front after we set the size, the cache is + // in an inconsistent state. + // But, even if the cache is inconsistent, we'll likely lose some data, but + // we should eventually be able to recover. Any expected entries with no + // data will be discarded and any unexpected entries with data will be + // cleared out before we write data there. + LOG_ERROR_MESSAGE(L"Cache inconsistent: Updated Size but not Front: %#X", + result.inspectErr().AsHResult()); + } + return result; +} + +/** + * This function reads a DWORD cache key's value and returns it. If the expected + * argument is true and the key is missing, this will delete the entire entry + * and return mozilla::Nothing(). + */ +MaybeDwordResult Cache::ReadEntryKeyDword(const std::wstring& regKey, + const wchar_t* regName, + bool expected) { + MaybeDwordResult result = + RegistryGetValueDword(IsPrefixed::Unprefixed, regName, regKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName, + regKey.c_str(), result.inspectErr().AsHResult()); + return mozilla::Err(result.unwrapErr()); + } + MaybeDword maybeValue = result.unwrap(); + if (expected && maybeValue.isNothing()) { + LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName, + regKey.c_str()); + VoidResult result = DiscardFront(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + } + return maybeValue; +} + +/** + * This function reads a string cache key's value and returns it. If the + * expected argument is true and the key is missing, this will delete the entire + * entry and return mozilla::Nothing(). + */ +MaybeStringResult Cache::ReadEntryKeyString(const std::wstring& regKey, + const wchar_t* regName, + bool expected) { + MaybeStringResult result = + RegistryGetValueString(IsPrefixed::Unprefixed, regName, regKey.c_str()); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Failed to read \"%s\" from \"%s\": %#X", regName, + regKey.c_str(), result.inspectErr().AsHResult()); + return mozilla::Err(result.unwrapErr()); + } + MaybeString maybeValue = result.unwrap(); + if (expected && maybeValue.isNothing()) { + LOG_ERROR_MESSAGE(L"Missing expected value \"%s\" from \"%s\"", regName, + regKey.c_str()); + VoidResult result = DiscardFront(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + } + return maybeValue; +} + +Cache::MaybeEntryResult Cache::Dequeue() { + VoidResult result = Init(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + + std::wstring subKey = MakeEntryRegKeyName(mFront); + + // We are going to read within a loop so that if we find incomplete entries, + // we can just discard them and try to read the next entry. We'll put a limit + // on the maximum number of times this loop can possibly run so that if + // something goes horribly wrong, we don't loop forever. If we exit this loop + // without returning, it means that not only were we not able to read + // anything, but something very unexpected happened. + // We are going to potentially loop over this mCapacity + 1 times so that if + // we end up discarding every item in the cache, we return mozilla::Nothing() + // rather than an error. + for (uint32_t i = 0; i <= mCapacity; ++i) { + if (mSize == 0) { + return MaybeEntry(mozilla::Nothing()); + } + + Cache::VersionedEntry entry; + + // CacheEntryVersion + MaybeDwordResult dResult = + ReadEntryKeyDword(subKey, Cache::kEntryVersionKey, true); + if (dResult.isErr()) { + return mozilla::Err(dResult.unwrapErr()); + } + MaybeDword maybeDValue = dResult.unwrap(); + if (maybeDValue.isNothing()) { + // Note that we only call continue in this function after DiscardFront() + // has been called (either directly, or by one of the ReadEntryKey.* + // functions). So the continue call results in attempting to read the + // next entry in the cache. + continue; + } + entry.entryVersion = maybeDValue.value(); + if (entry.entryVersion < 1) { + LOG_ERROR_MESSAGE(L"Invalid entry version of %u in \"%s\"", + entry.entryVersion, subKey.c_str()); + VoidResult result = DiscardFront(); + if (result.isErr()) { + return mozilla::Err(result.unwrapErr()); + } + continue; + } + + // NotificationType + MaybeStringResult sResult = + ReadEntryKeyString(subKey, Cache::kNotificationTypeKey, true); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + MaybeString maybeSValue = sResult.unwrap(); + if (maybeSValue.isNothing()) { + continue; + } + entry.notificationType = maybeSValue.value(); + + // NotificationShown + sResult = ReadEntryKeyString(subKey, Cache::kNotificationShownKey, true); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + maybeSValue = sResult.unwrap(); + if (maybeSValue.isNothing()) { + continue; + } + entry.notificationShown = maybeSValue.value(); + + // NotificationAction + sResult = ReadEntryKeyString(subKey, Cache::kNotificationActionKey, true); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + maybeSValue = sResult.unwrap(); + if (maybeSValue.isNothing()) { + continue; + } + entry.notificationAction = maybeSValue.value(); + + // PrevNotificationAction + bool expected = + entry.entryVersion >= Cache::kInitialVersionPrevNotificationActionKey; + sResult = + ReadEntryKeyString(subKey, Cache::kPrevNotificationActionKey, expected); + if (sResult.isErr()) { + return mozilla::Err(sResult.unwrapErr()); + } + maybeSValue = sResult.unwrap(); + if (expected && maybeSValue.isNothing()) { + continue; + } + entry.prevNotificationAction = maybeSValue; + + // We successfully read the entry. Now we need to remove it from the cache. + VoidResult result = DiscardFront(); + if (result.isErr()) { + // If we aren't able to remove the entry from the cache, don't return it. + // We don't want to return the same item over and over again if we get + // into a bad state. + return mozilla::Err(result.unwrapErr()); + } + + return mozilla::Some(entry); + } + + LOG_ERROR_MESSAGE(L"Unexpected: This line shouldn't be reached"); + return mozilla::Err(mozilla::WindowsError::FromHResult(E_FAIL)); +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/Cache.h b/toolkit/mozapps/defaultagent/Cache.h new file mode 100644 index 0000000000..1deacb17df --- /dev/null +++ b/toolkit/mozapps/defaultagent/Cache.h @@ -0,0 +1,189 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_CACHE_H__ +#define __DEFAULT_BROWSER_AGENT_CACHE_H__ + +#include +#include +#include + +#include "Registry.h" + +namespace mozilla::default_agent { + +using DwordResult = mozilla::WindowsErrorResult; + +/** + * This cache functions as a FIFO queue which writes its data to the Windows + * registry. + * + * Note that the cache is not thread-safe, so it is recommended that the WDBA's + * RegistryMutex be acquired before accessing it. + * + * Some of the terminology used in this module is a easy to mix up, so let's + * just be clear about it: + * - registry key/sub-key + * A registry key is sort of like the registry's equivalent of a + * directory. It can contain values, each of which is made up of a name + * and corresponding data. We may also refer to a "sub-key", meaning a + * registry key nested in a registry key. + * - cache key/entry key + * A cache key refers to the string that we use to look up a single + * element of cache entry data. Example: "CacheEntryVersion" + * - entry + * This refers to an entire record stored using Cache::Enqueue or retrieved + * using Cache::Dequeue. It consists of numerous cache keys and their + * corresponding data. + * + * The first version of this cache was problematic because of how hard it was to + * extend. This version attempts to overcome this. It first migrates all data + * out of the version 1 cache. This means that the stored ping data will not + * be accessible to out-of-date clients, but presumably they will eventually + * be updated or the up-to-date client that performed the migration will send + * the pings itself. Because the WDBA telemetry has no client ID, all analysis + * is stateless, so even if the other clients send some pings before the stored + * ones get sent, that's ok. The ordering isn't really important. + * + * This version of the cache attempts to correct the problem of how hard it was + * to extend the old cache. The biggest problem that the old cache had was that + * when it dequeued data it had to shift data, but it wouldn't shift keys that + * it didn't know about, causing them to become associated with the wrong cache + * entries. + * + * Version 2 of the cache will make 4 improvements to attempt to avoid problems + * like this in the future: + * 1. Each cache entry will get its own registry key. This will help to keep + * cache entries isolated from each other. + * 2. Each cache entry will include version data so that we know what cache + * keys to expect when we read it. + * 3. Rather than having to shift every entry every time we dequeue, we will + * implement a circular queue so that we just have to update what index + * currently represents the front + * 4. We will store the cache capacity in the cache so that we can expand the + * cache later, if we want, without breaking previous versions. + */ +class Cache { + public: + // cacheRegKey is the registry sub-key that the cache will be stored in. If + // null is passed (the default), we will use the default cache name. This is + // what ought to be used in production. When testing, we will pass a different + // key in so that our testing caches don't conflict with each other or with + // a possible production cache on the test machine. + explicit Cache(const wchar_t* cacheRegKey = nullptr); + ~Cache(); + + // The version of the cache (not to be confused with the version of the cache + // entries). This should only be incremented if we need to make breaking + // changes that require migration to a new cache location, like we did between + // versions 1 and 2. This value will be used as part of the sub-key that the + // cache is stored in (ex: "PingCache\version2"). + static constexpr const uint32_t kVersion = 2; + // This value will be written into each entry. This allows us to know what + // cache keys to expect in the event that additional cache keys are added in + // later entry versions. + static constexpr const uint32_t kEntryVersion = 2; + static constexpr const uint32_t kDefaultCapacity = 2; + // We want to allow the cache to be expandable, but we don't really want it to + // be infinitely expandable. So we'll set an upper bound. + static constexpr const uint32_t kMaxCapacity = 100; + static constexpr const wchar_t* kDefaultPingCacheRegKey = L"PingCache"; + + // Used to read the version 1 cache entries during data migration. Full cache + // key names are formatted like: "" + // For example: "PingCacheNotificationType0" + static constexpr const wchar_t* kVersion1KeyPrefix = L"PingCache"; + static constexpr const uint32_t kVersion1MaxSize = 2; + + static constexpr const wchar_t* kCapacityRegName = L"Capacity"; + static constexpr const wchar_t* kFrontRegName = L"Front"; + static constexpr const wchar_t* kSizeRegName = L"Size"; + + // Cache Entry keys + static constexpr const wchar_t* kEntryVersionKey = L"CacheEntryVersion"; + // Note that the next 3 must also match the base key names from version 1 + // since we use them to construct those key names. + static constexpr const wchar_t* kNotificationTypeKey = L"NotificationType"; + static constexpr const wchar_t* kNotificationShownKey = L"NotificationShown"; + static constexpr const wchar_t* kNotificationActionKey = + L"NotificationAction"; + static constexpr const wchar_t* kPrevNotificationActionKey = + L"PrevNotificationAction"; + + // The version key wasn't added until version 2, but we add it to the version + // 1 entries when migrating them to the cache. + static constexpr const uint32_t kInitialVersionEntryVersionKey = 1; + static constexpr const uint32_t kInitialVersionNotificationTypeKey = 1; + static constexpr const uint32_t kInitialVersionNotificationShownKey = 1; + static constexpr const uint32_t kInitialVersionNotificationActionKey = 1; + static constexpr const uint32_t kInitialVersionPrevNotificationActionKey = 2; + + // We have two cache entry structs: one for the current version, and one + // generic one that can handle any version. There are a couple of reasons + // for this: + // - We only want to support writing the current version, but we want to + // support reading any version. + // - It makes things a bit nicer for the caller when Enqueue-ing, since + // they don't have to set the version or wrap values that were added + // later in a mozilla::Maybe. + // - It keeps us from having to worry about writing an invalid cache entry, + // such as one that claims to be version 2, but doesn't have + // prevNotificationAction. + // Note that the entry struct for the current version does not contain a + // version member value because we already know that its version is equal to + // Cache::kEntryVersion. + struct Entry { + std::string notificationType; + std::string notificationShown; + std::string notificationAction; + std::string prevNotificationAction; + }; + struct VersionedEntry { + uint32_t entryVersion; + std::string notificationType; + std::string notificationShown; + std::string notificationAction; + mozilla::Maybe prevNotificationAction; + }; + + using MaybeEntry = mozilla::Maybe; + using MaybeEntryResult = mozilla::WindowsErrorResult; + + VoidResult Init(); + VoidResult Enqueue(const Entry& entry); + MaybeEntryResult Dequeue(); + + private: + const std::wstring mCacheRegKey; + + // We can't easily copy a VoidResult, so just store the raw HRESULT here. + mozilla::Maybe mInitializeResult; + // How large the cache will grow before it starts rejecting new entries. + uint32_t mCapacity; + // The index of the first present cache entry. + uint32_t mFront; + // How many entries are present in the cache. + uint32_t mSize; + + DwordResult EnsureDwordSetting(const wchar_t* regName, uint32_t defaultValue); + VoidResult SetupCache(); + VoidResult MaybeMigrateVersion1(); + std::wstring MakeEntryRegKeyName(uint32_t index); + VoidResult WriteEntryKeys(uint32_t index, const VersionedEntry& entry); + VoidResult DeleteEntry(uint32_t index); + VoidResult SetFront(uint32_t newFront); + VoidResult SetSize(uint32_t newSize); + VoidResult VersionedEnqueue(const VersionedEntry& entry); + VoidResult DiscardFront(); + MaybeDwordResult ReadEntryKeyDword(const std::wstring& regKey, + const wchar_t* regName, bool expected); + MaybeStringResult ReadEntryKeyString(const std::wstring& regKey, + const wchar_t* regName, bool expected); +}; + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_CACHE_H__ diff --git a/toolkit/mozapps/defaultagent/DefaultAgent.cpp b/toolkit/mozapps/defaultagent/DefaultAgent.cpp new file mode 100644 index 0000000000..2ebb5e466e --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultAgent.cpp @@ -0,0 +1,491 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 +#include +#include +#include +#include + +#include "nsAutoRef.h" +#include "nsDebug.h" +#include "nsProxyRelease.h" +#include "nsWindowsHelpers.h" +#include "nsString.h" + +#include "common.h" +#include "DefaultBrowser.h" +#include "DefaultPDF.h" +#include "EventLog.h" +#include "Notification.h" +#include "Policy.h" +#include "Registry.h" +#include "ScheduledTask.h" +#include "ScheduledTaskRemove.h" +#include "SetDefaultBrowser.h" +#include "Telemetry.h" +#include "xpcpublic.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/ErrorResult.h" + +#include "DefaultAgent.h" + +// The AGENT_REGKEY_NAME is dependent on MOZ_APP_VENDOR and MOZ_APP_BASENAME, +// so using those values in the mutex name prevents waiting on processes that +// are using completely different data. +#define REGISTRY_MUTEX_NAME \ + L"" MOZ_APP_VENDOR MOZ_APP_BASENAME L"DefaultBrowserAgentRegistryMutex" +// How long to wait on the registry mutex before giving up on it. This should +// be short. Although the WDBA runs in the background, uninstallation happens +// synchronously in the foreground. +#define REGISTRY_MUTEX_TIMEOUT_MS (3 * 1000) + +namespace mozilla::default_agent { + +// This class is designed to prevent concurrency problems when accessing the +// registry. It should be acquired before any usage of unprefixed registry +// entries. +class RegistryMutex { + private: + nsAutoHandle mMutex; + bool mLocked; + + public: + RegistryMutex() : mMutex(nullptr), mLocked(false) {} + ~RegistryMutex() { + Release(); + // nsAutoHandle will take care of closing the mutex's handle. + } + + // Returns true on success, false on failure. + bool Acquire() { + if (mLocked) { + return true; + } + + if (mMutex.get() == nullptr) { + // It seems like we would want to set the second parameter (bInitialOwner) + // to TRUE, but the documentation for CreateMutexW suggests that, because + // we aren't sure that the mutex doesn't already exist, we can't be sure + // whether we got ownership via this mechanism. + mMutex.own(CreateMutexW(nullptr, FALSE, REGISTRY_MUTEX_NAME)); + if (mMutex.get() == nullptr) { + LOG_ERROR_MESSAGE(L"Couldn't open registry mutex: %#X", GetLastError()); + return false; + } + } + + DWORD mutexStatus = + WaitForSingleObject(mMutex.get(), REGISTRY_MUTEX_TIMEOUT_MS); + if (mutexStatus == WAIT_OBJECT_0) { + mLocked = true; + } else if (mutexStatus == WAIT_TIMEOUT) { + LOG_ERROR_MESSAGE(L"Timed out waiting for registry mutex"); + } else if (mutexStatus == WAIT_ABANDONED) { + // This isn't really an error for us. No one else is using the registry. + // This status code means that we are supposed to check our data for + // consistency, but there isn't really anything we can fix here. + // This is an indication that an agent crashed though, which is clearly an + // error, so log an error message. + LOG_ERROR_MESSAGE(L"Found abandoned registry mutex. Continuing..."); + mLocked = true; + } else { + // The only other documented status code is WAIT_FAILED. In the case that + // we somehow get some other code, that is also an error. + LOG_ERROR_MESSAGE(L"Failed to wait on registry mutex: %#X", + GetLastError()); + } + return mLocked; + } + + bool IsLocked() { return mLocked; } + + void Release() { + if (mLocked) { + if (mMutex.get() == nullptr) { + LOG_ERROR_MESSAGE(L"Unexpectedly missing registry mutex"); + return; + } + BOOL success = ReleaseMutex(mMutex.get()); + if (!success) { + LOG_ERROR_MESSAGE(L"Failed to release registry mutex"); + } + mLocked = false; + } + } +}; + +// Returns true if the registry value name given is one of the +// install-directory-prefixed values used by the Windows Default Browser Agent. +// ex: "C:\Program Files\Mozilla Firefox|PreviousDefault" +// Returns true +// ex: "InitialNotificationShown" +// Returns false +static bool IsPrefixedValueName(const wchar_t* valueName) { + // Prefixed value names use '|' as a delimiter. None of the + // non-install-directory-prefixed value names contain one. + return wcschr(valueName, L'|') != nullptr; +} + +static void RemoveAllRegistryEntries() { + mozilla::UniquePtr installPath = mozilla::GetFullBinaryPath(); + if (!PathRemoveFileSpecW(installPath.get())) { + return; + } + + HKEY rawRegKey = nullptr; + if (ERROR_SUCCESS != + RegOpenKeyExW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME, 0, + KEY_WRITE | KEY_QUERY_VALUE | KEY_WOW64_64KEY, + &rawRegKey)) { + return; + } + nsAutoRegKey regKey(rawRegKey); + + DWORD maxValueNameLen = 0; + if (ERROR_SUCCESS != RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, + &maxValueNameLen, nullptr, nullptr, + nullptr)) { + return; + } + // The length that RegQueryInfoKeyW returns is without a terminator. + maxValueNameLen += 1; + + mozilla::UniquePtr valueName = + mozilla::MakeUnique(maxValueNameLen); + + DWORD valueIndex = 0; + // Set this to true if we encounter values in this key that are prefixed with + // different install directories, indicating that this key is still in use + // by other installs. + bool keyStillInUse = false; + + while (true) { + DWORD valueNameLen = maxValueNameLen; + LSTATUS ls = + RegEnumValueW(regKey.get(), valueIndex, valueName.get(), &valueNameLen, + nullptr, nullptr, nullptr, nullptr); + if (ls != ERROR_SUCCESS) { + break; + } + + if (!wcsnicmp(valueName.get(), installPath.get(), + wcslen(installPath.get()))) { + RegDeleteValueW(regKey.get(), valueName.get()); + // Only increment the index if we did not delete this value, because if + // we did then the indexes of all the values after that one just got + // decremented, meaning the index we already have now refers to a value + // that we haven't looked at yet. + } else { + valueIndex++; + if (IsPrefixedValueName(valueName.get())) { + // If this is not one of the unprefixed value names, it must be one of + // the install-directory prefixed values. + keyStillInUse = true; + } + } + } + + regKey.reset(); + + // If no other installs are using this key, remove it now. + if (!keyStillInUse) { + // Use RegDeleteTreeW to remove the cache as well, which is in subkey. + RegDeleteTreeW(HKEY_CURRENT_USER, AGENT_REGKEY_NAME); + } +} + +// This function adds a registry value with this format: +// |Installed=1 +// RemoveAllRegistryEntries() determines whether the registry key is in use +// by other installations by checking for install-directory-prefixed value +// names. Although Firefox mirrors some preferences into install-directory- +// prefixed values, the WDBA no longer uses any prefixed values. Adding this one +// makes uninstallation work as expected slightly more reliably. +static void WriteInstallationRegistryEntry() { + mozilla::WindowsErrorResult result = + RegistrySetValueBool(IsPrefixed::Prefixed, L"Installed", true); + if (result.isErr()) { + LOG_ERROR_MESSAGE(L"Failed to write installation registry entry: %#X", + result.unwrapErr().AsHResult()); + } +} + +// Returns false (without setting aResult) if reading last run time failed. +static bool CheckIfAppRanRecently(bool* aResult) { + const ULONGLONG kTaskExpirationDays = 90; + const ULONGLONG kTaskExpirationSeconds = kTaskExpirationDays * 24 * 60 * 60; + + MaybeQwordResult lastRunTimeResult = + RegistryGetValueQword(IsPrefixed::Prefixed, L"AppLastRunTime"); + if (lastRunTimeResult.isErr()) { + return false; + } + mozilla::Maybe lastRunTimeMaybe = lastRunTimeResult.unwrap(); + if (!lastRunTimeMaybe.isSome()) { + return false; + } + + ULONGLONG secondsSinceLastRunTime = + SecondsPassedSince(lastRunTimeMaybe.value()); + + *aResult = secondsSinceLastRunTime < kTaskExpirationSeconds; + return true; +} + +// Use the macro to inject all of the definitions for nsISupports. +NS_IMPL_ISUPPORTS(DefaultAgent, nsIDefaultAgent) + +NS_IMETHODIMP +DefaultAgent::RegisterTask(const nsAString& aUniqueToken) { + // We aren't actually going to check whether we got the mutex here. + // Ideally we would acquire it since registration might migrate registry + // entries. But it is preferable to ignore a mutex wait timeout here + // because: + // 1. Otherwise the task doesn't get registered at all + // 2. If another installation's agent is holding the mutex, it either + // is far enough out of date that it doesn't yet use the migrated + // values, or it already did the migration for us. + RegistryMutex regMutex; + regMutex.Acquire(); + + WriteInstallationRegistryEntry(); + + HRESULT hr = + default_agent::RegisterTask(PromiseFlatString(aUniqueToken).get()); + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +DefaultAgent::UpdateTask(const nsAString& aUniqueToken) { + // Not checking if we got the mutex for the same reason we didn't in + // register-task + RegistryMutex regMutex; + regMutex.Acquire(); + + WriteInstallationRegistryEntry(); + + HRESULT hr = default_agent::UpdateTask(PromiseFlatString(aUniqueToken).get()); + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +DefaultAgent::UnregisterTask(const nsAString& aUniqueToken) { + HRESULT hr = RemoveTasks(PromiseFlatString(aUniqueToken).get(), + WhichTasks::WdbaTaskOnly); + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +DefaultAgent::Uninstall(const nsAString& aUniqueToken) { + // We aren't actually going to check whether we got the mutex here. + // Ideally we would acquire it since we are about to access the registry, + // so we would like to block simultaneous users of our registry key. + // But there are two reasons that it is preferable to ignore a mutex + // wait timeout here: + // 1. If we fail to uninstall our prefixed registry entries, the + // registry key containing them will never be removed, even when the + // last installation is uninstalled. + // 2. If we timed out waiting on the mutex, it implies that there are + // other installations. If there are other installations, there will + // be other prefixed registry entries. If there are other prefixed + // registry entries, we won't remove the whole key or touch the + // unprefixed entries during uninstallation. Therefore, we should + // be able to safely uninstall without stepping on anyone's toes. + RegistryMutex regMutex; + regMutex.Acquire(); + + RemoveAllRegistryEntries(); + return NS_OK; +} + +NS_IMETHODIMP +DefaultAgent::DoTask(const nsAString& aUniqueToken, const bool aForce) { + // Acquire() has a short timeout. Since this runs in the background, we + // could use a longer timeout in this situation. However, if another + // installation's agent is already running, it will update CurrentDefault, + // possibly send a ping, and possibly show a notification. + // Once all that has happened, there is no real reason to do it again. We + // only send one ping per day, so we aren't going to do that again. And + // the only time we ever show a second notification is 7 days after the + // first one, so we aren't going to do that again either. + // If the other process didn't take those actions, there is no reason that + // this process would take them. + // If the other process fails, this one will most likely fail for the same + // reason. + // So we'll just bail if we can't get the mutex quickly. + RegistryMutex regMutex; + if (!regMutex.Acquire()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Check that Firefox ran recently, if not then stop here. + // Also stop if no timestamp was found, which most likely indicates + // that Firefox was not yet run. + bool ranRecently = false; + if (!aForce && (!CheckIfAppRanRecently(&ranRecently) || !ranRecently)) { + return NS_ERROR_FAILURE; + } + + DefaultBrowserResult defaultBrowserResult = GetDefaultBrowserInfo(); + DefaultBrowserInfo browserInfo{}; + if (defaultBrowserResult.isOk()) { + browserInfo = defaultBrowserResult.unwrap(); + } else { + browserInfo.currentDefaultBrowser = Browser::Error; + browserInfo.previousDefaultBrowser = Browser::Error; + } + + DefaultPdfResult defaultPdfResult = GetDefaultPdfInfo(); + DefaultPdfInfo pdfInfo{}; + if (defaultPdfResult.isOk()) { + pdfInfo = defaultPdfResult.unwrap(); + } else { + pdfInfo.currentDefaultPdf = PDFHandler::Error; + } + + NotificationActivities activitiesPerformed; + // We block while waiting for the notification which prevents STA thread + // callbacks from running as the event loop won't run. Moving notification + // handling to an MTA thread prevents this conflict. + activitiesPerformed = MaybeShowNotification( + browserInfo, PromiseFlatString(aUniqueToken).get(), aForce); + + HRESULT hr = SendDefaultAgentPing(browserInfo, pdfInfo, activitiesPerformed); + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +DefaultAgent::AppRanRecently(bool* aRanRecently) { + bool ranRecently = false; + *aRanRecently = CheckIfAppRanRecently(&ranRecently) && ranRecently; + return NS_OK; +} + +NS_IMETHODIMP +DefaultAgent::GetDefaultBrowser(nsAString& aDefaultBrowser) { + Browser browser = default_agent::GetDefaultBrowser(); + aDefaultBrowser = NS_ConvertUTF8toUTF16(GetStringForBrowser(browser)); + return NS_OK; +} + +NS_IMETHODIMP +DefaultAgent::GetReplacePreviousDefaultBrowser( + const nsAString& aDefaultBrowser, nsAString& aPreviousDefaultBrowser) { + Browser browser = + GetBrowserFromString(std::string(NS_ConvertUTF16toUTF8(aDefaultBrowser))); + Browser previousBrowser = + default_agent::GetReplacePreviousDefaultBrowser(browser); + aPreviousDefaultBrowser = + NS_ConvertUTF8toUTF16(GetStringForBrowser(previousBrowser)); + return NS_OK; +} + +NS_IMETHODIMP +DefaultAgent::GetDefaultPdfHandler(nsAString& aDefaultPdfHandler) { + PDFHandler pdf = default_agent::GetDefaultPdfInfo() + .unwrapOr({PDFHandler::Error}) + .currentDefaultPdf; + aDefaultPdfHandler = NS_ConvertUTF8toUTF16(GetStringForPDFHandler(pdf)); + return NS_OK; +} + +NS_IMETHODIMP +DefaultAgent::SendPing(const nsAString& aDefaultBrowser, + const nsAString& aPreviousDefaultBrowser, + const nsAString& aDefaultPdfHandler, + const nsAString& aNotificationShown, + const nsAString& aNotificationAction) { + DefaultBrowserInfo browserInfo = { + GetBrowserFromString(std::string(NS_ConvertUTF16toUTF8(aDefaultBrowser))), + GetBrowserFromString( + std::string(NS_ConvertUTF16toUTF8(aPreviousDefaultBrowser)))}; + + DefaultPdfInfo pdfInfo = {GetPDFHandlerFromString( + std::string(NS_ConvertUTF16toUTF8(aDefaultPdfHandler)))}; + + // The JS implementation has never supported the "two notification flow", + // i.e., displaying a followup notification. + NotificationShown shown = GetNotificationShownFromString(aNotificationShown); + NotificationAction action = + GetNotificationActionFromString(aNotificationAction); + NotificationActivities activitiesPerformed = {NotificationType::Initial, + shown, action}; + + HRESULT hr = SendDefaultAgentPing(browserInfo, pdfInfo, activitiesPerformed); + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +DefaultAgent::SetDefaultBrowserUserChoice( + const nsAString& aAumid, const nsTArray& aExtraFileExtensions) { + return default_agent::SetDefaultBrowserUserChoice( + PromiseFlatString(aAumid).get(), aExtraFileExtensions); +} + +NS_IMETHODIMP +DefaultAgent::SetDefaultBrowserUserChoiceAsync( + const nsAString& aAumid, const nsTArray& aExtraFileExtensions, + JSContext* aCx, dom::Promise** aPromise) { + if (!NS_IsMainThread()) { + return NS_ERROR_NOT_SAME_THREAD; + } + + ErrorResult rv; + RefPtr promise = + dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv); + if (MOZ_UNLIKELY(rv.Failed())) { + return rv.StealNSResult(); + } + + // A holder to pass the promise through the background task and back to + // the main thread when finished. + auto promiseHolder = MakeRefPtr>( + "SetDefaultBrowserUserChoiceAsync promise", promise); + + nsresult result = NS_DispatchBackgroundTask( + NS_NewRunnableFunction( + "SetDefaultBrowserUserChoiceAsync", + // Make a local copy of the aAudmid parameter which is a reference + // which will go out of scope + [aumid = nsString(aAumid), promiseHolder = std::move(promiseHolder), + aExtraFileExtensions = + CopyableTArray(aExtraFileExtensions)] { + nsresult rv = default_agent::SetDefaultBrowserUserChoice( + PromiseFlatString(aumid).get(), aExtraFileExtensions); + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "SetDefaultBrowserUserChoiceAsync callback", + [rv, promiseHolder = std::move(promiseHolder)] { + dom::Promise* promise = promiseHolder.get()->get(); + if (NS_SUCCEEDED(rv)) { + promise->MaybeResolveWithUndefined(); + } else { + promise->MaybeReject(rv); + } + })); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + promise.forget(aPromise); + return result; +} + +NS_IMETHODIMP +DefaultAgent::SetDefaultExtensionHandlersUserChoice( + const nsAString& aAumid, const nsTArray& aFileExtensions) { + return default_agent::SetDefaultExtensionHandlersUserChoice( + PromiseFlatString(aAumid).get(), aFileExtensions); +} + +NS_IMETHODIMP +DefaultAgent::AgentDisabled(bool* aDisabled) { + *aDisabled = IsAgentDisabled(); + return NS_OK; +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/DefaultAgent.h b/toolkit/mozapps/defaultagent/DefaultAgent.h new file mode 100644 index 0000000000..518ac44afe --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultAgent.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_H__ +#define __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_H__ + +#include "nsIDefaultAgent.h" + +namespace mozilla::default_agent { + +class DefaultAgent final : public nsIDefaultAgent { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDEFAULTAGENT + + DefaultAgent() = default; + + private: + // A private destructor must be declared. + ~DefaultAgent() = default; +}; + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_H__ diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.cpp b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp new file mode 100644 index 0000000000..87d3f62632 --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp @@ -0,0 +1,240 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "DefaultBrowser.h" + +#include + +#include + +#include "EventLog.h" +#include "Registry.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Try.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla::default_agent { + +using BrowserResult = mozilla::WindowsErrorResult; + +constexpr std::string_view kUnknownBrowserString = ""; + +constexpr std::pair kStringBrowserMap[]{ + {"error", Browser::Error}, + {kUnknownBrowserString, Browser::Unknown}, + {"firefox", Browser::Firefox}, + {"chrome", Browser::Chrome}, + {"edge", Browser::EdgeWithEdgeHTML}, + {"edge-chrome", Browser::EdgeWithBlink}, + {"ie", Browser::InternetExplorer}, + {"opera", Browser::Opera}, + {"brave", Browser::Brave}, + {"yandex", Browser::Yandex}, + {"qq-browser", Browser::QQBrowser}, + {"360-browser", Browser::_360Browser}, + {"sogou", Browser::Sogou}, + {"duckduckgo", Browser::DuckDuckGo}, +}; + +static_assert(mozilla::ArrayLength(kStringBrowserMap) == kBrowserCount); + +std::string GetStringForBrowser(Browser browser) { + for (const auto& [mapString, mapBrowser] : kStringBrowserMap) { + if (browser == mapBrowser) { + return std::string{mapString}; + } + } + + return std::string(kUnknownBrowserString); +} + +Browser GetBrowserFromString(const std::string& browserString) { + for (const auto& [mapString, mapBrowser] : kStringBrowserMap) { + if (browserString == mapString) { + return mapBrowser; + } + } + + return Browser::Unknown; +} + +BrowserResult TryGetDefaultBrowser() { + RefPtr pAAR; + HRESULT hr = CoCreateInstance( + CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC, + IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR)); + if (FAILED(hr)) { + LOG_ERROR(hr); + return BrowserResult(mozilla::WindowsError::FromHResult(hr)); + } + + // Whatever is handling the HTTP protocol is effectively the default browser. + mozilla::UniquePtr registeredApp; + { + wchar_t* rawRegisteredApp; + hr = pAAR->QueryCurrentDefault(L"http", AT_URLPROTOCOL, AL_EFFECTIVE, + &rawRegisteredApp); + if (FAILED(hr)) { + LOG_ERROR(hr); + return BrowserResult(mozilla::WindowsError::FromHResult(hr)); + } + registeredApp = mozilla::UniquePtr( + rawRegisteredApp); + } + + // Get the application Friendly Name associated to the found ProgID. This is + // sized to be larger than any observed or expected friendly names. Long + // friendly names tend to be in the form `[Company] [Browser] [Variant]` + std::array friendlyName{}; + DWORD friendlyNameLen = friendlyName.size(); + hr = AssocQueryStringW(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, + registeredApp.get(), nullptr, friendlyName.data(), + &friendlyNameLen); + if (FAILED(hr)) { + LOG_ERROR(hr); + return BrowserResult(mozilla::WindowsError::FromHResult(hr)); + } + + // This maps a browser's Friendly Name prefix to an enum variant that we'll + // use to identify that browser in our telemetry ping (which is this + // function's return value). + constexpr std::pair kFriendlyNamePrefixes[] = { + {L"Firefox", Browser::Firefox}, + {L"Google Chrome", Browser::Chrome}, + {L"Microsoft Edge", Browser::EdgeWithBlink}, + {L"Internet Explorer", Browser::InternetExplorer}, + {L"Opera", Browser::Opera}, + {L"Brave", Browser::Brave}, + {L"Yandex", Browser::Yandex}, + {L"QQBrowser", Browser::QQBrowser}, + // 360安全浏览器 UTF-16 encoding + {L"\u0033\u0036\u0030\u5b89\u5168\u6d4f\u89c8\u5668", + Browser::_360Browser}, + // 搜狗高速浏览器 UTF-16 encoding + {L"\u641c\u72d7\u9ad8\u901f\u6d4f\u89c8\u5668", Browser::Sogou}, + {L"DuckDuckGo", Browser::DuckDuckGo}, + }; + + // We should have one prefix for every browser we track, minus exceptions + // listed below. + // Error - not a real browser. + // Unknown - not a real browser. + // EdgeWithEdgeHTML - duplicate friendly name with EdgeWithBlink with special + // handling below. + static_assert(mozilla::ArrayLength(kFriendlyNamePrefixes) == + kBrowserCount - 3); + + for (const auto& [prefix, browser] : kFriendlyNamePrefixes) { + // Find matching Friendly Name prefix. + if (!wcsnicmp(friendlyName.data(), prefix.data(), prefix.length())) { + if (browser == Browser::EdgeWithBlink) { + // Disambiguate EdgeWithEdgeHTML and EdgeWithBlink. + // The ProgID below is documented as having not changed while Edge was + // actively developed. It's assumed but unverified this is true in all + // cases (e.g. across locales). + // + // Note: at time of commit EdgeWithBlink from the Windows Store was a + // wrapper for Edge Installer instead of a package containing Edge, + // therefore the Default Browser associating ProgID was not in the form + // "AppX[hash]" as expected. It is unclear if the EdgeWithEdgeHTML and + // EdgeWithBlink ProgIDs would differ if the latter is changed into a + // package containing Edge. + constexpr std::wstring_view progIdEdgeHtml1{ + L"AppXq0fevzme2pys62n3e0fbqa7peapykr8v"}; + // Apparently there is at least one other ProgID used by EdgeHTML Edge. + constexpr std::wstring_view progIdEdgeHtml2{ + L"AppXd4nrz8ff68srnhf9t5a8sbjyar1cr723"}; + + if (!wcsnicmp(registeredApp.get(), progIdEdgeHtml1.data(), + progIdEdgeHtml1.length()) || + !wcsnicmp(registeredApp.get(), progIdEdgeHtml2.data(), + progIdEdgeHtml2.length())) { + return Browser::EdgeWithEdgeHTML; + } + } + + return browser; + } + } + + // The default browser is one that we don't know about. + return Browser::Unknown; +} + +BrowserResult TryGetReplacePreviousDefaultBrowser(Browser currentDefault) { + // This function uses a registry value which stores the current default + // browser. It returns the data stored in that registry value and replaces the + // stored string with the current default browser string that was passed in. + + std::string currentDefaultStr = GetStringForBrowser(currentDefault); + std::string previousDefault = + RegistryGetValueString(IsPrefixed::Unprefixed, L"CurrentDefault") + .unwrapOr(mozilla::Some(currentDefaultStr)) + .valueOr(currentDefaultStr); + + mozilla::Unused << RegistrySetValueString( + IsPrefixed::Unprefixed, L"CurrentDefault", currentDefaultStr.c_str()); + + return GetBrowserFromString(previousDefault); +} + +DefaultBrowserResult GetDefaultBrowserInfo() { + DefaultBrowserInfo browserInfo; + + MOZ_TRY_VAR(browserInfo.currentDefaultBrowser, TryGetDefaultBrowser()); + MOZ_TRY_VAR( + browserInfo.previousDefaultBrowser, + TryGetReplacePreviousDefaultBrowser(browserInfo.currentDefaultBrowser)); + + return browserInfo; +} + +// We used to prefix this key with the installation directory, but that causes +// problems with our new "only one ping per day across installs" restriction. +// To make sure all installations use consistent data, the value's name is +// being migrated to a shared, non-prefixed name. +// This function doesn't really do any error handling, because there isn't +// really anything to be done if it fails. +void MaybeMigrateCurrentDefault() { + const wchar_t* valueName = L"CurrentDefault"; + + MaybeStringResult valueResult = + RegistryGetValueString(IsPrefixed::Prefixed, valueName); + if (valueResult.isErr()) { + return; + } + mozilla::Maybe maybeValue = valueResult.unwrap(); + if (maybeValue.isNothing()) { + // No value to migrate + return; + } + std::string value = maybeValue.value(); + + mozilla::Unused << RegistryDeleteValue(IsPrefixed::Prefixed, valueName); + + // Only migrate the value if no value is in the new location yet. + valueResult = RegistryGetValueString(IsPrefixed::Unprefixed, valueName); + if (valueResult.isErr()) { + return; + } + if (valueResult.unwrap().isNothing()) { + mozilla::Unused << RegistrySetValueString(IsPrefixed::Unprefixed, valueName, + value.c_str()); + } +} + +Browser GetDefaultBrowser() { + return TryGetDefaultBrowser().unwrapOr(Browser::Error); +} +Browser GetReplacePreviousDefaultBrowser(Browser currentBrowser) { + return TryGetReplacePreviousDefaultBrowser(currentBrowser) + .unwrapOr(Browser::Error); +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.h b/toolkit/mozapps/defaultagent/DefaultBrowser.h new file mode 100644 index 0000000000..f1b940959f --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultBrowser.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_DEFAULT_BROWSER_H__ +#define __DEFAULT_BROWSER_DEFAULT_BROWSER_H__ + +#include + +#include "mozilla/DefineEnum.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla::default_agent { + +MOZ_DEFINE_ENUM_CLASS(Browser, + (Error, Unknown, Firefox, Chrome, EdgeWithEdgeHTML, + EdgeWithBlink, InternetExplorer, Opera, Brave, Yandex, + QQBrowser, _360Browser, Sogou, DuckDuckGo)); + +struct DefaultBrowserInfo { + Browser currentDefaultBrowser; + Browser previousDefaultBrowser; +}; + +using DefaultBrowserResult = mozilla::WindowsErrorResult; + +DefaultBrowserResult GetDefaultBrowserInfo(); +Browser GetDefaultBrowser(); +Browser GetReplacePreviousDefaultBrowser(Browser currentBrowser); + +std::string GetStringForBrowser(Browser browser); +Browser GetBrowserFromString(const std::string& browserString); +void MaybeMigrateCurrentDefault(); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_DEFAULT_BROWSER_H__ diff --git a/toolkit/mozapps/defaultagent/DefaultPDF.cpp b/toolkit/mozapps/defaultagent/DefaultPDF.cpp new file mode 100644 index 0000000000..e0a9f2e85a --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultPDF.cpp @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "DefaultPDF.h" + +#include + +#include +#include + +#include "EventLog.h" + +#include "mozilla/Buffer.h" +#include "mozilla/RefPtr.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "mozilla/Try.h" + +namespace mozilla::default_agent { + +constexpr std::string_view kUnknownPdfString = ""; + +constexpr std::pair kStringPdfHandlerMap[]{ + {"error", PDFHandler::Error}, + {kUnknownPdfString, PDFHandler::Unknown}, + {"Firefox", PDFHandler::Firefox}, + {"Microsoft Edge", PDFHandler::MicrosoftEdge}, + {"Google Chrome", PDFHandler::GoogleChrome}, + {"Adobe Acrobat", PDFHandler::AdobeAcrobat}, + {"WPS", PDFHandler::WPS}, + {"Nitro", PDFHandler::Nitro}, + {"Foxit", PDFHandler::Foxit}, + {"PDF-XChange", PDFHandler::PDFXChange}, + {"Avast", PDFHandler::AvastSecureBrowser}, + {"Sumatra", PDFHandler::SumatraPDF}, +}; + +static_assert(mozilla::ArrayLength(kStringPdfHandlerMap) == kPDFHandlerCount); + +std::string GetStringForPDFHandler(PDFHandler handler) { + for (const auto& [mapString, mapPdf] : kStringPdfHandlerMap) { + if (handler == mapPdf) { + return std::string{mapString}; + } + } + + return std::string(kUnknownPdfString); +} + +PDFHandler GetPDFHandlerFromString(const std::string& pdfHandlerString) { + for (const auto& [mapString, mapPdfHandler] : kStringPdfHandlerMap) { + if (pdfHandlerString == mapString) { + return mapPdfHandler; + } + } + + return PDFHandler::Unknown; +} + +using PdfResult = mozilla::WindowsErrorResult; + +static PdfResult GetDefaultPdf() { + RefPtr pAAR; + HRESULT hr = CoCreateInstance( + CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC, + IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR)); + if (FAILED(hr)) { + LOG_ERROR(hr); + return PdfResult(mozilla::WindowsError::FromHResult(hr)); + } + + mozilla::UniquePtr registeredApp; + { + wchar_t* rawRegisteredApp; + hr = pAAR->QueryCurrentDefault(L".pdf", AT_FILEEXTENSION, AL_EFFECTIVE, + &rawRegisteredApp); + if (FAILED(hr)) { + LOG_ERROR(hr); + return PdfResult(mozilla::WindowsError::FromHResult(hr)); + } + registeredApp = mozilla::UniquePtr{ + rawRegisteredApp}; + } + + // Get the application Friendly Name associated to the found ProgID. This is + // sized to be larger than any observed or expected friendly names. Long + // friendly names tend to be in the form `[Company] [Viewer] [Variant]` + DWORD friendlyNameLen = 0; + hr = AssocQueryStringW(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, + registeredApp.get(), nullptr, nullptr, + &friendlyNameLen); + if (FAILED(hr)) { + LOG_ERROR(hr); + return PdfResult(mozilla::WindowsError::FromHResult(hr)); + } + + mozilla::Buffer friendlyNameBuffer(friendlyNameLen); + hr = AssocQueryStringW(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, + registeredApp.get(), nullptr, + friendlyNameBuffer.Elements(), &friendlyNameLen); + if (FAILED(hr)) { + LOG_ERROR(hr); + return PdfResult(mozilla::WindowsError::FromHResult(hr)); + } + + constexpr std::pair kFriendlyNamePrefixes[] = { + {L"Firefox", PDFHandler::Firefox}, + {L"Microsoft Edge", PDFHandler::MicrosoftEdge}, + {L"Google Chrome", PDFHandler::GoogleChrome}, + {L"Adobe", PDFHandler::AdobeAcrobat}, + {L"Acrobat", PDFHandler::AdobeAcrobat}, + {L"WPS", PDFHandler::WPS}, + {L"Nitro", PDFHandler::Nitro}, + {L"Foxit", PDFHandler::Foxit}, + {L"PDF-XChange", PDFHandler::PDFXChange}, + {L"Avast", PDFHandler::AvastSecureBrowser}, + {L"Sumatra", PDFHandler::SumatraPDF}, + }; + + // We should have one prefix for every PDF handler we track, with exceptions + // listed below. + // Error - removed; not a real pdf handler. + // Unknown - removed; not a real pdf handler. + // AdobeAcrobat - duplicate; `Adobe` and `Acrobat` prefixes are both seen in + // telemetry. + static_assert(mozilla::ArrayLength(kFriendlyNamePrefixes) == + kPDFHandlerCount - 2 + 1); + + PDFHandler resolvedHandler = PDFHandler::Unknown; + for (const auto& [knownHandlerSubstring, handlerEnum] : + kFriendlyNamePrefixes) { + if (!wcsnicmp(friendlyNameBuffer.Elements(), knownHandlerSubstring.data(), + knownHandlerSubstring.length())) { + resolvedHandler = handlerEnum; + break; + } + } + + return resolvedHandler; +} + +DefaultPdfResult GetDefaultPdfInfo() { + DefaultPdfInfo pdfInfo; + MOZ_TRY_VAR(pdfInfo.currentDefaultPdf, GetDefaultPdf()); + + return pdfInfo; +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/DefaultPDF.h b/toolkit/mozapps/defaultagent/DefaultPDF.h new file mode 100644 index 0000000000..73afaa7025 --- /dev/null +++ b/toolkit/mozapps/defaultagent/DefaultPDF.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DEFAULT_BROWSER_DEFAULT_PDF_H__ +#define DEFAULT_BROWSER_DEFAULT_PDF_H__ + +#include + +#include "mozilla/DefineEnum.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla::default_agent { + +MOZ_DEFINE_ENUM_CLASS(PDFHandler, + (Error, Unknown, Firefox, MicrosoftEdge, GoogleChrome, + AdobeAcrobat, WPS, Nitro, Foxit, PDFXChange, + AvastSecureBrowser, SumatraPDF)); + +struct DefaultPdfInfo { + PDFHandler currentDefaultPdf; +}; + +using DefaultPdfResult = mozilla::WindowsErrorResult; + +DefaultPdfResult GetDefaultPdfInfo(); +std::string GetStringForPDFHandler(PDFHandler handler); +PDFHandler GetPDFHandlerFromString(const std::string& pdfHandlerString); + +} // namespace mozilla::default_agent + +#endif // DEFAULT_BROWSER_DEFAULT_PDF_H__ diff --git a/toolkit/mozapps/defaultagent/EventLog.cpp b/toolkit/mozapps/defaultagent/EventLog.cpp new file mode 100644 index 0000000000..eaac1161bb --- /dev/null +++ b/toolkit/mozapps/defaultagent/EventLog.cpp @@ -0,0 +1,11 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "EventLog.h" + +// This is an easy way to expose `MOZ_APP_DISPLAYNAME` to Rust code. +const wchar_t* gWinEventLogSourceName = + L"" MOZ_APP_DISPLAYNAME " Default Browser Agent"; diff --git a/toolkit/mozapps/defaultagent/EventLog.h b/toolkit/mozapps/defaultagent/EventLog.h new file mode 100644 index 0000000000..84b35010f8 --- /dev/null +++ b/toolkit/mozapps/defaultagent/EventLog.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__ +#define __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__ + +#include "mozilla/Types.h" + +MOZ_BEGIN_EXTERN_C + +extern MOZ_EXPORT const wchar_t* gWinEventLogSourceName; + +MOZ_END_EXTERN_C + +#include "mozilla/WindowsEventLog.h" + +#define LOG_ERROR(hr) MOZ_WIN_EVENT_LOG_ERROR(gWinEventLogSourceName, hr) +#define LOG_ERROR_MESSAGE(format, ...) \ + MOZ_WIN_EVENT_LOG_ERROR_MESSAGE(gWinEventLogSourceName, format, __VA_ARGS__) + +#endif // __DEFAULT_BROWSER_AGENT_EVENT_LOG_H__ diff --git a/toolkit/mozapps/defaultagent/Notification.cpp b/toolkit/mozapps/defaultagent/Notification.cpp new file mode 100644 index 0000000000..961e57c9b3 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Notification.cpp @@ -0,0 +1,709 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "Notification.h" + +#include +#include +#include +#include + +#include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/mscom/EnsureMTA.h" +#include "mozilla/intl/FileSource.h" +#include "mozilla/intl/Localization.h" +#include "mozilla/ShellHeaderOnlyUtils.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWindowsHelpers.h" +#include "readstrings.h" +#include "updatererrors.h" +#include "WindowsDefaultBrowser.h" + +#include "common.h" +#include "DefaultBrowser.h" +#include "EventLog.h" +#include "Registry.h" +#include "SetDefaultBrowser.h" + +#include "wintoastlib.h" + +using mozilla::intl::Localization; + +#define SEVEN_DAYS_IN_SECONDS (7 * 24 * 60 * 60) + +// If the notification hasn't been activated or dismissed within 12 hours, +// stop waiting for it. +#define NOTIFICATION_WAIT_TIMEOUT_MS (12 * 60 * 60 * 1000) +// If the mutex hasn't been released within a few minutes, something is wrong +// and we should give up on it +#define MUTEX_TIMEOUT_MS (10 * 60 * 1000) + +namespace mozilla::default_agent { + +bool FirefoxInstallIsEnglish(); + +static bool SetInitialNotificationShown(bool wasShown) { + return !RegistrySetValueBool(IsPrefixed::Unprefixed, + L"InitialNotificationShown", wasShown) + .isErr(); +} + +static bool GetInitialNotificationShown() { + return RegistryGetValueBool(IsPrefixed::Unprefixed, + L"InitialNotificationShown") + .unwrapOr(mozilla::Some(false)) + .valueOr(false); +} + +static bool ResetInitialNotificationShown() { + return RegistryDeleteValue(IsPrefixed::Unprefixed, + L"InitialNotificationShown") + .isOk(); +} + +static bool SetFollowupNotificationShown(bool wasShown) { + return !RegistrySetValueBool(IsPrefixed::Unprefixed, + L"FollowupNotificationShown", wasShown) + .isErr(); +} + +static bool GetFollowupNotificationShown() { + return RegistryGetValueBool(IsPrefixed::Unprefixed, + L"FollowupNotificationShown") + .unwrapOr(mozilla::Some(false)) + .valueOr(false); +} + +static bool SetFollowupNotificationSuppressed(bool value) { + return !RegistrySetValueBool(IsPrefixed::Unprefixed, + L"FollowupNotificationSuppressed", value) + .isErr(); +} + +static bool GetFollowupNotificationSuppressed() { + return RegistryGetValueBool(IsPrefixed::Unprefixed, + L"FollowupNotificationSuppressed") + .unwrapOr(mozilla::Some(false)) + .valueOr(false); +} + +// Returns 0 if no value is set. +static ULONGLONG GetFollowupNotificationRequestTime() { + return RegistryGetValueQword(IsPrefixed::Unprefixed, L"FollowupRequestTime") + .unwrapOr(mozilla::Some(0)) + .valueOr(0); +} + +// Returns false if no value is set. +static bool GetPrefSetDefaultBrowserUserChoice() { + return RegistryGetValueBool(IsPrefixed::Prefixed, + L"SetDefaultBrowserUserChoice") + .unwrapOr(mozilla::Some(false)) + .valueOr(false); +} + +struct ToastStrings { + mozilla::UniquePtr text1; + mozilla::UniquePtr text2; + mozilla::UniquePtr action1; + mozilla::UniquePtr action2; + mozilla::UniquePtr relImagePath; +}; + +struct Strings { + // Toast notification button text is hard to localize because it tends to + // overflow. Thus, we have 3 different toast notifications: + // - The initial notification, which includes a button with text like + // "Ask me later". Since we cannot easily localize this, we will display + // it only in English. + // - The followup notification, to be shown if the user clicked "Ask me + // later". Since we only have that button in English, we only need this + // notification in English. + // - The localized notification, which has much shorter button text to + // (hopefully) prevent overflow: just "Yes" and "No". Since we no longer + // have an "Ask me later" button, a followup localized notification is not + // needed. + ToastStrings initialToast; + ToastStrings followupToast; + ToastStrings localizedToast; + + // Returned pointer points within this struct and should not be freed. + const ToastStrings* GetToastStrings(NotificationType whichToast, + bool englishStrings) const { + if (!englishStrings) { + return &localizedToast; + } + if (whichToast == NotificationType::Initial) { + return &initialToast; + } + return &followupToast; + } +}; + +// Gets all strings out of the relevant INI files. +// Returns true on success, false on failure +static bool GetStrings(Strings& strings) { + mozilla::UniquePtr installPath; + bool success = GetInstallDirectory(installPath); + if (!success) { + LOG_ERROR_MESSAGE(L"Failed to get install directory when getting strings"); + return false; + } + nsTArray resIds = {"branding/brand.ftl"_ns, + "browser/backgroundtasks/defaultagent.ftl"_ns}; + RefPtr l10n = Localization::Create(resIds, true); + nsAutoCString daHeaderText, daBodyText, daYesButton, daNoButton; + mozilla::ErrorResult daRv; + l10n->FormatValueSync("default-browser-notification-header-text"_ns, {}, + daHeaderText, daRv); + ENSURE_SUCCESS(daRv, false); + l10n->FormatValueSync("default-browser-notification-body-text"_ns, {}, + daBodyText, daRv); + ENSURE_SUCCESS(daRv, false); + l10n->FormatValueSync("default-browser-notification-yes-button-text"_ns, {}, + daYesButton, daRv); + ENSURE_SUCCESS(daRv, false); + l10n->FormatValueSync("default-browser-notification-no-button-text"_ns, {}, + daNoButton, daRv); + ENSURE_SUCCESS(daRv, false); + + NS_ConvertUTF8toUTF16 daHeaderTextW(daHeaderText), daBodyTextW(daBodyText), + daYesButtonW(daYesButton), daNoButtonW(daNoButton); + strings.localizedToast.text1 = + mozilla::MakeUnique(daHeaderTextW.Length() + 1); + wcsncpy(strings.localizedToast.text1.get(), daHeaderTextW.get(), + daHeaderTextW.Length() + 1); + strings.localizedToast.text2 = + mozilla::MakeUnique(daBodyTextW.Length() + 1); + wcsncpy(strings.localizedToast.text2.get(), daBodyTextW.get(), + daBodyTextW.Length() + 1); + strings.localizedToast.action1 = + mozilla::MakeUnique(daYesButtonW.Length() + 1); + wcsncpy(strings.localizedToast.action1.get(), daYesButtonW.get(), + daYesButtonW.Length() + 1); + strings.localizedToast.action2 = + mozilla::MakeUnique(daNoButtonW.Length() + 1); + wcsncpy(strings.localizedToast.action2.get(), daNoButtonW.get(), + daNoButtonW.Length() + 1); + const wchar_t* iniFormat = L"%s\\defaultagent.ini"; + int bufferSize = _scwprintf(iniFormat, installPath.get()); + ++bufferSize; // Extra character for terminating null + mozilla::UniquePtr iniPath = + mozilla::MakeUnique(bufferSize); + _snwprintf_s(iniPath.get(), bufferSize, _TRUNCATE, iniFormat, + installPath.get()); + + IniReader nonlocalizedReader(iniPath.get(), "Nonlocalized"); + nonlocalizedReader.AddKey("InitialToastRelativeImagePath", + &strings.initialToast.relImagePath); + nonlocalizedReader.AddKey("FollowupToastRelativeImagePath", + &strings.followupToast.relImagePath); + nonlocalizedReader.AddKey("LocalizedToastRelativeImagePath", + &strings.localizedToast.relImagePath); + int result = nonlocalizedReader.Read(); + if (result != OK) { + LOG_ERROR_MESSAGE(L"Unable to read non-localized strings: %d", result); + return false; + } + + return true; +} + +static mozilla::WindowsError LaunchFirefoxToHandleDefaultBrowserAgent() { + // Could also be `MOZ_APP_NAME.exe`, but there's no generality to be gained: + // the WDBA is Firefox-only. + FilePathResult firefoxPathResult = GetRelativeBinaryPath(L"firefox.exe"); + if (firefoxPathResult.isErr()) { + return firefoxPathResult.unwrapErr(); + } + std::wstring firefoxPath = firefoxPathResult.unwrap(); + + _bstr_t cmd = firefoxPath.c_str(); + // Omit argv[0] because ShellExecute doesn't need it. + _variant_t args(L"-to-handle-default-browser-agent"); + _variant_t operation(L"open"); + _variant_t directory; + _variant_t showCmd(SW_SHOWNORMAL); + + // To prevent inheriting environment variables from the background task, we + // run Firefox via Explorer instead of our own process. This mimics the + // implementation of the Windows Launcher Process. + auto result = + ShellExecuteByExplorer(cmd, args, operation, directory, showCmd); + NS_ENSURE_TRUE(result.isOk(), result.unwrapErr()); + + return mozilla::WindowsError::CreateSuccess(); +} + +/* + * Set the default browser. + * + * First check if we can directly write UserChoice, if so attempt that. + * If we can't write UserChoice, or if the attempt fails, fall back to + * showing the Default Apps page of Settings. + * + * @param aAumi The AUMI of the installation to set as default. + */ +static void SetDefaultBrowserFromNotification(const wchar_t* aumi) { + nsresult rv = NS_ERROR_FAILURE; + if (GetPrefSetDefaultBrowserUserChoice()) { + rv = SetDefaultBrowserUserChoice(aumi); + } + + if (NS_SUCCEEDED(rv)) { + mozilla::Unused << LaunchFirefoxToHandleDefaultBrowserAgent(); + } else { + LOG_ERROR_MESSAGE(L"Failed to SetDefaultBrowserUserChoice: %#X", + GetLastError()); + LaunchModernSettingsDialogDefaultApps(); + } +} + +// This encapsulates the data that needs to be protected by a mutex because it +// will be shared by the main thread and the handler thread. +// To ensure the data is only written once, handlerDataHasBeenSet should be +// initialized to false, then set to true when the handler writes data into the +// structure. +struct HandlerData { + NotificationActivities activitiesPerformed; + bool handlerDataHasBeenSet; +}; + +// The value that ToastHandler writes into should be a global. We can't control +// when ToastHandler is called, and if this value isn't a global, ToastHandler +// may be called and attempt to access this after it has been deconstructed. +// Since this value is accessed by the handler thread and the main thread, it +// is protected by a mutex (gHandlerMutex). +// Since ShowNotification deconstructs the mutex, it might seem like once +// ShowNotification exits, we can just rely on the inability to wait on an +// invalid mutex to protect the deconstructed data, but it's possible that +// we could deconstruct the mutex while the handler is holding it and is +// already accessing the protected data. +static HandlerData gHandlerReturnData; +static HANDLE gHandlerMutex = INVALID_HANDLE_VALUE; + +class ToastHandler : public WinToastLib::IWinToastHandler { + private: + NotificationType mWhichNotification; + HANDLE mEvent; + const std::wstring mAumiStr; + + public: + ToastHandler(NotificationType whichNotification, HANDLE event, + const wchar_t* aumi) + : mWhichNotification(whichNotification), mEvent(event), mAumiStr(aumi) {} + + void FinishHandler(NotificationActivities& returnData) const { + SetReturnData(returnData); + + BOOL success = SetEvent(mEvent); + if (!success) { + LOG_ERROR_MESSAGE(L"Event could not be set: %#X", GetLastError()); + } + } + + void SetReturnData(NotificationActivities& toSet) const { + DWORD result = WaitForSingleObject(gHandlerMutex, MUTEX_TIMEOUT_MS); + if (result == WAIT_TIMEOUT) { + LOG_ERROR_MESSAGE(L"Unable to obtain mutex ownership"); + return; + } else if (result == WAIT_FAILED) { + LOG_ERROR_MESSAGE(L"Failed to wait on mutex: %#X", GetLastError()); + return; + } else if (result == WAIT_ABANDONED) { + LOG_ERROR_MESSAGE(L"Found abandoned mutex"); + ReleaseMutex(gHandlerMutex); + return; + } + + // Only set this data once + if (!gHandlerReturnData.handlerDataHasBeenSet) { + gHandlerReturnData.activitiesPerformed = toSet; + gHandlerReturnData.handlerDataHasBeenSet = true; + } + + BOOL success = ReleaseMutex(gHandlerMutex); + if (!success) { + LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X", + GetLastError()); + } + } + + void toastActivated() const override { + NotificationActivities activitiesPerformed; + activitiesPerformed.type = mWhichNotification; + activitiesPerformed.shown = NotificationShown::Shown; + activitiesPerformed.action = NotificationAction::ToastClicked; + + // Notification strings are written to indicate the default browser is + // restored to Firefox when the notification body is clicked to prevent + // ambiguity when buttons aren't pressed. + SetDefaultBrowserFromNotification(mAumiStr.c_str()); + + FinishHandler(activitiesPerformed); + } + + void toastActivated(int actionIndex) const override { + NotificationActivities activitiesPerformed; + activitiesPerformed.type = mWhichNotification; + activitiesPerformed.shown = NotificationShown::Shown; + // Override this below + activitiesPerformed.action = NotificationAction::NoAction; + + if (actionIndex == 0) { + // "Make Firefox the default" button, on both the initial and followup + // notifications. "Yes" button on the localized notification. + activitiesPerformed.action = NotificationAction::MakeFirefoxDefaultButton; + + SetDefaultBrowserFromNotification(mAumiStr.c_str()); + } else if (actionIndex == 1) { + // Do nothing. As long as we don't call + // SetFollowupNotificationRequestTime, there will be no followup + // notification. + activitiesPerformed.action = NotificationAction::DismissedByButton; + } + + FinishHandler(activitiesPerformed); + } + + void toastDismissed(WinToastDismissalReason state) const override { + NotificationActivities activitiesPerformed; + activitiesPerformed.type = mWhichNotification; + activitiesPerformed.shown = NotificationShown::Shown; + // Override this below + activitiesPerformed.action = NotificationAction::NoAction; + + if (state == WinToastDismissalReason::TimedOut) { + activitiesPerformed.action = NotificationAction::DismissedByTimeout; + } else if (state == WinToastDismissalReason::ApplicationHidden) { + activitiesPerformed.action = + NotificationAction::DismissedByApplicationHidden; + } else if (state == WinToastDismissalReason::UserCanceled) { + activitiesPerformed.action = NotificationAction::DismissedToActionCenter; + } + + FinishHandler(activitiesPerformed); + } + + void toastFailed() const override { + NotificationActivities activitiesPerformed; + activitiesPerformed.type = mWhichNotification; + activitiesPerformed.shown = NotificationShown::Error; + activitiesPerformed.action = NotificationAction::NoAction; + + LOG_ERROR_MESSAGE(L"Toast notification failed to display"); + FinishHandler(activitiesPerformed); + } +}; + +// This function blocks until the shown notification is activated or dismissed. +static NotificationActivities ShowNotification( + NotificationType whichNotification, const wchar_t* aumi) { + // Initially set the value that will be returned to error. If the notification + // is shown successfully, we'll update it. + NotificationActivities activitiesPerformed = {whichNotification, + NotificationShown::Error, + NotificationAction::NoAction}; + + bool isEnglishInstall = FirefoxInstallIsEnglish(); + + Strings strings; + if (!GetStrings(strings)) { + return activitiesPerformed; + } + const ToastStrings* toastStrings = + strings.GetToastStrings(whichNotification, isEnglishInstall); + + mozilla::mscom::EnsureMTA([&] { + using namespace WinToastLib; + + if (!WinToast::isCompatible()) { + LOG_ERROR_MESSAGE(L"System is not compatible with WinToast"); + return; + } + WinToast::instance()->setAppName(L"" MOZ_APP_DISPLAYNAME); + std::wstring aumiStr = aumi; + WinToast::instance()->setAppUserModelId(aumiStr); + WinToast::instance()->setShortcutPolicy( + WinToastLib::WinToast::SHORTCUT_POLICY_REQUIRE_NO_CREATE); + WinToast::WinToastError error; + if (!WinToast::instance()->initialize(&error)) { + LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str()); + return; + } + + // This event object will let the handler notify us when it has handled the + // notification. + nsAutoHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr)); + if (event.get() == nullptr) { + LOG_ERROR_MESSAGE(L"Unable to create event object: %#X", GetLastError()); + return; + } + + bool success = false; + if (whichNotification == NotificationType::Initial) { + success = SetInitialNotificationShown(true); + } else { + success = SetFollowupNotificationShown(true); + } + if (!success) { + // Return early in this case to prevent the notification from being shown + // on every run. + LOG_ERROR_MESSAGE(L"Unable to set notification as displayed"); + return; + } + + // We need the absolute image path, not the relative path. + mozilla::UniquePtr installPath; + success = GetInstallDirectory(installPath); + if (!success) { + LOG_ERROR_MESSAGE(L"Failed to get install directory for the image path"); + return; + } + const wchar_t* absPathFormat = L"%s\\%s"; + int bufferSize = _scwprintf(absPathFormat, installPath.get(), + toastStrings->relImagePath.get()); + ++bufferSize; // Extra character for terminating null + mozilla::UniquePtr absImagePath = + mozilla::MakeUnique(bufferSize); + _snwprintf_s(absImagePath.get(), bufferSize, _TRUNCATE, absPathFormat, + installPath.get(), toastStrings->relImagePath.get()); + + // This is used to protect gHandlerReturnData. + gHandlerMutex = CreateMutexW(nullptr, TRUE, nullptr); + if (gHandlerMutex == nullptr) { + LOG_ERROR_MESSAGE(L"Unable to create mutex: %#X", GetLastError()); + return; + } + // Automatically close this mutex when this function exits. + nsAutoHandle autoMutex(gHandlerMutex); + // No need to initialize gHandlerReturnData.activitiesPerformed, since it + // will be set by the handler. But we do need to initialize + // gHandlerReturnData.handlerDataHasBeenSet so the handler knows that no + // data has been set yet. + gHandlerReturnData.handlerDataHasBeenSet = false; + success = ReleaseMutex(gHandlerMutex); + if (!success) { + LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X", + GetLastError()); + } + + // Finally ready to assemble the notification and dispatch it. + WinToastTemplate toastTemplate = + WinToastTemplate(WinToastTemplate::ImageAndText02); + toastTemplate.setTextField(toastStrings->text1.get(), + WinToastTemplate::FirstLine); + toastTemplate.setTextField(toastStrings->text2.get(), + WinToastTemplate::SecondLine); + toastTemplate.addAction(toastStrings->action1.get()); + toastTemplate.addAction(toastStrings->action2.get()); + toastTemplate.setImagePath(absImagePath.get()); + toastTemplate.setScenario(WinToastTemplate::Scenario::Reminder); + ToastHandler* handler = + new ToastHandler(whichNotification, event.get(), aumi); + INT64 id = WinToast::instance()->showToast(toastTemplate, handler, &error); + if (id < 0) { + LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str()); + return; + } + + DWORD result = + WaitForSingleObject(event.get(), NOTIFICATION_WAIT_TIMEOUT_MS); + // Don't return after these errors. Attempt to hide the notification. + if (result == WAIT_FAILED) { + LOG_ERROR_MESSAGE(L"Unable to wait on event object: %#X", GetLastError()); + } else if (result == WAIT_TIMEOUT) { + LOG_ERROR_MESSAGE(L"Timed out waiting for event object"); + } else { + result = WaitForSingleObject(gHandlerMutex, MUTEX_TIMEOUT_MS); + if (result == WAIT_TIMEOUT) { + LOG_ERROR_MESSAGE(L"Unable to obtain mutex ownership"); + // activitiesPerformed is already set to error. No change needed. + } else if (result == WAIT_FAILED) { + LOG_ERROR_MESSAGE(L"Failed to wait on mutex: %#X", GetLastError()); + // activitiesPerformed is already set to error. No change needed. + } else if (result == WAIT_ABANDONED) { + LOG_ERROR_MESSAGE(L"Found abandoned mutex"); + ReleaseMutex(gHandlerMutex); + // activitiesPerformed is already set to error. No change needed. + } else { + // Mutex is being held. It is safe to access gHandlerReturnData. + // If gHandlerReturnData.handlerDataHasBeenSet is false, the handler + // never ran. Use the error value activitiesPerformed already contains. + if (gHandlerReturnData.handlerDataHasBeenSet) { + activitiesPerformed = gHandlerReturnData.activitiesPerformed; + } + + success = ReleaseMutex(gHandlerMutex); + if (!success) { + LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X", + GetLastError()); + } + } + } + + if (!WinToast::instance()->hideToast(id)) { + LOG_ERROR_MESSAGE(L"Failed to hide notification"); + } + }); + return activitiesPerformed; +} + +// Previously this function checked that the Firefox build was using English. +// This was checked because of the peculiar way we were localizing toast +// notifications where we used a completely different set of strings in English. +// +// We've since unified the notification flows but need to clean up unused code +// and config files - Bug 1826375. +bool FirefoxInstallIsEnglish() { return false; } + +// If a notification is shown, this function will block until the notification +// is activated or dismissed. +// aumi is the App User Model ID. +NotificationActivities MaybeShowNotification( + const DefaultBrowserInfo& browserInfo, const wchar_t* aumi, bool force) { + // Default to not showing a notification. Any other value will be returned + // directly from ShowNotification. + NotificationActivities activitiesPerformed = {NotificationType::Initial, + NotificationShown::NotShown, + NotificationAction::NoAction}; + + // Reset notification state machine, user setting default browser to Firefox + // is a strong signal that they intend to have it as the default browser. + if (browserInfo.currentDefaultBrowser == Browser::Firefox) { + ResetInitialNotificationShown(); + } + + bool initialNotificationShown = GetInitialNotificationShown(); + if (!initialNotificationShown || force) { + if ((browserInfo.currentDefaultBrowser == Browser::EdgeWithBlink && + browserInfo.previousDefaultBrowser == Browser::Firefox) || + force) { + return ShowNotification(NotificationType::Initial, aumi); + } + return activitiesPerformed; + } + activitiesPerformed.type = NotificationType::Followup; + + ULONGLONG followupNotificationRequestTime = + GetFollowupNotificationRequestTime(); + bool followupNotificationRequested = followupNotificationRequestTime != 0; + bool followupNotificationShown = GetFollowupNotificationShown(); + if (followupNotificationRequested && !followupNotificationShown && + !GetFollowupNotificationSuppressed()) { + ULONGLONG secondsSinceRequestTime = + SecondsPassedSince(followupNotificationRequestTime); + + if (secondsSinceRequestTime >= SEVEN_DAYS_IN_SECONDS) { + // If we go to show the followup notification and the user has already + // changed the default browser, permanently suppress the followup since + // it's no longer relevant. + if (browserInfo.currentDefaultBrowser == Browser::EdgeWithBlink) { + return ShowNotification(NotificationType::Followup, aumi); + } else { + SetFollowupNotificationSuppressed(true); + } + } + } + return activitiesPerformed; +} + +std::string GetStringForNotificationType(NotificationType type) { + switch (type) { + case NotificationType::Initial: + return std::string("initial"); + case NotificationType::Followup: + return std::string("followup"); + } +} + +std::string GetStringForNotificationShown(NotificationShown shown) { + switch (shown) { + case NotificationShown::NotShown: + return std::string("not-shown"); + case NotificationShown::Shown: + return std::string("shown"); + case NotificationShown::Error: + return std::string("error"); + } +} + +NotificationShown GetNotificationShownFromString(const nsAString& shown) { + if (shown == u"not-shown"_ns) { + return NotificationShown::NotShown; + } else if (shown == u"shown"_ns) { + return NotificationShown::Shown; + } else if (shown == u"error"_ns) { + return NotificationShown::Error; + } else { + // Catch all. + return NotificationShown::Error; + } +} + +std::string GetStringForNotificationAction(NotificationAction action) { + switch (action) { + case NotificationAction::DismissedByTimeout: + return std::string("dismissed-by-timeout"); + case NotificationAction::DismissedToActionCenter: + return std::string("dismissed-to-action-center"); + case NotificationAction::DismissedByButton: + return std::string("dismissed-by-button"); + case NotificationAction::DismissedByApplicationHidden: + return std::string("dismissed-by-application-hidden"); + case NotificationAction::RemindMeLater: + return std::string("remind-me-later"); + case NotificationAction::MakeFirefoxDefaultButton: + return std::string("make-firefox-default-button"); + case NotificationAction::ToastClicked: + return std::string("toast-clicked"); + case NotificationAction::NoAction: + return std::string("no-action"); + } +} + +NotificationAction GetNotificationActionFromString(const nsAString& action) { + if (action == u"dismissed-by-timeout"_ns) { + return NotificationAction::DismissedByTimeout; + } else if (action == u"dismissed-to-action-center"_ns) { + return NotificationAction::DismissedToActionCenter; + } else if (action == u"dismissed-by-button"_ns) { + return NotificationAction::DismissedByButton; + } else if (action == u"dismissed-by-application-hidden"_ns) { + return NotificationAction::DismissedByApplicationHidden; + } else if (action == u"remind-me-later"_ns) { + return NotificationAction::RemindMeLater; + } else if (action == u"make-firefox-default-button"_ns) { + return NotificationAction::MakeFirefoxDefaultButton; + } else if (action == u"toast-clicked"_ns) { + return NotificationAction::ToastClicked; + } else if (action == u"no-action"_ns) { + return NotificationAction::NoAction; + } else { + // Catch all. + return NotificationAction::NoAction; + } +} + +void EnsureValidNotificationAction(std::string& actionString) { + if (actionString != "dismissed-by-timeout" && + actionString != "dismissed-to-action-center" && + actionString != "dismissed-by-button" && + actionString != "dismissed-by-application-hidden" && + actionString != "remind-me-later" && + actionString != "make-firefox-default-button" && + actionString != "toast-clicked" && actionString != "no-action") { + actionString = "no-action"; + } +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/Notification.h b/toolkit/mozapps/defaultagent/Notification.h new file mode 100644 index 0000000000..210c55f559 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Notification.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_NOTIFICATION_H__ +#define __DEFAULT_BROWSER_NOTIFICATION_H__ + +#include "DefaultBrowser.h" + +namespace mozilla::default_agent { + +enum class NotificationType { + Initial, + Followup, +}; + +enum class NotificationShown { + NotShown, + Shown, + Error, +}; + +enum class NotificationAction { + DismissedByTimeout, + DismissedToActionCenter, + DismissedByButton, + DismissedByApplicationHidden, + RemindMeLater, + MakeFirefoxDefaultButton, + ToastClicked, + NoAction, // Should not be used with NotificationShown::Shown +}; + +struct NotificationActivities { + NotificationType type; + NotificationShown shown; + NotificationAction action; +}; + +NotificationActivities MaybeShowNotification( + const DefaultBrowserInfo& browserInfo, const wchar_t* aumi, bool force); + +// These take enum values and get strings suitable for telemetry +std::string GetStringForNotificationType(NotificationType type); +std::string GetStringForNotificationShown(NotificationShown shown); +NotificationShown GetNotificationShownFromString(const nsAString& shown); +std::string GetStringForNotificationAction(NotificationAction action); +NotificationAction GetNotificationActionFromString(const nsAString& action); + +// If actionString is a valid action string (i.e. corresponds to one of the +// NotificationAction values), this function has no effect. If actionString is +// not a valid action string, its value will be replaced with the string for +// NotificationAction::NoAction. +void EnsureValidNotificationAction(std::string& actionString); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_NOTIFICATION_H__ diff --git a/toolkit/mozapps/defaultagent/Policy.cpp b/toolkit/mozapps/defaultagent/Policy.cpp new file mode 100644 index 0000000000..f8efdf24a2 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Policy.cpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "Policy.h" + +#include +#include +#include + +#include "common.h" +#include "Registry.h" +#include "UtfConvert.h" + +#include "json/json.h" +#include "mozilla/HelperMacros.h" +#include "mozilla/Maybe.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +// There is little logging or error handling in this file, because the file and +// registry values we are reading here are normally absent, so never finding +// anything that we look for at all would not be an error worth generating an +// event log for. + +#define AGENT_POLICY_NAME "DisableDefaultBrowserAgent" +#define TELEMETRY_POLICY_NAME "DisableTelemetry" + +// The Firefox policy engine hardcodes the string "Mozilla" in its registry +// key accesses rather than using the configured vendor name, so we should do +// the same here to be sure we're compatible with it. +#define POLICY_REGKEY_NAME L"SOFTWARE\\Policies\\Mozilla\\" MOZ_APP_BASENAME + +namespace mozilla::default_agent { + +// This enum is the return type for the functions that check policy values. +enum class PolicyState { + Enabled, // There is a policy explicitly set to enabled + Disabled, // There is a policy explicitly set to disabled + NoPolicy, // This policy isn't configured +}; + +static PolicyState FindPolicyInRegistry(HKEY rootKey, + const wchar_t* policyName) { + HKEY rawRegKey = nullptr; + RegOpenKeyExW(rootKey, POLICY_REGKEY_NAME, 0, KEY_READ, &rawRegKey); + + nsAutoRegKey regKey(rawRegKey); + + if (!regKey) { + return PolicyState::NoPolicy; + } + + // If this key is empty and doesn't have any actual policies in it, + // treat that the same as the key not existing and return no result. + DWORD numSubKeys = 0, numValues = 0; + LSTATUS ls = RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr, + &numSubKeys, nullptr, nullptr, &numValues, + nullptr, nullptr, nullptr, nullptr); + if (ls != ERROR_SUCCESS) { + return PolicyState::NoPolicy; + } + + DWORD policyValue = UINT32_MAX; + DWORD policyValueSize = sizeof(policyValue); + ls = RegGetValueW(regKey.get(), nullptr, policyName, RRF_RT_REG_DWORD, + nullptr, &policyValue, &policyValueSize); + + if (ls != ERROR_SUCCESS) { + return PolicyState::NoPolicy; + } + return policyValue == 0 ? PolicyState::Disabled : PolicyState::Enabled; +} + +static PolicyState FindPolicyInFile(const char* policyName) { + mozilla::UniquePtr thisBinaryPath = mozilla::GetFullBinaryPath(); + if (!PathRemoveFileSpecW(thisBinaryPath.get())) { + return PolicyState::NoPolicy; + } + + wchar_t policiesFilePath[MAX_PATH] = L""; + if (!PathCombineW(policiesFilePath, thisBinaryPath.get(), L"distribution")) { + return PolicyState::NoPolicy; + } + + if (!PathAppendW(policiesFilePath, L"policies.json")) { + return PolicyState::NoPolicy; + } + + // We need a narrow string-based std::ifstream because that's all jsoncpp can + // use; that means we need to supply it the file path as a narrow string. + Utf16ToUtf8Result policiesFilePathToUtf8 = Utf16ToUtf8(policiesFilePath); + if (policiesFilePathToUtf8.isErr()) { + return PolicyState::NoPolicy; + } + std::string policiesFilePathA = policiesFilePathToUtf8.unwrap(); + + Json::Value jsonRoot; + std::ifstream stream(policiesFilePathA); + Json::Reader().parse(stream, jsonRoot); + + if (jsonRoot.isObject() && jsonRoot.isMember("Policies") && + jsonRoot["Policies"].isObject()) { + if (jsonRoot["Policies"].isMember(policyName) && + jsonRoot["Policies"][policyName].isBool()) { + return jsonRoot["Policies"][policyName].asBool() ? PolicyState::Enabled + : PolicyState::Disabled; + } else { + return PolicyState::NoPolicy; + } + } + + return PolicyState::NoPolicy; +} + +static PolicyState IsDisabledByPref(const wchar_t* prefRegValue) { + auto prefValueResult = + RegistryGetValueBool(IsPrefixed::Prefixed, prefRegValue); + + if (prefValueResult.isErr()) { + return PolicyState::NoPolicy; + } + auto prefValue = prefValueResult.unwrap(); + if (prefValue.isNothing()) { + return PolicyState::NoPolicy; + } + return prefValue.value() ? PolicyState::Enabled : PolicyState::Disabled; +} + +// Everything we call from this function wants wide strings, except for jsoncpp, +// which cannot work with them at all, so at some point we need both formats. +// It's awkward to take both formats as individual arguments, but it would be +// more awkward to take one and runtime convert it to the other, or to turn +// this function into a macro so that the preprocessor can trigger the +// conversion for us, so this is what we've got. +static bool IsThingDisabled(const char* thing, const wchar_t* wideThing) { + // The logic here is intended to be the same as that used by Firefox's policy + // engine implementation; they should be kept in sync. We have added the pref + // check at the end though, since that's our own custom mechanism. + PolicyState state = FindPolicyInRegistry(HKEY_LOCAL_MACHINE, wideThing); + if (state == PolicyState::NoPolicy) { + state = FindPolicyInRegistry(HKEY_CURRENT_USER, wideThing); + } + if (state == PolicyState::NoPolicy) { + state = FindPolicyInFile(thing); + } + if (state == PolicyState::NoPolicy) { + state = IsDisabledByPref(wideThing); + } + return state == PolicyState::Enabled ? true : false; +} + +bool IsAgentDisabled() { + return IsThingDisabled(AGENT_POLICY_NAME, L"" AGENT_POLICY_NAME); +} + +bool IsTelemetryDisabled() { + return IsThingDisabled(TELEMETRY_POLICY_NAME, L"" TELEMETRY_POLICY_NAME); +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/Policy.h b/toolkit/mozapps/defaultagent/Policy.h new file mode 100644 index 0000000000..2a07a94543 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Policy.h @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_POLICY_H__ +#define __DEFAULT_BROWSER_AGENT_POLICY_H__ + +namespace mozilla::default_agent { + +bool IsAgentDisabled(); +bool IsTelemetryDisabled(); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_POLICY_H__ diff --git a/toolkit/mozapps/defaultagent/Registry.cpp b/toolkit/mozapps/defaultagent/Registry.cpp new file mode 100644 index 0000000000..a2153c50ac --- /dev/null +++ b/toolkit/mozapps/defaultagent/Registry.cpp @@ -0,0 +1,330 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "Registry.h" + +#include +#include + +#include "common.h" +#include "EventLog.h" +#include "UtfConvert.h" + +#include "mozilla/Buffer.h" +#include "mozilla/Try.h" + +namespace mozilla::default_agent { + +using WStringResult = mozilla::WindowsErrorResult; + +static WStringResult MaybePrefixRegistryValueName( + IsPrefixed isPrefixed, const wchar_t* registryValueNameSuffix) { + if (isPrefixed == IsPrefixed::Unprefixed) { + std::wstring registryValueName = registryValueNameSuffix; + return registryValueName; + } + + mozilla::UniquePtr installPath = mozilla::GetFullBinaryPath(); + if (!PathRemoveFileSpecW(installPath.get())) { + HRESULT hr = HRESULT_FROM_WIN32(ERROR_BAD_PATHNAME); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + std::wstring registryValueName(installPath.get()); + registryValueName.append(L"|"); + registryValueName.append(registryValueNameSuffix); + + return registryValueName; +} + +// Creates a sub key of AGENT_REGKEY_NAME by appending the passed subKey. If +// subKey is null, nothing is appended. +static std::wstring MakeKeyName(const wchar_t* subKey) { + std::wstring keyName = AGENT_REGKEY_NAME; + if (subKey) { + keyName += L"\\"; + keyName += subKey; + } + return keyName; +} + +MaybeStringResult RegistryGetValueString( + IsPrefixed isPrefixed, const wchar_t* registryValueName, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Get the string size + DWORD wideDataSize = 0; + LSTATUS ls = + RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + RRF_RT_REG_SZ, nullptr, nullptr, &wideDataSize); + if (ls == ERROR_FILE_NOT_FOUND) { + return mozilla::Maybe(mozilla::Nothing()); + } else if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + // Convert bytes to characters. The extra character should be unnecessary, but + // addresses the possible rounding problem inherent with integer division. + DWORD charCount = (wideDataSize / sizeof(wchar_t)) + 1; + + // Read the data from the registry into a wide string + mozilla::Buffer wideData(charCount); + ls = RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + RRF_RT_REG_SZ, nullptr, wideData.Elements(), &wideDataSize); + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + // Convert to narrow string and return. + std::string narrowData; + MOZ_TRY_VAR(narrowData, Utf16ToUtf8(wideData.Elements())); + + return mozilla::Some(narrowData); +} + +VoidResult RegistrySetValueString(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const char* newValue, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Convert the value from a narrow string to a wide string + std::wstring wideValue; + MOZ_TRY_VAR(wideValue, Utf8ToUtf16(newValue)); + + // Store the value + LSTATUS ls = RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), + valueName.c_str(), REG_SZ, wideValue.c_str(), + (wideValue.size() + 1) * sizeof(wchar_t)); + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Ok(); +} + +MaybeBoolResult RegistryGetValueBool(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Read the integer value from the registry + DWORD value; + DWORD valueSize = sizeof(DWORD); + LSTATUS ls = + RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + RRF_RT_REG_DWORD, nullptr, &value, &valueSize); + if (ls == ERROR_FILE_NOT_FOUND) { + return mozilla::Maybe(mozilla::Nothing()); + } + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Some(value != 0); +} + +VoidResult RegistrySetValueBool(IsPrefixed isPrefixed, + const wchar_t* registryValueName, bool newValue, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Write the value to the registry + DWORD value = newValue ? 1 : 0; + LSTATUS ls = + RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + REG_DWORD, &value, sizeof(DWORD)); + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Ok(); +} + +MaybeQwordResult RegistryGetValueQword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Read the integer value from the registry + ULONGLONG value; + DWORD valueSize = sizeof(ULONGLONG); + LSTATUS ls = + RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + RRF_RT_REG_QWORD, nullptr, &value, &valueSize); + if (ls == ERROR_FILE_NOT_FOUND) { + return mozilla::Maybe(mozilla::Nothing()); + } + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Some(value); +} + +VoidResult RegistrySetValueQword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + ULONGLONG newValue, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Write the value to the registry + LSTATUS ls = + RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + REG_QWORD, &newValue, sizeof(ULONGLONG)); + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Ok(); +} + +MaybeDwordResult RegistryGetValueDword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Read the integer value from the registry + uint32_t value; + DWORD valueSize = sizeof(uint32_t); + LSTATUS ls = + RegGetValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + RRF_RT_DWORD, nullptr, &value, &valueSize); + if (ls == ERROR_FILE_NOT_FOUND) { + return mozilla::Maybe(mozilla::Nothing()); + } + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Some(value); +} + +VoidResult RegistrySetValueDword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + uint32_t newValue, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + // Write the value to the registry + LSTATUS ls = + RegSetKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str(), + REG_DWORD, &newValue, sizeof(uint32_t)); + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Ok(); +} + +VoidResult RegistryDeleteValue(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey /* = nullptr */) { + // Get the full registry value name + WStringResult registryValueNameResult = + MaybePrefixRegistryValueName(isPrefixed, registryValueName); + if (registryValueNameResult.isErr()) { + return mozilla::Err(registryValueNameResult.unwrapErr()); + } + std::wstring valueName = registryValueNameResult.unwrap(); + + std::wstring keyName = MakeKeyName(subKey); + + LSTATUS ls = + RegDeleteKeyValueW(HKEY_CURRENT_USER, keyName.c_str(), valueName.c_str()); + if (ls != ERROR_SUCCESS) { + HRESULT hr = HRESULT_FROM_WIN32(ls); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return mozilla::Ok(); +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/Registry.h b/toolkit/mozapps/defaultagent/Registry.h new file mode 100644 index 0000000000..26bea1ae72 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Registry.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_REGISTRY_H__ +#define __DEFAULT_BROWSER_AGENT_REGISTRY_H__ + +#include +#include + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla::default_agent { + +// Indicates whether or not a registry value name is prefixed with the install +// directory path (Prefixed), or not (Unprefixed). Prefixing a registry value +// name with the install directory makes that value specific to this +// installation's default browser agent. +enum class IsPrefixed { + Prefixed, + Unprefixed, +}; + +// The result of an operation only, containing no other data on success. +using VoidResult = mozilla::WindowsErrorResult; + +using MaybeString = mozilla::Maybe; +using MaybeStringResult = mozilla::WindowsErrorResult; +// Get a string from the registry. If necessary, value name prefixing will be +// performed automatically. +// Strings are stored as wide strings, but are converted to narrow UTF-8 before +// being returned. +MaybeStringResult RegistryGetValueString(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey = nullptr); + +// Set a string in the registry. If necessary, value name prefixing will be +// performed automatically. +// Strings are converted to wide strings for registry storage. +VoidResult RegistrySetValueString(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const char* newValue, + const wchar_t* subKey = nullptr); + +using MaybeBoolResult = mozilla::WindowsErrorResult>; +// Get a bool from the registry. +// Bools are stored as a single DWORD, with 0 meaning false and any other value +// meaning true. +MaybeBoolResult RegistryGetValueBool(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey = nullptr); + +// Set a bool in the registry. If necessary, value name prefixing will be +// performed automatically. +// Bools are stored as a single DWORD, with 0 meaning false and any other value +// meaning true. +VoidResult RegistrySetValueBool(IsPrefixed isPrefixed, + const wchar_t* registryValueName, bool newValue, + const wchar_t* subKey = nullptr); + +using MaybeQwordResult = mozilla::WindowsErrorResult>; +// Get a QWORD (ULONGLONG) from the registry. If necessary, value name prefixing +// will be performed automatically. +MaybeQwordResult RegistryGetValueQword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey = nullptr); + +// Get a QWORD (ULONGLONG) in the registry. If necessary, value name prefixing +// will be performed automatically. +VoidResult RegistrySetValueQword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + ULONGLONG newValue, + const wchar_t* subKey = nullptr); + +using MaybeDword = mozilla::Maybe; +using MaybeDwordResult = mozilla::WindowsErrorResult; +// Get a DWORD (uint32_t) from the registry. If necessary, value name prefixing +// will be performed automatically. +MaybeDwordResult RegistryGetValueDword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey = nullptr); + +// Get a DWORD (uint32_t) in the registry. If necessary, value name prefixing +// will be performed automatically. +VoidResult RegistrySetValueDword(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + uint32_t newValue, + const wchar_t* subKey = nullptr); + +VoidResult RegistryDeleteValue(IsPrefixed isPrefixed, + const wchar_t* registryValueName, + const wchar_t* subKey = nullptr); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_REGISTRY_H__ diff --git a/toolkit/mozapps/defaultagent/ScheduledTask.cpp b/toolkit/mozapps/defaultagent/ScheduledTask.cpp new file mode 100644 index 0000000000..a9cd647c03 --- /dev/null +++ b/toolkit/mozapps/defaultagent/ScheduledTask.cpp @@ -0,0 +1,328 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "ScheduledTask.h" +#include "ScheduledTaskRemove.h" + +#include +#include + +#include +#include + +#include "readstrings.h" +#include "updatererrors.h" +#include "EventLog.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "WindowsDefaultBrowser.h" + +#include "DefaultBrowser.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/intl/Localization.h" +#include "nsString.h" +#include "nsTArray.h" +using mozilla::intl::Localization; + +namespace mozilla::default_agent { + +// The task scheduler requires its time values to come in the form of a string +// in the format YYYY-MM-DDTHH:MM:SSZ. This format string is used to get that +// out of the C library wcsftime function. +const wchar_t* kTimeFormat = L"%Y-%m-%dT%H:%M:%SZ"; +// The expanded time string should always be this length, for example: +// 2020-02-12T16:59:32Z +const size_t kTimeStrMaxLen = 20; + +#define ENSURE(x) \ + if (FAILED(hr = (x))) { \ + LOG_ERROR(hr); \ + return hr; \ + } + +bool GetTaskDescription(mozilla::UniquePtr& description) { + mozilla::UniquePtr installPath; + bool success = GetInstallDirectory(installPath); + if (!success) { + LOG_ERROR_MESSAGE(L"Failed to get install directory"); + return false; + } + nsTArray resIds = {"branding/brand.ftl"_ns, + "browser/backgroundtasks/defaultagent.ftl"_ns}; + RefPtr l10n = Localization::Create(resIds, true); + nsAutoCString daTaskDesc; + mozilla::ErrorResult rv; + l10n->FormatValueSync("default-browser-agent-task-description"_ns, {}, + daTaskDesc, rv); + if (rv.Failed()) { + LOG_ERROR_MESSAGE(L"Failed to read task description"); + return false; + } + NS_ConvertUTF8toUTF16 daTaskDescW(daTaskDesc); + description = mozilla::MakeUnique(daTaskDescW.Length() + 1); + wcsncpy(description.get(), daTaskDescW.get(), daTaskDescW.Length() + 1); + return true; +} + +HRESULT RegisterTask(const wchar_t* uniqueToken, + BSTR startTime /* = nullptr */) { + // Do data migration during the task installation. This might seem like it + // belongs in UpdateTask, but we want to be able to call + // RemoveTasks(); + // RegisterTask(); + // and still have data migration happen. Also, UpdateTask calls this function, + // so migration will still get run in that case. + MaybeMigrateCurrentDefault(); + + // Make sure we don't try to register a task that already exists. + RemoveTasks(uniqueToken, WhichTasks::WdbaTaskOnly); + + // If we create a folder and then fail to create the task, we need to + // remember to delete the folder so that whatever set of permissions it ends + // up with doesn't interfere with trying to create the task again later, and + // so that we don't just leave an empty folder behind. + bool createdFolder = false; + + HRESULT hr = S_OK; + RefPtr scheduler; + ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, + IID_ITaskService, getter_AddRefs(scheduler))); + + ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{})); + + RefPtr rootFolder; + BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\")); + ENSURE( + scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder))); + + RefPtr taskFolder; + BStrPtr vendorBStr = BStrPtr(SysAllocString(kTaskVendor)); + if (FAILED(rootFolder->GetFolder(vendorBStr.get(), + getter_AddRefs(taskFolder)))) { + hr = rootFolder->CreateFolder(vendorBStr.get(), VARIANT{}, + getter_AddRefs(taskFolder)); + + if (SUCCEEDED(hr)) { + createdFolder = true; + } else if (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) { + // `CreateFolder` doesn't assign to the out pointer on + // `ERROR_ALREADY_EXISTS`, so try to get the folder again. This behavior + // is undocumented but was verified in a debugger. + HRESULT priorHr = hr; + hr = rootFolder->GetFolder(vendorBStr.get(), getter_AddRefs(taskFolder)); + + if (FAILED(hr)) { + LOG_ERROR(priorHr); + LOG_ERROR(hr); + return hr; + } + } else { + LOG_ERROR(hr); + return hr; + } + } + + auto cleanupFolder = + mozilla::MakeScopeExit([&hr, createdFolder, &rootFolder, &vendorBStr] { + if (createdFolder && FAILED(hr)) { + // If this fails, we can't really handle that intelligently, so + // don't even bother to check the return code. + rootFolder->DeleteFolder(vendorBStr.get(), 0); + } + }); + + RefPtr newTask; + ENSURE(scheduler->NewTask(0, getter_AddRefs(newTask))); + + mozilla::UniquePtr description; + if (!GetTaskDescription(description)) { + return E_FAIL; + } + BStrPtr descriptionBstr = BStrPtr(SysAllocString(description.get())); + + RefPtr taskRegistration; + ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(taskRegistration))); + ENSURE(taskRegistration->put_Description(descriptionBstr.get())); + + RefPtr taskSettings; + ENSURE(newTask->get_Settings(getter_AddRefs(taskSettings))); + ENSURE(taskSettings->put_DisallowStartIfOnBatteries(VARIANT_FALSE)); + ENSURE(taskSettings->put_MultipleInstances(TASK_INSTANCES_IGNORE_NEW)); + ENSURE(taskSettings->put_StartWhenAvailable(VARIANT_TRUE)); + ENSURE(taskSettings->put_StopIfGoingOnBatteries(VARIANT_FALSE)); + // This cryptic string means "12 hours 5 minutes". So, if the task runs for + // longer than that, the process will be killed, because that should never + // happen. See + // https://docs.microsoft.com/en-us/windows/win32/taskschd/tasksettings-executiontimelimit + // for a detailed explanation of these strings. + BStrPtr execTimeLimitBStr = BStrPtr(SysAllocString(L"PT12H5M")); + ENSURE(taskSettings->put_ExecutionTimeLimit(execTimeLimitBStr.get())); + + RefPtr regInfo; + ENSURE(newTask->get_RegistrationInfo(getter_AddRefs(regInfo))); + + ENSURE(regInfo->put_Author(vendorBStr.get())); + + RefPtr triggers; + ENSURE(newTask->get_Triggers(getter_AddRefs(triggers))); + + RefPtr newTrigger; + ENSURE(triggers->Create(TASK_TRIGGER_DAILY, getter_AddRefs(newTrigger))); + + RefPtr dailyTrigger; + ENSURE(newTrigger->QueryInterface(IID_IDailyTrigger, + getter_AddRefs(dailyTrigger))); + + if (startTime) { + ENSURE(dailyTrigger->put_StartBoundary(startTime)); + } else { + // The time that the task is scheduled to run at every day is taken from the + // time in the trigger's StartBoundary property. We'll set this to the + // current time, on the theory that the time at which we're being installed + // is a time that the computer is likely to be on other days. If our + // theory is wrong and the computer is offline at the scheduled time, then + // because we've set StartWhenAvailable above, the task will run whenever + // it wakes up. Since our task is entirely in the background and doesn't use + // a lot of resources, we're not concerned about it bothering the user if it + // runs while they're actively using this computer. + time_t now_t = time(nullptr); + // Subtract a minute from the current time, to avoid "winning" a potential + // race with the scheduler that might have it start the task immediately + // after we register it, if we finish doing that and then it evaluates the + // trigger during the same second. We haven't seen this happen in practice, + // but there's no documented guarantee that it won't, so let's be sure. + now_t -= 60; + + tm now_tm; + errno_t errno_rv = gmtime_s(&now_tm, &now_t); + if (errno_rv != 0) { + // The C runtime has a (private) function to convert Win32 error codes to + // errno values, but there's nothing that goes the other way, and it + // isn't worth including one here for something that's this unlikely to + // fail anyway. So just return a generic error. + hr = HRESULT_FROM_WIN32(ERROR_INVALID_TIME); + LOG_ERROR(hr); + return hr; + } + + mozilla::UniquePtr timeStr = + mozilla::MakeUnique(kTimeStrMaxLen + 1); + + if (wcsftime(timeStr.get(), kTimeStrMaxLen + 1, kTimeFormat, &now_tm) == + 0) { + hr = E_NOT_SUFFICIENT_BUFFER; + LOG_ERROR(hr); + return hr; + } + + BStrPtr startTimeBStr = BStrPtr(SysAllocString(timeStr.get())); + ENSURE(dailyTrigger->put_StartBoundary(startTimeBStr.get())); + } + + ENSURE(dailyTrigger->put_DaysInterval(1)); + + RefPtr actions; + ENSURE(newTask->get_Actions(getter_AddRefs(actions))); + + RefPtr action; + ENSURE(actions->Create(TASK_ACTION_EXEC, getter_AddRefs(action))); + + RefPtr execAction; + ENSURE(action->QueryInterface(IID_IExecAction, getter_AddRefs(execAction))); + + // Register proxy instead of Firefox background task. + mozilla::UniquePtr installPath = mozilla::GetFullBinaryPath(); + if (!PathRemoveFileSpecW(installPath.get())) { + return E_FAIL; + } + std::wstring proxyPath(installPath.get()); + proxyPath += L"\\default-browser-agent.exe"; + + BStrPtr binaryPathBStr = BStrPtr(SysAllocString(proxyPath.c_str())); + ENSURE(execAction->put_Path(binaryPathBStr.get())); + + std::wstring taskArgs = L"do-task \""; + taskArgs += uniqueToken; + taskArgs += L"\""; + BStrPtr argsBStr = BStrPtr(SysAllocString(taskArgs.c_str())); + ENSURE(execAction->put_Arguments(argsBStr.get())); + + std::wstring taskName(kTaskName); + taskName += uniqueToken; + BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str())); + + RefPtr registeredTask; + ENSURE(taskFolder->RegisterTaskDefinition( + taskNameBStr.get(), newTask, TASK_CREATE_OR_UPDATE, VARIANT{}, VARIANT{}, + TASK_LOGON_INTERACTIVE_TOKEN, VARIANT{}, getter_AddRefs(registeredTask))); + + return hr; +} + +HRESULT UpdateTask(const wchar_t* uniqueToken) { + RefPtr scheduler; + HRESULT hr = S_OK; + ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, + IID_ITaskService, getter_AddRefs(scheduler))); + + ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{})); + + RefPtr taskFolder; + BStrPtr folderBStr = BStrPtr(SysAllocString(kTaskVendor)); + + if (FAILED( + scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)))) { + // If our folder doesn't exist, create it and the task. + return RegisterTask(uniqueToken); + } + + std::wstring taskName(kTaskName); + taskName += uniqueToken; + BStrPtr taskNameBStr = BStrPtr(SysAllocString(taskName.c_str())); + + RefPtr task; + if (FAILED(taskFolder->GetTask(taskNameBStr.get(), getter_AddRefs(task)))) { + // If our task doesn't exist at all, just create one. + return RegisterTask(uniqueToken); + } + + // If we have a task registered already, we need to recreate it because + // something might have changed that we need to update. But we don't + // want to restart the schedule from now, because that might mean the + // task never runs at all for e.g. Nightly. So create a new task, but + // first get and preserve the existing trigger. + RefPtr definition; + if (FAILED(task->get_Definition(getter_AddRefs(definition)))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + + RefPtr triggerList; + if (FAILED(definition->get_Triggers(getter_AddRefs(triggerList)))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + + RefPtr trigger; + if (FAILED(triggerList->get_Item(1, getter_AddRefs(trigger)))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + + BSTR startTimeBstr; + if (FAILED(trigger->get_StartBoundary(&startTimeBstr))) { + // This task is broken, make a new one. + return RegisterTask(uniqueToken); + } + BStrPtr startTime(startTimeBstr); + + return RegisterTask(uniqueToken, startTime.get()); +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/ScheduledTask.h b/toolkit/mozapps/defaultagent/ScheduledTask.h new file mode 100644 index 0000000000..a3709823ad --- /dev/null +++ b/toolkit/mozapps/defaultagent/ScheduledTask.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__ +#define __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__ + +#include +#include + +namespace mozilla::default_agent { + +// uniqueToken should be a string unique to the installation, so that a +// separate task can be created for each installation. Typically this will be +// the install hash string. +HRESULT RegisterTask(const wchar_t* uniqueToken, BSTR startTime = nullptr); +HRESULT UpdateTask(const wchar_t* uniqueToken); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_H__ diff --git a/toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp new file mode 100644 index 0000000000..e672a813e3 --- /dev/null +++ b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.cpp @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "ScheduledTaskRemove.h" + +#include + +#include +#include + +#include "EventLog.h" +#include "mozilla/RefPtr.h" + +namespace mozilla::default_agent { + +#define ENSURE(x) \ + if (FAILED(hr = (x))) { \ + LOG_ERROR(hr); \ + return hr; \ + } + +bool EndsWith(const wchar_t* string, const wchar_t* suffix) { + size_t string_len = wcslen(string); + size_t suffix_len = wcslen(suffix); + if (suffix_len > string_len) { + return false; + } + const wchar_t* substring = string + string_len - suffix_len; + return wcscmp(substring, suffix) == 0; +} + +HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove) { + if (!uniqueToken || wcslen(uniqueToken) == 0) { + return E_INVALIDARG; + } + + RefPtr scheduler; + HRESULT hr = S_OK; + ENSURE(CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER, + IID_ITaskService, getter_AddRefs(scheduler))); + + ENSURE(scheduler->Connect(VARIANT{}, VARIANT{}, VARIANT{}, VARIANT{})); + + RefPtr taskFolder; + BStrPtr folderBStr(SysAllocString(kTaskVendor)); + + hr = scheduler->GetFolder(folderBStr.get(), getter_AddRefs(taskFolder)); + if (FAILED(hr)) { + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { + // Don't return an error code if our folder doesn't exist, + // because that just means it's been removed already. + return S_OK; + } else { + return hr; + } + } + + RefPtr tasksInFolder; + ENSURE(taskFolder->GetTasks(TASK_ENUM_HIDDEN, getter_AddRefs(tasksInFolder))); + + LONG numTasks = 0; + ENSURE(tasksInFolder->get_Count(&numTasks)); + + std::wstring WdbaTaskName(kTaskName); + WdbaTaskName += uniqueToken; + + // This will be set to the last error that we encounter while deleting tasks. + // This allows us to keep attempting to remove the remaining tasks, even if + // we encounter an error, while still preserving what error we encountered so + // we can return it from this function. + HRESULT deleteResult = S_OK; + // Set to true if we intentionally skip any tasks. + bool tasksSkipped = false; + + for (LONG i = 0; i < numTasks; ++i) { + RefPtr task; + // IRegisteredTaskCollection's are 1-indexed. + hr = tasksInFolder->get_Item(_variant_t(i + 1), getter_AddRefs(task)); + if (FAILED(hr)) { + deleteResult = hr; + continue; + } + + BSTR taskName; + hr = task->get_Name(&taskName); + if (FAILED(hr)) { + deleteResult = hr; + continue; + } + // Automatically free taskName when we are done with it. + BStrPtr uniqueTaskName(taskName); + + if (tasksToRemove == WhichTasks::WdbaTaskOnly) { + if (WdbaTaskName.compare(taskName) != 0) { + tasksSkipped = true; + continue; + } + } else { // tasksToRemove == WhichTasks::AllTasksForInstallation + if (!EndsWith(taskName, uniqueToken)) { + tasksSkipped = true; + continue; + } + } + + hr = taskFolder->DeleteTask(taskName, 0 /* flags */); + if (FAILED(hr)) { + deleteResult = hr; + } + } + + // If we successfully removed all the tasks, delete the folder too. + if (!tasksSkipped && SUCCEEDED(deleteResult)) { + RefPtr rootFolder; + BStrPtr rootFolderBStr = BStrPtr(SysAllocString(L"\\")); + ENSURE( + scheduler->GetFolder(rootFolderBStr.get(), getter_AddRefs(rootFolder))); + ENSURE(rootFolder->DeleteFolder(folderBStr.get(), 0)); + } + + return deleteResult; +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/ScheduledTaskRemove.h b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.h new file mode 100644 index 0000000000..17fd75d5e1 --- /dev/null +++ b/toolkit/mozapps/defaultagent/ScheduledTaskRemove.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_REMOVE_H__ +#define __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_REMOVE_H__ + +#include +#include + +#include + +#include "mozilla/UniquePtr.h" + +namespace mozilla::default_agent { + +struct SysFreeStringDeleter { + void operator()(BSTR aPtr) { ::SysFreeString(aPtr); } +}; +using BStrPtr = mozilla::UniquePtr; + +static const wchar_t* kTaskVendor = L"" MOZ_APP_VENDOR; +// kTaskName should have the unique token appended before being used. +static const wchar_t* kTaskName = + L"" MOZ_APP_DISPLAYNAME " Default Browser Agent "; + +enum class WhichTasks { + WdbaTaskOnly, + AllTasksForInstallation, +}; +HRESULT RemoveTasks(const wchar_t* uniqueToken, WhichTasks tasksToRemove); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_SCHEDULED_TASK_REMOVE_H__ diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp new file mode 100644 index 0000000000..8bc0889e67 --- /dev/null +++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp @@ -0,0 +1,347 @@ +/* -*- 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 +#include +#include // for SHChangeNotify and IApplicationAssociationRegistration +#include +#include + +#include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "WindowsUserChoice.h" +#include "nsThreadUtils.h" + +#include "EventLog.h" +#include "SetDefaultBrowser.h" + +namespace mozilla::default_agent { + +/* + * The implementation for setting extension handlers by writing UserChoice. + * + * This is used by both SetDefaultBrowserUserChoice and + * SetDefaultExtensionHandlersUserChoice. + * + * @param aAumi The AUMI of the installation to set as default. + * + * @param aSid Current user's string SID + * + * @param aExtraFileExtensions Optional array of extra file association pairs to + * set as default, like `[ ".pdf", "FirefoxPDF" ]`. + * + * @returns NS_OK All associations set and checked + * successfully. + * NS_ERROR_WDBA_REJECTED UserChoice was set, but checking the default + * did not return our ProgID. + * NS_ERROR_FAILURE Failed to set at least one association. + */ +static nsresult SetDefaultExtensionHandlersUserChoiceImpl( + const wchar_t* aAumi, const wchar_t* const aSid, + const nsTArray& aFileExtensions); + +static bool AddMillisecondsToSystemTime(SYSTEMTIME& aSystemTime, + ULONGLONG aIncrementMS) { + FILETIME fileTime; + ULARGE_INTEGER fileTimeInt; + if (!::SystemTimeToFileTime(&aSystemTime, &fileTime)) { + return false; + } + fileTimeInt.LowPart = fileTime.dwLowDateTime; + fileTimeInt.HighPart = fileTime.dwHighDateTime; + + // FILETIME is in units of 100ns. + fileTimeInt.QuadPart += aIncrementMS * 1000 * 10; + + fileTime.dwLowDateTime = fileTimeInt.LowPart; + fileTime.dwHighDateTime = fileTimeInt.HighPart; + SYSTEMTIME tmpSystemTime; + if (!::FileTimeToSystemTime(&fileTime, &tmpSystemTime)) { + return false; + } + + aSystemTime = tmpSystemTime; + return true; +} + +// Compare two SYSTEMTIMEs as FILETIME after clearing everything +// below minutes. +static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1, + SYSTEMTIME aSystemTime2) { + aSystemTime1.wSecond = 0; + aSystemTime1.wMilliseconds = 0; + + aSystemTime2.wSecond = 0; + aSystemTime2.wMilliseconds = 0; + + FILETIME fileTime1; + FILETIME fileTime2; + if (!::SystemTimeToFileTime(&aSystemTime1, &fileTime1) || + !::SystemTimeToFileTime(&aSystemTime2, &fileTime2)) { + return false; + } + + return (fileTime1.dwLowDateTime == fileTime2.dwLowDateTime) && + (fileTime1.dwHighDateTime == fileTime2.dwHighDateTime); +} + +static bool SetUserChoiceRegistry(const wchar_t* aExt, const wchar_t* aProgID, + mozilla::UniquePtr aHash) { + auto assocKeyPath = GetAssociationKeyPath(aExt); + if (!assocKeyPath) { + return false; + } + + LSTATUS ls; + HKEY rawAssocKey; + ls = ::RegOpenKeyExW(HKEY_CURRENT_USER, assocKeyPath.get(), 0, + KEY_READ | KEY_WRITE, &rawAssocKey); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + nsAutoRegKey assocKey(rawAssocKey); + + // When Windows creates this key, it is read-only (Deny Set Value), so we need + // to delete it first. + // We don't set any similar special permissions. + ls = ::RegDeleteKeyW(assocKey.get(), L"UserChoice"); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + + HKEY rawUserChoiceKey; + ls = ::RegCreateKeyExW(assocKey.get(), L"UserChoice", 0, nullptr, + 0 /* options */, KEY_READ | KEY_WRITE, + 0 /* security attributes */, &rawUserChoiceKey, + nullptr); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + nsAutoRegKey userChoiceKey(rawUserChoiceKey); + + DWORD progIdByteCount = (::lstrlenW(aProgID) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"ProgID", 0, REG_SZ, + reinterpret_cast(aProgID), + progIdByteCount); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + + DWORD hashByteCount = (::lstrlenW(aHash.get()) + 1) * sizeof(wchar_t); + ls = ::RegSetValueExW(userChoiceKey.get(), L"Hash", 0, REG_SZ, + reinterpret_cast(aHash.get()), + hashByteCount); + if (ls != ERROR_SUCCESS) { + LOG_ERROR(HRESULT_FROM_WIN32(ls)); + return false; + } + + return true; +} + +/* + * Set an association with a UserChoice key + * + * Removes the old key, creates a new one with ProgID and Hash set to + * enable a new asociation. + * + * @param aExt File type or protocol to associate + * @param aSid Current user's string SID + * @param aProgID ProgID to use for the asociation + * @param inMsix Are we running from in an msix package? + * + * @return true if successful, false on error. + */ +static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid, + const wchar_t* aProgID, bool inMsix) { + if (inMsix) { + LOG_ERROR_MESSAGE(L"SetUserChoice does not work on MSIX builds."); + return false; + } + + SYSTEMTIME hashTimestamp; + ::GetSystemTime(&hashTimestamp); + auto hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp); + if (!hash) { + return false; + } + + // The hash changes at the end of each minute, so check that the hash should + // be the same by the time we're done writing. + const ULONGLONG kWriteTimingThresholdMilliseconds = 1000; + // Generating the hash could have taken some time, so start from now. + SYSTEMTIME writeEndTimestamp; + ::GetSystemTime(&writeEndTimestamp); + if (!AddMillisecondsToSystemTime(writeEndTimestamp, + kWriteTimingThresholdMilliseconds)) { + return false; + } + if (!CheckEqualMinutes(hashTimestamp, writeEndTimestamp)) { + LOG_ERROR_MESSAGE( + L"Hash is too close to expiration, sleeping until next hash."); + ::Sleep(kWriteTimingThresholdMilliseconds * 2); + + // For consistency, use the current time. + ::GetSystemTime(&hashTimestamp); + hash = GenerateUserChoiceHash(aExt, aSid, aProgID, hashTimestamp); + if (!hash) { + return false; + } + } + + // We're outside of an MSIX package and can use the Win32 Registry API. + return SetUserChoiceRegistry(aExt, aProgID, std::move(hash)); +} + +static bool VerifyUserDefault(const wchar_t* aExt, const wchar_t* aProgID) { + RefPtr pAAR; + HRESULT hr = ::CoCreateInstance( + CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC, + IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR)); + if (FAILED(hr)) { + LOG_ERROR(hr); + return false; + } + + wchar_t* rawRegisteredApp; + bool isProtocol = aExt[0] != L'.'; + // Note: Checks AL_USER instead of AL_EFFECTIVE. + hr = pAAR->QueryCurrentDefault(aExt, + isProtocol ? AT_URLPROTOCOL : AT_FILEEXTENSION, + AL_USER, &rawRegisteredApp); + if (FAILED(hr)) { + if (hr == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) { + LOG_ERROR_MESSAGE(L"UserChoice ProgID %s for %s was rejected", aProgID, + aExt); + } else { + LOG_ERROR(hr); + } + return false; + } + mozilla::UniquePtr registeredApp( + rawRegisteredApp); + + if (::CompareStringOrdinal(registeredApp.get(), -1, aProgID, -1, FALSE) != + CSTR_EQUAL) { + LOG_ERROR_MESSAGE( + L"Default was %s after writing ProgID %s to UserChoice for %s", + registeredApp.get(), aProgID, aExt); + return false; + } + + return true; +} + +nsresult SetDefaultBrowserUserChoice( + const wchar_t* aAumi, const nsTArray& aExtraFileExtensions) { + // Verify that the implementation of UserChoice hashing has not changed by + // computing the current default hash and comparing with the existing value. + if (!CheckBrowserUserChoiceHashes()) { + LOG_ERROR_MESSAGE(L"UserChoice Hash mismatch"); + return NS_ERROR_WDBA_HASH_CHECK; + } + + if (!mozilla::IsWin10CreatorsUpdateOrLater()) { + LOG_ERROR_MESSAGE(L"UserChoice hash matched, but Windows build is too old"); + return NS_ERROR_WDBA_BUILD; + } + + auto sid = GetCurrentUserStringSid(); + if (!sid) { + return NS_ERROR_FAILURE; + } + + nsTArray browserDefaults = { + u"https"_ns, u"FirefoxURL"_ns, u"http"_ns, u"FirefoxURL"_ns, + u".html"_ns, u"FirefoxHTML"_ns, u".htm"_ns, u"FirefoxHTML"_ns}; + + browserDefaults.AppendElements(aExtraFileExtensions); + + nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(), + browserDefaults); + if (!NS_SUCCEEDED(rv)) { + LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); + } + + // Notify shell to refresh icons + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); + + return rv; +} + +nsresult SetDefaultExtensionHandlersUserChoice( + const wchar_t* aAumi, const nsTArray& aFileExtensions) { + auto sid = GetCurrentUserStringSid(); + if (!sid) { + return NS_ERROR_FAILURE; + } + + nsresult rv = SetDefaultExtensionHandlersUserChoiceImpl(aAumi, sid.get(), + aFileExtensions); + if (!NS_SUCCEEDED(rv)) { + LOG_ERROR_MESSAGE(L"Failed setting default with %s", aAumi); + } + + // Notify shell to refresh icons + ::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); + + return rv; +} + +nsresult SetDefaultExtensionHandlersUserChoiceImpl( + const wchar_t* aAumi, const wchar_t* const aSid, + const nsTArray& aFileExtensions) { + UINT32 pfnLen = 0; + bool inMsix = + GetCurrentPackageFullName(&pfnLen, nullptr) != APPMODEL_ERROR_NO_PACKAGE; + + if (inMsix) { + // MSIX packages can not meaningfully modify the registry keys related to + // default handlers + return NS_ERROR_FAILURE; + } + + for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) { + const wchar_t* extraFileExtension = aFileExtensions[i].get(); + const wchar_t* extraProgIDRoot = aFileExtensions[i + 1].get(); + // Formatting the ProgID here prevents using this helper to target arbitrary + // ProgIDs. + mozilla::UniquePtr extraProgID; + if (inMsix) { + nsresult rv = GetMsixProgId(extraFileExtension, extraProgID); + if (NS_FAILED(rv)) { + LOG_ERROR_MESSAGE(L"Failed to retrieve MSIX progID for %s", + extraFileExtension); + return rv; + } + } else { + extraProgID = FormatProgID(extraProgIDRoot, aAumi); + if (!CheckProgIDExists(extraProgID.get())) { + LOG_ERROR_MESSAGE(L"ProgID %s not found", extraProgID.get()); + return NS_ERROR_WDBA_NO_PROGID; + } + } + + if (!SetUserChoice(extraFileExtension, aSid, extraProgID.get(), inMsix)) { + return NS_ERROR_FAILURE; + } + + if (!VerifyUserDefault(extraFileExtension, extraProgID.get())) { + return NS_ERROR_WDBA_REJECTED; + } + } + + return NS_OK; +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.h b/toolkit/mozapps/defaultagent/SetDefaultBrowser.h new file mode 100644 index 0000000000..bb33365058 --- /dev/null +++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.h @@ -0,0 +1,66 @@ +/* -*- 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 DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__ +#define DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__ + +#include "nsStringFwd.h" +#include "nsArray.h" +#include + +namespace mozilla::default_agent { + +/* + * Set the default browser by writing the UserChoice registry keys. + * + * This sets the associations for https, http, .html, and .htm, and + * optionally for additional extra file extensions. + * + * When the agent is run with set-default-browser-user-choice, + * the exit code is the result of this function. + * + * @param aAumi The AUMI of the installation to set as default. + * + * @param aExtraFileExtensions Optional array of extra file association pairs to + * set as default, like `[ ".pdf", "FirefoxPDF" ]`. + * + * @return NS_OK All associations set and checked + * successfully. + * NS_ERROR_WDBA_NO_PROGID The ProgID classes had not been registered. + * NS_ERROR_WDBA_HASH_CHECK The existing UserChoice Hash could not be + * verified. + * NS_ERROR_WDBA_REJECTED UserChoice was set, but checking the default + * did not return our ProgID. + * NS_ERROR_WDBA_BUILD The existing UserChoice Hash was verified, + * but we're on an older, unsupported Windows + * build, so do not attempt to update the + * UserChoice hash. + * NS_ERROR_FAILURE other failure + */ +nsresult SetDefaultBrowserUserChoice( + const wchar_t* aAumi, + const nsTArray& aExtraFileExtensions = nsTArray()); + +/* + * Set the default extension handlers for the given file extensions by writing + * the UserChoice registry keys. + * + * @param aAumi The AUMI of the installation to set as default. + * + * @param aExtraFileExtensions Optional array of extra file association pairs to + * set as default, like `[ ".pdf", "FirefoxPDF" ]`. + * + * @returns NS_OK All associations set and checked + * successfully. + * NS_ERROR_WDBA_REJECTED UserChoice was set, but checking the default + * did not return our ProgID. + * NS_ERROR_FAILURE Failed to set at least one association. + */ +nsresult SetDefaultExtensionHandlersUserChoice( + const wchar_t* aAumi, const nsTArray& aFileExtensions); + +} // namespace mozilla::default_agent + +#endif // DEFAULT_BROWSER_SET_DEFAULT_BROWSER_H__ diff --git a/toolkit/mozapps/defaultagent/Telemetry.cpp b/toolkit/mozapps/defaultagent/Telemetry.cpp new file mode 100644 index 0000000000..0b71fb7949 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Telemetry.cpp @@ -0,0 +1,585 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "Telemetry.h" + +#include +#include + +#include + +#include +#include + +#include "common.h" +#include "Cache.h" +#include "EventLog.h" +#include "Notification.h" +#include "Policy.h" +#include "UtfConvert.h" +#include "Registry.h" + +#include "json/json.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/glean/GleanPings.h" +#include "mozilla/HelperMacros.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WinHeaderOnlyUtils.h" +#include "nsStringFwd.h" + +#define TELEMETRY_BASE_URL "https://incoming.telemetry.mozilla.org/submit" +#define TELEMETRY_NAMESPACE "default-browser-agent" +#define TELEMETRY_PING_VERSION "1" +#define TELEMETRY_PING_DOCTYPE "default-browser" + +// This is almost the complete URL, just needs a UUID appended. +#define TELEMETRY_PING_URL \ + TELEMETRY_BASE_URL "/" TELEMETRY_NAMESPACE "/" TELEMETRY_PING_DOCTYPE \ + "/" TELEMETRY_PING_VERSION "/" + +// We only want to send one ping per day. However, this is slightly less than 24 +// hours so that we have a little bit of wiggle room on our task, which is also +// supposed to run every 24 hours. +#define MINIMUM_PING_PERIOD_SEC ((23 * 60 * 60) + (45 * 60)) + +#define PREV_NOTIFICATION_ACTION_REG_NAME L"PrevNotificationAction" + +#if !defined(RRF_SUBKEY_WOW6464KEY) +# define RRF_SUBKEY_WOW6464KEY 0x00010000 +#endif // !defined(RRF_SUBKEY_WOW6464KEY) + +namespace mozilla::default_agent { + +using TelemetryFieldResult = mozilla::WindowsErrorResult; +using BoolResult = mozilla::WindowsErrorResult; + +// This function was copied from the implementation of +// nsITelemetry::isOfficialTelemetry, currently found in the file +// toolkit/components/telemetry/core/Telemetry.cpp. +static bool IsOfficialTelemetry() { +#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && \ + !defined(DEBUG) + return true; +#else + return false; +#endif +} + +static TelemetryFieldResult GetOSVersion() { + OSVERSIONINFOEXW osv = {sizeof(osv)}; + if (::GetVersionExW(reinterpret_cast(&osv))) { + std::ostringstream oss; + oss << osv.dwMajorVersion << "." << osv.dwMinorVersion << "." + << osv.dwBuildNumber; + + if (osv.dwMajorVersion == 10 && osv.dwMinorVersion == 0) { + // Get the "Update Build Revision" (UBR) value + DWORD ubrValue; + DWORD ubrValueLen = sizeof(ubrValue); + LSTATUS ubrOk = + ::RegGetValueW(HKEY_LOCAL_MACHINE, + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + L"UBR", RRF_RT_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, + &ubrValue, &ubrValueLen); + if (ubrOk == ERROR_SUCCESS) { + oss << "." << ubrValue; + } + } + + return oss.str(); + } + + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); +} + +static TelemetryFieldResult GetOSLocale() { + wchar_t localeName[LOCALE_NAME_MAX_LENGTH] = L""; + if (!GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH)) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); + } + + // We'll need the locale string in UTF-8 to be able to submit it. + Utf16ToUtf8Result narrowLocaleName = Utf16ToUtf8(localeName); + + return narrowLocaleName.unwrapOr(""); +} + +static FilePathResult GetPingFilePath(std::wstring& uuid) { + wchar_t* rawAppDataPath; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, + &rawAppDataPath); + if (FAILED(hr)) { + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + mozilla::UniquePtr appDataPath( + rawAppDataPath); + + // The Path* functions don't set LastError, but this is the only thing that + // can really cause them to fail, so if they ever do we assume this is why. + hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); + + wchar_t pingFilePath[MAX_PATH] = L""; + if (!PathCombineW(pingFilePath, appDataPath.get(), L"" MOZ_APP_VENDOR)) { + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + + if (!PathAppendW(pingFilePath, L"" MOZ_APP_BASENAME)) { + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + + if (!PathAppendW(pingFilePath, L"Pending Pings")) { + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + + if (!PathAppendW(pingFilePath, uuid.c_str())) { + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + + return std::wstring(pingFilePath); +} + +// Sends Firefox Desktop telemetry ping. Note: this is sent in parallel to Glean +// telemetry. +static mozilla::WindowsError SendDesktopTelemetryPing( + const std::string defaultBrowser, const std::string previousDefaultBrowser, + const std::string defaultPdf, const std::string osVersion, + const std::string prevOSVersion, const std::string osLocale, + const std::string notificationType, const std::string notificationShown, + const std::string notificationAction, + const std::string prevNotificationAction) { + // Fill in the ping JSON object. + Json::Value ping; + ping["build_channel"] = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL); + ping["build_version"] = MOZILLA_VERSION; + ping["default_browser"] = defaultBrowser; + ping["previous_default_browser"] = previousDefaultBrowser; + ping["default_pdf_viewer_raw"] = defaultPdf; + ping["os_version"] = osVersion; + ping["previous_os_version"] = prevOSVersion; + ping["os_locale"] = osLocale; + ping["notification_type"] = notificationType; + ping["notification_shown"] = notificationShown; + ping["notification_action"] = notificationAction; + ping["previous_notification_action"] = prevNotificationAction; + + // Stringify the JSON. + Json::StreamWriterBuilder jsonStream; + jsonStream["indentation"] = ""; + std::string pingStr = Json::writeString(jsonStream, ping); + + // Generate a UUID for the ping. + FilePathResult uuidResult = GenerateUUIDStr(); + if (uuidResult.isErr()) { + return uuidResult.unwrapErr(); + } + std::wstring uuid = uuidResult.unwrap(); + + // Write the JSON string to a file. Use the UUID in the file name so that if + // multiple instances of this task are running they'll have their own files. + FilePathResult pingFilePathResult = GetPingFilePath(uuid); + if (pingFilePathResult.isErr()) { + return pingFilePathResult.unwrapErr(); + } + std::wstring pingFilePath = pingFilePathResult.unwrap(); + + { + std::ofstream outFile(pingFilePath); + outFile << pingStr; + if (outFile.fail()) { + // We have no way to get a specific error code out of a file stream + // other than to catch an exception, so substitute a generic error code. + HRESULT hr = HRESULT_FROM_WIN32(ERROR_IO_DEVICE); + LOG_ERROR(hr); + return mozilla::WindowsError::FromHResult(hr); + } + } + + // Hand the file off to pingsender to submit. + FilePathResult pingsenderPathResult = + GetRelativeBinaryPath(L"pingsender.exe"); + if (pingsenderPathResult.isErr()) { + return pingsenderPathResult.unwrapErr(); + } + std::wstring pingsenderPath = pingsenderPathResult.unwrap(); + + std::wstring url(L"" TELEMETRY_PING_URL); + url.append(uuid); + + const wchar_t* pingsenderArgs[] = {pingsenderPath.c_str(), url.c_str(), + pingFilePath.c_str()}; + mozilla::UniquePtr pingsenderCmdLine( + mozilla::MakeCommandLine(mozilla::ArrayLength(pingsenderArgs), + const_cast(pingsenderArgs))); + + PROCESS_INFORMATION pi; + STARTUPINFOW si = {sizeof(si)}; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + if (!::CreateProcessW(pingsenderPath.c_str(), pingsenderCmdLine.get(), + nullptr, nullptr, FALSE, + DETACHED_PROCESS | NORMAL_PRIORITY_CLASS, nullptr, + nullptr, &si, &pi)) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return mozilla::WindowsError::FromHResult(hr); + } + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + + return mozilla::WindowsError::CreateSuccess(); +} + +// This function checks if a ping has already been sent today. If one has not, +// it assumes that we are about to send one and sets a registry entry that will +// cause this function to return true for the next day. +// This function uses unprefixed registry entries, so a RegistryMutex should be +// held before calling. +static BoolResult GetPingAlreadySentToday() { + const wchar_t* valueName = L"LastPingSentAt"; + MaybeQwordResult readResult = + RegistryGetValueQword(IsPrefixed::Unprefixed, valueName); + if (readResult.isErr()) { + HRESULT hr = readResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr); + return BoolResult(mozilla::WindowsError::FromHResult(hr)); + } + mozilla::Maybe maybeValue = readResult.unwrap(); + ULONGLONG now = GetCurrentTimestamp(); + if (maybeValue.isSome()) { + ULONGLONG lastPingTime = maybeValue.value(); + if (SecondsPassedSince(lastPingTime, now) < MINIMUM_PING_PERIOD_SEC) { + return true; + } + } + + mozilla::WindowsErrorResult writeResult = + RegistrySetValueQword(IsPrefixed::Unprefixed, valueName, now); + if (writeResult.isErr()) { + HRESULT hr = readResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr); + return BoolResult(mozilla::WindowsError::FromHResult(hr)); + } + return false; +} + +// This both retrieves a value from the registry and writes new data +// (currentDefault) to the same value. If there is no value stored, the value +// passed for prevDefault will be converted to a string and returned instead. +// +// Although we already store and retrieve a cached previous default browser +// value elsewhere, it may be updated when we don't send a ping. The value we +// retrieve here will only be updated when we are sending a ping to ensure +// that pings don't miss a default browser transition. +static TelemetryFieldResult GetAndUpdatePreviousDefaultBrowser( + const std::string& currentDefault, Browser prevDefault) { + const wchar_t* registryValueName = L"PingCurrentDefault"; + + MaybeStringResult readResult = + RegistryGetValueString(IsPrefixed::Unprefixed, registryValueName); + if (readResult.isErr()) { + HRESULT hr = readResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr); + return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); + } + mozilla::Maybe maybeValue = readResult.unwrap(); + std::string oldCurrentDefault; + if (maybeValue.isSome()) { + oldCurrentDefault = maybeValue.value(); + } else { + oldCurrentDefault = GetStringForBrowser(prevDefault); + } + + mozilla::WindowsErrorResult writeResult = RegistrySetValueString( + IsPrefixed::Unprefixed, registryValueName, currentDefault.c_str()); + if (writeResult.isErr()) { + HRESULT hr = writeResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr); + return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); + } + return oldCurrentDefault; +} + +// This both retrieves a value from the registry and writes new data +// (`currentOSVersion`) to the same value. If there is no value stored, +// `currentOSVersion` is returned instead. +// +// The value we retrieve here will only be updated when we are sending a ping to +// ensure that pings don't miss a Windows OS version transition. +static TelemetryFieldResult GetAndUpdatePreviousOSVersion( + const std::string& currentOSVersion) { + const wchar_t* registryValueName = L"PingCurrentOSVersion"; + + MaybeStringResult readResult = + RegistryGetValueString(IsPrefixed::Unprefixed, registryValueName); + if (readResult.isErr()) { + HRESULT hr = readResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to read registry: %#X", hr); + return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); + } + mozilla::Maybe maybeValue = readResult.unwrap(); + std::string oldOSVersion = maybeValue.valueOr(currentOSVersion); + + mozilla::WindowsErrorResult writeResult = RegistrySetValueString( + IsPrefixed::Unprefixed, registryValueName, currentOSVersion.c_str()); + if (writeResult.isErr()) { + HRESULT hr = writeResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to write registry: %#X", hr); + return TelemetryFieldResult(mozilla::WindowsError::FromHResult(hr)); + } + return oldOSVersion; +} + +// If notifications actions occurred, we want to make sure a ping gets sent for +// them. If we aren't sending a ping right now, we want to cache the ping values +// for the next time the ping is sent. +// The values passed will only be cached if actions were actually taken +// (i.e. not when notificationShown == "not-shown") +HRESULT MaybeCache(Cache& cache, const std::string& notificationType, + const std::string& notificationShown, + const std::string& notificationAction, + const std::string& prevNotificationAction) { + std::string notShown = + GetStringForNotificationShown(NotificationShown::NotShown); + if (notificationShown == notShown) { + return S_OK; + } + + Cache::Entry entry{ + .notificationType = notificationType, + .notificationShown = notificationShown, + .notificationAction = notificationAction, + .prevNotificationAction = prevNotificationAction, + }; + VoidResult result = cache.Enqueue(entry); + if (result.isErr()) { + return result.unwrapErr().AsHResult(); + } + return S_OK; +} + +// This function retrieves values cached by MaybeCache. If any values were +// loaded from the cache, the values passed in to this function are passed to +// MaybeCache so that they are not lost. If there are no values in the cache, +// the values passed will not be changed. +// Values retrieved from the cache will also be removed from it. +HRESULT MaybeSwapForCached(Cache& cache, std::string& notificationType, + std::string& notificationShown, + std::string& notificationAction, + std::string& prevNotificationAction) { + Cache::MaybeEntryResult result = cache.Dequeue(); + if (result.isErr()) { + HRESULT hr = result.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Failed to read cache: %#X", hr); + return hr; + } + Cache::MaybeEntry maybeEntry = result.unwrap(); + if (maybeEntry.isNothing()) { + return S_OK; + } + + MaybeCache(cache, notificationType, notificationShown, notificationAction, + prevNotificationAction); + notificationType = maybeEntry.value().notificationType; + notificationShown = maybeEntry.value().notificationShown; + notificationAction = maybeEntry.value().notificationAction; + if (maybeEntry.value().prevNotificationAction.isSome()) { + prevNotificationAction = maybeEntry.value().prevNotificationAction.value(); + } else { + prevNotificationAction = + GetStringForNotificationAction(NotificationAction::NoAction); + } + return S_OK; +} + +HRESULT ReadPreviousNotificationAction(std::string& prevAction) { + MaybeStringResult maybePrevActionResult = RegistryGetValueString( + IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME); + if (maybePrevActionResult.isErr()) { + HRESULT hr = maybePrevActionResult.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to read prev action from registry: %#X", hr); + return hr; + } + mozilla::Maybe maybePrevAction = maybePrevActionResult.unwrap(); + if (maybePrevAction.isNothing()) { + prevAction = GetStringForNotificationAction(NotificationAction::NoAction); + } else { + prevAction = maybePrevAction.value(); + // There's no good reason why there should be an invalid value stored here. + // But it's also not worth aborting the whole ping over. This function will + // silently change it to "no-action" if the value isn't valid to prevent us + // from sending unexpected telemetry values. + EnsureValidNotificationAction(prevAction); + } + return S_OK; +} + +// Writes the previous notification action to the registry, but only if a +// notification was shown. +HRESULT MaybeWritePreviousNotificationAction( + const NotificationActivities& activitiesPerformed) { + if (activitiesPerformed.shown != NotificationShown::Shown) { + return S_OK; + } + std::string notificationAction = + GetStringForNotificationAction(activitiesPerformed.action); + mozilla::WindowsErrorResult result = RegistrySetValueString( + IsPrefixed::Unprefixed, PREV_NOTIFICATION_ACTION_REG_NAME, + notificationAction.c_str()); + if (result.isErr()) { + HRESULT hr = result.unwrapErr().AsHResult(); + LOG_ERROR_MESSAGE(L"Unable to write prev action to registry: %#X", hr); + return hr; + } + return S_OK; +} + +// Sends Firefox Desktop and Glean telemetry for the Default Agent in parallel. +HRESULT SendDefaultAgentPing( + const DefaultBrowserInfo& browserInfo, const DefaultPdfInfo& pdfInfo, + const NotificationActivities& activitiesPerformed) { + std::string currentDefaultBrowser = + GetStringForBrowser(browserInfo.currentDefaultBrowser); + std::string currentDefaultPdf = + GetStringForPDFHandler(pdfInfo.currentDefaultPdf); + std::string notificationType = + GetStringForNotificationType(activitiesPerformed.type); + std::string notificationShown = + GetStringForNotificationShown(activitiesPerformed.shown); + std::string notificationAction = + GetStringForNotificationAction(activitiesPerformed.action); + + TelemetryFieldResult osVersionResult = GetOSVersion(); + if (osVersionResult.isErr()) { + return osVersionResult.unwrapErr().AsHResult(); + } + std::string osVersion = osVersionResult.unwrap(); + + TelemetryFieldResult osLocaleResult = GetOSLocale(); + if (osLocaleResult.isErr()) { + return osLocaleResult.unwrapErr().AsHResult(); + } + std::string osLocale = osLocaleResult.unwrap(); + + std::string prevNotificationAction; + HRESULT hr = ReadPreviousNotificationAction(prevNotificationAction); + if (FAILED(hr)) { + return hr; + } + // Intentionally discard the result of this write. There's no real reason + // to abort sending the ping in the error case and it already wrote an error + // message. So there isn't really anything to do at this point. + MaybeWritePreviousNotificationAction(activitiesPerformed); + + Cache cache; + + // Do not send the ping if we are not an official telemetry-enabled build; + // don't even generate the ping in fact, because if we write the file out + // then some other build might find it later and decide to submit it. + if (!IsOfficialTelemetry() || IsTelemetryDisabled()) { + return MaybeCache(cache, notificationType, notificationShown, + notificationAction, prevNotificationAction); + } + + // Glean notification pings are handled asynchronously from system defaults + // pings; caching is unnecessary as we need not adhere to the system default + // ping's 24 hour cadence. + if (activitiesPerformed.shown != NotificationShown::NotShown) { + mozilla::glean::notification::show_success.Set(activitiesPerformed.shown == + NotificationShown::Shown); + if (activitiesPerformed.shown == NotificationShown::Shown) { + mozilla::glean::notification::action.Set( + nsDependentCString(notificationAction.c_str())); + } + } + + // Pings are limited to one per day (across all installations), so check if we + // already sent one today. + // This will also set a registry entry indicating that the last ping was + // just sent, to prevent another one from being sent today. We'll do this + // now even though we haven't sent the ping yet. After this check, we send + // a ping unconditionally. The only exception is for errors, and any error + // that we get now will probably be hit every time. + // Because unsent pings attempted with pingsender can get automatically + // re-sent later, we don't even want to try again on transient network + // failures. + hr = [&]() { + BoolResult pingAlreadySentResult = GetPingAlreadySentToday(); + if (pingAlreadySentResult.isErr()) { + return pingAlreadySentResult.unwrapErr().AsHResult(); + } + bool pingAlreadySent = pingAlreadySentResult.unwrap(); + if (pingAlreadySent) { + return MaybeCache(cache, notificationType, notificationShown, + notificationAction, prevNotificationAction); + } + + hr = MaybeSwapForCached(cache, notificationType, notificationShown, + notificationAction, prevNotificationAction); + if (FAILED(hr)) { + return hr; + } + + // Don't update the registry's default browser data until we are sure we + // want to send a ping. Otherwise it could be updated to reflect a ping we + // never sent. Same logic for witnessing Windows updates, but they're less + // valuable, so try (and potentially fail) those first. + TelemetryFieldResult previousOSVersionResult = + GetAndUpdatePreviousOSVersion(osVersion); + if (previousOSVersionResult.isErr()) { + return previousOSVersionResult.unwrapErr().AsHResult(); + } + std::string prevOSVersion = previousOSVersionResult.unwrap(); + + mozilla::glean::system::os_version.Set( + nsDependentCString(osVersion.c_str())); + mozilla::glean::system::previous_os_version.Set( + nsDependentCString(prevOSVersion.c_str())); + + TelemetryFieldResult previousDefaultBrowserResult = + GetAndUpdatePreviousDefaultBrowser(currentDefaultBrowser, + browserInfo.previousDefaultBrowser); + if (previousDefaultBrowserResult.isErr()) { + return previousDefaultBrowserResult.unwrapErr().AsHResult(); + } + std::string previousDefaultBrowser = previousDefaultBrowserResult.unwrap(); + + mozilla::glean::system_default::browser.Set( + nsDependentCString(currentDefaultBrowser.c_str())); + // Glean telemetry doesn't use registry cached ping values for + // notifications, so we shouldn't use the registry cached values for the + // previous default browser either. + std::string uncachedPreviousDefaultBrowser = + GetStringForBrowser(browserInfo.previousDefaultBrowser); + mozilla::glean::system_default::previous_browser.Set( + nsDependentCString(uncachedPreviousDefaultBrowser.c_str())); + mozilla::glean::system_default::pdf_handler.Set( + nsDependentCString(currentDefaultPdf.c_str())); + + return SendDesktopTelemetryPing( + currentDefaultBrowser, previousDefaultBrowser, currentDefaultPdf, + osVersion, prevOSVersion, osLocale, notificationType, + notificationShown, notificationAction, prevNotificationAction) + .AsHResult(); + }(); + + mozilla::glean_pings::DefaultAgent.Submit("daily_ping"_ns); + + return hr; +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/Telemetry.h b/toolkit/mozapps/defaultagent/Telemetry.h new file mode 100644 index 0000000000..028746b6f1 --- /dev/null +++ b/toolkit/mozapps/defaultagent/Telemetry.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_TELEMETRY_H__ +#define __DEFAULT_BROWSER_TELEMETRY_H__ + +#include + +#include "DefaultBrowser.h" +#include "DefaultPDF.h" +#include "Notification.h" + +namespace mozilla::default_agent { + +HRESULT SendDefaultAgentPing(const DefaultBrowserInfo& browserInfo, + const DefaultPdfInfo& pdfInfo, + const NotificationActivities& activitiesPerformed); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_TELEMETRY_H__ diff --git a/toolkit/mozapps/defaultagent/UtfConvert.cpp b/toolkit/mozapps/defaultagent/UtfConvert.cpp new file mode 100644 index 0000000000..2259a2db6f --- /dev/null +++ b/toolkit/mozapps/defaultagent/UtfConvert.cpp @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "UtfConvert.h" + +#include + +#include "EventLog.h" + +#include "mozilla/Buffer.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla::default_agent { + +Utf16ToUtf8Result Utf16ToUtf8(const wchar_t* const utf16) { + int utf8Len = + WideCharToMultiByte(CP_UTF8, 0, utf16, -1, nullptr, 0, nullptr, nullptr); + if (utf8Len == 0) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return Utf16ToUtf8Result(mozilla::WindowsError::FromHResult(hr)); + } + + mozilla::Buffer utf8(utf8Len); + int bytesWritten = WideCharToMultiByte(CP_UTF8, 0, utf16, -1, utf8.Elements(), + utf8.Length(), nullptr, nullptr); + if (bytesWritten == 0) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return Utf16ToUtf8Result(mozilla::WindowsError::FromHResult(hr)); + } + + return std::string(utf8.Elements()); +} + +Utf8ToUtf16Result Utf8ToUtf16(const char* const utf8) { + int utf16Len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0); + if (utf16Len == 0) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + mozilla::Buffer utf16(utf16Len); + int charsWritten = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, utf16.Elements(), + utf16.Length()); + if (charsWritten == 0) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return mozilla::Err(mozilla::WindowsError::FromHResult(hr)); + } + + return std::wstring(utf16.Elements()); +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/UtfConvert.h b/toolkit/mozapps/defaultagent/UtfConvert.h new file mode 100644 index 0000000000..8cfbb9ad0b --- /dev/null +++ b/toolkit/mozapps/defaultagent/UtfConvert.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 DEFAULT_BROWSER_UTF_CONVERT_H__ +#define DEFAULT_BROWSER_UTF_CONVERT_H__ + +#include + +#include "mozilla/WinHeaderOnlyUtils.h" + +namespace mozilla::default_agent { + +using Utf16ToUtf8Result = mozilla::WindowsErrorResult; +using Utf8ToUtf16Result = mozilla::WindowsErrorResult; + +Utf16ToUtf8Result Utf16ToUtf8(const wchar_t* const utf16); +Utf8ToUtf16Result Utf8ToUtf16(const char* const utf8); + +} // namespace mozilla::default_agent + +#endif // DEFAULT_BROWSER_UTF_CONVERT_H__ diff --git a/toolkit/mozapps/defaultagent/WindowsMutex.cpp b/toolkit/mozapps/defaultagent/WindowsMutex.cpp new file mode 100644 index 0000000000..804c3e6d75 --- /dev/null +++ b/toolkit/mozapps/defaultagent/WindowsMutex.cpp @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/Logging.h" + +#include "WindowsMutex.h" + +namespace mozilla::default_agent { + +using mozilla::LogLevel; + +static LazyLogModule gWindowsMutexLog("WindowsMutex"); + +NS_IMPL_ISUPPORTS(WindowsMutexFactory, nsIWindowsMutexFactory) + +NS_IMETHODIMP +WindowsMutexFactory::CreateMutex(const nsAString& aName, + nsIWindowsMutex** aWindowsMutex) { + nsAutoHandle mutex; + auto name = PromiseFlatString(aName); + + mutex.own(CreateMutexW(nullptr, FALSE, name.get())); + if (mutex.get() == nullptr) { + MOZ_LOG(gWindowsMutexLog, LogLevel::Error, + ("Couldn't open mutex \"%s\": %#lX", + NS_ConvertUTF16toUTF8(name).get(), GetLastError())); + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr nsMutex = new WindowsMutex(name, mutex); + nsMutex.forget(aWindowsMutex); + return NS_OK; +} + +WindowsMutex::WindowsMutex(const nsString& aName, nsAutoHandle& aMutex) + : mName(NS_ConvertUTF16toUTF8(aName)), mLocked(false) { + mMutex.steal(aMutex); +} + +WindowsMutex::~WindowsMutex() { + Unlock(); + // nsAutoHandle will take care of closing the mutex's handle. +} + +NS_IMPL_ISUPPORTS(WindowsMutex, nsIWindowsMutex) + +NS_IMETHODIMP +WindowsMutex::TryLock() { + // This object may be used on the main thread, so don't wait if it's + // not signaled. + DWORD mutexStatus = WaitForSingleObject(mMutex.get(), 0); + if (mutexStatus == WAIT_OBJECT_0) { + mLocked = true; + } else if (mutexStatus == WAIT_TIMEOUT) { + MOZ_LOG(gWindowsMutexLog, LogLevel::Warning, + ("Timed out waiting for mutex \"%s\"", mName.get())); + } else if (mutexStatus == WAIT_ABANDONED) { + // This status code means that we are supposed to check our data for + // consistency as the last locking process didn't signal intentional + // unlocking which might indicate it crashed mid-operation. Current uses of + // this `WindowsMutex` don't need to worry about corruption of the locked + // object, if needed the `nsIWindowsMutex` interface should be extended. + MOZ_LOG(gWindowsMutexLog, LogLevel::Warning, + ("Found abandoned mutex \"%s\". Continuing...", mName.get())); + mLocked = true; + } else { + // The only other documented status code is WAIT_FAILED. In the case that + // we somehow get some other code, that is also an error. + MOZ_LOG(gWindowsMutexLog, LogLevel::Error, + ("Failed to wait on mutex: mName: %s, error %#lX", mName.get(), + GetLastError())); + } + return mLocked ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +WindowsMutex::IsLocked(bool* aLocked) { + *aLocked = mLocked; + return NS_OK; +} + +NS_IMETHODIMP +WindowsMutex::Unlock() { + nsresult rv = NS_OK; + + if (mLocked) { + BOOL success = ReleaseMutex(mMutex.get()); + if (!success) { + MOZ_LOG(gWindowsMutexLog, LogLevel::Error, + ("Failed to release mutex \"%s\"", mName.get())); + rv = NS_ERROR_UNEXPECTED; + } + + mLocked = false; + } + + return rv; +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/WindowsMutex.h b/toolkit/mozapps/defaultagent/WindowsMutex.h new file mode 100644 index 0000000000..5e8b32314c --- /dev/null +++ b/toolkit/mozapps/defaultagent/WindowsMutex.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_MUTEX_H__ +#define __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_MUTEX_H__ + +#include "nsString.h" +#include "nsWindowsHelpers.h" + +#include "nsIWindowsMutex.h" + +namespace mozilla::default_agent { + +class WindowsMutexFactory final : public nsIWindowsMutexFactory { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWINDOWSMUTEXFACTORY + + WindowsMutexFactory() = default; + + private: + ~WindowsMutexFactory() = default; +}; + +class WindowsMutex final : public nsIWindowsMutex { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWINDOWSMUTEX + + WindowsMutex(const nsString& aName, nsAutoHandle& aMutex); + + private: + nsAutoHandle mMutex; + nsCString mName; + bool mLocked; + + ~WindowsMutex(); +}; + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_DEFAULT_AGENT_MUTEX_H__ diff --git a/toolkit/mozapps/defaultagent/common.cpp b/toolkit/mozapps/defaultagent/common.cpp new file mode 100644 index 0000000000..0e660d1207 --- /dev/null +++ b/toolkit/mozapps/defaultagent/common.cpp @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "common.h" + +#include "EventLog.h" + +#include + +namespace mozilla::default_agent { + +ULONGLONG GetCurrentTimestamp() { + FILETIME filetime; + GetSystemTimeAsFileTime(&filetime); + ULARGE_INTEGER integerTime; + integerTime.u.LowPart = filetime.dwLowDateTime; + integerTime.u.HighPart = filetime.dwHighDateTime; + return integerTime.QuadPart; +} + +// Passing a zero as the second argument (or omitting it) causes the function +// to get the current time rather than using a passed value. +ULONGLONG SecondsPassedSince(ULONGLONG initialTime, + ULONGLONG currentTime /* = 0 */) { + if (currentTime == 0) { + currentTime = GetCurrentTimestamp(); + } + // Since this is returning an unsigned value, let's make sure we don't try to + // return anything negative + if (initialTime >= currentTime) { + return 0; + } + + // These timestamps are expressed in 100-nanosecond intervals + return (currentTime - initialTime) / 10 // To microseconds + / 1000 // To milliseconds + / 1000; // To seconds +} + +FilePathResult GenerateUUIDStr() { + UUID uuid; + RPC_STATUS status = UuidCreate(&uuid); + if (status != RPC_S_OK) { + HRESULT hr = MAKE_HRESULT(1, FACILITY_RPC, status); + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + + // 39 == length of a UUID string including braces and NUL. + wchar_t guidBuf[39] = {}; + if (StringFromGUID2(uuid, guidBuf, 39) != 39) { + LOG_ERROR(HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)); + return FilePathResult( + mozilla::WindowsError::FromWin32Error(ERROR_INSUFFICIENT_BUFFER)); + } + + // Remove the curly braces. + return std::wstring(guidBuf + 1, guidBuf + 37); +} + +FilePathResult GetRelativeBinaryPath(const wchar_t* suffix) { + // The Path* functions don't set LastError, but this is the only thing that + // can really cause them to fail, so if they ever do we assume this is why. + HRESULT hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); + + mozilla::UniquePtr thisBinaryPath = mozilla::GetFullBinaryPath(); + if (!PathRemoveFileSpecW(thisBinaryPath.get())) { + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + + wchar_t relativePath[MAX_PATH] = L""; + + if (!PathCombineW(relativePath, thisBinaryPath.get(), suffix)) { + LOG_ERROR(hr); + return FilePathResult(mozilla::WindowsError::FromHResult(hr)); + } + + return std::wstring(relativePath); +} + +} // namespace mozilla::default_agent diff --git a/toolkit/mozapps/defaultagent/common.h b/toolkit/mozapps/defaultagent/common.h new file mode 100644 index 0000000000..ddd0ca6a67 --- /dev/null +++ b/toolkit/mozapps/defaultagent/common.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 __DEFAULT_BROWSER_AGENT_COMMON_H__ +#define __DEFAULT_BROWSER_AGENT_COMMON_H__ + +#include "mozilla/WinHeaderOnlyUtils.h" + +#define AGENT_REGKEY_NAME \ + L"SOFTWARE\\" MOZ_APP_VENDOR "\\" MOZ_APP_BASENAME "\\Default Browser Agent" + +namespace mozilla::default_agent { + +ULONGLONG GetCurrentTimestamp(); +// Passing a zero as the second argument (or omitting it) causes the function +// to get the current time rather than using a passed value. +ULONGLONG SecondsPassedSince(ULONGLONG initialTime, ULONGLONG currentTime = 0); + +using FilePathResult = mozilla::WindowsErrorResult; +FilePathResult GenerateUUIDStr(); + +FilePathResult GetRelativeBinaryPath(const wchar_t* suffix); + +} // namespace mozilla::default_agent + +#endif // __DEFAULT_BROWSER_AGENT_COMMON_H__ diff --git a/toolkit/mozapps/defaultagent/components.conf b/toolkit/mozapps/defaultagent/components.conf new file mode 100644 index 0000000000..62b5055d56 --- /dev/null +++ b/toolkit/mozapps/defaultagent/components.conf @@ -0,0 +1,21 @@ +# -*- 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/. + +if buildconfig.substs['CC_TYPE'] in ('msvc', 'clang-cl'): + Classes = [ + { + 'cid': '{edc38cb5-b6f6-4aeb-bd45-7be8e00fc364}', + 'contract_ids': ['@mozilla.org/default-agent;1'], + 'type': 'mozilla::default_agent::DefaultAgent', + 'headers': ['mozilla/DefaultAgent.h'], + }, + { + 'cid': '{d54fe2b7-438f-4629-9706-1acda5b51088}', + 'contract_ids': ['@mozilla.org/windows-mutex-factory;1'], + 'type': 'mozilla::default_agent::WindowsMutexFactory', + 'headers': ['mozilla/WindowsMutex.h'], + }, + ] diff --git a/toolkit/mozapps/defaultagent/defaultagent.ini b/toolkit/mozapps/defaultagent/defaultagent.ini new file mode 100644 index 0000000000..9300b20c46 --- /dev/null +++ b/toolkit/mozapps/defaultagent/defaultagent.ini @@ -0,0 +1,9 @@ +; 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 file is in the UTF-8 encoding +[Nonlocalized] +InitialToastRelativeImagePath=browser/VisualElements/VisualElements_150.png +FollowupToastRelativeImagePath=browser/VisualElements/VisualElements_150.png +LocalizedToastRelativeImagePath=browser/VisualElements/VisualElements_150.png diff --git a/toolkit/mozapps/defaultagent/docs/index.rst b/toolkit/mozapps/defaultagent/docs/index.rst new file mode 100644 index 0000000000..f977f234cf --- /dev/null +++ b/toolkit/mozapps/defaultagent/docs/index.rst @@ -0,0 +1,49 @@ +===================== +Default Browser Agent +===================== + +The Default Browser Agent is a Windows-only scheduled task which runs in the background to collect and submit data about the browser that the user has set as their OS default (that is, the browser that will be invoked by the operating system to open web links that the user clicks on in other programs). Its purpose is to help Mozilla understand user's default browser choices and, in the future, to engage with users at a time when they may not be actively running Firefox. + +For information about the specific data that the agent sends, see :doc:`the ping documentation `. + + +Scheduled Task +============== + +The agent runs as a `Windows scheduled task `_. The scheduled task proxy executable invokes the Firefox ``BackgroundTask_defaultagent`` which executes all of the agent's primary functions; all of its other functions relate to managing the task. The Windows installer is responsible for creating (and the uninstaller for removing) the agent's task entry, but the code for actually doing this resides in the agent itself, and the installers simply call it using dedicated command line parameters (``register-task`` and ``uninstall``). The :doc:`PostUpdate ` code also calls the agent to update any properties of an existing task registration that need to be updated, or to create one during an application update if none exists. + +The tasks are normal entries in the Windows Task Scheduler, managed using `its Win32 API `_. They're created in a tasks folder called "Mozilla" (or whatever the application's vendor name is), and there's one for each installation of Firefox (or other Mozilla application). The task is set to run automatically every 24 hours starting at the time it's registered (with the first run being 24 hours after that), or the nearest time after that the computer is awake. The task is configured with one action, which is to run the agent binary with the command line parameter ``do-task``, the command that invokes the actual agent functionality. + +The default browser agent needs to run as some OS-level user, as opposed to, say, ``LOCAL SERVICE``, in order to read the user's default browser setting. Therefore, the default browser agent runs as the user that ran the Firefox installer (although always without elevation, whether the installer had it or not). + + +Remote Disablement +------------------ + +The default browser agent can be remotely disabled and (re-)enabled. Each time the scheduled task runs it queries `Firefox Remote Settings `_ to determine if the agent has been remotely disabled or (re-)enabled. + +If the default browser agent is disabled by policy, remote disablement will not be checked. However, the notification functionality of the agent is distinct from the telemetry functionality of the agent, and remote disablement must apply to both functions. Therefore, even if the user has opted out of sending telemetry (by policy or by preference), the agent must check for remote disablement. For a user who is currently opted out of telemetry, they will not be opted in due to the default browser agent being remotely (re-)enabled. + + +Data Management +=============== + +The default browser agent has to be able to work with settings at several different levels: a Firefox profile, an OS user, a Firefox installation, and the entire system. This need creates an information architecture mismatch between all of those things, mostly because no Firefox profile is available to the agent while it's running; it's not really feasible to either directly use or to clone Firefox's profile selection functionality, and even if we could select a profile, whatever code we might use to actually work with it would have the same problems. So, in order to allow for controlling the agent from Firefox, certain settings are mirrored from Firefox to a location where the agent can read them. Since the agent operates in the context only of an OS-level user, that means that in this situation a single OS-level user who uses multiple Firefox profiles may be able to observe the agent's settings changing as the different profiles race to be the active mirror, without them knowingly taking any action. + + +Pref Reflection +--------------- + +The agent needs to be able to read (but not set) values that have their canonical representation in the form of Firefox prefs. This means those pref values have to be copied out to a place where the agent can read them. The Windows registry was chosen as that place; it's easier to use than a file, and we already have keys there which are reserved by Firefox. Specifically, the subkey used for these prefs is ``HKEY_CURRENT_USER\Software\[app vendor name]\[app name]\Default Browser Agent\``. During Firefox startup, the values of the prefs that control the agent are reflected to this key, and those values are updated whenever the prefs change after that. + +The list of reflected prefs includes the global telemetry opt-out pref ``datareporting.healthreport.uploadEnabled`` and a pref called ``default-browser-agent.enabled``, which can enable or disable the entire agent. The agent checks these registry-reflected pref values when its scheduled task runs, they do not actually prevent the scheduled task from running. + +Enterprise policies also exist to perform the same functions as these prefs. These work the same way as all other Firefox policies and `the documentation for those `_ explains how to use them. + +In addition, the following Firefox Remote Settings pref is reflected: ``services.settings.server``. It is the service endpoint to consult for remote-disablement. + + +Default Browser Setting +----------------------- + +The agent is responsible for reporting both the user's current default browser and their previous default browser. Nothing in the operating system records past associations, so the agent must do this for itself. First, it gets the current default browser by calling `IApplicationAssociationRegistration::QueryCurrentDefault `_ for the ``http`` protocol. It then checks that against a value stored in its own registry key and, if those are different, it knows that the default browser has changed, and records the new and old defaults. diff --git a/toolkit/mozapps/defaultagent/metrics.yaml b/toolkit/mozapps/defaultagent/metrics.yaml new file mode 100644 index 0000000000..355a80d7e6 --- /dev/null +++ b/toolkit/mozapps/defaultagent/metrics.yaml @@ -0,0 +1,208 @@ +# 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/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Toolkit :: Default Browser Agent' + +system: + os_version: + type: string + description: > + The current Windows OS version, usually as a dotted quad ("x.y.z.w") with + Windows Update Build Revision (UBR), but potentially as a dotted triple + ("x.y.z") without UBR. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - default-agent + + previous_os_version: + type: string + description: > + The Windows OS version before it was changed to the current setting. The + possible values are the same as for `system.os_version`. + + The OS does not keep track of the previous OS version, so the agent + records this information itself. That means that it will be inaccurate + until the first time the default is changed after the agent task begins + running. Before then, the value of `previous_os_version` will be the same + as `os_version`. + + This value is updated every time the Default Agent runs, so when the + default browser is first changed the values for `os_version` and + `previous_os_version` will be different. But on subsequent executions of + the Default Agent, the two values will be the same. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1850149 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - default-agent + +system_default: + browser: + type: string + description: > + Which browser is currently set as the system default web browser. This is + simply a string with the name of the browser binned to a fixed set of + known browsers. + + Possible values currently include the following (from + [DefaultBrowser.cpp](https://searchfox.org/mozilla-central/source/toolkit/mozapps/defaultagent/DefaultBrowser.cpp)): + * "error" + * "" (unknown) + * "firefox" + * "chrome" + * "edge" + * "edge-chrome" + * "ie" + * "opera" + * "brave" + * "yandex" + * "qq-browser" + * "360-browser" + * "sogou" + * "duckduckgo" + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - default-agent + + previous_browser: + type: string + description: > + Which browser was set as the system default before it was changed to the + current setting. The possible values are the same as for + `system_default.browser`. + + The OS does not keep track of previous default settings, so the agent + records this information itself. That means that it will be inaccurate + until the first time the default is changed after the agent task begins + running. Before then, the value of `previous_browser` will be the same + as `browser`. + + This value is updated every time the Default Agent runs, so when the + default browser is first changed the values for `browser` and + `previous_browser` will be different. But on subsequent executions of + the Default Agent, the two values will be the same. + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - default-agent + + pdf_handler: + type: string + description: > + Which pdf handler is currently set as the system default handler. This is + simply a string with the name of the handler binned to a fixed set of + known handlers. + + Possible values currently include the following (from + [DefaultPDF.cpp](https://searchfox.org/mozilla-central/source/toolkit/mozapps/defaultagent/DefaultPDF.cpp)): + * "Error" + * "" (unknown) + * "Firefox" + * "Microsoft Edge" + * "Google Chrome" + * "Adobe Acrobat" + * "WPS" + * "Nitro" + * "Foxit" + * "PDF-XChange" + * "Avast" + * "Sumatra" + lifetime: application + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1756900 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1756900 + data_sensitivity: + - technical + notification_emails: + - install-update@mozilla.com + expires: never + send_in_pings: + - default-agent + +notification: + show_success: + type: boolean + description: > + Whether a notification was shown or not. Possible value include "shown" and "error". + notification_emails: + - install-update@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293 + expires: never + send_in_pings: + - default-agent + + action: + type: string + description: > + The action that the user took in response to the notification. Possible + values currently include the following: + * “dismissed-by-timeout” + * “dismissed-to-action-center” + * “dismissed-by-button” + * “dismissed-by-application-hidden” + * “make-firefox-default-button” + * “toast-clicked” + + Many of the values correspond to buttons on the notification and should be + pretty self explanatory, but a few are less so. + * “dismissed-to-action-center” will be used if the user clicks the arrow in + the top right corner of the notification to dismiss it to the + action center. + * “dismissed-by-application-hidden” is provided because that is a method of + dismissal that the notification API could give but, in practice, should + never be seen. + * “dismissed-by-timeout” indicates that the user did not interact with the + notification and it timed out. + notification_emails: + - install-update@mozilla.com + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293 + expires: never + send_in_pings: + - default-agent diff --git a/toolkit/mozapps/defaultagent/module.ver b/toolkit/mozapps/defaultagent/module.ver new file mode 100644 index 0000000000..92b692b62c --- /dev/null +++ b/toolkit/mozapps/defaultagent/module.ver @@ -0,0 +1 @@ +WIN32_MODULE_DESCRIPTION=@MOZ_APP_DISPLAYNAME@ Default Browser Agent diff --git a/toolkit/mozapps/defaultagent/moz.build b/toolkit/mozapps/defaultagent/moz.build new file mode 100644 index 0000000000..86b68c6371 --- /dev/null +++ b/toolkit/mozapps/defaultagent/moz.build @@ -0,0 +1,113 @@ +# -*- 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/. + +SPHINX_TREES["default-browser-agent"] = "docs" + +DIRS += ["proxy"] + +UNIFIED_SOURCES += [ + "Cache.cpp", + "common.cpp", + "DefaultAgent.cpp", + "DefaultBrowser.cpp", + "DefaultPDF.cpp", + "EventLog.cpp", + "Policy.cpp", + "Registry.cpp", + "ScheduledTask.cpp", + "ScheduledTaskRemove.cpp", + "SetDefaultBrowser.cpp", + "Telemetry.cpp", + "UtfConvert.cpp", + "WindowsMutex.cpp", +] + +SOURCES += [ + "/third_party/WinToast/wintoastlib.cpp", + "/toolkit/mozapps/update/common/readstrings.cpp", + "Notification.cpp", +] + +# Suppress warnings from third-party code. +SOURCES["/third_party/WinToast/wintoastlib.cpp"].flags += [ + "-Wno-implicit-fallthrough", + "-Wno-nonportable-include-path", # Needed for wintoastlib.h including "Windows.h" +] +SOURCES["Notification.cpp"].flags += [ + "-Wno-nonportable-include-path", # Needed for wintoastlib.h including "Windows.h" +] + +EXPORTS.mozilla += [ + "DefaultAgent.h", + "WindowsMutex.h", +] + +USE_LIBS += [ + "jsoncpp", +] + +LOCAL_INCLUDES += [ + "/browser/components/shell/", + "/other-licenses/nsis/Contrib/CityHash/cityhash", + "/third_party/WinToast", + "/toolkit/components/jsoncpp/include", + "/toolkit/mozapps/update/common", +] + +OS_LIBS += [ + "advapi32", + "bcrypt", + "comsupp", + "crypt32", + "kernel32", + "netapi32", + "ole32", + "oleaut32", + "rpcrt4", + "shell32", + "shlwapi", + "taskschd", + "userenv", + "wininet", + "ws2_32", + "ntdll", +] + +XPIDL_SOURCES += [ + "nsIDefaultAgent.idl", + "nsIWindowsMutex.idl", +] + +XPIDL_MODULE = "default-agent" + +XPCOM_MANIFESTS += [ + "components.conf", +] + +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"] + +# If defines are added to this list that are required by the Cache, +# SetDefaultBrowser, or their dependencies (Registry, EventLog, common), +# tests/gtest/moz.build will need to be updated as well. +for var in ("MOZ_APP_BASENAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VENDOR"): + DEFINES[var] = '"%s"' % CONFIG[var] + +DEFINES["UNICODE"] = True +DEFINES["_UNICODE"] = True + +FINAL_TARGET_FILES += ["defaultagent.ini"] + +FINAL_LIBRARY = "xul" + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["tests/gtest"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Default Browser Agent") + +EXTRA_JS_MODULES.backgroundtasks += [ + "BackgroundTask_defaultagent.sys.mjs", +] diff --git a/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl b/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl new file mode 100644 index 0000000000..7e78e1b30d --- /dev/null +++ b/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl @@ -0,0 +1,167 @@ +/* -*- Mode: IDL; 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 "nsISupports.idl" + +[scriptable, uuid(edc38cb5-b6f6-4aeb-bd45-7be8e00fc364)] +interface nsIDefaultAgent : nsISupports +{ + /** + * Create a Windows scheduled task that will launch this binary with the + * do-task command every 24 hours, starting from 24 hours after register-task + * is run. + * + * @param {AString} aUniqueToken + * A unique identifier for this installation; typically the install path + * hash that's used for the update directory, the AppUserModelID, and + * other related purposes. + */ + void registerTask(in AString aUniqueToken); + + /** + * Update an existing task registration, without changing its schedule. This + * should be called during updates of the application, in case this program + * has been updated and any of the task parameters have changed. + * + * @param {AString} aUniqueToken + * A unique identifier for this installation; the same one provided when + * the task was registered. + */ + void updateTask(in AString aUniqueToken); + + /** + * Removes the previously created task. The unique token argument is required + * and should be the same one that was passed in when the task was registered. + * + * @param {AString} aUniqueToken + * A unique identifier for this installation; the same one provided when + * the task was registered. + */ + void unregisterTask(in AString aUniqueToken); + + /** + * Removes the previously created task, and also removes all registry entries + * running the task may have created. + * + * @param {AString} aUniqueToken + * A unique identifier for this installation; the same one provided when + * the task was registered. + */ + void uninstall(in AString aUniqueToken); + + /** + * Actually performs the default agent task, which currently means generating + * and sending our telemetry ping and possibly showing a notification to the + * user if their browser has switched from Firefox to Edge with Blink. + * + * @param {AString} aUniqueToken + * A unique identifier for this installation; the same one provided when + * the task was registered. + * @param {boolean} aForce + * For debugging, forces the task to run even if it has run in the last + * 24 hours, and forces the notification to show. + */ + void doTask(in AString aUniqueToken, in boolean aForce); + + /** + * Checks that the main app ran recently. + * + * @return {boolean} true if the app ran recently. + */ + boolean appRanRecently(); + + /** + * Returns a string for the default browser if known, binned to known browsers. + * + * @return {AString} + * The current default browser. + */ + AString getDefaultBrowser(); + + /** + * Gets and replaces the previously found default browser from the registry. + * + * @param {AString} aCurrentBrowser + * The current known browser to save to the registry. + * @return {AString} + * The previous known browser from the registry. + */ + AString getReplacePreviousDefaultBrowser(in AString aCurrentBrowser); + + /** + * Returns a string for the default PDF handler if known, binned to known + * PDF handlers. + * + * @return {AString} + * The previous default PDF handler. + */ + AString getDefaultPdfHandler(); + + /** + * Sends a Default Agent telemetry ping. + * + * @param {AString} aCurrentBrowser + * The current known browser. + * @param {AString} aPreviousBrowser + * The previous known browser. + * @param {AString} aPdfHandler + * The current known PDF handler. + * @param {AString} aNotificationShown + * If the notification was or wasn't shown. See + * `toolkit/mozapps/defaultagent/Notification.h` for valid values. + * @param {AString} aNotificationAction + * The notification action taken by the user. See + * `toolkit/mozapps/defaultagent/Notification.h` for valid values. + * + */ + void sendPing(in AString aCurrentBrowser, in AString aPreviousBrowser, in AString aPdfHandler, in AString aNotificationShown, in AString aNotificationAction); + + /** + * Set the default browser and optionally additional file extensions via the + * UserChoice registry keys. + * + * @param {AString} aAumid + * Suffix to be appended to ProgIDs when registering system defaults. + * @param {Array} aExtraFileExtensions + * Additional optional file extensions to register specified as argument + * pairs: the first element is the file extension, the second element is + * the root of a ProgID, which will be suffixed with `-{aAumid}`. + */ + void setDefaultBrowserUserChoice(in AString aAumid, in Array aExtraFileExtensions); + + /** + * Set the default browser and optionally additional file extensions via the + * UserChoice registry keys, asynchronously. Does the actual work on a + * background thread. + * + * @param {AString} aAumid + * Suffix to be appended to ProgIDs when registering system defaults. + * @param {Array} aExtraFileExtensions + * Additional optional file extensions to register specified as argument + * pairs: the first element is the file extension, the second element is + * the root of a ProgID, which will be suffixed with `-{aAumid}`. + */ + [implicit_jscontext] + Promise setDefaultBrowserUserChoiceAsync(in AString aAumid, in Array aExtraFileExtensions); + + /** + * Sets file extensions via the UserChoice registry keys. + * + * @param {AString} aAumid + * Suffix to be appended to ProgIDs when registering system defaults. + * @param {Array} aExtraFileExtensions + * File extensions to register specified as argument pairs: the first + * element is the file extension, the second element is the root of a + * ProgID, which will be suffixed with `-{aAumid}`. + */ + void setDefaultExtensionHandlersUserChoice(in AString aAumid, in Array aFileExtensions); + + /** + * Checks if the default agent has been disabled. + * + * @return {boolean} true if the default agent is disabled. + */ + boolean agentDisabled(); +}; diff --git a/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl b/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl new file mode 100644 index 0000000000..69090aa764 --- /dev/null +++ b/toolkit/mozapps/defaultagent/nsIWindowsMutex.idl @@ -0,0 +1,62 @@ +/* -*- Mode: IDL; 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 "nsISupports.idl" + +/** +* Interact with Windows named mutexes. +* +* Generally you don't want a Windows named mutex, you want one of the many Gecko +* locking primitives. But when you do want cross-application instance or even +* cross-installation coordination, a Windows named mutex might be an +* appropriate tool. +*/ +[scriptable, uuid(26f09999-c26e-4b72-8747-5adaefa0914c)] +interface nsIWindowsMutex : nsISupports +{ + /** + * Locks the mutex. + * + * Note that this will not block waiting to lock. It attempts to lock the mutex + * and if it can't immediately, NS_ERROR_NOT_AVAILABLE will be thrown. + * + * This function succeeds when an abandoned mutex is found, therefore is + * inappropriate for use if an abandoned mutex might imply the locked resource + * is in a corrupt state. + * + * @throws NS_ERROR_NOT_AVAILABLE + * If unable to lock the mutex. + */ + void tryLock(); + + /** + * Returns whether the mutex is locked. + * + * @return {boolean} true if locked, false if unlocked. + */ + bool isLocked(); + + /** + * Unlocks the mutex. + * @throws NS_ERROR_UNEXPECTED + * If unable to release mutex. + */ + void unlock(); +}; + +[scriptable, uuid(d54fe2b7-438f-4629-9706-1acda5b51088)] +interface nsIWindowsMutexFactory : nsISupports { + /** + * Creates a Windows named mutex. + * + * @param {AString} aName + * The system-wide name of the mutex. + * @return {nsIWindowsMutex} + * The created Windows mutex. + * @throws NS_ERROR_NOT_AVAILABLE + * If unable to create mutex. + */ + nsIWindowsMutex createMutex(in AString aName); +}; diff --git a/toolkit/mozapps/defaultagent/pings.yaml b/toolkit/mozapps/defaultagent/pings.yaml new file mode 100644 index 0000000000..5e607f8c8f --- /dev/null +++ b/toolkit/mozapps/defaultagent/pings.yaml @@ -0,0 +1,42 @@ +# 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/. + +--- +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 + +default-agent: + description: > + This opt-out ping is sent from the Default Agent, which is a Windows-only + Firefox Background Task that is registered during Firefox installation with + the Windows scheduled tasks system so that it runs automatically every 24 + hours, whether Firefox is running or not. + + Opting out of telemetry is handled via the pref value being copied to the + registry so that the Default Agent can read it without needing to work with + profiles. Relevant policies are consulted as well. The agent also has its own + pref, `default-agent.enabled`, which if set to false disables all agent + functionality, including generating this ping. + + Each installation of Firefox has its own copy of the agent and its own + scheduled task which shares a common `LastPingSentAt` user registry key with + other installations. Installations race to send a single ping per 24 hour + window per installing user. If multiple operating system-level users are all + using one copy of Firefox, only one scheduled task will have been created and + only one ping will be sent, even though the users might have different + default browser settings. If multiple users have installed Firefox then each + installing user will have a scheduled task and ping. + + Additional information for the Default Agent can be found in the + [Default Browser Agent docs](https://firefox-source-docs.mozilla.org/toolkit/mozapps/defaultagent/default-browser-agent/index.html). + include_client_id: false + send_if_empty: false + reasons: + daily_ping: | + The ping was sent as part of the daily scheduled Default Agent run. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1838755 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1621293 + notification_emails: + - install-update@mozilla.com diff --git a/toolkit/mozapps/defaultagent/proxy/Makefile.in b/toolkit/mozapps/defaultagent/proxy/Makefile.in new file mode 100644 index 0000000000..dadff2846b --- /dev/null +++ b/toolkit/mozapps/defaultagent/proxy/Makefile.in @@ -0,0 +1,16 @@ +# 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 binary should never open a console window in release builds, because +# it's going to run in the background when the user may not expect it, and +# we don't want a console window to just appear out of nowhere on them. +# For debug builds though, it's okay to use the existing MOZ_WINCONSOLE value. +ifndef MOZ_DEBUG +MOZ_WINCONSOLE = 0 +endif + +# Rebuild if the resources or manifest change. +EXTRA_DEPS += $(srcdir)/default-browser-agent.exe.manifest + +include $(topsrcdir)/config/rules.mk diff --git a/toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest b/toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest new file mode 100644 index 0000000000..ceb2839697 --- /dev/null +++ b/toolkit/mozapps/defaultagent/proxy/default-browser-agent.exe.manifest @@ -0,0 +1,31 @@ + + + +Default Browser Agent + + + + + + + + + + + + + + + + + + True/PM + PerMonitorV2,PerMonitor + + + diff --git a/toolkit/mozapps/defaultagent/proxy/main.cpp b/toolkit/mozapps/defaultagent/proxy/main.cpp new file mode 100644 index 0000000000..53efdcb9ae --- /dev/null +++ b/toolkit/mozapps/defaultagent/proxy/main.cpp @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 +#include +#include +#include +#include + +#include "../ScheduledTask.h" +#include "../ScheduledTaskRemove.h" +#include "mozilla/CmdLineAndEnvUtils.h" + +using namespace mozilla::default_agent; + +// See BackgroundTask_defaultagent.sys.mjs for arguments. +int wmain(int argc, wchar_t** argv) { + // Firefox deescalates process permissions, so handle task unscheduling step + // here instead of the Firefox Background Tasks to ensure cleanup for other + // users. See Bug 1710143. + if (!wcscmp(argv[1], L"uninstall")) { + if (argc < 3 || !argv[2]) { + return E_INVALIDARG; + } + + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(hr)) { + return hr; + } + + RemoveTasks(argv[2], WhichTasks::AllTasksForInstallation); + + CoUninitialize(); + + // Background Task handles remainder of uninstall. + } + + std::vector path(MAX_PATH, 0); + DWORD charsWritten = GetModuleFileNameW(nullptr, path.data(), path.size()); + + // GetModuleFileNameW returns the count of characters written including null + // when truncated, excluding null otherwise. Therefore the count will always + // be less than the buffer size when not truncated. + while (charsWritten == path.size()) { + path.resize(path.size() * 2, 0); + charsWritten = GetModuleFileNameW(nullptr, path.data(), path.size()); + } + + if (charsWritten == 0) { + return E_UNEXPECTED; + } + + std::filesystem::path programPath = path.data(); + programPath = programPath.parent_path(); + programPath += L"\\" MOZ_APP_NAME L".exe"; + + std::vector childArgv; + childArgv.push_back(programPath.c_str()); + childArgv.push_back(L"--backgroundtask"); + childArgv.push_back(L"defaultagent"); + // Skip argv[0], path to this exectuable. + for (int i = 1; i < argc; i++) { + childArgv.push_back(argv[i]); + } + + auto cmdLine = mozilla::MakeCommandLine(childArgv.size(), childArgv.data()); + + STARTUPINFOW si = {}; + si.cb = sizeof(STARTUPINFOW); + PROCESS_INFORMATION pi = {}; + + // Runs `{program path} --backgoundtask defaultagent`. + CreateProcessW(programPath.c_str(), cmdLine.get(), nullptr, nullptr, false, + DETACHED_PROCESS | NORMAL_PRIORITY_CLASS, nullptr, nullptr, + &si, &pi); + + // Wait until process exists so uninstalling doesn't interrupt the background + // task cleaning registry entries. + DWORD exitCode; + if (WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_OBJECT_0 && + ::GetExitCodeProcess(pi.hProcess, &exitCode)) { + // Match EXIT_CODE in BackgroundTasksManager.sys.mjs and + // BackgroundTask_defaultagent.sys.mjs. + enum EXIT_CODE { + SUCCESS = 0, + NOT_FOUND = 2, + EXCEPTION = 3, + TIMEOUT = 4, + DISABLED_BY_POLICY = 11, + INVALID_ARGUMENT = 12, + MUTEX_NOT_LOCKABLE = 13, + }; + + switch (exitCode) { + case SUCCESS: + return S_OK; + case NOT_FOUND: + return E_UNEXPECTED; + case EXCEPTION: + return E_FAIL; + case TIMEOUT: + return E_FAIL; + case DISABLED_BY_POLICY: + return HRESULT_FROM_WIN32(ERROR_ACCESS_DISABLED_BY_POLICY); + case INVALID_ARGUMENT: + return E_INVALIDARG; + case MUTEX_NOT_LOCKABLE: + return HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); + default: + return E_UNEXPECTED; + } + } + + return E_UNEXPECTED; +} diff --git a/toolkit/mozapps/defaultagent/proxy/moz.build b/toolkit/mozapps/defaultagent/proxy/moz.build new file mode 100644 index 0000000000..40d7655dfa --- /dev/null +++ b/toolkit/mozapps/defaultagent/proxy/moz.build @@ -0,0 +1,68 @@ +# -*- 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/. + +Program("default-browser-agent") + +SPHINX_TREES["default-browser-agent"] = "docs" + +UNIFIED_SOURCES += [ + "../EventLog.cpp", + "../ScheduledTaskRemove.cpp", + "main.cpp", +] + +SOURCES += [ + "/browser/components/shell/WindowsDefaultBrowser.cpp", + "/other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp", + "/toolkit/mozapps/update/common/readstrings.cpp", +] + +LOCAL_INCLUDES += [ + "../", + "/browser/components/shell/", + "/mfbt/", + "/other-licenses/nsis/Contrib/CityHash/cityhash", + "/toolkit/mozapps/update/common/", +] + +OS_LIBS += [ + "advapi32", + "comsupp", + "netapi32", + "ole32", + "oleaut32", + "shell32", + "shlwapi", + "taskschd", +] + +DEFINES["NS_NO_XPCOM"] = True +DEFINES["IMPL_MFBT"] = True + +DEFINES["UNICODE"] = True +DEFINES["_UNICODE"] = True + +for var in ( + "MOZ_APP_NAME", + "MOZ_APP_DISPLAYNAME", + "MOZ_APP_VENDOR", + "MOZ_APP_BASENAME", +): + DEFINES[var] = '"%s"' % CONFIG[var] + +# We need STL headers that aren't allowed when wrapping is on (at least +# , and possibly others). +DisableStlWrapping() + +# We need this to be able to use wmain as the entry point on MinGW; +# otherwise it will try to use WinMain. +if CONFIG["CC_TYPE"] == "clang-cl": + WIN32_EXE_LDFLAGS += ["-ENTRY:wmainCRTStartup"] +else: + WIN32_EXE_LDFLAGS += ["-municode"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Default Browser Agent") diff --git a/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp b/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp new file mode 100644 index 0000000000..892be6b2f7 --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/gtest/CacheTest.cpp @@ -0,0 +1,301 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "gtest/gtest.h" + +#include + +#include "Cache.h" +#include "common.h" +#include "Registry.h" +#include "UtfConvert.h" + +#include "mozilla/Result.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +using namespace mozilla::default_agent; + +class WDBACacheTest : public ::testing::Test { + protected: + std::wstring mCacheRegKey; + + void SetUp() override { + // Create a unique registry key to put the cache in for each test. + const ::testing::TestInfo* const testInfo = + ::testing::UnitTest::GetInstance()->current_test_info(); + Utf8ToUtf16Result testCaseResult = Utf8ToUtf16(testInfo->test_case_name()); + ASSERT_TRUE(testCaseResult.isOk()); + mCacheRegKey = testCaseResult.unwrap(); + + Utf8ToUtf16Result testNameResult = Utf8ToUtf16(testInfo->name()); + ASSERT_TRUE(testNameResult.isOk()); + mCacheRegKey += L'.'; + mCacheRegKey += testNameResult.unwrap(); + + FilePathResult uuidResult = GenerateUUIDStr(); + ASSERT_TRUE(uuidResult.isOk()); + mCacheRegKey += L'.'; + mCacheRegKey += uuidResult.unwrap(); + } + + void TearDown() override { + // It seems like the TearDown probably doesn't run if SetUp doesn't + // succeed, but I can't find any documentation saying that. And we don't + // want to accidentally clobber the entirety of AGENT_REGKEY_NAME. + if (!mCacheRegKey.empty()) { + std::wstring regKey = AGENT_REGKEY_NAME; + regKey += L'\\'; + regKey += mCacheRegKey; + RegDeleteTreeW(HKEY_CURRENT_USER, regKey.c_str()); + } + } +}; + +TEST_F(WDBACacheTest, BasicFunctionality) { + Cache cache(mCacheRegKey.c_str()); + VoidResult result = cache.Init(); + ASSERT_TRUE(result.isOk()); + + // Test that the cache starts empty + Cache::MaybeEntryResult entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + Cache::MaybeEntry entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isNothing()); + + // Test that the cache stops accepting items when it is full. + ASSERT_EQ(Cache::kDefaultCapacity, 2U); + Cache::Entry toWrite = Cache::Entry{ + .notificationType = "string1", + .notificationShown = "string2", + .notificationAction = "string3", + .prevNotificationAction = "string4", + }; + result = cache.Enqueue(toWrite); + ASSERT_TRUE(result.isOk()); + toWrite = Cache::Entry{ + .notificationType = "string5", + .notificationShown = "string6", + .notificationAction = "string7", + .prevNotificationAction = "string8", + }; + result = cache.Enqueue(toWrite); + ASSERT_TRUE(result.isOk()); + toWrite = Cache::Entry{ + .notificationType = "string9", + .notificationShown = "string10", + .notificationAction = "string11", + .prevNotificationAction = "string12", + }; + result = cache.Enqueue(toWrite); + ASSERT_TRUE(result.isErr()); + + // Read the two cache entries back out and test that they match the expected + // values. + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 2U); + ASSERT_EQ(entry.value().notificationType, "string1"); + ASSERT_EQ(entry.value().notificationShown, "string2"); + ASSERT_EQ(entry.value().notificationAction, "string3"); + ASSERT_TRUE(entry.value().prevNotificationAction.isSome()); + ASSERT_EQ(entry.value().prevNotificationAction.value(), "string4"); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 2U); + ASSERT_EQ(entry.value().notificationType, "string5"); + ASSERT_EQ(entry.value().notificationShown, "string6"); + ASSERT_EQ(entry.value().notificationAction, "string7"); + ASSERT_TRUE(entry.value().prevNotificationAction.isSome()); + ASSERT_EQ(entry.value().prevNotificationAction.value(), "string8"); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isNothing()); +} + +TEST_F(WDBACacheTest, Version1Migration) { + // Set up 2 version 1 cache entries + VoidResult result = RegistrySetValueString( + IsPrefixed::Unprefixed, L"PingCacheNotificationType0", "string1"); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + L"PingCacheNotificationShown0", "string2"); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + L"PingCacheNotificationAction0", "string3"); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + L"PingCacheNotificationType1", "string4"); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + L"PingCacheNotificationShown1", "string5"); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + L"PingCacheNotificationAction1", "string6"); + ASSERT_TRUE(result.isOk()); + + Cache cache(mCacheRegKey.c_str()); + result = cache.Init(); + ASSERT_TRUE(result.isOk()); + + Cache::MaybeEntryResult entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + Cache::MaybeEntry entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 1U); + ASSERT_EQ(entry.value().notificationType, "string1"); + ASSERT_EQ(entry.value().notificationShown, "string2"); + ASSERT_EQ(entry.value().notificationAction, "string3"); + ASSERT_TRUE(entry.value().prevNotificationAction.isNothing()); + + // Insert a new item to test coexistence of different versions + Cache::Entry toWrite = Cache::Entry{ + .notificationType = "string7", + .notificationShown = "string8", + .notificationAction = "string9", + .prevNotificationAction = "string10", + }; + result = cache.Enqueue(toWrite); + ASSERT_TRUE(result.isOk()); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 1U); + ASSERT_EQ(entry.value().notificationType, "string4"); + ASSERT_EQ(entry.value().notificationShown, "string5"); + ASSERT_EQ(entry.value().notificationAction, "string6"); + ASSERT_TRUE(entry.value().prevNotificationAction.isNothing()); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 2U); + ASSERT_EQ(entry.value().notificationType, "string7"); + ASSERT_EQ(entry.value().notificationShown, "string8"); + ASSERT_EQ(entry.value().notificationAction, "string9"); + ASSERT_TRUE(entry.value().prevNotificationAction.isSome()); + ASSERT_EQ(entry.value().prevNotificationAction.value(), "string10"); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isNothing()); +} + +TEST_F(WDBACacheTest, ForwardsCompatibility) { + // Set up a cache that might have been made by a future version with a larger + // capacity set and more keys per entry. + std::wstring settingsKey = mCacheRegKey + L"\\version2"; + VoidResult result = RegistrySetValueDword( + IsPrefixed::Unprefixed, Cache::kCapacityRegName, 8, settingsKey.c_str()); + ASSERT_TRUE(result.isOk()); + // We're going to insert the future version's entry at index 6 so there's + // space for 1 more before we loop back to index 0. Then we are going to + // enqueue 2 new values to test that this works properly. + result = RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kFrontRegName, + 6, settingsKey.c_str()); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kSizeRegName, 1, + settingsKey.c_str()); + ASSERT_TRUE(result.isOk()); + + // Insert an entry as if it was inserted by a future version + std::wstring entryRegKey = settingsKey + L"\\6"; + result = + RegistrySetValueDword(IsPrefixed::Unprefixed, Cache::kEntryVersionKey, + 9999, entryRegKey.c_str()); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + Cache::kNotificationTypeKey, "string1", + entryRegKey.c_str()); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + Cache::kNotificationShownKey, "string2", + entryRegKey.c_str()); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + Cache::kNotificationActionKey, "string3", + entryRegKey.c_str()); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, + Cache::kPrevNotificationActionKey, "string4", + entryRegKey.c_str()); + ASSERT_TRUE(result.isOk()); + result = RegistrySetValueString(IsPrefixed::Unprefixed, L"UnknownFutureKey", + "string5", entryRegKey.c_str()); + ASSERT_TRUE(result.isOk()); + + Cache cache(mCacheRegKey.c_str()); + result = cache.Init(); + ASSERT_TRUE(result.isOk()); + + // Insert 2 new items to test that these features work with a different + // capacity. + Cache::Entry toWrite = Cache::Entry{ + .notificationType = "string6", + .notificationShown = "string7", + .notificationAction = "string8", + .prevNotificationAction = "string9", + }; + result = cache.Enqueue(toWrite); + ASSERT_TRUE(result.isOk()); + toWrite = Cache::Entry{ + .notificationType = "string10", + .notificationShown = "string11", + .notificationAction = "string12", + .prevNotificationAction = "string13", + }; + result = cache.Enqueue(toWrite); + ASSERT_TRUE(result.isOk()); + + // Read cache and verify the output + Cache::MaybeEntryResult entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + Cache::MaybeEntry entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 9999U); + ASSERT_EQ(entry.value().notificationType, "string1"); + ASSERT_EQ(entry.value().notificationShown, "string2"); + ASSERT_EQ(entry.value().notificationAction, "string3"); + ASSERT_TRUE(entry.value().prevNotificationAction.isSome()); + ASSERT_EQ(entry.value().prevNotificationAction.value(), "string4"); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 2U); + ASSERT_EQ(entry.value().notificationType, "string6"); + ASSERT_EQ(entry.value().notificationShown, "string7"); + ASSERT_EQ(entry.value().notificationAction, "string8"); + ASSERT_TRUE(entry.value().prevNotificationAction.isSome()); + ASSERT_EQ(entry.value().prevNotificationAction.value(), "string9"); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isSome()); + ASSERT_EQ(entry.value().entryVersion, 2U); + ASSERT_EQ(entry.value().notificationType, "string10"); + ASSERT_EQ(entry.value().notificationShown, "string11"); + ASSERT_EQ(entry.value().notificationAction, "string12"); + ASSERT_TRUE(entry.value().prevNotificationAction.isSome()); + ASSERT_EQ(entry.value().prevNotificationAction.value(), "string13"); + + entryResult = cache.Dequeue(); + ASSERT_TRUE(entryResult.isOk()); + entry = entryResult.unwrap(); + ASSERT_TRUE(entry.isNothing()); +} diff --git a/toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp b/toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp new file mode 100644 index 0000000000..7c491184d9 --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/gtest/SetDefaultBrowserTest.cpp @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "gtest/gtest.h" + +#include +#include "mozilla/UniquePtr.h" +#include "WindowsUserChoice.h" + +#include "SetDefaultBrowser.h" + +using namespace mozilla::default_agent; + +TEST(SetDefaultBrowserUserChoice, Hash) +{ + // Hashes set by System Settings on 64-bit Windows 10 Pro 20H2 (19042.928). + const wchar_t* sid = L"S-1-5-21-636376821-3290315252-1794850287-1001"; + + // length mod 8 = 0 + EXPECT_STREQ( + GenerateUserChoiceHash(L"https", sid, L"FirefoxURL-308046B0AF4A39CB", + (SYSTEMTIME){2021, 4, 1, 19, 23, 7, 56, 506}) + .get(), + L"uzpIsMVyZ1g="); + + // length mod 8 = 2 (confirm that the incomplete last block is dropped) + EXPECT_STREQ( + GenerateUserChoiceHash(L".html", sid, L"FirefoxHTML-308046B0AF4A39CB", + (SYSTEMTIME){2021, 4, 1, 19, 23, 7, 56, 519}) + .get(), + L"7fjRtUPASlc="); + + // length mod 8 = 4 + EXPECT_STREQ( + GenerateUserChoiceHash(L"https", sid, L"MSEdgeHTM", + (SYSTEMTIME){2021, 4, 1, 19, 23, 3, 48, 119}) + .get(), + L"Fz0kA3Ymmps="); + + // length mod 8 = 6 + EXPECT_STREQ( + GenerateUserChoiceHash(L".html", sid, L"ChromeHTML", + (SYSTEMTIME){2021, 4, 1, 19, 23, 6, 3, 628}) + .get(), + L"R5TD9LGJ5Xw="); + + // non-ASCII + EXPECT_STREQ( + GenerateUserChoiceHash(L".html", sid, L"FirefoxHTML-ÀBÇDË😀†", + (SYSTEMTIME){2021, 4, 2, 20, 0, 38, 55, 101}) + .get(), + L"F3NsK3uNv5E="); +} diff --git a/toolkit/mozapps/defaultagent/tests/gtest/moz.build b/toolkit/mozapps/defaultagent/tests/gtest/moz.build new file mode 100644 index 0000000000..07fa68228c --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/gtest/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; c-basic-offset: 4; 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 https://mozilla.org/MPL/2.0/. + +Library("DefaultAgentTest") + +UNIFIED_SOURCES += [ + "CacheTest.cpp", + "SetDefaultBrowserTest.cpp", +] + +LOCAL_INCLUDES += [ + "/browser/components/shell/", + "/toolkit/mozapps/defaultagent", +] + +OS_LIBS += [ + "advapi32", + "bcrypt", + "crypt32", + "kernel32", + "rpcrt4", +] + +DEFINES["UNICODE"] = True +DEFINES["_UNICODE"] = True + +for var in ("MOZ_APP_BASENAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VENDOR"): + DEFINES[var] = '"%s"' % CONFIG[var] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js b/toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js new file mode 100644 index 0000000000..ac020ae9f2 --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/xpcshell/test_windows_mutex.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Multiple instances of a named mutex on Windows can lock on the same thread, so +// we have to run each test across at least two distinct threads. Running on a +// separate process achieves the same. +do_load_child_test_harness(); + +let parentFactory = Cc["@mozilla.org/windows-mutex-factory;1"].createInstance( + Ci.nsIWindowsMutexFactory +); + +function promiseCommand(aCommand) { + // Exceptions don't propogate to the process that called `sendCommand` nor + // tigger a test failure, so wrap the command to ensure we fail appropriately. + let wrappedCommand = `try {${aCommand}} catch(e) {Assert.ok(false, "Error running command received in child process. Note the passed in function must be self-contained. Error: \${e.toString()}");}`; + return new Promise(resolve => sendCommand(wrappedCommand, resolve)); +} + +// This is passed as a string to a child process, thus must be self-contained. +function assertLockOkOnChild(aName, aTestString) { + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + Assert.ok(false, `${assertLockOkOnChild.name} run on child process.`); + } + + let childFactory = Cc["@mozilla.org/windows-mutex-factory;1"].createInstance( + Ci.nsIWindowsMutexFactory + ); + + let lockingMutex = childFactory.createMutex(aName); + + info(`Locking mutex for subtest "${aTestString}"`); + lockingMutex.tryLock(); + try { + Assert.ok(lockingMutex.isLocked(), aTestString); + } finally { + lockingMutex.unlock(); + } +} + +// This is passed as a string to a child process, thus must be self-contained. +function assertLockThrowsOnChild(aName, aTestString) { + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + Assert.ok(false, `${assertLockThrowsOnChild.name} run on child process.`); + } + + let childFactory = Cc["@mozilla.org/windows-mutex-factory;1"].createInstance( + Ci.nsIWindowsMutexFactory + ); + + let blockedMutex = childFactory.createMutex(aName); + + info(`Locking mutex for subtest "${aTestString}"`); + Assert.throws(blockedMutex.tryLock, /NS_ERROR_NOT_AVAILABLE/, aTestString); + Assert.ok(!blockedMutex.isLocked(), "Not locked after error."); +} + +add_task(async function test_lock_blocks() { + const kTestMutexName = Services.uuid.generateUUID().toString(); + let lockingMutex = parentFactory.createMutex(kTestMutexName); + + Assert.ok(!lockingMutex.isLocked(), "Reported unlocked before locking."); + + info(`Locking mutex named "${kTestMutexName}"`); + lockingMutex.tryLock(); + try { + Assert.ok(lockingMutex.isLocked(), "Reported locked after locking."); + + await promiseCommand( + `(${assertLockThrowsOnChild.toString()})("${kTestMutexName}", "Concurrent attempts to lock identically named mutex throws.");` + ); + } finally { + lockingMutex.unlock(); + } +}); + +add_task(async function test_unlock_unblocks() { + const kTestMutexName = Services.uuid.generateUUID().toString(); + let lockingMutex = parentFactory.createMutex(kTestMutexName); + + info(`Locking mutex named "${kTestMutexName}"`); + lockingMutex.tryLock(); + lockingMutex.unlock(); + + Assert.ok(!lockingMutex.isLocked(), "Reported unlocked after unlocking."); + + await promiseCommand( + `(${assertLockOkOnChild.toString()})("${kTestMutexName}", "Locked previously unlocked mutex.");` + ); +}); + +add_task(async function test_names_dont_conflict() { + const kTestMutexName = Services.uuid.generateUUID().toString(); + let mutex1 = parentFactory.createMutex(kTestMutexName); + + info(`Locking mutex named "${kTestMutexName}"`); + mutex1.tryLock(); + try { + await promiseCommand( + `(${assertLockOkOnChild.toString()})(Services.uuid.generateUUID().toString(), "Differently named mutexes don't conflict");` + ); + } finally { + mutex1.unlock(); + } +}); + +add_task(async function test_relock_when_locked() { + const kTestMutexName = Services.uuid.generateUUID().toString(); + let mutex = parentFactory.createMutex(kTestMutexName); + + mutex.tryLock(); + try { + Assert.ok(() => mutex.tryLock(), "Relocking locked mutex succeeds."); + Assert.ok( + mutex.isLocked(), + "Reported locked after relocking locked mutex." + ); + } finally { + mutex.unlock(); + } +}); + +add_task(async function test_unlock_without_lock() { + const kTestMutexName = Services.uuid.generateUUID().toString(); + let mutex = parentFactory.createMutex(kTestMutexName); + + mutex.unlock(); + Assert.ok( + !mutex.isLocked(), + "Reported unlocked after unnecessarily unlocking mutex." + ); + + mutex.tryLock(); + try { + Assert.ok( + mutex.isLocked(), + "Reported locked after locking unnecessarily unlocked mutex." + ); + } finally { + mutex.unlock(); + } +}); diff --git a/toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml b/toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..df1992fb13 --- /dev/null +++ b/toolkit/mozapps/defaultagent/tests/xpcshell/xpcshell.toml @@ -0,0 +1,4 @@ +[DEFAULT] +run-if = ["os == 'win'"] + +["test_windows_mutex.js"] diff --git a/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs b/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs new file mode 100644 index 0000000000..da0439dcc5 --- /dev/null +++ b/toolkit/mozapps/downloads/DownloadLastDir.sys.mjs @@ -0,0 +1,254 @@ +/* -*- 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/. */ + +/* + * The behavior implemented by gDownloadLastDir is documented here. + * + * In normal browsing sessions, gDownloadLastDir uses the browser.download.lastDir + * preference to store the last used download directory. The first time the user + * switches into the private browsing mode, the last download directory is + * preserved to the pref value, but if the user switches to another directory + * during the private browsing mode, that directory is not stored in the pref, + * and will be merely kept in memory. When leaving the private browsing mode, + * this in-memory value will be discarded, and the last download directory + * will be reverted to the pref value. + * + * Both the pref and the in-memory value will be cleared when clearing the + * browsing history. This effectively changes the last download directory + * to the default download directory on each platform. + * + * If passed a URI, the last used directory is also stored with that URI in the + * content preferences database. This can be disabled by setting the pref + * browser.download.lastDir.savePerSite to false. + */ + +const LAST_DIR_PREF = "browser.download.lastDir"; +const SAVE_PER_SITE_PREF = LAST_DIR_PREF + ".savePerSite"; +const nsIFile = Ci.nsIFile; + +import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; +XPCOMUtils.defineLazyServiceGetter( + lazy, + "cps2", + "@mozilla.org/content-pref/service;1", + "nsIContentPrefService2" +); + +let nonPrivateLoadContext = Cu.createLoadContext(); +let privateLoadContext = Cu.createPrivateLoadContext(); + +var observer = { + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "last-pb-context-exited": + gDownloadLastDirFile = null; + break; + case "browser:purge-session-history": + gDownloadLastDirFile = null; + if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) { + Services.prefs.clearUserPref(LAST_DIR_PREF); + } + // Ensure that purging session history causes both the session-only PB cache + // and persistent prefs to be cleared. + let promises = [ + new Promise(resolve => + lazy.cps2.removeByName(LAST_DIR_PREF, nonPrivateLoadContext, { + handleCompletion: resolve, + }) + ), + new Promise(resolve => + lazy.cps2.removeByName(LAST_DIR_PREF, privateLoadContext, { + handleCompletion: resolve, + }) + ), + ]; + // This is for testing purposes. + if (aSubject && typeof subject == "object") { + aSubject.promise = Promise.all(promises); + } + break; + } + }, +}; + +Services.obs.addObserver(observer, "last-pb-context-exited", true); +Services.obs.addObserver(observer, "browser:purge-session-history", true); + +function readLastDirPref() { + try { + return Services.prefs.getComplexValue(LAST_DIR_PREF, nsIFile); + } catch (e) { + return null; + } +} + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "isContentPrefEnabled", + SAVE_PER_SITE_PREF, + true +); + +var gDownloadLastDirFile = readLastDirPref(); + +export class DownloadLastDir { + // aForcePrivate is only used when aWindow is null. + constructor(aWindow, aForcePrivate) { + let isPrivate = false; + if (aWindow === null) { + isPrivate = + aForcePrivate || PrivateBrowsingUtils.permanentPrivateBrowsing; + } else { + let loadContext = aWindow.docShell.QueryInterface(Ci.nsILoadContext); + isPrivate = loadContext.usePrivateBrowsing; + } + + // We always use a fake load context because we may not have one (i.e., + // in the aWindow == null case) and because the load context associated + // with aWindow may disappear by the time we need it. This approach is + // safe because we only care about the private browsing state. All the + // rest of the load context isn't of interest to the content pref service. + this.fakeContext = isPrivate ? privateLoadContext : nonPrivateLoadContext; + } + + isPrivate() { + return this.fakeContext.usePrivateBrowsing; + } + + // compat shims + get file() { + return this.#getLastFile(); + } + set file(val) { + this.setFile(null, val); + } + + cleanupPrivateFile() { + gDownloadLastDirFile = null; + } + + #getLastFile() { + if (gDownloadLastDirFile && !gDownloadLastDirFile.exists()) { + gDownloadLastDirFile = null; + } + + if (this.isPrivate()) { + if (!gDownloadLastDirFile) { + gDownloadLastDirFile = readLastDirPref(); + } + return gDownloadLastDirFile; + } + return readLastDirPref(); + } + + async getFileAsync(aURI) { + let plainPrefFile = this.#getLastFile(); + if (!aURI || !lazy.isContentPrefEnabled) { + return plainPrefFile; + } + + return new Promise(resolve => { + lazy.cps2.getByDomainAndName( + this.#cpsGroupFromURL(aURI), + LAST_DIR_PREF, + this.fakeContext, + { + _result: null, + handleResult(aResult) { + this._result = aResult; + }, + handleCompletion(aReason) { + let file = plainPrefFile; + if ( + aReason == Ci.nsIContentPrefCallback2.COMPLETE_OK && + this._result instanceof Ci.nsIContentPref + ) { + try { + file = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + file.initWithPath(this._result.value); + } catch (e) { + file = plainPrefFile; + } + } + resolve(file); + }, + } + ); + }); + } + + setFile(aURI, aFile) { + if (aURI && lazy.isContentPrefEnabled) { + if (aFile instanceof Ci.nsIFile) { + lazy.cps2.set( + this.#cpsGroupFromURL(aURI), + LAST_DIR_PREF, + aFile.path, + this.fakeContext + ); + } else { + lazy.cps2.removeByDomainAndName( + this.#cpsGroupFromURL(aURI), + LAST_DIR_PREF, + this.fakeContext + ); + } + } + if (this.isPrivate()) { + if (aFile instanceof Ci.nsIFile) { + gDownloadLastDirFile = aFile.clone(); + } else { + gDownloadLastDirFile = null; + } + } else if (aFile instanceof Ci.nsIFile) { + Services.prefs.setComplexValue(LAST_DIR_PREF, nsIFile, aFile); + } else if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) { + Services.prefs.clearUserPref(LAST_DIR_PREF); + } + } + + /** + * Pre-processor to extract a domain name to be used with the content-prefs + * service. This specially handles data and file URIs so that the download + * dirs are recalled in a more consistent way: + * - all file:/// URIs share the same folder + * - data: URIs share a folder per mime-type. If a mime-type is not + * specified text/plain is assumed. + * - blob: URIs share the same folder as their origin. This is done by + * ContentPrefs already, so we just let the url fall-through. + * In any other case the original URL is returned as a string and ContentPrefs + * will do its usual parsing. + * + * @param {string|nsIURI|URL} url The URL to parse + * @returns {string} the domain name to use, or the original url. + */ + #cpsGroupFromURL(url) { + if (typeof url == "string") { + url = new URL(url); + } else if (url instanceof Ci.nsIURI) { + url = URL.fromURI(url); + } + if (!URL.isInstance(url)) { + return url; + } + if (url.protocol == "data:") { + return url.href.match(/^data:[^;,]*/i)[0].replace(/:$/, ":text/plain"); + } + if (url.protocol == "file:") { + return "file:///"; + } + return url.href; + } +} diff --git a/toolkit/mozapps/downloads/DownloadUtils.sys.mjs b/toolkit/mozapps/downloads/DownloadUtils.sys.mjs new file mode 100644 index 0000000000..3bf97f3c9e --- /dev/null +++ b/toolkit/mozapps/downloads/DownloadUtils.sys.mjs @@ -0,0 +1,616 @@ +/* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript + * 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 module provides the DownloadUtils object which contains useful methods + * for downloads such as displaying file sizes, transfer times, and download + * locations. + * + * List of methods: + * + * [string status, double newLast] + * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes, + * [optional] double aSpeed, [optional] double aLastSec) + * + * string progress + * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes) + * + * [string timeLeft, double newLast] + * getTimeLeft(double aSeconds, [optional] double aLastSec) + * + * [string dateCompact, string dateComplete] + * getReadableDates(Date aDate, [optional] Date aNow) + * + * [string displayHost, string fullHost] + * getURIHost(string aURIString) + * + * [string convertedBytes, string units] + * convertByteUnits(int aBytes) + * + * [int time, string units, int subTime, string subUnits] + * convertTimeUnits(double aSecs) + */ + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +const BYTE_UNITS = [ + "download-utils-bytes", + "download-utils-kilobyte", + "download-utils-megabyte", + "download-utils-gigabyte", +]; + +const TIME_UNITS = [ + "download-utils-short-seconds", + "download-utils-short-minutes", + "download-utils-short-hours", + "download-utils-short-days", +]; + +// These are the maximum values for seconds, minutes, hours corresponding +// with TIME_UNITS without the last item +const TIME_SIZES = [60, 60, 24]; + +var localeNumberFormatCache = new Map(); +function getLocaleNumberFormat(fractionDigits) { + if (!localeNumberFormatCache.has(fractionDigits)) { + localeNumberFormatCache.set( + fractionDigits, + new Services.intl.NumberFormat(undefined, { + maximumFractionDigits: fractionDigits, + minimumFractionDigits: fractionDigits, + }) + ); + } + return localeNumberFormatCache.get(fractionDigits); +} + +const l10n = new Localization(["toolkit/downloads/downloadUtils.ftl"], true); + +// Keep track of at most this many second/lastSec pairs so that multiple calls +// to getTimeLeft produce the same time left +const kCachedLastMaxSize = 10; +var gCachedLast = []; + +export var DownloadUtils = { + /** + * Generate a full status string for a download given its current progress, + * total size, speed, last time remaining + * + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @param [optional] aSpeed + * Current transfer rate in bytes/sec or -1 for unknown + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A pair: [download status text, new value of "last seconds"] + */ + getDownloadStatus: function DU_getDownloadStatus( + aCurrBytes, + aMaxBytes, + aSpeed, + aLastSec + ) { + let [transfer, timeLeft, newLast, normalizedSpeed] = + this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); + + let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed); + + let status; + if (rate === "Infinity") { + // Infinity download speed doesn't make sense. Show a localized phrase instead. + status = l10n.formatValueSync("download-utils-status-infinite-rate", { + transfer, + timeLeft, + }); + } else { + status = l10n.formatValueSync("download-utils-status", { + transfer, + rate, + unit, + timeLeft, + }); + } + return [status, newLast]; + }, + + /** + * Generate a status string for a download given its current progress, + * total size, speed, last time remaining. The status string contains the + * time remaining, as well as the total bytes downloaded. Unlike + * getDownloadStatus, it does not include the rate of download. + * + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @param [optional] aSpeed + * Current transfer rate in bytes/sec or -1 for unknown + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A pair: [download status text, new value of "last seconds"] + */ + getDownloadStatusNoRate: function DU_getDownloadStatusNoRate( + aCurrBytes, + aMaxBytes, + aSpeed, + aLastSec + ) { + let [transfer, timeLeft, newLast] = this._deriveTransferRate( + aCurrBytes, + aMaxBytes, + aSpeed, + aLastSec + ); + + let status = l10n.formatValueSync("download-utils-status-no-rate", { + transfer, + timeLeft, + }); + return [status, newLast]; + }, + + /** + * Helper function that returns a transfer string, a time remaining string, + * and a new value of "last seconds". + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @param [optional] aSpeed + * Current transfer rate in bytes/sec or -1 for unknown + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A triple: [amount transferred string, time remaining string, + * new value of "last seconds"] + */ + _deriveTransferRate: function DU__deriveTransferRate( + aCurrBytes, + aMaxBytes, + aSpeed, + aLastSec + ) { + if (aMaxBytes == null) { + aMaxBytes = -1; + } + if (aSpeed == null) { + aSpeed = -1; + } + if (aLastSec == null) { + aLastSec = Infinity; + } + + // Calculate the time remaining if we have valid values + let seconds = + aSpeed > 0 && aMaxBytes > 0 ? (aMaxBytes - aCurrBytes) / aSpeed : -1; + + let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes); + let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec); + return [transfer, timeLeft, newLast, aSpeed]; + }, + + /** + * Generate the transfer progress string to show the current and total byte + * size. Byte units will be as large as possible and the same units for + * current and max will be suppressed for the former. + * + * @param aCurrBytes + * Number of bytes transferred so far + * @param [optional] aMaxBytes + * Total number of bytes or -1 for unknown + * @return The transfer progress text + */ + getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes) { + if (aMaxBytes == null) { + aMaxBytes = -1; + } + + let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes); + let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes); + + // Figure out which byte progress string to display + let name; + if (aMaxBytes < 0) { + name = "download-utils-transfer-no-total"; + } else if (progressUnits == totalUnits) { + name = "download-utils-transfer-same-units"; + } else { + name = "download-utils-transfer-diff-units"; + } + + return l10n.formatValueSync(name, { + progress, + progressUnits, + total, + totalUnits, + }); + }, + + /** + * Generate a "time left" string given an estimate on the time left and the + * last time. The extra time is used to give a better estimate on the time to + * show. Both the time values are doubles instead of integers to help get + * sub-second accuracy for current and future estimates. + * + * @param aSeconds + * Current estimate on number of seconds left for the download + * @param [optional] aLastSec + * Last time remaining in seconds or Infinity for unknown + * @return A pair: [time left text, new value of "last seconds"] + */ + getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec) { + let nf = new Services.intl.NumberFormat(); + if (aLastSec == null) { + aLastSec = Infinity; + } + + if (aSeconds < 0) { + return [l10n.formatValueSync("download-utils-time-unknown"), aLastSec]; + } + + // Try to find a cached lastSec for the given second + aLastSec = gCachedLast.reduce( + (aResult, aItem) => (aItem[0] == aSeconds ? aItem[1] : aResult), + aLastSec + ); + + // Add the current second/lastSec pair unless we have too many + gCachedLast.push([aSeconds, aLastSec]); + if (gCachedLast.length > kCachedLastMaxSize) { + gCachedLast.shift(); + } + + // Apply smoothing only if the new time isn't a huge change -- e.g., if the + // new time is more than half the previous time; this is useful for + // downloads that start/resume slowly + if (aSeconds > aLastSec / 2) { + // Apply hysteresis to favor downward over upward swings + // 30% of down and 10% of up (exponential smoothing) + let diff = aSeconds - aLastSec; + aSeconds = aLastSec + (diff < 0 ? 0.3 : 0.1) * diff; + + // If the new time is similar, reuse something close to the last seconds, + // but subtract a little to provide forward progress + let diffPct = (diff / aLastSec) * 100; + if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) { + aSeconds = aLastSec - (diff < 0 ? 0.4 : 0.2); + } + } + + // Decide what text to show for the time + let timeLeft; + if (aSeconds < 4) { + // Be friendly in the last few seconds + timeLeft = l10n.formatValueSync("download-utils-time-few-seconds"); + } else { + // Convert the seconds into its two largest units to display + let [time1, unit1, time2, unit2] = + DownloadUtils.convertTimeUnits(aSeconds); + + const pair1 = l10n.formatValueSync("download-utils-time-pair", { + time: nf.format(time1), + unit: unit1, + }); + + // Only show minutes for under 1 hour unless there's a few minutes left; + // or the second pair is 0. + if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) { + timeLeft = l10n.formatValueSync("download-utils-time-left-single", { + time: pair1, + }); + } else { + // We've got 2 pairs of times to display + const pair2 = l10n.formatValueSync("download-utils-time-pair", { + time: nf.format(time2), + unit: unit2, + }); + timeLeft = l10n.formatValueSync("download-utils-time-left-double", { + time1: pair1, + time2: pair2, + }); + } + } + + return [timeLeft, aSeconds]; + }, + + /** + * Converts a Date object to two readable formats, one compact, one complete. + * The compact format is relative to the current date, and is not an accurate + * representation. For example, only the time is displayed for today. The + * complete format always includes both the date and the time, excluding the + * seconds, and is often shown when hovering the cursor over the compact + * representation. + * + * @param aDate + * Date object representing the date and time to format. It is assumed + * that this value represents a past date. + * @param [optional] aNow + * Date object representing the current date and time. The real date + * and time of invocation is used if this parameter is omitted. + * @return A pair: [compact text, complete text] + */ + getReadableDates: function DU_getReadableDates(aDate, aNow) { + if (!aNow) { + aNow = new Date(); + } + + // Figure out when today begins + let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate()); + + let dateTimeCompact; + let dateTimeFull; + + // Figure out if the time is from today, yesterday, this week, etc. + if (aDate >= today) { + let dts = new Services.intl.DateTimeFormat(undefined, { + timeStyle: "short", + }); + dateTimeCompact = dts.format(aDate); + } else if (today - aDate < MS_PER_DAY) { + // After yesterday started, show yesterday + dateTimeCompact = l10n.formatValueSync("download-utils-yesterday"); + } else if (today - aDate < 6 * MS_PER_DAY) { + // After last week started, show day of week + dateTimeCompact = aDate.toLocaleDateString(undefined, { + weekday: "long", + }); + } else { + // Show month/day + dateTimeCompact = aDate.toLocaleString(undefined, { + month: "long", + day: "numeric", + }); + } + + const dtOptions = { dateStyle: "long", timeStyle: "short" }; + dateTimeFull = new Services.intl.DateTimeFormat( + undefined, + dtOptions + ).format(aDate); + + return [dateTimeCompact, dateTimeFull]; + }, + + /** + * Get the appropriate display host string for a URI string depending on if + * the URI has an eTLD + 1, is an IP address, a local file, or other protocol + * + * @param aURIString + * The URI string to try getting an eTLD + 1, etc. + * @return A pair: [display host for the URI string, full host name] + */ + getURIHost: function DU_getURIHost(aURIString) { + let idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + + // Get a URI that knows about its components + let uri; + try { + uri = Services.io.newURI(aURIString); + } catch (ex) { + return ["", ""]; + } + + // Get the inner-most uri for schemes like jar: + if (uri instanceof Ci.nsINestedURI) { + uri = uri.innermostURI; + } + + if (uri.scheme == "blob") { + let origin = new URL(uri.spec).origin; + // Origin can be "null" for blob URIs from a sandbox. + if (origin != "null") { + // `newURI` can throw (like for null) and throwing here breaks... + // a lot of stuff. So let's avoid doing that in case there are other + // edgecases we're missing here. + try { + uri = Services.io.newURI(origin); + } catch (ex) { + console.error(ex); + } + } + } + + let fullHost; + try { + // Get the full host name; some special URIs fail (data: jar:) + fullHost = uri.host; + } catch (e) { + fullHost = ""; + } + + let displayHost; + try { + // This might fail if it's an IP address or doesn't have more than 1 part + let baseDomain = Services.eTLD.getBaseDomain(uri); + + // Convert base domain for display; ignore the isAscii out param + displayHost = idnService.convertToDisplayIDN(baseDomain, {}); + } catch (e) { + // Default to the host name + displayHost = fullHost; + } + + // Check if we need to show something else for the host + if (uri.scheme == "file") { + // Display special text for file protocol + displayHost = l10n.formatValueSync("download-utils-done-file-scheme"); + fullHost = displayHost; + } else if (!displayHost.length) { + // Got nothing; show the scheme (data: about: moz-icon:) + displayHost = l10n.formatValueSync("download-utils-done-scheme", { + scheme: uri.scheme, + }); + fullHost = displayHost; + } else if (uri.port != -1) { + // Tack on the port if it's not the default port + let port = ":" + uri.port; + displayHost += port; + fullHost += port; + } + + return [displayHost, fullHost]; + }, + + /** + * Converts a number of bytes to the appropriate unit that results in an + * internationalized number that needs fewer than 4 digits. + * + * @param aBytes + * Number of bytes to convert + * @return A pair: [new value with 3 sig. figs., its unit] + */ + convertByteUnits: function DU_convertByteUnits(aBytes) { + let unitIndex = 0; + + // Convert to next unit if it needs 4 digits (after rounding), but only if + // we know the name of the next unit + while (aBytes >= 999.5 && unitIndex < BYTE_UNITS.length - 1) { + aBytes /= 1024; + unitIndex++; + } + + // Get rid of insignificant bits by truncating to 1 or 0 decimal points + // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 + // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100 + let fractionDigits = aBytes > 0 && aBytes < 100 && unitIndex != 0 ? 1 : 0; + + // Don't try to format Infinity values using NumberFormat. + if (aBytes === Infinity) { + aBytes = "Infinity"; + } else { + aBytes = getLocaleNumberFormat(fractionDigits).format(aBytes); + } + + return [aBytes, l10n.formatValueSync(BYTE_UNITS[unitIndex])]; + }, + + /** + * Converts a number of seconds to the two largest units. Time values are + * whole numbers, and units have the correct plural/singular form. + * + * @param aSecs + * Seconds to convert into the appropriate 2 units + * @return 4-item array [first value, its unit, second value, its unit] + */ + convertTimeUnits: function DU_convertTimeUnits(aSecs) { + let time = aSecs; + let scale = 1; + let unitIndex = 0; + + // Keep converting to the next unit while we have units left and the + // current one isn't the largest unit possible + while (unitIndex < TIME_SIZES.length && time >= TIME_SIZES[unitIndex]) { + time /= TIME_SIZES[unitIndex]; + scale *= TIME_SIZES[unitIndex]; + unitIndex++; + } + + let value = convertTimeUnitsValue(time); + let units = convertTimeUnitsUnits(value, unitIndex); + + let extra = aSecs - value * scale; + let nextIndex = unitIndex - 1; + + // Convert the extra time to the next largest unit + for (let index = 0; index < nextIndex; index++) { + extra /= TIME_SIZES[index]; + } + + let value2 = convertTimeUnitsValue(extra); + let units2 = convertTimeUnitsUnits(value2, nextIndex); + + return [value, units, value2, units2]; + }, + + /** + * Converts a number of seconds to "downloading file opens in X" status. + * @param aSeconds + * Seconds to convert into the time format. + * @return status object, example: + * status = { + * l10n: { + * id: "downloading-file-opens-in-minutes-and-seconds", + * args: { minutes: 2, seconds: 30 }, + * }, + * }; + */ + getFormattedTimeStatus: function DU_getFormattedTimeStatus(aSeconds) { + aSeconds = Math.floor(aSeconds); + let l10n; + if (!isFinite(aSeconds) || aSeconds < 0) { + l10n = { + id: "downloading-file-opens-in-some-time-2", + }; + } else if (aSeconds < 60) { + l10n = { + id: "downloading-file-opens-in-seconds-2", + args: { seconds: aSeconds }, + }; + } else if (aSeconds < 3600) { + let minutes = Math.floor(aSeconds / 60); + let seconds = aSeconds % 60; + l10n = seconds + ? { + args: { seconds, minutes }, + id: "downloading-file-opens-in-minutes-and-seconds-2", + } + : { args: { minutes }, id: "downloading-file-opens-in-minutes-2" }; + } else { + let hours = Math.floor(aSeconds / 3600); + let minutes = Math.floor((aSeconds % 3600) / 60); + l10n = { + args: { hours, minutes }, + id: "downloading-file-opens-in-hours-and-minutes-2", + }; + } + return { l10n }; + }, +}; + +/** + * Private helper for convertTimeUnits that gets the display value of a time + * + * @param aTime + * Time value for display + * @return An integer value for the time rounded down + */ +function convertTimeUnitsValue(aTime) { + return Math.floor(aTime); +} + +/** + * Private helper for convertTimeUnits that gets the display units of a time + * + * @param timeValue + * Time value for display + * @param aIndex + * Index into gStr.timeUnits for the appropriate unit + * @return The appropriate plural form of the unit for the time + */ +function convertTimeUnitsUnits(timeValue, aIndex) { + // Negative index would be an invalid unit, so just give empty + if (aIndex < 0) { + return ""; + } + + return l10n.formatValueSync(TIME_UNITS[aIndex], { timeValue }); +} + +/** + * Private helper function to log errors to the error console and command line + * + * @param aMsg + * Error message to log or an array of strings to concat + */ +// function log(aMsg) { +// let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg); +// Services.console.logStringMessage(msg); +// dump(msg + "\n"); +// } diff --git a/toolkit/mozapps/downloads/HelperAppDlg.sys.mjs b/toolkit/mozapps/downloads/HelperAppDlg.sys.mjs new file mode 100644 index 0000000000..66f77d38e4 --- /dev/null +++ b/toolkit/mozapps/downloads/HelperAppDlg.sys.mjs @@ -0,0 +1,1349 @@ +/* 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/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + EnableDelayHelper: "resource://gre/modules/PromptUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gReputationService", + "@mozilla.org/reputationservice/application-reputation-service;1", + Ci.nsIApplicationReputationService +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gMIMEService", + "@mozilla.org/mime;1", + Ci.nsIMIMEService +); + +import { Integration } from "resource://gre/modules/Integration.sys.mjs"; + +Integration.downloads.defineESModuleGetter( + lazy, + "DownloadIntegration", + "resource://gre/modules/DownloadIntegration.sys.mjs" +); + +// ///////////////////////////////////////////////////////////////////////////// +// // Helper Functions + +/** + * Determines if a given directory is able to be used to download to. + * + * @param aDirectory + * The directory to check. + * @return true if we can use the directory, false otherwise. + */ +function isUsableDirectory(aDirectory) { + return ( + aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable() + ); +} + +// Web progress listener so we can detect errors while mLauncher is +// streaming the data to a temporary file. +function nsUnknownContentTypeDialogProgressListener(aHelperAppDialog) { + this.helperAppDlg = aHelperAppDialog; +} + +nsUnknownContentTypeDialogProgressListener.prototype = { + // nsIWebProgressListener methods. + // Look for error notifications and display alert to user. + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + if (aStatus != Cr.NS_OK) { + // Display error alert (using text supplied by back-end). + // FIXME this.dialog is undefined? + Services.prompt.alert(this.dialog, this.helperAppDlg.mTitle, aMessage); + // Close the dialog. + this.helperAppDlg.onCancel(); + if (this.helperAppDlg.mDialog) { + this.helperAppDlg.mDialog.close(); + } + } + }, + + // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, onContentBlockingEvent and onRefreshAttempted notifications. + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) {}, + + onProgressChange64( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) {}, + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {}, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + + onSecurityChange(aWebProgress, aRequest, aState) {}, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) {}, + + onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { + return true; + }, +}; + +// ///////////////////////////////////////////////////////////////////////////// +// // nsUnknownContentTypeDialog + +/* This file implements the nsIHelperAppLauncherDialog interface. + * + * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog, + * comprised of: + * - a JS constructor function + * - a prototype providing all the interface methods and implementation stuff + */ + +const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir"; +const nsITimer = Ci.nsITimer; + +import * as downloadModule from "resource://gre/modules/DownloadLastDir.sys.mjs"; +import { DownloadPaths } from "resource://gre/modules/DownloadPaths.sys.mjs"; + +import { DownloadUtils } from "resource://gre/modules/DownloadUtils.sys.mjs"; +import { Downloads } from "resource://gre/modules/Downloads.sys.mjs"; +import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs"; + +/* ctor + */ +export function nsUnknownContentTypeDialog() { + // Initialize data properties. + this.mLauncher = null; + this.mContext = null; + this.mReason = null; + this.chosenApp = null; + this.givenDefaultApp = false; + this.updateSelf = true; + this.mTitle = ""; +} + +nsUnknownContentTypeDialog.prototype = { + classID: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"), + + nsIMIMEInfo: Ci.nsIMIMEInfo, + + QueryInterface: ChromeUtils.generateQI([ + "nsIHelperAppLauncherDialog", + "nsITimerCallback", + ]), + + // ---------- nsIHelperAppLauncherDialog methods ---------- + + // show: Open XUL dialog using window watcher. Since the dialog is not + // modal, it needs to be a top level window and the way to open + // one of those is via that route). + show(aLauncher, aContext, aReason) { + this.mLauncher = aLauncher; + this.mContext = aContext; + this.mReason = aReason; + + // Cache some information in case this context goes away: + try { + let parent = aContext.getInterface(Ci.nsIDOMWindow); + this._mDownloadDir = new downloadModule.DownloadLastDir(parent); + } catch (ex) { + console.error( + "Missing window information when showing nsIHelperAppLauncherDialog:", + ex + ); + } + + const nsITimer = Ci.nsITimer; + this._showTimer = Cc["@mozilla.org/timer;1"].createInstance(nsITimer); + this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT); + }, + + // When opening from new tab, if tab closes while dialog is opening, + // (which is a race condition on the XUL file being cached and the timer + // in nsExternalHelperAppService), the dialog gets a blur and doesn't + // activate the OK button. So we wait a bit before doing opening it. + reallyShow() { + try { + let docShell = this.mContext.getInterface(Ci.nsIDocShell); + let rootWin = docShell.browsingContext.topChromeWindow; + this.mDialog = Services.ww.openWindow( + rootWin, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + null, + "chrome,centerscreen,titlebar,dialog=yes,dependent", + null + ); + } catch (ex) { + // The containing window may have gone away. Break reference + // cycles and stop doing the download. + this.mLauncher.cancel(Cr.NS_BINDING_ABORTED); + return; + } + + // Hook this object to the dialog. + this.mDialog.dialog = this; + + // Hook up utility functions. + this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey; + + // Watch for error notifications. + var progressListener = new nsUnknownContentTypeDialogProgressListener(this); + this.mLauncher.setWebProgressListener(progressListener); + }, + + // + // displayBadPermissionAlert() + // + // Diplay an alert panel about the bad permission of folder/directory. + // + displayBadPermissionAlert() { + let bundle = Services.strings.createBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties" + ); + + Services.prompt.alert( + this.dialog, + bundle.GetStringFromName("badPermissions.title"), + bundle.GetStringFromName("badPermissions") + ); + }, + + promptForSaveToFileAsync( + aLauncher, + aContext, + aDefaultFileName, + aSuggestedFileExtension, + aForcePrompt + ) { + var result = null; + + this.mLauncher = aLauncher; + + let bundle = Services.strings.createBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties" + ); + + let parent; + let gDownloadLastDir; + try { + parent = aContext.getInterface(Ci.nsIDOMWindow); + } catch (ex) {} + + if (parent) { + gDownloadLastDir = new downloadModule.DownloadLastDir(parent); + } else { + // Use the cached download info, but pick an arbitrary parent window + // because the original one is definitely gone (and nsIFilePicker doesn't like + // a null parent): + gDownloadLastDir = this._mDownloadDir; + for (let someWin of Services.wm.getEnumerator("")) { + // We need to make sure we don't end up with this dialog, because otherwise + // that's going to go away when the user clicks "Save", and that breaks the + // windows file picker that's supposed to show up if we let the user choose + // where to save files... + if (someWin != this.mDialog) { + parent = someWin; + } + } + if (!parent) { + console.error( + "No candidate parent windows were found for the save filepicker." + + "This should never happen." + ); + } + } + + (async () => { + if (!aForcePrompt) { + // Check to see if the user wishes to auto save to the default download + // folder without prompting. Note that preference might not be set. + let autodownload = Services.prefs.getBoolPref( + PREF_BD_USEDOWNLOADDIR, + false + ); + + if (autodownload) { + // Retrieve the user's default download directory + let preferredDir = await Downloads.getPreferredDownloadsDirectory(); + let defaultFolder = new FileUtils.File(preferredDir); + + try { + if (aDefaultFileName) { + result = this.validateLeafName( + defaultFolder, + aDefaultFileName, + aSuggestedFileExtension + ); + } + } catch (ex) { + // When the default download directory is write-protected, + // prompt the user for a different target file. + } + + // Check to make sure we have a valid directory, otherwise, prompt + if (result) { + // This path is taken when we have a writable default download directory. + aLauncher.saveDestinationAvailable(result); + return; + } + } + } + + // Use file picker to show dialog. + var nsIFilePicker = Ci.nsIFilePicker; + var picker = + Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + var windowTitle = bundle.GetStringFromName("saveDialogTitle"); + picker.init(parent, windowTitle, nsIFilePicker.modeSave); + if (aDefaultFileName) { + picker.defaultString = this.getFinalLeafName(aDefaultFileName); + } + + if (aSuggestedFileExtension) { + // aSuggestedFileExtension includes the period, so strip it + picker.defaultExtension = aSuggestedFileExtension.substring(1); + } else { + try { + picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension; + } catch (ex) {} + } + + var wildCardExtension = "*"; + if (aSuggestedFileExtension) { + wildCardExtension += aSuggestedFileExtension; + picker.appendFilter( + this.mLauncher.MIMEInfo.description, + wildCardExtension + ); + } + + picker.appendFilters(nsIFilePicker.filterAll); + + // Default to lastDir if it is valid, otherwise use the user's default + // downloads directory. getPreferredDownloadsDirectory should always + // return a valid directory path, so we can safely default to it. + let preferredDir = await Downloads.getPreferredDownloadsDirectory(); + picker.displayDirectory = new FileUtils.File(preferredDir); + + gDownloadLastDir.getFileAsync(aLauncher.source).then(lastDir => { + if (lastDir && isUsableDirectory(lastDir)) { + picker.displayDirectory = lastDir; + } + + picker.open(returnValue => { + if (returnValue == nsIFilePicker.returnCancel) { + // null result means user cancelled. + aLauncher.saveDestinationAvailable(null); + return; + } + + // Be sure to save the directory the user chose through the Save As... + // dialog as the new browser.download.dir since the old one + // didn't exist. + result = picker.file; + + if (result) { + let allowOverwrite = false; + try { + // If we're overwriting, avoid renaming our file, and assume + // overwriting it does the right thing. + if ( + result.exists() && + this.getFinalLeafName(result.leafName, "", true) == + result.leafName + ) { + allowOverwrite = true; + } + } catch (ex) { + // As it turns out, the failure to remove the file, for example due to + // permission error, will be handled below eventually somehow. + } + + var newDir = result.parent.QueryInterface(Ci.nsIFile); + + // Do not store the last save directory as a pref inside the private browsing mode + gDownloadLastDir.setFile(aLauncher.source, newDir); + + try { + result = this.validateLeafName( + newDir, + result.leafName, + null, + allowOverwrite, + true + ); + } catch (ex) { + // When the chosen download directory is write-protected, + // display an informative error message. + // In all cases, download will be stopped. + + if (ex.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) { + this.displayBadPermissionAlert(); + aLauncher.saveDestinationAvailable(null); + return; + } + } + } + // Don't pop up the downloads panel redundantly. + aLauncher.saveDestinationAvailable(result, true); + }); + }); + })().catch(console.error); + }, + + getFinalLeafName(aLeafName, aFileExt, aAfterFilePicker) { + return ( + DownloadPaths.sanitize(aLeafName, { + compressWhitespaces: !aAfterFilePicker, + allowInvalidFilenames: aAfterFilePicker, + }) || "unnamed" + (aFileExt ? "." + aFileExt : "") + ); + }, + + /** + * Ensures that a local folder/file combination does not already exist in + * the file system (or finds such a combination with a reasonably similar + * leaf name), creates the corresponding file, and returns it. + * + * @param aLocalFolder + * the folder where the file resides + * @param aLeafName + * the string name of the file (may be empty if no name is known, + * in which case a name will be chosen) + * @param aFileExt + * the extension of the file, if one is known; this will be ignored + * if aLeafName is non-empty + * @param aAllowExisting + * if set to true, avoid creating a unique file. + * @param aAfterFilePicker + * if set to true, this was a file entered by the user from a file picker. + * @return nsIFile + * the created file + * @throw an error such as permission doesn't allow creation of + * file, etc. + */ + validateLeafName( + aLocalFolder, + aLeafName, + aFileExt, + aAllowExisting = false, + aAfterFilePicker = false + ) { + if (!(aLocalFolder && isUsableDirectory(aLocalFolder))) { + throw new Components.Exception( + "Destination directory non-existing or permission error", + Cr.NS_ERROR_FILE_ACCESS_DENIED + ); + } + + aLeafName = this.getFinalLeafName(aLeafName, aFileExt, aAfterFilePicker); + aLocalFolder.append(aLeafName); + + if (!aAllowExisting) { + // The following assignment can throw an exception, but + // is now caught properly in the caller of validateLeafName. + var validatedFile = DownloadPaths.createNiceUniqueFile(aLocalFolder); + } else { + validatedFile = aLocalFolder; + } + + return validatedFile; + }, + + // ---------- implementation methods ---------- + + // initDialog: Fill various dialog fields with initial content. + initDialog() { + // Put file name in window title. + var suggestedFileName = this.mLauncher.suggestedFileName; + + this.mDialog.document.addEventListener("dialogaccept", this); + this.mDialog.document.addEventListener("dialogcancel", this); + + let url = this.mLauncher.source; + + if (url instanceof Ci.nsINestedURI) { + url = url.innermostURI; + } + + let iconPath = "goat"; + let fname = ""; + if (suggestedFileName) { + fname = iconPath = suggestedFileName; + } else if (url instanceof Ci.nsIURL) { + // A url, use file name from it. + fname = iconPath = url.fileName; + } else if (["data", "blob"].includes(url.scheme)) { + // The path is useless for these, so use a reasonable default. + let { MIMEType } = this.mLauncher.MIMEInfo; + fname = lazy.gMIMEService.getValidFileName(null, MIMEType, url, 0); + } else { + fname = url.pathQueryRef; + } + + this.mSourcePath = url.prePath; + // Some URIs do not implement nsIURL, so we can't just QI. + if (url instanceof Ci.nsIURL) { + this.mSourcePath += url.directory; + } else { + // Don't make the url excessively long (e.g. for data URIs) + // (this doesn't use a temp var to avoid copying a potentially + // several mb-long string) + this.mSourcePath += + url.pathQueryRef.length > 500 + ? url.pathQueryRef.substring(0, 500) + "\u2026" + : url.pathQueryRef; + } + + var displayName = fname.replace(/ +/g, " "); + + this.mTitle = this.dialogElement("strings").getFormattedString("title", [ + displayName, + ]); + this.mDialog.document.title = this.mTitle; + + // Put content type, filename and location into intro. + this.initIntro(url, displayName); + + var iconString = + "moz-icon://" + + iconPath + + "?size=16&contentType=" + + this.mLauncher.MIMEInfo.MIMEType; + this.dialogElement("contentTypeImage").setAttribute("src", iconString); + + let dialog = this.mDialog.document.getElementById("unknownContentType"); + + // if always-save and is-executable and no-handler + // then set up simple ui + var mimeType = this.mLauncher.MIMEInfo.MIMEType; + let isPlain = mimeType == "text/plain"; + + this.isExemptExecutableExtension = + Services.policies.isExemptExecutableExtension( + url.spec, + fname?.split(".").at(-1) + ); + + var shouldntRememberChoice = + mimeType == "application/octet-stream" || + mimeType == "application/x-msdownload" || + (this.mLauncher.targetFileIsExecutable && + !this.isExemptExecutableExtension) || + // Do not offer to remember text/plain mimetype choices if the file + // isn't actually a 'plain' text file. + (isPlain && lazy.gReputationService.isBinary(suggestedFileName)); + if ( + (shouldntRememberChoice && !this.openWithDefaultOK()) || + Services.prefs.getBoolPref("browser.download.forbid_open_with") + ) { + // hide featured choice + this.dialogElement("normalBox").collapsed = true; + // show basic choice + this.dialogElement("basicBox").collapsed = false; + // change button labels and icons; use "save" icon for the accept + // button since it's the only action possible + let acceptButton = dialog.getButton("accept"); + acceptButton.label = this.dialogElement("strings").getString( + "unknownAccept.label" + ); + acceptButton.setAttribute("icon", "save"); + dialog.getButton("cancel").label = this.dialogElement( + "strings" + ).getString("unknownCancel.label"); + // hide other handler + this.dialogElement("openHandler").collapsed = true; + // set save as the selected option + this.dialogElement("mode").selectedItem = this.dialogElement("save"); + } else { + this.initInteractiveControls(); + + // Initialize "always ask me" box. This should always be disabled + // and set to true for the ambiguous type application/octet-stream. + // We don't also check for application/x-msdownload here since we + // want users to be able to autodownload .exe files. + var rememberChoice = this.dialogElement("rememberChoice"); + + // Just because we have a content-type of application/octet-stream + // here doesn't actually mean that the content is of that type. Many + // servers default to sending text/plain for file types they don't know + // about. To account for this, the uriloader does some checking to see + // if a file sent as text/plain contains binary characters, and if so (*) + // it morphs the content-type into application/octet-stream so that + // the file can be properly handled. Since this is not generic binary + // data, rather, a data format that the system probably knows about, + // we don't want to use the content-type provided by this dialog's + // opener, as that's the generic application/octet-stream that the + // uriloader has passed, rather we want to ask the MIME Service. + // This is so we don't needlessly disable the "autohandle" checkbox. + + if (shouldntRememberChoice) { + rememberChoice.checked = false; + rememberChoice.hidden = true; + } else { + rememberChoice.checked = + !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling && + this.mLauncher.MIMEInfo.preferredAction != + this.nsIMIMEInfo.handleInternally; + } + this.toggleRememberChoice(rememberChoice); + } + + this.mDialog.setTimeout(function () { + this.dialog.postShowCallback(); + }, 0); + + this.delayHelper = new lazy.EnableDelayHelper({ + disableDialog: () => { + dialog.getButton("accept").disabled = true; + }, + enableDialog: () => { + dialog.getButton("accept").disabled = false; + }, + focusTarget: this.mDialog, + }); + }, + + notify(aTimer) { + if (aTimer == this._showTimer) { + if (!this.mDialog) { + this.reallyShow(); + } + // The timer won't release us, so we have to release it. + this._showTimer = null; + } else if (aTimer == this._saveToDiskTimer) { + // Since saveToDisk may open a file picker and therefore block this routine, + // we should only call it once the dialog is closed. + this.mLauncher.promptForSaveDestination(); + this._saveToDiskTimer = null; + } + }, + + postShowCallback() { + this.mDialog.sizeToContent(); + + // Set initial focus + this.dialogElement("mode").focus(); + }, + + initIntro(url, displayName) { + this.dialogElement("location").value = displayName; + this.dialogElement("location").setAttribute("tooltiptext", displayName); + + // if mSourcePath is a local file, then let's use the pretty path name + // instead of an ugly url... + let pathString; + if (url instanceof Ci.nsIFileURL) { + try { + // Getting .file might throw, or .parent could be null + pathString = url.file.parent.path; + } catch (ex) {} + } + + if (!pathString) { + pathString = BrowserUtils.formatURIForDisplay(url, { + showInsecureHTTP: true, + }); + } + + // Set the location text, which is separate from the intro text so it can be cropped + var location = this.dialogElement("source"); + location.value = pathString; + location.setAttribute("tooltiptext", this.mSourcePath); + + // Show the type of file. + var type = this.dialogElement("type"); + var mimeInfo = this.mLauncher.MIMEInfo; + + // 1. Try to use the pretty description of the type, if one is available. + var typeString = mimeInfo.description; + + if (typeString == "") { + // 2. If there is none, use the extension to identify the file, e.g. "ZIP file" + var primaryExtension = ""; + try { + primaryExtension = mimeInfo.primaryExtension; + } catch (ex) {} + if (primaryExtension != "") { + typeString = this.dialogElement("strings").getFormattedString( + "fileType", + [primaryExtension.toUpperCase()] + ); + } + // 3. If we can't even do that, just give up and show the MIME type. + else { + typeString = mimeInfo.MIMEType; + } + } + // When the length is unknown, contentLength would be -1 + if (this.mLauncher.contentLength >= 0) { + let [size, unit] = DownloadUtils.convertByteUnits( + this.mLauncher.contentLength + ); + type.value = this.dialogElement("strings").getFormattedString( + "orderedFileSizeWithType", + [typeString, size, unit] + ); + } else { + type.value = typeString; + } + }, + + // Returns true if opening the default application makes sense. + openWithDefaultOK() { + // The checking is different on Windows... + if (AppConstants.platform == "win") { + // Windows presents some special cases. + // We need to prevent use of "system default" when the file is + // executable (so the user doesn't launch nasty programs downloaded + // from the web), and, enable use of "system default" if it isn't + // executable (because we will prompt the user for the default app + // in that case). + + // Default is Ok if the file isn't executable (and vice-versa). + return ( + !this.mLauncher.targetFileIsExecutable || + this.isExemptExecutableExtension + ); + } + // On other platforms, default is Ok if there is a default app. + // Note that nsIMIMEInfo providers need to ensure that this holds true + // on each platform. + return this.mLauncher.MIMEInfo.hasDefaultHandler; + }, + + // Set "default" application description field. + initDefaultApp() { + // Use description, if we can get one. + var desc = this.mLauncher.MIMEInfo.defaultDescription; + if (desc) { + var defaultApp = this.dialogElement("strings").getFormattedString( + "defaultApp", + [desc] + ); + this.dialogElement("defaultHandler").label = defaultApp; + } else { + this.dialogElement("modeDeck").setAttribute("selectedIndex", "1"); + // Hide the default handler item too, in case the user picks a + // custom handler at a later date which triggers the menulist to show. + this.dialogElement("defaultHandler").hidden = true; + } + }, + + getPath(aFile) { + if (AppConstants.platform == "macosx") { + return aFile.leafName || aFile.path; + } + return aFile.path; + }, + + initInteractiveControls() { + var modeGroup = this.dialogElement("mode"); + + // We don't let users open .exe files or random binary data directly + // from the browser at the moment because of security concerns. + var openWithDefaultOK = this.openWithDefaultOK(); + var mimeType = this.mLauncher.MIMEInfo.MIMEType; + var openHandler = this.dialogElement("openHandler"); + if ( + (this.mLauncher.targetFileIsExecutable && + !this.isExemptExecutableExtension) || + ((mimeType == "application/octet-stream" || + mimeType == "application/x-msdos-program" || + mimeType == "application/x-msdownload") && + !openWithDefaultOK) + ) { + this.dialogElement("open").disabled = true; + openHandler.disabled = true; + openHandler.selectedItem = null; + modeGroup.selectedItem = this.dialogElement("save"); + return; + } + + // Fill in helper app info, if there is any. + try { + this.chosenApp = + this.mLauncher.MIMEInfo.preferredApplicationHandler.QueryInterface( + Ci.nsILocalHandlerApp + ); + } catch (e) { + this.chosenApp = null; + } + // Initialize "default application" field. + this.initDefaultApp(); + + var otherHandler = this.dialogElement("otherHandler"); + + // Fill application name textbox. + if ( + this.chosenApp && + this.chosenApp.executable && + this.chosenApp.executable.path + ) { + otherHandler.setAttribute( + "path", + this.getPath(this.chosenApp.executable) + ); + + otherHandler.label = this.getFileDisplayName(this.chosenApp.executable); + otherHandler.hidden = false; + } + + openHandler.selectedIndex = 0; + var defaultOpenHandler = this.dialogElement("defaultHandler"); + + if (this.shouldShowInternalHandlerOption()) { + this.dialogElement("handleInternally").hidden = false; + } + + if ( + this.mLauncher.MIMEInfo.preferredAction == + this.nsIMIMEInfo.useSystemDefault + ) { + // Open (using system default). + modeGroup.selectedItem = this.dialogElement("open"); + } else if ( + this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp + ) { + // Open with given helper app. + modeGroup.selectedItem = this.dialogElement("open"); + openHandler.selectedItem = + otherHandler && !otherHandler.hidden + ? otherHandler + : defaultOpenHandler; + } else if ( + !this.dialogElement("handleInternally").hidden && + this.mLauncher.MIMEInfo.preferredAction == + this.nsIMIMEInfo.handleInternally + ) { + // Handle internally + modeGroup.selectedItem = this.dialogElement("handleInternally"); + } else { + // Save to disk. + modeGroup.selectedItem = this.dialogElement("save"); + } + + // If we don't have a "default app" then disable that choice. + if (!openWithDefaultOK) { + var isSelected = defaultOpenHandler.selected; + + // Disable that choice. + defaultOpenHandler.hidden = true; + // If that's the default, then switch to "save to disk." + if (isSelected) { + openHandler.selectedIndex = 1; + if (this.dialogElement("open").selected) { + modeGroup.selectedItem = this.dialogElement("save"); + } + } + } + + otherHandler.nextSibling.hidden = + otherHandler.nextSibling.nextSibling.hidden = false; + this.updateOKButton(); + }, + + // Returns the user-selected application + helperAppChoice() { + return this.chosenApp; + }, + + get saveToDisk() { + return this.dialogElement("save").selected; + }, + + get useOtherHandler() { + return ( + this.dialogElement("open").selected && + this.dialogElement("openHandler").selectedIndex == 1 + ); + }, + + get useSystemDefault() { + return ( + this.dialogElement("open").selected && + this.dialogElement("openHandler").selectedIndex == 0 + ); + }, + + get handleInternally() { + return this.dialogElement("handleInternally").selected; + }, + + toggleRememberChoice(aCheckbox) { + this.dialogElement("settingsChange").hidden = !aCheckbox.checked; + this.mDialog.sizeToContent(); + }, + + openHandlerCommand() { + var openHandler = this.dialogElement("openHandler"); + if (openHandler.selectedItem.id == "choose") { + this.chooseApp(); + } else { + openHandler.setAttribute( + "lastSelectedItemID", + openHandler.selectedItem.id + ); + } + }, + + updateOKButton() { + var ok = false; + if (this.dialogElement("save").selected) { + // This is always OK. + ok = true; + } else if (this.dialogElement("open").selected) { + switch (this.dialogElement("openHandler").selectedIndex) { + case 0: + // No app need be specified in this case. + ok = true; + break; + case 1: + // only enable the OK button if we have a default app to use or if + // the user chose an app.... + ok = + this.chosenApp || + /\S/.test(this.dialogElement("otherHandler").getAttribute("path")); + break; + } + } + + // Enable Ok button if ok to press. + let dialog = this.mDialog.document.getElementById("unknownContentType"); + dialog.getButton("accept").disabled = !ok; + }, + + // Returns true iff the user-specified helper app has been modified. + appChanged() { + return ( + this.helperAppChoice() != + this.mLauncher.MIMEInfo.preferredApplicationHandler + ); + }, + + updateMIMEInfo() { + let { MIMEInfo } = this.mLauncher; + + // Don't erase the preferred choice being internal handler + // -- this dialog is often the result of the handler fallback + // (e.g. Content-Disposition was set as attachment) and we don't + // want to inadvertently cause that to always show the dialog if + // users don't want that behaviour. + + // Note: this is the same condition as the one in initDialog + // which avoids ticking the checkbox. The user can still change + // the action by ticking the checkbox, or by using the prefs to + // manually select always ask (at which point `areAlwaysOpeningInternally` + // will be false, which means `discardUpdate` will be false, which means + // we'll store the last-selected option even if the filetype's pref is + // set to always ask). + let areAlwaysOpeningInternally = + MIMEInfo.preferredAction == Ci.nsIMIMEInfo.handleInternally && + !MIMEInfo.alwaysAskBeforeHandling; + let discardUpdate = + areAlwaysOpeningInternally && + !this.dialogElement("rememberChoice").checked; + + var needUpdate = false; + // If current selection differs from what's in the mime info object, + // then we need to update. + if (this.saveToDisk) { + needUpdate = + this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk; + if (needUpdate) { + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk; + } + } else if (this.useSystemDefault) { + needUpdate = + this.mLauncher.MIMEInfo.preferredAction != + this.nsIMIMEInfo.useSystemDefault; + if (needUpdate) { + this.mLauncher.MIMEInfo.preferredAction = + this.nsIMIMEInfo.useSystemDefault; + } + } else if (this.useOtherHandler) { + // For "open with", we need to check both preferred action and whether the user chose + // a new app. + needUpdate = + this.mLauncher.MIMEInfo.preferredAction != + this.nsIMIMEInfo.useHelperApp || this.appChanged(); + if (needUpdate) { + this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp; + // App may have changed - Update application + var app = this.helperAppChoice(); + this.mLauncher.MIMEInfo.preferredApplicationHandler = app; + } + } else if (this.handleInternally) { + needUpdate = + this.mLauncher.MIMEInfo.preferredAction != + this.nsIMIMEInfo.handleInternally; + if (needUpdate) { + this.mLauncher.MIMEInfo.preferredAction = + this.nsIMIMEInfo.handleInternally; + } + } + // We will also need to update if the "always ask" flag has changed. + needUpdate = + needUpdate || + this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != + !this.dialogElement("rememberChoice").checked; + + // One last special case: If the input "always ask" flag was false, then we always + // update. In that case we are displaying the helper app dialog for the first + // time for this mime type and we need to store the user's action in the handler service + // (whether that action has changed or not; if it didn't change, then we need + // to store the "always ask" flag so the helper app dialog will or won't display + // next time, per the user's selection). + needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling; + + // Make sure mime info has updated setting for the "always ask" flag. + this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = + !this.dialogElement("rememberChoice").checked; + + return needUpdate && !discardUpdate; + }, + + // See if the user changed things, and if so, store this mime type in the + // handler service. + updateHelperAppPref() { + var handlerInfo = this.mLauncher.MIMEInfo; + var hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + hs.store(handlerInfo); + }, + + onOK(aEvent) { + // Verify typed app path, if necessary. + if (this.useOtherHandler) { + var helperApp = this.helperAppChoice(); + if ( + !helperApp || + !helperApp.executable || + !helperApp.executable.exists() + ) { + // Show alert and try again. + var bundle = this.dialogElement("strings"); + var msg = bundle.getFormattedString("badApp", [ + this.dialogElement("otherHandler").getAttribute("path"), + ]); + Services.prompt.alert( + this.mDialog, + bundle.getString("badApp.title"), + msg + ); + + // Disable the OK button. + let dialog = this.mDialog.document.getElementById("unknownContentType"); + dialog.getButton("accept").disabled = true; + this.dialogElement("mode").focus(); + + // Clear chosen application. + this.chosenApp = null; + + // Leave dialog up. + aEvent.preventDefault(); + } + } + + // Remove our web progress listener (a progress dialog will be + // taking over). + this.mLauncher.setWebProgressListener(null); + + // saveToDisk and setDownloadToLaunch can return errors in + // certain circumstances (e.g. The user clicks cancel in the + // "Save to Disk" dialog. In those cases, we don't want to + // update the helper application preferences in the RDF file. + try { + var needUpdate = this.updateMIMEInfo(); + + if (this.dialogElement("save").selected) { + // see @notify + // we cannot use opener's setTimeout, see bug 420405 + this._saveToDiskTimer = + Cc["@mozilla.org/timer;1"].createInstance(nsITimer); + this._saveToDiskTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT); + } else { + let uri = this.mLauncher.source; + // Launch local files immediately without downloading them: + if (uri instanceof Ci.nsIFileURL) { + this.mLauncher.launchLocalFile(); + } else { + this.mLauncher.setDownloadToLaunch(this.handleInternally, null); + } + } + + // Update user pref for this mime type (if necessary). We do not + // store anything in the mime type preferences for the ambiguous + // type application/octet-stream. We do NOT do this for + // application/x-msdownload since we want users to be able to + // autodownload these to disk. + if ( + needUpdate && + this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream" + ) { + this.updateHelperAppPref(); + } + } catch (e) { + console.error(e); + } + + this.onUnload(); + }, + + onCancel() { + // Remove our web progress listener. + this.mLauncher.setWebProgressListener(null); + + // Cancel app launcher. + try { + this.mLauncher.cancel(Cr.NS_BINDING_ABORTED); + } catch (e) { + console.error(e); + } + + this.onUnload(); + }, + + onUnload() { + this.mDialog.document.removeEventListener("dialogaccept", this); + this.mDialog.document.removeEventListener("dialogcancel", this); + + // Unhook dialog from this object. + this.mDialog.dialog = null; + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "dialogaccept": + this.onOK(aEvent); + break; + case "dialogcancel": + this.onCancel(); + break; + } + }, + + dialogElement(id) { + return this.mDialog.document.getElementById(id); + }, + + // Retrieve the pretty description from the file + getFileDisplayName: function getFileDisplayName(file) { + if (AppConstants.platform == "win") { + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } + } else if (AppConstants.platform == "macosx") { + if (file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } + } + return file.leafName; + }, + + finishChooseApp() { + if (this.chosenApp) { + // Show the "handler" menulist since we have a (user-specified) + // application now. + this.dialogElement("modeDeck").setAttribute("selectedIndex", "0"); + + // Update dialog. + var otherHandler = this.dialogElement("otherHandler"); + otherHandler.removeAttribute("hidden"); + otherHandler.setAttribute( + "path", + this.getPath(this.chosenApp.executable) + ); + if (AppConstants.platform == "win") { + otherHandler.label = this.getFileDisplayName(this.chosenApp.executable); + } else { + otherHandler.label = this.chosenApp.name; + } + this.dialogElement("openHandler").selectedIndex = 1; + this.dialogElement("openHandler").setAttribute( + "lastSelectedItemID", + "otherHandler" + ); + + this.dialogElement("mode").selectedItem = this.dialogElement("open"); + } else { + var openHandler = this.dialogElement("openHandler"); + var lastSelectedID = openHandler.getAttribute("lastSelectedItemID"); + if (!lastSelectedID) { + lastSelectedID = "defaultHandler"; + } + openHandler.selectedItem = this.dialogElement(lastSelectedID); + } + }, + // chooseApp: Open file picker and prompt user for application. + chooseApp() { + if (AppConstants.platform == "win") { + // Protect against the lack of an extension + var fileExtension = ""; + try { + fileExtension = this.mLauncher.MIMEInfo.primaryExtension; + } catch (ex) {} + + // Try to use the pretty description of the type, if one is available. + var typeString = this.mLauncher.MIMEInfo.description; + + if (!typeString) { + // If there is none, use the extension to + // identify the file, e.g. "ZIP file" + if (fileExtension) { + typeString = this.dialogElement("strings").getFormattedString( + "fileType", + [fileExtension.toUpperCase()] + ); + } else { + // If we can't even do that, just give up and show the MIME type. + typeString = this.mLauncher.MIMEInfo.MIMEType; + } + } + + var params = {}; + params.title = this.dialogElement("strings").getString( + "chooseAppFilePickerTitle" + ); + params.description = typeString; + params.filename = this.mLauncher.suggestedFileName; + params.mimeInfo = this.mLauncher.MIMEInfo; + params.handlerApp = null; + + this.mDialog.openDialog( + "chrome://global/content/appPicker.xhtml", + null, + "chrome,modal,centerscreen,titlebar,dialog=yes", + params + ); + + if ( + params.handlerApp && + params.handlerApp.executable && + params.handlerApp.executable.isFile() + ) { + // Remember the file they chose to run. + this.chosenApp = params.handlerApp; + } + } else if ("@mozilla.org/applicationchooser;1" in Cc) { + var nsIApplicationChooser = Ci.nsIApplicationChooser; + var appChooser = Cc["@mozilla.org/applicationchooser;1"].createInstance( + nsIApplicationChooser + ); + appChooser.init( + this.mDialog, + this.dialogElement("strings").getString("chooseAppFilePickerTitle") + ); + var contentTypeDialogObj = this; + let appChooserCallback = function appChooserCallback_done(aResult) { + if (aResult) { + contentTypeDialogObj.chosenApp = aResult.QueryInterface( + Ci.nsILocalHandlerApp + ); + } + contentTypeDialogObj.finishChooseApp(); + }; + appChooser.open(this.mLauncher.MIMEInfo.MIMEType, appChooserCallback); + // The finishChooseApp is called from appChooserCallback + return; + } else { + var nsIFilePicker = Ci.nsIFilePicker; + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + fp.init( + this.mDialog, + this.dialogElement("strings").getString("chooseAppFilePickerTitle"), + nsIFilePicker.modeOpen + ); + + fp.appendFilters(nsIFilePicker.filterApps); + + fp.open(aResult => { + if (aResult == nsIFilePicker.returnOK && fp.file) { + // Remember the file they chose to run. + var localHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandlerApp.executable = fp.file; + this.chosenApp = localHandlerApp; + } + this.finishChooseApp(); + }); + // The finishChooseApp is called from fp.open() callback + return; + } + + this.finishChooseApp(); + }, + + shouldShowInternalHandlerOption() { + let browsingContext = this.mDialog.BrowsingContext.get( + this.mLauncher.browsingContextId + ); + let primaryExtension = ""; + try { + // The primaryExtension getter may throw if there are no + // known extensions for this mimetype. + primaryExtension = this.mLauncher.MIMEInfo.primaryExtension; + } catch (e) {} + + // Only available for PDF files when pdf.js is enabled. + // Skip if the current window uses the resource scheme, to avoid + // showing the option when using the Download button in pdf.js. + if (primaryExtension == "pdf") { + return ( + !( + this.mLauncher.source.schemeIs("blob") || + this.mLauncher.source.equalsExceptRef( + browsingContext.currentWindowGlobal.documentURI + ) + ) && + !Services.prefs.getBoolPref("pdfjs.disabled", true) && + Services.prefs.getBoolPref( + "browser.helperApps.showOpenOptionForPdfJS", + false + ) + ); + } + + return ( + Services.prefs.getBoolPref( + "browser.helperApps.showOpenOptionForViewableInternally", + false + ) && + lazy.DownloadIntegration.shouldViewDownloadInternally( + this.mLauncher.MIMEInfo.MIMEType, + primaryExtension + ) + ); + }, + + // Turn this on to get debugging messages. + debug: false, + + // Dump text (if debug is on). + dump(text) { + if (this.debug) { + dump(text); + } + }, +}; diff --git a/toolkit/mozapps/downloads/components.conf b/toolkit/mozapps/downloads/components.conf new file mode 100644 index 0000000000..8446fb2150 --- /dev/null +++ b/toolkit/mozapps/downloads/components.conf @@ -0,0 +1,14 @@ +# -*- 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 = [ + { + 'cid': '{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}', + 'contract_ids': ['@mozilla.org/helperapplauncherdialog;1'], + 'esModule': 'resource://gre/modules/HelperAppDlg.sys.mjs', + 'constructor': 'nsUnknownContentTypeDialog', + }, +] diff --git a/toolkit/mozapps/downloads/content/unknownContentType.xhtml b/toolkit/mozapps/downloads/content/unknownContentType.xhtml new file mode 100644 index 0000000000..47cf9d3117 --- /dev/null +++ b/toolkit/mozapps/downloads/content/unknownContentType.xhtml @@ -0,0 +1,104 @@ + +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# 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/. + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/toolkit/mozapps/extensions/content/aboutaddons.js b/toolkit/mozapps/extensions/content/aboutaddons.js new file mode 100644 index 0000000000..37687a8be7 --- /dev/null +++ b/toolkit/mozapps/extensions/content/aboutaddons.js @@ -0,0 +1,4231 @@ +/* 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] */ +/* import-globals-from aboutaddonsCommon.js */ +/* import-globals-from abuse-reports.js */ +/* import-globals-from view-controller.js */ +/* global windowRoot */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AMBrowserExtensionsImport: "resource://gre/modules/AddonManager.sys.mjs", + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", + BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs", + ClientID: "resource://gre/modules/ClientID.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", + ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs", + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", + ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "manifestV3enabled", + "extensions.manifestV3.enabled" +); + +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/icons/plugin.svg"; +const EXTENSION_ICON_URL = + "chrome://mozapps/skin/extensions/extensionGeneric.svg"; + +const PERMISSION_MASKS = { + 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: [], +}; + +const L10N_ID_MAPPING = { + "theme-disabled-heading": "theme-disabled-heading2", +}; + +function getL10nIdMapping(id) { + return L10N_ID_MAPPING[id] || id; +} + +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 && + AbuseReporter.isSupportedAddonType(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 { STATE_BLOCKED, STATE_SOFTBLOCKED } = Ci.nsIBlocklistService; + + if (addon.blocklistState === STATE_BLOCKED) { + return { + linkUrl: await addon.getBlocklistURL(), + linkId: "details-notification-blocked-link", + messageId: "details-notification-blocked2", + messageArgs: { name }, + type: "error", + }; + } else if (isDisabledUnsigned(addon)) { + return { + linkUrl: SUPPORT_URL + "unsigned-addons", + linkId: "details-notification-unsigned-and-disabled-link", + messageId: "details-notification-unsigned-and-disabled2", + messageArgs: { name }, + type: "error", + }; + } else if ( + !addon.isCompatible && + (AddonManager.checkCompatibility || + addon.blocklistState !== STATE_SOFTBLOCKED) + ) { + return { + messageId: "details-notification-incompatible2", + messageArgs: { name, version: Services.appinfo.version }, + type: "error", + }; + } else if (!isCorrectlySigned(addon)) { + return { + linkUrl: SUPPORT_URL + "unsigned-addons", + linkId: "details-notification-unsigned-link", + messageId: "details-notification-unsigned2", + messageArgs: { name }, + type: "warning", + }; + } else if (addon.blocklistState === STATE_SOFTBLOCKED) { + return { + linkUrl: await addon.getBlocklistURL(), + linkId: "details-notification-softblocked-link", + messageId: "details-notification-softblocked2", + messageArgs: { name }, + type: "warning", + }; + } else if (addon.isGMPlugin && !addon.isInstalled && addon.isActive) { + return { + messageId: "details-notification-gmp-pending2", + messageArgs: { 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))); + gViewController.notifyEMUpdateCheckFinished(); + 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 ( + // Note: This function is async because isAllowedInPrivateBrowsing is async. + isAllowedInPrivateBrowsing(addon) + ); +} + +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 (addon.id == "default-theme@mozilla.org") { + return "chrome://mozapps/content/extensions/default-theme/preview.svg"; + } + const builtInThemePreview = BuiltInThemes.previewForBuiltInThemeId(addon.id); + if (builtInThemePreview) { + return builtInThemePreview; + } + + 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; + + this.taarRecommended = details.is_recommendation; + } +} + +/** + * 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.sys.mjs 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 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); + } + + disconnectedCallback() { + this.input.removeEventListener("command", this); + } + + handleEvent(e) { + if (e.type === "command") { + this.searchAddons(this.value); + } + } + + 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.openWebLinkIn(url, "tab"); + } +} +customElements.define("search-addons", SearchAddons); + +class MessageBarStackElement extends HTMLElement { + constructor() { + super(); + this._observer = null; + const shadowRoot = this.attachShadow({ mode: "open" }); + shadowRoot.append(this.constructor.template.content.cloneNode(true)); + } + + connectedCallback() { + // Close any message bar that should be allowed based on the + // maximum number of message bars. + this.closeMessageBars(); + + // Observe mutations to close older bars when new ones have been + // added. + this._observer = new MutationObserver(() => { + this._observer.disconnect(); + this.closeMessageBars(); + this._observer.observe(this, { childList: true }); + }); + this._observer.observe(this, { childList: true }); + } + + disconnectedCallback() { + this._observer.disconnect(); + this._observer = null; + } + + closeMessageBars() { + const { maxMessageBarCount } = this; + if (maxMessageBarCount > 1) { + // Remove the older message bars if the stack reached the + // maximum number of message bars allowed. + while (this.childElementCount > maxMessageBarCount) { + this.firstElementChild.remove(); + } + } + } + + get maxMessageBarCount() { + return parseInt(this.getAttribute("max-message-bar-count"), 10); + } + + static get template() { + const template = document.createElement("template"); + + const style = document.createElement("style"); + // Render the stack in the reverse order if the stack has the + // reverse attribute set. + style.textContent = ` + :host { + display: block; + } + :host([reverse]) > slot { + display: flex; + flex-direction: column-reverse; + } + `; + template.content.append(style); + template.content.append(document.createElement("slot")); + + Object.defineProperty(this, "template", { + value: template, + }); + + return template; + } +} + +customElements.define("message-bar-stack", MessageBarStackElement); + +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 if (AMBrowserExtensionsImport.canCompleteOrCancelInstalls) { + this.setWarning("imported-addons", { 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("moz-message-bar"); + this.globalWarning.setAttribute("warning-type", type); + let { messageId, buttonId } = this.getGlobalWarningL10nIds(type); + document.l10n.setAttributes(this.globalWarning, messageId); + this.globalWarning.setAttribute("data-l10n-attrs", "message"); + if (opts && opts.action) { + let button = document.createElement("button"); + document.l10n.setAttributes(button, buttonId); + button.setAttribute("action", type); + button.setAttribute("slot", "actions"); + this.globalWarning.appendChild(button); + } + this.appendChild(this.globalWarning); + } + } + + getGlobalWarningL10nIds(type) { + const WARNING_TYPE_TO_L10NID_MAPPING = { + "safe-mode": { + messageId: "extensions-warning-safe-mode2", + }, + "update-security": { + messageId: "extensions-warning-update-security2", + buttonId: "extensions-warning-update-security-button", + }, + "check-compatibility": { + messageId: "extensions-warning-check-compatibility2", + buttonId: "extensions-warning-check-compatibility-button", + }, + "imported-addons": { + messageId: "extensions-warning-imported-addons2", + buttonId: "extensions-warning-imported-addons-button", + }, + }; + + return WARNING_TYPE_TO_L10NID_MAPPING[type]; + } + + 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; + case "imported-addons": + AMBrowserExtensionsImport.completeInstalls(); + break; + } + } + } + + /** + * AddonManager listener events. + */ + + onCompatibilityModeChanged() { + this.refresh(); + } + + onCheckUpdateSecurityChanged() { + this.refresh(); + } + + onBrowserExtensionsImportChanged() { + 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") + ); + } + document.addEventListener("view-selected", this); + 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() { + document.removeEventListener("view-selected", this); + 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"; + + this.backButton.disabled = !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.inputSource == 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 + ); + } else if (e.target == document && e.type == "view-selected") { + const { type, param } = e.detail; + this.setViewInfo({ type, param }); + } + } +} +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) { + gViewController.loadView("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": + gViewController.loadView("updates/recent"); + break; + case "install-from-file": + if (XPINSTALL_ENABLED) { + installAddonsFromFilePicker(); + } + 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": + gViewController.loadView("shortcuts/shortcuts"); + break; + } + } + + async checkForUpdates(e) { + 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; + 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; + } + } + + async resetAutomaticUpdates() { + let addons = await AddonManager.getAllAddons(); + for (let addon of addons) { + if ("applyBackgroundUpdates" in addon) { + addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT; + } + } + } + + /** + * 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() { + gViewController.loadView(this.viewId); + } + + get isVisible() { + // Make a category button visible only if the related addon type is + // supported by the AddonManager Providers actually registered to + // the AddonManager. + return AddonManager.hasAddonType(this.name); + } + + 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" }); + +// Create the button-group element so it gets loaded. +document.createElement("button-group"); +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; + }); + } + + handleEvent(e) { + if (e.target == document && e.type == "view-selected") { + const { type, param } = e.detail; + this.select(`addons://${type}/${param}`); + return; + } + + if (e.target == this && e.type == "button-group:key-selected") { + this.activeChild.load(); + return; + } + + if (e.type == "click") { + const button = e.target.closest("[viewid]"); + if (button) { + button.load(); + return; + } + } + + // Forward the unhandled events to the button-group custom element. + super.handleEvent(e); + } + + disconnectedCallback() { + document.removeEventListener("view-selected", this); + this.removeEventListener("button-group:key-selected", this); + this.removeEventListener("click", this); + AddonManagerListenerHandler.removeListener(this); + super.disconnectedCallback(); + } + + async initialize() { + let hiddenTypes = new Set([]); + + for (let button of this.children) { + let { defaultHidden, name } = button; + button.hidden = + !button.isVisible || (defaultHidden && this.shouldHideCategory(name)); + + if (defaultHidden && AddonManager.hasAddonType(name)) { + hiddenTypes.add(name); + } + } + + let hiddenUpdated; + if (hiddenTypes.size) { + hiddenUpdated = this.updateHiddenCategories(Array.from(hiddenTypes)); + } + + this.updateAvailableCount(); + + document.addEventListener("view-selected", this); + this.addEventListener("button-group:key-selected", this); + this.addEventListener("click", this); + AddonManagerListenerHandler.addListener(this); + + this._resolveRendered(); + await hiddenUpdated; + } + + 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}"]`); + } + + 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. + gViewController.resetState(); + } + 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 systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + let prefsItem = this.createItem({ + icon: "chrome://global/skin/icons/settings.svg", + createLinkElement: () => { + let link = document.createElement("a"); + link.href = "about:preferences"; + link.id = "preferencesButton"; + return link; + }, + titleL10nId: "sidebar-settings-button-title", + labelL10nId: "addons-settings-button", + onClick: e => { + e.preventDefault(); + windowRoot.ownerGlobal.switchToTabHavingURI("about:preferences", true, { + ignoreFragment: "whenComparing", + triggeringPrincipal: systemPrincipal, + }); + }, + }); + + let supportItem = this.createItem({ + icon: "chrome://global/skin/icons/help.svg", + createLinkElement: () => { + let link = document.createElement("a", { is: "moz-support-link" }); + link.setAttribute("support-page", "addons-help"); + link.id = "help-button"; + return link; + }, + titleL10nId: "sidebar-help-button-title", + labelL10nId: "help-button", + }); + + list.append(prefsItem, supportItem); + this.append(list); + } + + createItem({ onClick, titleL10nId, labelL10nId, icon, createLinkElement }) { + let listItem = document.createElement("li"); + + let link = createLinkElement(); + link.classList.add("sidebar-footer-link"); + link.addEventListener("click", onClick); + document.l10n.setAttributes(link, titleL10nId); + + let img = document.createElement("img"); + img.src = icon; + img.className = "sidebar-footer-icon"; + + let label = document.createElement("span"); + label.className = "sidebar-footer-label"; + document.l10n.setAttributes(label, labelL10nId); + + link.append(img, label); + listItem.append(link); + return listItem; + } +} +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.localName == "hr") { + 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: "moz-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 = { + "always-activate": false, + "never-activate": true, + }; + const action = el.getAttribute("action"); + if (action in userDisabledStates) { + let userDisabled = userDisabledStates[action]; + el.checked = addon.userDisabled === userDisabled; + el.disabled = !(el.checked || hasPermission(addon, action)); + } else { + super.setElementState(el, card, addon); + } + } +} +customElements.define("plugin-options", PluginOptions); + +class ProxyContextMenu extends HTMLElement { + openPopupAtScreen(...args) { + // prettier-ignore + const parentContextMenuPopup = + windowRoot.ownerGlobal.document.getElementById("contentAreaContextMenu"); + return parentContextMenuPopup.openPopupAtScreen(...args); + } +} +customElements.define("proxy-context-menu", ProxyContextMenu); + +class InlineOptionsBrowser extends HTMLElement { + constructor() { + super(); + // Force the options_ui remote browser to recompute window.mozInnerScreenX + // and window.mozInnerScreenY when the "addon details" page has been + // scrolled (See Bug 1390445 for rationale). + // Also force a repaint to fix an issue where the click location was + // getting out of sync (see bug 1548687). + this.updatePositionTask = new DeferredTask(() => { + if (this.browser && this.browser.isRemoteBrowser) { + // Select boxes can appear in the wrong spot after scrolling, this will + // clear that up. Bug 1390445. + this.browser.frameLoader.requestUpdatePosition(); + } + }, 100); + + this._embedderElement = null; + this._promiseDisconnected = new Promise( + resolve => (this._resolveDisconnected = resolve) + ); + } + + connectedCallback() { + window.addEventListener("scroll", this, true); + const { embedderElement } = top.browsingContext; + this._embedderElement = embedderElement; + embedderElement.addEventListener("FullZoomChange", this); + embedderElement.addEventListener("TextZoomChange", this); + } + + disconnectedCallback() { + this._resolveDisconnected(); + window.removeEventListener("scroll", this, true); + this._embedderElement?.removeEventListener("FullZoomChange", this); + this._embedderElement?.removeEventListener("TextZoomChange", this); + this._embedderElement = null; + } + + handleEvent(e) { + switch (e.type) { + case "scroll": + return this.updatePositionTask.arm(); + case "FullZoomChange": + case "TextZoomChange": + return this.maybeUpdateZoom(); + } + return undefined; + } + + maybeUpdateZoom() { + let bc = this.browser?.browsingContext; + let topBc = top.browsingContext; + if (!bc || !topBc) { + return; + } + // Use the same full-zoom as our top window. + bc.fullZoom = topBc.fullZoom; + bc.textZoom = topBc.textZoom; + } + + setAddon(addon) { + this.addon = addon; + } + + destroyBrowser() { + this.textContent = ""; + } + + ensureBrowserCreated() { + if (this.childElementCount === 0) { + this.render(); + } + } + + async render() { + let { addon } = this; + if (!addon) { + throw new Error("addon required to create inline options"); + } + + let browser = document.createXULElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("messagemanagergroup", "webext-browsers"); + browser.setAttribute("id", "addon-inline-options"); + browser.setAttribute("transparent", "true"); + browser.setAttribute("forcemessagemanager", "true"); + browser.setAttribute("autocompletepopup", "PopupAutoComplete"); + + let { optionsURL, optionsBrowserStyle } = addon; + if (addon.isWebExtension) { + let policy = ExtensionParent.WebExtensionPolicy.getByID(addon.id); + browser.setAttribute( + "initialBrowsingContextGroupId", + policy.browsingContextGroupId + ); + } + + let readyPromise; + let remoteSubframes = window.docShell.QueryInterface( + Ci.nsILoadContext + ).useRemoteSubframes; + // For now originAttributes have no effect, which will change if the + // optionsURL becomes anything but moz-extension* or we start considering + // OA for extensions. + var oa = E10SUtils.predictOriginAttributes({ browser }); + let loadRemote = E10SUtils.canLoadURIInRemoteType( + optionsURL, + remoteSubframes, + E10SUtils.EXTENSION_REMOTE_TYPE, + oa + ); + if (loadRemote) { + browser.setAttribute("remote", "true"); + browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE); + + readyPromise = promiseEvent("XULFrameLoaderCreated", browser); + } else { + readyPromise = promiseEvent("load", browser, true); + } + + let stack = document.createXULElement("stack"); + stack.classList.add("inline-options-stack"); + stack.appendChild(browser); + this.appendChild(stack); + this.browser = browser; + + // Force bindings to apply synchronously. + browser.clientTop; + + await readyPromise; + + this.maybeUpdateZoom(); + + if (!browser.messageManager) { + // If the browser.messageManager is undefined, the browser element has + // been removed from the document in the meantime (e.g. due to a rapid + // sequence of addon reload), return null. + return; + } + + ExtensionParent.apiManager.emit("extension-browser-inserted", browser); + + await new Promise(resolve => { + let messageListener = { + receiveMessage({ name, data }) { + if (name === "Extension:BrowserResized") { + browser.style.height = `${data.height}px`; + } else if (name === "Extension:BrowserContentLoaded") { + resolve(); + } + }, + }; + + let mm = browser.messageManager; + + if (!mm) { + // If the browser.messageManager is undefined, the browser element has + // been removed from the document in the meantime (e.g. due to a rapid + // sequence of addon reload), return null. + resolve(); + return; + } + + mm.loadFrameScript( + "chrome://extensions/content/ext-browser-content.js", + false, + true + ); + mm.addMessageListener("Extension:BrowserContentLoaded", messageListener); + mm.addMessageListener("Extension:BrowserResized", messageListener); + + let browserOptions = { + fixedWidth: true, + isInline: true, + }; + + if (optionsBrowserStyle) { + // aboutaddons.js is not used on Android. extension.css is included in + // Firefox desktop and Thunderbird. + // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit + browserOptions.stylesheets = ["chrome://browser/content/extension.css"]; + } + + mm.sendAsyncMessage("Extension:InitBrowser", browserOptions); + + if (browser.isConnectedAndReady) { + this.fixupAndLoadURIString(optionsURL); + } else { + // browser custom element does opt-in the delayConnectedCallback + // behavior (see connectedCallback in the custom element definition + // from browser-custom-element.js) and so calling browser.loadURI + // would fail if the about:addons document is not yet fully loaded. + Promise.race([ + promiseEvent("DOMContentLoaded", document), + this._promiseDisconnected, + ]).then(() => { + this.fixupAndLoadURIString(optionsURL); + }); + } + }); + } + + fixupAndLoadURIString(uriString) { + if (!this.browser || !this.browser.isConnectedAndReady) { + throw new Error("Fail to loadURI"); + } + + this.browser.fixupAndLoadURIString(uriString, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } +} +customElements.define("inline-options-browser", InlineOptionsBrowser); + +class UpdateReleaseNotes extends HTMLElement { + connectedCallback() { + this.addEventListener("click", this); + } + + disconnectedCallback() { + this.removeEventListener("click", this); + } + + handleEvent(e) { + // We used to strip links, but ParserUtils.parseFragment() leaves them in, + // so just make sure we open them using the null principal in a new tab. + if (e.type == "click" && e.target.localName == "a" && e.target.href) { + e.preventDefault(); + e.stopPropagation(); + windowRoot.ownerGlobal.openWebLinkIn(e.target.href, "tab"); + } + } + + async loadForUri(uri) { + // Can't load the release notes without a URL to load. + if (!uri || !uri.spec) { + this.setErrorMessage(); + this.dispatchEvent(new CustomEvent("release-notes-error")); + return; + } + + // Don't try to load for the same update a second time. + if (this.url == uri.spec) { + this.dispatchEvent(new CustomEvent("release-notes-cached")); + return; + } + + // Store the URL to skip the network if loaded again. + this.url = uri.spec; + + // Set the loading message before hitting the network. + this.setLoadingMessage(); + this.dispatchEvent(new CustomEvent("release-notes-loading")); + + try { + // loadReleaseNotes will fetch and sanitize the release notes. + let fragment = await loadReleaseNotes(uri); + this.textContent = ""; + this.appendChild(fragment); + this.dispatchEvent(new CustomEvent("release-notes-loaded")); + } catch (e) { + this.setErrorMessage(); + this.dispatchEvent(new CustomEvent("release-notes-error")); + } + } + + setMessage(id) { + this.textContent = ""; + let message = document.createElement("p"); + document.l10n.setAttributes(message, id); + this.appendChild(message); + } + + setLoadingMessage() { + this.setMessage("release-notes-loading"); + } + + setErrorMessage() { + this.setMessage("release-notes-error"); + } +} +customElements.define("update-release-notes", UpdateReleaseNotes); + +class AddonPermissionsList extends HTMLElement { + setAddon(addon) { + this.addon = addon; + this.render(); + } + + async render() { + let empty = { origins: [], permissions: [] }; + let requiredPerms = { ...(this.addon.userPermissions ?? empty) }; + let optionalPerms = { ...(this.addon.optionalPermissions ?? empty) }; + let grantedPerms = await ExtensionPermissions.get(this.addon.id); + + if (manifestV3enabled) { + // If optional permissions include , extension can request and + // be granted permission for individual sites not listed in the manifest. + // Include them as well in the optional origins list. + optionalPerms.origins = [ + ...optionalPerms.origins, + ...grantedPerms.origins.filter(o => !requiredPerms.origins.includes(o)), + ]; + } + + let permissions = Extension.formatPermissionStrings( + { + permissions: requiredPerms, + optionalPermissions: optionalPerms, + }, + { buildOptionalOrigins: manifestV3enabled } + ); + let optionalEntries = [ + ...Object.entries(permissions.optionalPermissions), + ...Object.entries(permissions.optionalOrigins), + ]; + + this.textContent = ""; + let frag = importTemplate("addon-permissions-list"); + + if (permissions.msgs.length) { + let section = frag.querySelector(".addon-permissions-required"); + section.hidden = false; + let list = section.querySelector(".addon-permissions-list"); + + for (let msg of permissions.msgs) { + let item = document.createElement("li"); + item.classList.add("permission-info", "permission-checked"); + item.appendChild(document.createTextNode(msg)); + list.appendChild(item); + } + } + + if (optionalEntries.length) { + let section = frag.querySelector(".addon-permissions-optional"); + section.hidden = false; + let list = section.querySelector(".addon-permissions-list"); + + for (let id = 0; id < optionalEntries.length; id++) { + let [perm, msg] = optionalEntries[id]; + + let type = "permission"; + if (permissions.optionalOrigins[perm]) { + type = "origin"; + } + let item = document.createElement("li"); + item.classList.add("permission-info"); + + let toggle = document.createElement("moz-toggle"); + toggle.setAttribute("label", msg); + toggle.id = `permission-${id}`; + toggle.setAttribute("permission-type", type); + + let checked = + grantedPerms.permissions.includes(perm) || + grantedPerms.origins.includes(perm); + + // If this is one of the "all sites" permissions + if (Extension.isAllSitesPermission(perm)) { + // mark it as checked if ANY of the "all sites" permission is granted. + checked = await AddonCard.optionalAllSitesGranted(this.addon.id); + toggle.toggleAttribute("permission-all-sites", true); + } + + toggle.pressed = checked; + item.classList.toggle("permission-checked", checked); + + toggle.setAttribute("permission-key", perm); + toggle.setAttribute("action", "toggle-permission"); + item.appendChild(toggle); + list.appendChild(item); + } + } + if (!permissions.msgs.length && !optionalEntries.length) { + let row = frag.querySelector(".addon-permissions-empty"); + row.hidden = false; + } + + this.appendChild(frag); + } +} +customElements.define("addon-permissions-list", AddonPermissionsList); + +class AddonSitePermissionsList extends HTMLElement { + setAddon(addon) { + this.addon = addon; + this.render(); + } + + async render() { + let permissions = Extension.formatPermissionStrings({ + sitePermissions: this.addon.sitePermissions, + siteOrigin: this.addon.siteOrigin, + }); + + this.textContent = ""; + let frag = importTemplate("addon-sitepermissions-list"); + + if (permissions.msgs.length) { + let section = frag.querySelector(".addon-permissions-required"); + section.hidden = false; + let list = section.querySelector(".addon-permissions-list"); + let header = section.querySelector(".permission-header"); + document.l10n.setAttributes(header, "addon-sitepermissions-required", { + hostname: new URL(this.addon.siteOrigin).hostname, + }); + + for (let msg of permissions.msgs) { + let item = document.createElement("li"); + item.classList.add("permission-info", "permission-checked"); + item.appendChild(document.createTextNode(msg)); + list.appendChild(item); + } + } + + this.appendChild(frag); + } +} +customElements.define("addon-sitepermissions-list", AddonSitePermissionsList); + +class AddonDetails extends HTMLElement { + connectedCallback() { + if (!this.children.length) { + this.render(); + } + this.deck.addEventListener("view-changed", this); + this.descriptionShowMoreButton.addEventListener("click", this); + } + + disconnectedCallback() { + this.inlineOptions.destroyBrowser(); + this.deck.removeEventListener("view-changed", this); + this.descriptionShowMoreButton.removeEventListener("click", this); + } + + handleEvent(e) { + if (e.type == "view-changed" && e.target == this.deck) { + switch (this.deck.selectedViewName) { + case "release-notes": + let releaseNotes = this.querySelector("update-release-notes"); + let uri = this.releaseNotesUri; + if (uri) { + releaseNotes.loadForUri(uri); + } + break; + case "preferences": + if (getOptionsType(this.addon) == "inline") { + this.inlineOptions.ensureBrowserCreated(); + } + break; + } + + // When a details view is rendered again, the default details view is + // unconditionally shown. So if any other tab is selected, do not save + // the current scroll offset, but start at the top of the page instead. + ScrollOffsets.canRestore = this.deck.selectedViewName === "details"; + } else if ( + e.type == "click" && + e.target == this.descriptionShowMoreButton + ) { + this.toggleDescription(); + } + } + + onInstalled() { + let policy = WebExtensionPolicy.getByID(this.addon.id); + let extension = policy && policy.extension; + if (extension && extension.startupReason === "ADDON_UPGRADE") { + // Ensure the options browser is recreated when a new version starts. + this.extensionShutdown(); + this.extensionStartup(); + } + } + + onDisabled(addon) { + this.extensionShutdown(); + } + + onEnabled(addon) { + this.extensionStartup(); + } + + extensionShutdown() { + this.inlineOptions.destroyBrowser(); + } + + extensionStartup() { + if (this.deck.selectedViewName === "preferences") { + this.inlineOptions.ensureBrowserCreated(); + } + } + + toggleDescription() { + this.descriptionCollapsed = !this.descriptionCollapsed; + + this.descriptionWrapper.classList.toggle( + "addon-detail-description-collapse", + this.descriptionCollapsed + ); + + this.descriptionShowMoreButton.hidden = false; + document.l10n.setAttributes( + this.descriptionShowMoreButton, + this.descriptionCollapsed + ? "addon-detail-description-expand" + : "addon-detail-description-collapse" + ); + } + + get releaseNotesUri() { + let { releaseNotesURI } = getUpdateInstall(this.addon) || this.addon; + return releaseNotesURI; + } + + setAddon(addon) { + this.addon = addon; + } + + update() { + let { addon } = this; + + // Hide tab buttons that won't have any content. + let getButtonByName = name => + this.tabGroup.querySelector(`[name="${name}"]`); + let permsBtn = getButtonByName("permissions"); + permsBtn.hidden = addon.type != "extension"; + let notesBtn = getButtonByName("release-notes"); + notesBtn.hidden = !this.releaseNotesUri; + let prefsBtn = getButtonByName("preferences"); + prefsBtn.hidden = getOptionsType(addon) !== "inline"; + if (prefsBtn.hidden) { + if (this.deck.selectedViewName === "preferences") { + this.deck.selectedViewName = "details"; + } + } else { + isAddonOptionsUIAllowed(addon).then(allowed => { + prefsBtn.hidden = !allowed; + }); + } + + // Hide the tab group if "details" is the only visible button. + let tabGroupButtons = this.tabGroup.querySelectorAll(".tab-button"); + this.tabGroup.hidden = Array.from(tabGroupButtons).every(button => { + return button.name == "details" || button.hidden; + }); + + // Show the update check button if necessary. The button might not exist if + // the add-on doesn't support updates. + let updateButton = this.querySelector('[action="update-check"]'); + if (updateButton) { + updateButton.hidden = + this.addon.updateInstall || AddonManager.shouldAutoUpdate(this.addon); + } + + // Set the value for auto updates. + let inputs = this.querySelectorAll(".addon-detail-row-updates input"); + for (let input of inputs) { + input.checked = input.value == addon.applyBackgroundUpdates; + } + } + + renderDescription(addon) { + this.descriptionWrapper = this.querySelector( + ".addon-detail-description-wrapper" + ); + this.descriptionContents = this.querySelector(".addon-detail-description"); + this.descriptionShowMoreButton = this.querySelector( + ".addon-detail-description-toggle" + ); + + if (addon.getFullDescription) { + this.descriptionContents.appendChild(addon.getFullDescription(document)); + } else if (addon.fullDescription) { + this.descriptionContents.appendChild(nl2br(addon.fullDescription)); + } + + this.descriptionCollapsed = false; + + requestAnimationFrame(() => { + const remSize = parseFloat( + getComputedStyle(document.documentElement).fontSize + ); + const { height } = this.descriptionContents.getBoundingClientRect(); + + // collapse description if there are too many lines,i.e. height > (20 rem) + if (height > 20 * remSize) { + this.toggleDescription(); + } + }); + } + + updateQuarantinedDomainsUserAllowed() { + const { addon } = this; + let quarantinedDomainsUserAllowedRow = this.querySelector( + ".addon-detail-row-quarantined-domains" + ); + if (addon.canChangeQuarantineIgnored) { + quarantinedDomainsUserAllowedRow.hidden = false; + quarantinedDomainsUserAllowedRow.nextElementSibling.hidden = false; + quarantinedDomainsUserAllowedRow.querySelector( + `[value="${addon.quarantineIgnoredByUser ? 1 : 0}"]` + ).checked = true; + } else { + quarantinedDomainsUserAllowedRow.hidden = true; + quarantinedDomainsUserAllowedRow.nextElementSibling.hidden = true; + } + } + + async render() { + let { addon } = this; + if (!addon) { + throw new Error("addon-details must be initialized by setAddon"); + } + + this.textContent = ""; + this.appendChild(importTemplate("addon-details")); + + this.deck = this.querySelector("named-deck"); + this.tabGroup = this.querySelector(".tab-group"); + + // Set the add-on for the permissions section. + this.permissionsList = this.querySelector("addon-permissions-list"); + this.permissionsList.setAddon(addon); + + // Set the add-on for the sitepermissions section. + this.sitePermissionsList = this.querySelector("addon-sitepermissions-list"); + if (addon.type == "sitepermission") { + this.sitePermissionsList.setAddon(addon); + } + this.querySelector(".addon-detail-sitepermissions").hidden = + addon.type !== "sitepermission"; + + // Set the add-on for the preferences section. + this.inlineOptions = this.querySelector("inline-options-browser"); + this.inlineOptions.setAddon(addon); + + // Full description. + this.renderDescription(addon); + this.querySelector(".addon-detail-contribute").hidden = + !addon.contributionURL; + this.querySelector(".addon-detail-row-updates").hidden = !hasPermission( + addon, + "upgrade" + ); + + if (addon.type != "extension") { + // Don't show any private browsing related section for non-extension + // addon types, because not relevant or they are either always allowed + // (e.g. static themes). + // + // TODO(Bug 1799090): introduce ad-hoc UI for "sitepermission" addon type. + } else if (addon.incognito == "not_allowed") { + let pbRowNotAllowed = this.querySelector( + ".addon-detail-row-private-browsing-disallowed" + ); + pbRowNotAllowed.hidden = false; + pbRowNotAllowed.nextElementSibling.hidden = false; + } else if (!hasPermission(addon, "change-privatebrowsing")) { + let pbRowRequired = this.querySelector( + ".addon-detail-row-private-browsing-required" + ); + pbRowRequired.hidden = false; + pbRowRequired.nextElementSibling.hidden = false; + } else { + let pbRow = this.querySelector(".addon-detail-row-private-browsing"); + pbRow.hidden = false; + pbRow.nextElementSibling.hidden = false; + let isAllowed = await isAllowedInPrivateBrowsing(addon); + pbRow.querySelector(`[value="${isAllowed ? 1 : 0}"]`).checked = true; + } + + this.updateQuarantinedDomainsUserAllowed(); + + // Author. + let creatorRow = this.querySelector(".addon-detail-row-author"); + if (addon.creator) { + let link = creatorRow.querySelector("a"); + link.hidden = !addon.creator.url; + if (link.hidden) { + creatorRow.appendChild(new Text(addon.creator.name)); + } else { + link.href = formatUTMParams( + "addons-manager-user-profile-link", + addon.creator.url + ); + link.target = "_blank"; + link.textContent = addon.creator.name; + } + } else { + creatorRow.hidden = true; + } + + // Version. Don't show a version for LWTs. + let version = this.querySelector(".addon-detail-row-version"); + if (addon.version && !/@personas\.mozilla\.org/.test(addon.id)) { + version.appendChild(new Text(addon.version)); + } else { + version.hidden = true; + } + + // Last updated. + let updateDate = this.querySelector(".addon-detail-row-lastUpdated"); + if (addon.updateDate) { + let lastUpdated = addon.updateDate.toLocaleDateString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + }); + updateDate.appendChild(new Text(lastUpdated)); + } else { + updateDate.hidden = true; + } + + // Homepage. + let homepageRow = this.querySelector(".addon-detail-row-homepage"); + if (addon.homepageURL) { + let homepageURL = homepageRow.querySelector("a"); + homepageURL.href = addon.homepageURL; + homepageURL.textContent = addon.homepageURL; + } else { + homepageRow.hidden = true; + } + + // Rating. + let ratingRow = this.querySelector(".addon-detail-row-rating"); + if (addon.reviewURL) { + ratingRow.querySelector("moz-five-star").rating = addon.averageRating; + let reviews = ratingRow.querySelector("a"); + reviews.href = formatUTMParams( + "addons-manager-reviews-link", + addon.reviewURL + ); + document.l10n.setAttributes(reviews, "addon-detail-reviews-link", { + numberOfReviews: addon.reviewCount, + }); + } else { + ratingRow.hidden = true; + } + + this.update(); + } + + showPrefs() { + if (getOptionsType(this.addon) == "inline") { + this.deck.selectedViewName = "preferences"; + this.inlineOptions.ensureBrowserCreated(); + } + } +} +customElements.define("addon-details", AddonDetails); + +/** + * A card component for managing an add-on. It should be initialized by setting + * the add-on with `setAddon()` before being connected to the document. + * + * let card = document.createElement("addon-card"); + * card.setAddon(addon); + * document.body.appendChild(card); + */ +class AddonCard extends HTMLElement { + connectedCallback() { + // If we've already rendered we can just update, otherwise render. + if (this.children.length) { + this.update(); + } else { + this.render(); + } + this.registerListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + } + + get expanded() { + return this.hasAttribute("expanded"); + } + + set expanded(val) { + if (val) { + this.setAttribute("expanded", "true"); + } else { + this.removeAttribute("expanded"); + } + } + + get updateInstall() { + return this._updateInstall; + } + + set updateInstall(install) { + this._updateInstall = install; + if (this.children.length) { + this.update(); + } + } + + get reloading() { + return this.hasAttribute("reloading"); + } + + set reloading(val) { + this.toggleAttribute("reloading", val); + } + + /** + * Set the add-on for this card. The card will be populated based on the + * add-on when it is connected to the DOM. + * + * @param {AddonWrapper} addon The add-on to use. + */ + setAddon(addon) { + this.addon = addon; + let install = getUpdateInstall(addon); + if ( + install && + (isInState(install, "available") || isInState(install, "postponed")) + ) { + this.updateInstall = install; + } else { + this.updateInstall = null; + } + if (this.children.length) { + this.render(); + } + } + + async setAddonPermission(permission, type, action) { + let { addon } = this; + let origins = [], + permissions = []; + if (!["add", "remove"].includes(action)) { + throw new Error("invalid action for permission change"); + } + if (type == "permission") { + if ( + action == "add" && + !addon.optionalPermissions.permissions.includes(permission) + ) { + throw new Error("permission missing from manifest"); + } + permissions = [permission]; + } else if (type == "origin") { + if (action === "add") { + let { origins } = addon.optionalPermissions; + let patternSet = new MatchPatternSet(origins, { ignorePath: true }); + if (!patternSet.subsumes(new MatchPattern(permission))) { + throw new Error("origin missing from manifest"); + } + } + origins = [permission]; + + // If this is one of the "all sites" permissions + if (Extension.isAllSitesPermission(permission)) { + // Grant/revoke ALL "all sites" optional permissions from the manifest. + origins = addon.optionalPermissions.origins.filter(perm => + Extension.isAllSitesPermission(perm) + ); + } + } else { + throw new Error("unknown permission type changed"); + } + let policy = WebExtensionPolicy.getByID(addon.id); + ExtensionPermissions[action]( + addon.id, + { origins, permissions }, + policy?.extension + ); + } + + async handleEvent(e) { + let { addon } = this; + let action = e.target.getAttribute("action"); + + if (e.type == "click") { + switch (action) { + case "toggle-disabled": + // Keep the checked state the same until the add-on's state changes. + e.target.checked = !addon.userDisabled; + if (addon.userDisabled) { + if (shouldShowPermissionsPrompt(addon)) { + await showPermissionsPrompt(addon); + } else { + await addon.enable(); + } + } else { + await addon.disable(); + } + break; + case "always-activate": + addon.userDisabled = false; + break; + case "never-activate": + addon.userDisabled = true; + break; + case "update-check": { + let { found } = await checkForUpdate(addon); + if (!found) { + this.sendEvent("no-update"); + } + break; + } + case "install-postponed": { + const { updateInstall } = this; + if (updateInstall && isInState(updateInstall, "postponed")) { + updateInstall.continuePostponedInstall(); + } + break; + } + case "install-update": + // Make sure that an update handler is attached to the install object + // before starting the update installation (otherwise the user would + // not be prompted for the new permissions requested if necessary), + // and also make sure that a prompt handler attached from a closed + // about:addons tab is replaced by the one attached by the currently + // active about:addons tab. + attachUpdateHandler(this.updateInstall); + this.updateInstall.install().then( + () => { + detachUpdateHandler(this.updateInstall); + // The card will update with the new add-on when it gets + // installed. + this.sendEvent("update-installed"); + }, + () => { + detachUpdateHandler(this.updateInstall); + // Update our state if the install is cancelled. + this.update(); + this.sendEvent("update-cancelled"); + } + ); + // Clear the install since it will be removed from the global list of + // available updates (whether it succeeds or fails). + this.updateInstall = null; + break; + case "contribute": + windowRoot.ownerGlobal.openWebLinkIn(addon.contributionURL, "tab"); + break; + case "preferences": + if (getOptionsType(addon) == "tab") { + openOptionsInTab(addon.optionsURL); + } else if (getOptionsType(addon) == "inline") { + gViewController.loadView(`detail/${this.addon.id}/preferences`); + } + break; + case "remove": + { + this.panel.hide(); + if (!hasPermission(addon, "uninstall")) { + this.sendEvent("remove-disabled"); + return; + } + let { BrowserAddonUI } = windowRoot.ownerGlobal; + let { remove, report } = await BrowserAddonUI.promptRemoveExtension( + addon + ); + if (remove) { + await addon.uninstall(true); + this.sendEvent("remove"); + if (report) { + openAbuseReport({ + addonId: addon.id, + reportEntryPoint: "uninstall", + }); + } + } else { + this.sendEvent("remove-cancelled"); + } + } + break; + case "expand": + gViewController.loadView(`detail/${this.addon.id}`); + break; + case "more-options": + // Open panel on click from the keyboard. + if (e.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) { + this.panel.toggle(e); + } + break; + case "report": + this.panel.hide(); + openAbuseReport({ addonId: addon.id, reportEntryPoint: "menu" }); + break; + case "link": + if (e.target.getAttribute("url")) { + windowRoot.ownerGlobal.openWebLinkIn( + e.target.getAttribute("url"), + "tab" + ); + } + break; + default: + // Handle a click on the card itself. + if ( + !this.expanded && + (e.target === this.addonNameEl || !e.target.closest("a")) + ) { + e.preventDefault(); + gViewController.loadView(`detail/${this.addon.id}`); + } + break; + } + } else if (e.type == "toggle" && action == "toggle-permission") { + let permission = e.target.getAttribute("permission-key"); + let type = e.target.getAttribute("permission-type"); + let fname = e.target.pressed ? "add" : "remove"; + this.setAddonPermission(permission, type, fname); + } else if (e.type == "change") { + let { name } = e.target; + switch (name) { + case "autoupdate": { + addon.applyBackgroundUpdates = e.target.value; + break; + } + case "private-browsing": { + let policy = WebExtensionPolicy.getByID(addon.id); + let extension = policy && policy.extension; + + if (e.target.value == "1") { + await ExtensionPermissions.add( + addon.id, + PRIVATE_BROWSING_PERMS, + extension + ); + } else { + await ExtensionPermissions.remove( + addon.id, + PRIVATE_BROWSING_PERMS, + extension + ); + } + // Reload the extension if it is already enabled. This ensures any + // change on the private browsing permission is properly handled. + if (addon.isActive) { + this.reloading = true; + // Reloading will trigger an enable and update the card. + addon.reload(); + } else { + // Update the card if the add-on isn't active. + this.update(); + } + break; + } + case "quarantined-domains-user-allowed": { + addon.quarantineIgnoredByUser = e.target.value == "1"; + break; + } + } + } else if (e.type == "mousedown") { + // Open panel on mousedown when the mouse is used. + if (action == "more-options" && e.button == 0) { + this.panel.toggle(e); + } + } else if (e.type === "shown" || e.type === "hidden") { + let panelOpen = e.type === "shown"; + // The card will be dimmed if it's disabled, but when the panel is open + // that should be reverted so the menu items can be easily read. + this.toggleAttribute("panelopen", panelOpen); + this.optionsButton.setAttribute("aria-expanded", panelOpen); + } + } + + get panel() { + return this.card.querySelector("panel-list"); + } + + get postponedMessageBar() { + return this.card.querySelector(".update-postponed-bar"); + } + + registerListeners() { + this.addEventListener("change", this); + this.addEventListener("click", this); + this.addEventListener("mousedown", this); + this.addEventListener("toggle", this); + this.panel.addEventListener("shown", this); + this.panel.addEventListener("hidden", this); + } + + removeListeners() { + this.removeEventListener("change", this); + this.removeEventListener("click", this); + this.removeEventListener("mousedown", this); + this.removeEventListener("toggle", this); + this.panel.removeEventListener("shown", this); + this.panel.removeEventListener("hidden", this); + } + + /** + * Update the card's contents based on the previously set add-on. This should + * be called if there has been a change to the add-on. + */ + update() { + let { addon, card } = this; + + card.setAttribute("active", addon.isActive); + + // Set the icon or theme preview. + let iconEl = card.querySelector(".addon-icon"); + let preview = card.querySelector(".card-heading-image"); + if (addon.type == "theme") { + iconEl.hidden = true; + let screenshotUrl = getScreenshotUrlForAddon(addon); + if (screenshotUrl) { + preview.src = screenshotUrl; + } + preview.hidden = !screenshotUrl; + } else { + preview.hidden = true; + iconEl.hidden = false; + if (addon.type == "plugin") { + iconEl.src = PLUGIN_ICON_URL; + } else { + iconEl.src = + AddonManager.getPreferredIconURL(addon, 32, window) || + EXTENSION_ICON_URL; + } + } + + // Update the name. + let name = this.addonNameEl; + let setDisabledStyle = !(addon.isActive || addon.type === "theme"); + if (!setDisabledStyle) { + name.textContent = addon.name; + name.removeAttribute("data-l10n-id"); + } else { + document.l10n.setAttributes(name, "addon-name-disabled", { + name: addon.name, + }); + } + name.title = `${addon.name} ${addon.version}`; + + let toggleDisabledButton = card.querySelector('[action="toggle-disabled"]'); + if (toggleDisabledButton) { + let toggleDisabledAction = addon.userDisabled ? "enable" : "disable"; + toggleDisabledButton.hidden = !hasPermission(addon, toggleDisabledAction); + if (addon.type === "theme") { + document.l10n.setAttributes( + toggleDisabledButton, + `${toggleDisabledAction}-addon-button` + ); + } else if ( + addon.type === "extension" || + addon.type === "sitepermission" + ) { + toggleDisabledButton.pressed = !addon.userDisabled; + } + } + + // Set the items in the more options menu. + this.options.update(this, addon, this.updateInstall); + + // Badge the more options button if there's an update. + let moreOptionsButton = card.querySelector(".more-options-button"); + moreOptionsButton.classList.toggle( + "more-options-button-badged", + !!(this.updateInstall && isInState(this.updateInstall, "available")) + ); + + // Postponed update addon card message bar. + const hasPostponedInstall = + this.updateInstall && isInState(this.updateInstall, "postponed"); + this.postponedMessageBar.hidden = !hasPostponedInstall; + + // Hide the more options button if it's empty. + moreOptionsButton.hidden = this.options.visibleItems.length === 0; + + // Ensure all badges are initially hidden. + for (let node of card.querySelectorAll(".addon-badge")) { + node.hidden = true; + } + + // Set the private browsing badge visibility. + // TODO: We don't show the badge for SitePermsAddon for now, but this should + // be handled in Bug 1799090. + if (addon.incognito != "not_allowed" && addon.type == "extension") { + // Keep update synchronous, the badge can appear later. + isAllowedInPrivateBrowsing(addon).then(isAllowed => { + card.querySelector(".addon-badge-private-browsing-allowed").hidden = + !isAllowed; + }); + } + + // Show the recommended badges if needed. + // Plugins don't have recommendationStates, so ensure a default. + let states = addon.recommendationStates || []; + for (let badgeName of states) { + let badge = card.querySelector(`.addon-badge-${badgeName}`); + if (badge) { + badge.hidden = false; + } + } + + // Update description. + card.querySelector(".addon-description").textContent = addon.description; + + this.updateMessage(); + + // Update the details if they're shown. + if (this.details) { + this.details.update(); + } + + this.sendEvent("update"); + } + + async updateMessage() { + const messageBar = this.card.querySelector(".addon-card-message"); + + const { + linkUrl, + linkId, + messageId, + messageArgs, + type = "", + } = await getAddonMessageInfo(this.addon); + + if (messageId) { + document.l10n.pauseObserving(); + document.l10n.setAttributes(messageBar, messageId, messageArgs); + messageBar.setAttribute("data-l10n-attrs", "message"); + + const link = messageBar.querySelector("button"); + if (linkUrl) { + document.l10n.setAttributes(link, linkId); + link.setAttribute("url", linkUrl); + link.setAttribute("slot", "actions"); + link.hidden = false; + } else { + link.removeAttribute("slot"); + link.hidden = true; + } + + document.l10n.resumeObserving(); + await document.l10n.translateFragment(messageBar); + messageBar.setAttribute("type", type); + messageBar.hidden = false; + } else { + messageBar.hidden = true; + } + } + + showPrefs() { + this.details.showPrefs(); + } + + expand() { + if (!this.children.length) { + this.expanded = true; + } else { + throw new Error("expand() is only supported before render()"); + } + } + + render() { + this.textContent = ""; + + let { addon } = this; + if (!addon) { + throw new Error("addon-card must be initialized with setAddon()"); + } + + this.setAttribute("addon-id", addon.id); + + this.card = importTemplate("card").firstElementChild; + let headingId = ExtensionCommon.makeWidgetId(`${addon.id}-heading`); + this.card.setAttribute("aria-labelledby", headingId); + + // Remove the toggle-disabled button(s) based on type. + if (addon.type != "theme") { + this.card.querySelector(".theme-enable-button").remove(); + } + if (addon.type != "extension" && addon.type != "sitepermission") { + this.card.querySelector(".extension-enable-button").remove(); + } + + let nameContainer = this.card.querySelector(".addon-name-container"); + let headingLevel = this.expanded ? "h1" : "h3"; + let nameHeading = document.createElement(headingLevel); + nameHeading.classList.add("addon-name"); + nameHeading.id = headingId; + if (!this.expanded) { + let name = document.createElement("a"); + name.classList.add("addon-name-link"); + name.href = `addons://detail/${addon.id}`; + nameHeading.appendChild(name); + this.addonNameEl = name; + } else { + this.addonNameEl = nameHeading; + } + nameContainer.prepend(nameHeading); + + let panelType = addon.type == "plugin" ? "plugin-options" : "addon-options"; + this.options = document.createElement(panelType); + this.options.render(); + this.card.appendChild(this.options); + this.optionsButton = this.card.querySelector(".more-options-button"); + + // Set the contents. + this.update(); + + let doneRenderPromise = Promise.resolve(); + if (this.expanded) { + if (!this.details) { + this.details = document.createElement("addon-details"); + } + this.details.setAddon(this.addon); + doneRenderPromise = this.details.render(); + + // If we're re-rendering we still need to append the details since the + // entire card was emptied at the beginning of the render. + this.card.appendChild(this.details); + } + + this.appendChild(this.card); + + if (this.expanded) { + requestAnimationFrame(() => this.optionsButton.focus()); + } + + // Return the promise of details rendering to wait on in DetailView. + return doneRenderPromise; + } + + sendEvent(name, detail) { + this.dispatchEvent(new CustomEvent(name, { detail })); + } + + /** + * AddonManager listener events. + */ + + onNewInstall(install) { + this.updateInstall = install; + this.sendEvent("update-found"); + } + + onInstallEnded(install) { + this.setAddon(install.addon); + } + + onInstallPostponed(install) { + this.updateInstall = install; + this.sendEvent("update-postponed"); + } + + onDisabled(addon) { + if (!this.reloading) { + this.update(); + } + } + + onEnabled(addon) { + this.reloading = false; + this.update(); + } + + onInstalled(addon) { + // When a temporary addon is reloaded, onInstalled is triggered instead of + // onEnabled. + this.reloading = false; + this.update(); + } + + onUninstalling() { + // Dispatch a remove event, the DetailView is listening for this to get us + // back to the list view when the current add-on is removed. + this.sendEvent("remove"); + } + + onUpdateModeChanged() { + this.update(); + } + + onPropertyChanged(addon, changed) { + if (this.details && changed.includes("applyBackgroundUpdates")) { + this.details.update(); + } else if (addon.type == "plugin" && changed.includes("userDisabled")) { + this.update(); + } + + if (this.details && changed.includes("quarantineIgnoredByUser")) { + this.details.updateQuarantinedDomainsUserAllowed(); + } + } + + /* Extension Permission change listener */ + async onChangePermissions(data) { + let perms = data.added || data.removed; + let hasAllSites = false; + for (let permission of perms.permissions.concat(perms.origins)) { + if (Extension.isAllSitesPermission(permission)) { + hasAllSites = true; + continue; + } + let target = document.querySelector(`[permission-key="${permission}"]`); + let checked = !data.removed; + if (target) { + target.closest("li").classList.toggle("permission-checked", checked); + target.pressed = checked; + } + } + if (hasAllSites) { + // special-case for finding the all-sites target by attribute. + let target = document.querySelector("[permission-all-sites]"); + let checked = await AddonCard.optionalAllSitesGranted(this.addon.id); + target.closest("li").classList.toggle("permission-checked", checked); + target.pressed = checked; + } + } + + // Only covers optional_permissions in MV2 and all host permissions in MV3. + static async optionalAllSitesGranted(addonId) { + let granted = await ExtensionPermissions.get(addonId); + return granted.origins.some(perm => Extension.isAllSitesPermission(perm)); + } +} +customElements.define("addon-card", AddonCard); + +/** + * A child element of ``. It should be initialized + * by calling `setDiscoAddon()` first. Call `setAddon(addon)` if it has been + * installed, and call `setAddon(null)` upon uninstall. + * + * let discoAddon = new DiscoAddonWrapper({ ... }); + * let card = document.createElement("recommended-addon-card"); + * card.setDiscoAddon(discoAddon); + * document.body.appendChild(card); + * + * AddonManager.getAddonsByID(discoAddon.id) + * .then(addon => card.setAddon(addon)); + */ +class RecommendedAddonCard extends HTMLElement { + /** + * @param {DiscoAddonWrapper} addon + * The details of the add-on that should be rendered in the card. + */ + setDiscoAddon(addon) { + this.addonId = addon.id; + + // Save the information so we can install. + this.discoAddon = addon; + + let card = importTemplate("card").firstElementChild; + let heading = card.querySelector(".addon-name-container"); + heading.textContent = ""; + heading.append(importTemplate("addon-name-container-in-disco-card")); + + this.setCardContent(card, addon); + if (addon.type != "theme") { + card + .querySelector(".addon-description") + .append(importTemplate("addon-description-in-disco-card")); + this.setCardDescription(card, addon); + } + this.registerButtons(card, addon); + + this.textContent = ""; + this.append(card); + + // We initially assume that the add-on is not installed. + this.setAddon(null); + } + + /** + * Fills in all static parts of the card. + * + * @param {HTMLElement} card + * The primary content of this card. + * @param {DiscoAddonWrapper} addon + */ + setCardContent(card, addon) { + // Set the icon. + if (addon.type == "theme") { + card.querySelector(".addon-icon").hidden = true; + } else { + card.querySelector(".addon-icon").src = AddonManager.getPreferredIconURL( + addon, + 32, + window + ); + } + + // Set the theme preview. + let preview = card.querySelector(".card-heading-image"); + if (addon.type == "theme") { + let screenshotUrl = getScreenshotUrlForAddon(addon); + if (screenshotUrl) { + preview.src = screenshotUrl; + preview.hidden = false; + } + } else { + preview.hidden = true; + } + + // Set the name. + card.querySelector(".disco-addon-name").textContent = addon.name; + + // Set the author name and link to AMO. + if (addon.creator) { + let authorInfo = card.querySelector(".disco-addon-author"); + document.l10n.setAttributes(authorInfo, "created-by-author", { + author: addon.creator.name, + }); + // This is intentionally a link to the add-on listing instead of the + // author page, because the add-on listing provides more relevant info. + authorInfo.querySelector("a").href = formatUTMParams( + "discopane-entry-link", + addon.amoListingUrl + ); + authorInfo.hidden = false; + } + } + + setCardDescription(card, addon) { + // Set the description. Note that this is the editorial description, not + // the add-on's original description that would normally appear on a card. + card.querySelector(".disco-description-main").textContent = + addon.editorialDescription; + + let hasStats = false; + if (addon.averageRating) { + hasStats = true; + card.querySelector("moz-five-star").rating = addon.averageRating; + } else { + card.querySelector("moz-five-star").hidden = true; + } + + if (addon.dailyUsers) { + hasStats = true; + let userCountElem = card.querySelector(".disco-user-count"); + document.l10n.setAttributes(userCountElem, "user-count", { + dailyUsers: addon.dailyUsers, + }); + } + + card.querySelector(".disco-description-statistics").hidden = !hasStats; + } + + registerButtons(card, addon) { + let installButton = card.querySelector("[action='install-addon']"); + if (addon.type == "theme") { + document.l10n.setAttributes(installButton, "install-theme-button"); + } else { + document.l10n.setAttributes(installButton, "install-extension-button"); + } + + this.addEventListener("click", this); + } + + handleEvent(event) { + let action = event.target.getAttribute("action"); + switch (action) { + case "install-addon": + this.installDiscoAddon(); + break; + case "manage-addon": + gViewController.loadView(`detail/${this.addonId}`); + break; + } + } + + async installDiscoAddon() { + let addon = this.discoAddon; + let url = addon.sourceURI.spec; + let install = await AddonManager.getInstallForURL(url, { + name: addon.name, + telemetryInfo: { + source: "disco", + taarRecommended: addon.taarRecommended, + }, + }); + // We are hosted in a in about:addons, but we can just use the + // main tab's browser since all of it is using the system principal. + let browser = window.docShell.chromeEventHandler; + AddonManager.installAddonFromWebpage( + "application/x-xpinstall", + browser, + Services.scriptSecurityManager.getSystemPrincipal(), + install + ); + } + + /** + * @param {AddonWrapper|null} addon + * The add-on that has been installed; null if it has been removed. + */ + setAddon(addon) { + let card = this.firstElementChild; + card.querySelector("[action='install-addon']").hidden = !!addon; + card.querySelector("[action='manage-addon']").hidden = !addon; + + this.dispatchEvent(new CustomEvent("disco-card-updated")); // For testing. + } +} +customElements.define("recommended-addon-card", RecommendedAddonCard); + +/** + * A list view for add-ons of a certain type. It should be initialized with the + * type of add-on to render and have section data set before being connected to + * the document. + * + * let list = document.createElement("addon-list"); + * list.type = "plugin"; + * list.setSections([{ + * headingId: "plugin-section-heading", + * filterFn: addon => !addon.isSystem, + * }]); + * document.body.appendChild(list); + */ +class AddonList extends HTMLElement { + constructor() { + super(); + this.sections = []; + this.pendingUninstallAddons = new Set(); + this._addonsToUpdate = new Set(); + this._userFocusListenersAdded = false; + } + + async connectedCallback() { + // Register the listener and get the add-ons, these operations should + // happpen as close to each other as possible. + this.registerListener(); + // Don't render again if we were rendered prior to being inserted. + if (!this.children.length) { + // Render the initial view. + this.render(); + } + } + + disconnectedCallback() { + // Remove content and stop listening until this is connected again. + this.textContent = ""; + this.removeListener(); + + // Process any pending uninstall related to this list. + for (const addon of this.pendingUninstallAddons) { + if (isPending(addon, "uninstall")) { + addon.uninstall(); + } + } + this.pendingUninstallAddons.clear(); + } + + /** + * Configure the sections in the list. + * + * @param {object[]} sections + * The options for the section. Each entry in the array should have: + * headingId: The fluent id for the section's heading. + * filterFn: A function that determines if an add-on belongs in + * the section. + */ + setSections(sections) { + this.sections = sections.map(section => Object.assign({}, section)); + } + + /** + * Set the add-on type for this list. This will be used to filter the add-ons + * that are displayed. + * + * @param {string} val The type to filter on. + */ + set type(val) { + this.setAttribute("type", val); + } + + get type() { + return this.getAttribute("type"); + } + + getSection(index) { + return this.sections[index].node; + } + + getCards(section) { + return section.querySelectorAll("addon-card"); + } + + getCard(addon) { + return this.querySelector(`addon-card[addon-id="${addon.id}"]`); + } + + getPendingUninstallBar(addon) { + return this.querySelector(`moz-message-bar[addon-id="${addon.id}"]`); + } + + sortByFn(aAddon, bAddon) { + return aAddon.name.localeCompare(bAddon.name); + } + + async getAddons() { + if (!this.type) { + throw new Error(`type must be set to find add-ons`); + } + + // Find everything matching our type, null will find all types. + let type = this.type == "all" ? null : [this.type]; + let addons = await AddonManager.getAddonsByTypes(type); + + if (type == "theme") { + await BuiltInThemes.ensureBuiltInThemes(); + } + + // Put the add-ons into the sections, an add-on goes in the first section + // that it matches the filterFn for. It might not go in any section. + let sectionedAddons = this.sections.map(() => []); + for (let addon of addons) { + let index = this.sections.findIndex(({ filterFn }) => filterFn(addon)); + if (index != -1) { + sectionedAddons[index].push(addon); + } else if (isPending(addon, "uninstall")) { + // A second tab may be opened on "about:addons" (or Firefox may + // have crashed) while there are still "pending uninstall" add-ons. + // Ensure to list them in the pendingUninstall message-bar-stack + // when the AddonList is initially rendered. + this.pendingUninstallAddons.add(addon); + } + } + + // Sort the add-ons in each section. + for (let [index, section] of sectionedAddons.entries()) { + let sortByFn = this.sections[index].sortByFn || this.sortByFn; + section.sort(sortByFn); + } + + return sectionedAddons; + } + + createPendingUninstallStack() { + const stack = document.createElement("message-bar-stack"); + stack.setAttribute("class", "pending-uninstall"); + stack.setAttribute("reverse", ""); + return stack; + } + + addPendingUninstallBar(addon) { + const stack = this.pendingUninstallStack; + const mb = document.createElement("moz-message-bar"); + mb.setAttribute("addon-id", addon.id); + mb.setAttribute("type", "info"); + + const undo = document.createElement("button"); + undo.setAttribute("action", "undo"); + undo.addEventListener("click", () => { + addon.cancelUninstall(); + }); + undo.setAttribute("slot", "actions"); + + document.l10n.setAttributes(mb, "pending-uninstall-description2", { + addon: addon.name, + }); + mb.setAttribute("data-l10n-attrs", "message"); + document.l10n.setAttributes(undo, "pending-uninstall-undo-button"); + + mb.appendChild(undo); + stack.append(mb); + } + + removePendingUninstallBar(addon) { + const messagebar = this.getPendingUninstallBar(addon); + if (messagebar) { + messagebar.remove(); + } + } + + createSectionHeading(headingIndex) { + let { headingId, subheadingId } = this.sections[headingIndex]; + let frag = document.createDocumentFragment(); + let heading = document.createElement("h2"); + heading.classList.add("list-section-heading"); + document.l10n.setAttributes(heading, headingId); + frag.append(heading); + + if (subheadingId) { + heading.className = "header-name"; + let subheading = document.createElement("h3"); + subheading.classList.add("list-section-subheading"); + document.l10n.setAttributes(subheading, subheadingId); + frag.append(subheading); + } + + return frag; + } + + createEmptyListMessage() { + let emptyMessage = "list-empty-get-extensions-message"; + let linkPref = "extensions.getAddons.link.url"; + + if (this.sections && this.sections.length) { + if (this.sections[0].headingId == "locale-enabled-heading") { + emptyMessage = "list-empty-get-language-packs-message"; + linkPref = "browser.dictionaries.download.url"; + } else if (this.sections[0].headingId == "dictionary-enabled-heading") { + emptyMessage = "list-empty-get-dictionaries-message"; + linkPref = "browser.dictionaries.download.url"; + } + } + + let messageContainer = document.createElement("p"); + messageContainer.id = "empty-addons-message"; + let a = document.createElement("a"); + a.href = Services.urlFormatter.formatURLPref(linkPref); + a.setAttribute("target", "_blank"); + a.setAttribute("data-l10n-name", "get-extensions"); + document.l10n.setAttributes(messageContainer, emptyMessage, { + domain: a.hostname, + }); + messageContainer.appendChild(a); + return messageContainer; + } + + updateSectionIfEmpty(section) { + // The header is added before any add-on cards, so if there's only one + // child then it's the header. In that case we should empty out the section. + if (section.children.length == 1) { + section.textContent = ""; + } + } + + insertCardInto(card, sectionIndex) { + let section = this.getSection(sectionIndex); + let sectionCards = this.getCards(section); + + // If this is the first card in the section, create the heading. + if (!sectionCards.length) { + section.appendChild(this.createSectionHeading(sectionIndex)); + } + + // Find where to insert the card. + let insertBefore = Array.from(sectionCards).find( + otherCard => this.sortByFn(card.addon, otherCard.addon) < 0 + ); + // This will append if insertBefore is null. + section.insertBefore(card, insertBefore || null); + } + + addAddon(addon) { + // Only insert add-ons of the right type. + if (addon.type != this.type && this.type != "all") { + this.sendEvent("skip-add", "type-mismatch"); + return; + } + + let insertSection = this._addonSectionIndex(addon); + + // Don't add the add-on if it doesn't go in a section. + if (insertSection == -1) { + return; + } + + // Create and insert the card. + let card = document.createElement("addon-card"); + card.setAddon(addon); + this.insertCardInto(card, insertSection); + this.sendEvent("add", { id: addon.id }); + } + + sendEvent(name, detail) { + this.dispatchEvent(new CustomEvent(name, { detail })); + } + + removeAddon(addon) { + let card = this.getCard(addon); + if (card) { + let section = card.parentNode; + card.remove(); + this.updateSectionIfEmpty(section); + this.sendEvent("remove", { id: addon.id }); + } + } + + updateAddon(addon) { + if (!this.getCard(addon)) { + // Try to add the add-on right away. + this.addAddon(addon); + } else if (this._addonSectionIndex(addon) == -1) { + // Try to remove the add-on right away. + this._updateAddon(addon); + } else if (this.isUserFocused) { + // Queue up a change for when the focus is cleared. + this.updateLater(addon); + } else { + // Not currently focused, make the change now. + this.withCardAnimation(() => this._updateAddon(addon)); + } + } + + updateLater(addon) { + this._addonsToUpdate.add(addon); + this._addUserFocusListeners(); + } + + _addUserFocusListeners() { + if (this._userFocusListenersAdded) { + return; + } + + this._userFocusListenersAdded = true; + this.addEventListener("mouseleave", this); + this.addEventListener("hidden", this, true); + this.addEventListener("focusout", this); + } + + _removeUserFocusListeners() { + if (!this._userFocusListenersAdded) { + return; + } + + this.removeEventListener("mouseleave", this); + this.removeEventListener("hidden", this, true); + this.removeEventListener("focusout", this); + this._userFocusListenersAdded = false; + } + + get hasMenuOpen() { + return !!this.querySelector("panel-list[open]"); + } + + get isUserFocused() { + return this.matches(":hover, :focus-within") || this.hasMenuOpen; + } + + update() { + if (this._addonsToUpdate.size) { + this.withCardAnimation(() => { + for (let addon of this._addonsToUpdate) { + this._updateAddon(addon); + } + this._addonsToUpdate = new Set(); + }); + } + } + + _getChildCoords() { + let results = new Map(); + for (let child of this.querySelectorAll("addon-card")) { + results.set(child, child.getBoundingClientRect()); + } + return results; + } + + withCardAnimation(changeFn) { + if (shouldSkipAnimations()) { + changeFn(); + return; + } + + let origChildCoords = this._getChildCoords(); + + changeFn(); + + let newChildCoords = this._getChildCoords(); + let cards = this.querySelectorAll("addon-card"); + let transitionCards = []; + for (let card of cards) { + let orig = origChildCoords.get(card); + let moved = newChildCoords.get(card); + let changeY = moved.y - (orig || moved).y; + let cardEl = card.firstElementChild; + + if (changeY != 0) { + cardEl.style.transform = `translateY(${changeY * -1}px)`; + transitionCards.push(card); + } + } + requestAnimationFrame(() => { + for (let card of transitionCards) { + card.firstElementChild.style.transition = "transform 125ms"; + } + + requestAnimationFrame(() => { + for (let card of transitionCards) { + let cardEl = card.firstElementChild; + cardEl.style.transform = ""; + cardEl.addEventListener("transitionend", function handler(e) { + if (e.target == cardEl && e.propertyName == "transform") { + cardEl.style.transition = ""; + cardEl.removeEventListener("transitionend", handler); + } + }); + } + }); + }); + } + + _addonSectionIndex(addon) { + return this.sections.findIndex(s => s.filterFn(addon)); + } + + _updateAddon(addon) { + let card = this.getCard(addon); + if (card) { + let sectionIndex = this._addonSectionIndex(addon); + if (sectionIndex != -1) { + // Move the card, if needed. This will allow an animation between + // page sections and provides clearer events for testing. + if (card.parentNode.getAttribute("section") != sectionIndex) { + let { activeElement } = document; + let refocus = card.contains(activeElement); + let oldSection = card.parentNode; + this.insertCardInto(card, sectionIndex); + this.updateSectionIfEmpty(oldSection); + if (refocus) { + activeElement.focus(); + } + this.sendEvent("move", { id: addon.id }); + } + } else { + this.removeAddon(addon); + } + } + } + + renderSection(addons, index) { + const { sectionClass } = this.sections[index]; + + let section = document.createElement("section"); + section.setAttribute("section", index); + if (sectionClass) { + section.setAttribute("class", sectionClass); + } + + // Render the heading and add-ons if there are any. + if (addons.length) { + section.appendChild(this.createSectionHeading(index)); + } + + for (let addon of addons) { + let card = document.createElement("addon-card"); + card.setAddon(addon); + card.render(); + section.appendChild(card); + } + + return section; + } + + async render() { + this.textContent = ""; + + let sectionedAddons = await this.getAddons(); + + let frag = document.createDocumentFragment(); + + // Render the pending uninstall message-bar-stack. + this.pendingUninstallStack = this.createPendingUninstallStack(); + for (let addon of this.pendingUninstallAddons) { + this.addPendingUninstallBar(addon); + } + frag.appendChild(this.pendingUninstallStack); + + // Render the sections. + for (let i = 0; i < sectionedAddons.length; i++) { + this.sections[i].node = this.renderSection(sectionedAddons[i], i); + frag.appendChild(this.sections[i].node); + } + + // Render the placeholder that is shown when all sections are empty. + // This call is after rendering the sections, because its visibility + // is controlled through the general sibling combinator relative to + // the sections (section ~). + let message = this.createEmptyListMessage(); + frag.appendChild(message); + + // Make sure fluent has set all the strings before we render. This will + // avoid the height changing as strings go from 0 height to having text. + await document.l10n.translateFragment(frag); + this.appendChild(frag); + } + + registerListener() { + AddonManagerListenerHandler.addListener(this); + } + + removeListener() { + AddonManagerListenerHandler.removeListener(this); + } + + handleEvent(e) { + if (!this.isUserFocused || (e.type == "mouseleave" && !this.hasMenuOpen)) { + this._removeUserFocusListeners(); + this.update(); + } + } + + /** + * AddonManager listener events. + */ + + onOperationCancelled(addon) { + if ( + this.pendingUninstallAddons.has(addon) && + !isPending(addon, "uninstall") + ) { + this.pendingUninstallAddons.delete(addon); + this.removePendingUninstallBar(addon); + } + this.updateAddon(addon); + } + + onEnabled(addon) { + this.updateAddon(addon); + } + + onDisabled(addon) { + this.updateAddon(addon); + } + + onUninstalling(addon) { + if ( + isPending(addon, "uninstall") && + (this.type === "all" || addon.type === this.type) + ) { + this.pendingUninstallAddons.add(addon); + this.addPendingUninstallBar(addon); + this.updateAddon(addon); + } + } + + onInstalled(addon) { + if (this.querySelector(`addon-card[addon-id="${addon.id}"]`)) { + return; + } + this.addAddon(addon); + } + + onUninstalled(addon) { + this.pendingUninstallAddons.delete(addon); + this.removePendingUninstallBar(addon); + this.removeAddon(addon); + } +} +customElements.define("addon-list", AddonList); + +class RecommendedAddonList extends HTMLElement { + connectedCallback() { + if (this.isConnected) { + this.loadCardsIfNeeded(); + this.updateCardsWithAddonManager(); + } + AddonManagerListenerHandler.addListener(this); + } + + disconnectedCallback() { + AddonManagerListenerHandler.removeListener(this); + } + + get type() { + return this.getAttribute("type"); + } + + /** + * Set the add-on type for this list. This will be used to filter the add-ons + * that are displayed. + * + * Must be set prior to the first render. + * + * @param {string} val The type to filter on. + */ + set type(val) { + this.setAttribute("type", val); + } + + get hideInstalled() { + return this.hasAttribute("hide-installed"); + } + + /** + * Set whether installed add-ons should be hidden from the list. If false, + * installed add-ons will be shown with a "Manage" button, otherwise they + * will be hidden. + * + * Must be set prior to the first render. + * + * @param {boolean} val Whether to show installed add-ons. + */ + set hideInstalled(val) { + this.toggleAttribute("hide-installed", val); + } + + getCardById(addonId) { + for (let card of this.children) { + if (card.addonId === addonId) { + return card; + } + } + return null; + } + + setAddonForCard(card, addon) { + card.setAddon(addon); + + let wasHidden = card.hidden; + card.hidden = this.hideInstalled && addon; + + if (wasHidden != card.hidden) { + let eventName = card.hidden ? "card-hidden" : "card-shown"; + this.dispatchEvent(new CustomEvent(eventName, { detail: { card } })); + } + } + + /** + * Whether the client ID should be preferred. This is disabled for themes + * since they don't use the telemetry data and don't show the TAAR notice. + */ + get preferClientId() { + return !this.type || this.type == "extension"; + } + + async updateCardsWithAddonManager() { + let cards = Array.from(this.children); + let addonIds = cards.map(card => card.addonId); + let addons = await AddonManager.getAddonsByIDs(addonIds); + for (let [i, card] of cards.entries()) { + let addon = addons[i]; + this.setAddonForCard(card, addon); + if (addon) { + // Already installed, move card to end. + this.append(card); + } + } + } + + async loadCardsIfNeeded() { + // Use promise as guard. Also used by tests to detect when load completes. + if (!this.cardsReady) { + this.cardsReady = this._loadCards(); + } + return this.cardsReady; + } + + async _loadCards() { + let recommendedAddons; + try { + recommendedAddons = await DiscoveryAPI.getResults(this.preferClientId); + } catch (e) { + return; + } + + let frag = document.createDocumentFragment(); + for (let addon of recommendedAddons) { + if (this.type && addon.type != this.type) { + continue; + } + let card = document.createElement("recommended-addon-card"); + card.setDiscoAddon(addon); + frag.append(card); + } + this.append(frag); + await this.updateCardsWithAddonManager(); + } + + /** + * AddonManager listener events. + */ + + onInstalled(addon) { + let card = this.getCardById(addon.id); + if (card) { + this.setAddonForCard(card, addon); + } + } + + onUninstalled(addon) { + let card = this.getCardById(addon.id); + if (card) { + this.setAddonForCard(card, null); + } + } +} +customElements.define("recommended-addon-list", RecommendedAddonList); + +class TaarMessageBar extends HTMLElement { + connectedCallback() { + this.hidden = + Services.prefs.getBoolPref(PREF_RECOMMENDATION_HIDE_NOTICE, false) || + !DiscoveryAPI.clientIdDiscoveryEnabled; + if (this.childElementCount == 0 && !this.hidden) { + this.appendChild(importTemplate("taar-notice")); + this.addEventListener("click", this); + this.messageBar = this.querySelector("moz-message-bar"); + this.messageBar.addEventListener("message-bar:user-dismissed", this); + } + } + + handleEvent(e) { + if (e.type == "message-bar:user-dismissed") { + Services.prefs.setBoolPref(PREF_RECOMMENDATION_HIDE_NOTICE, true); + } + } +} +customElements.define("taar-notice", TaarMessageBar); + +class RecommendedFooter extends HTMLElement { + connectedCallback() { + if (this.childElementCount == 0) { + this.appendChild(importTemplate("recommended-footer")); + this.querySelector(".privacy-policy-link").href = + Services.prefs.getStringPref(PREF_PRIVACY_POLICY_URL); + this.addEventListener("click", this); + } + } + + handleEvent(event) { + let action = event.target.getAttribute("action"); + switch (action) { + case "open-amo": + openAmoInTab(this); + break; + } + } +} +customElements.define("recommended-footer", RecommendedFooter, { + extends: "footer", +}); + +class RecommendedThemesFooter extends HTMLElement { + connectedCallback() { + if (this.childElementCount == 0) { + this.appendChild(importTemplate("recommended-themes-footer")); + let themeRecommendationRow = this.querySelector(".theme-recommendation"); + let themeRecommendationUrl = Services.prefs.getStringPref( + PREF_THEME_RECOMMENDATION_URL + ); + if (themeRecommendationUrl) { + themeRecommendationRow.querySelector("a").href = themeRecommendationUrl; + } + themeRecommendationRow.hidden = !themeRecommendationUrl; + this.addEventListener("click", this); + } + } + + handleEvent(event) { + let action = event.target.getAttribute("action"); + switch (action) { + case "open-amo": + openAmoInTab(this, "themes"); + break; + } + } +} +customElements.define("recommended-themes-footer", RecommendedThemesFooter, { + extends: "footer", +}); + +/** + * This element will handle showing recommendations with a + * and a