summaryrefslogtreecommitdiffstats
path: root/uriloader/exthandler
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /uriloader/exthandler
parentInitial commit. (diff)
downloadfirefox-upstream/124.0.1.tar.xz
firefox-upstream/124.0.1.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--uriloader/exthandler/ContentHandlerService.cpp263
-rw-r--r--uriloader/exthandler/ContentHandlerService.h55
-rw-r--r--uriloader/exthandler/DBusHelpers.h84
-rw-r--r--uriloader/exthandler/ExtHandlerService.sys.mjs757
-rw-r--r--uriloader/exthandler/ExternalHelperAppChild.cpp93
-rw-r--r--uriloader/exthandler/ExternalHelperAppChild.h44
-rw-r--r--uriloader/exthandler/ExternalHelperAppParent.cpp477
-rw-r--r--uriloader/exthandler/ExternalHelperAppParent.h118
-rw-r--r--uriloader/exthandler/HandlerList.sys.mjs206
-rw-r--r--uriloader/exthandler/HandlerServiceChild.h25
-rw-r--r--uriloader/exthandler/HandlerServiceParent.cpp381
-rw-r--r--uriloader/exthandler/HandlerServiceParent.h67
-rw-r--r--uriloader/exthandler/PExternalHelperApp.ipdl29
-rw-r--r--uriloader/exthandler/PHandlerService.ipdl62
-rw-r--r--uriloader/exthandler/WebHandlerApp.sys.mjs185
-rw-r--r--uriloader/exthandler/android/nsMIMEInfoAndroid.cpp340
-rw-r--r--uriloader/exthandler/android/nsMIMEInfoAndroid.h62
-rw-r--r--uriloader/exthandler/android/nsOSHelperAppService.cpp61
-rw-r--r--uriloader/exthandler/android/nsOSHelperAppService.h38
-rw-r--r--uriloader/exthandler/components.conf20
-rw-r--r--uriloader/exthandler/docs/index.rst76
-rw-r--r--uriloader/exthandler/mac/nsDecodeAppleFile.cpp361
-rw-r--r--uriloader/exthandler/mac/nsDecodeAppleFile.h116
-rw-r--r--uriloader/exthandler/mac/nsLocalHandlerAppMac.h26
-rw-r--r--uriloader/exthandler/mac/nsLocalHandlerAppMac.mm81
-rw-r--r--uriloader/exthandler/mac/nsMIMEInfoMac.h33
-rw-r--r--uriloader/exthandler/mac/nsMIMEInfoMac.mm111
-rw-r--r--uriloader/exthandler/mac/nsOSHelperAppService.h53
-rw-r--r--uriloader/exthandler/mac/nsOSHelperAppService.mm613
-rw-r--r--uriloader/exthandler/moz.build143
-rw-r--r--uriloader/exthandler/nsCExternalHandlerService.idl32
-rw-r--r--uriloader/exthandler/nsContentHandlerApp.h30
-rw-r--r--uriloader/exthandler/nsDBusHandlerApp.cpp164
-rw-r--r--uriloader/exthandler/nsDBusHandlerApp.h31
-rw-r--r--uriloader/exthandler/nsExternalHelperAppService.cpp3745
-rw-r--r--uriloader/exthandler/nsExternalHelperAppService.h571
-rw-r--r--uriloader/exthandler/nsExternalProtocolHandler.cpp550
-rw-r--r--uriloader/exthandler/nsExternalProtocolHandler.h37
-rw-r--r--uriloader/exthandler/nsIContentDispatchChooser.idl40
-rw-r--r--uriloader/exthandler/nsIExternalHelperAppService.idl191
-rw-r--r--uriloader/exthandler/nsIExternalProtocolService.idl155
-rw-r--r--uriloader/exthandler/nsIHandlerService.idl179
-rw-r--r--uriloader/exthandler/nsIHelperAppLauncherDialog.idl90
-rw-r--r--uriloader/exthandler/nsISharingHandlerApp.idl11
-rw-r--r--uriloader/exthandler/nsLocalHandlerApp.cpp158
-rw-r--r--uriloader/exthandler/nsLocalHandlerApp.h59
-rw-r--r--uriloader/exthandler/nsMIMEInfoChild.h54
-rw-r--r--uriloader/exthandler/nsMIMEInfoImpl.cpp526
-rw-r--r--uriloader/exthandler/nsMIMEInfoImpl.h224
-rw-r--r--uriloader/exthandler/nsOSHelperAppServiceChild.cpp119
-rw-r--r--uriloader/exthandler/nsOSHelperAppServiceChild.h48
-rw-r--r--uriloader/exthandler/tests/HandlerServiceTestUtils.sys.mjs253
-rw-r--r--uriloader/exthandler/tests/WriteArgument.cpp20
-rw-r--r--uriloader/exthandler/tests/gtest/ExternalHelperAppServiceTest.cpp46
-rw-r--r--uriloader/exthandler/tests/gtest/moz.build11
-rw-r--r--uriloader/exthandler/tests/mochitest/FTPprotocolHandler.html16
-rw-r--r--uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js104
-rw-r--r--uriloader/exthandler/tests/mochitest/blank.html11
-rw-r--r--uriloader/exthandler/tests/mochitest/browser.toml164
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_auto_close_window.js342
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_auto_close_window_nodialog.js308
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_bad_download_dir.js90
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js25
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_idn_blocklist.js47
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js814
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_preferred_action.js299
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js78
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_skips_dialog.js59
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_spam_permissions.js158
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_urlescape.js75
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_extension_correction.js222
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_filehandling_loop.js93
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js70
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_ftp_protocol_handlers.js111
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_launched_app_save_directory.js111
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_local_files_no_save_without_asking.js61
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_local_files_open_doesnt_duplicate.js121
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js310
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_pdf_save_as.js121
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js467
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js199
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js1383
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js140
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js38
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js77
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_remember_download_option.js61
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_save_filenames.js860
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js303
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_standalone_application_chooser_window_fallback.js67
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js168
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js85
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js130
-rw-r--r--uriloader/exthandler/tests/mochitest/download.bin1
-rw-r--r--uriloader/exthandler/tests/mochitest/download.sjs42
-rw-r--r--uriloader/exthandler/tests/mochitest/download_page.html22
-rw-r--r--uriloader/exthandler/tests/mochitest/file_as.exe1
-rw-r--r--uriloader/exthandler/tests/mochitest/file_as.exe^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html1
-rw-r--r--uriloader/exthandler/tests/mochitest/file_green.webpbin0 -> 42 bytes
-rw-r--r--uriloader/exthandler/tests/mochitest/file_green.webp^headers^3
-rw-r--r--uriloader/exthandler/tests/mochitest/file_image_svgxml.svg3
-rw-r--r--uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html1
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt0
-rw-r--r--uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with@@funny_name.pngbin0 -> 1991 bytes
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with[funny_name.webmbin0 -> 512 bytes
-rw-r--r--uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml4
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml4
-rw-r--r--uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^2
-rw-r--r--uriloader/exthandler/tests/mochitest/head.js537
-rw-r--r--uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs18
-rw-r--r--uriloader/exthandler/tests/mochitest/mailto.html11
-rw-r--r--uriloader/exthandler/tests/mochitest/mime_type_download.sjs21
-rw-r--r--uriloader/exthandler/tests/mochitest/mochitest.toml25
-rw-r--r--uriloader/exthandler/tests/mochitest/protocolHandler.html16
-rw-r--r--uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs28
-rw-r--r--uriloader/exthandler/tests/mochitest/redirect_helper.sjs28
-rw-r--r--uriloader/exthandler/tests/mochitest/save_filenames.html377
-rw-r--r--uriloader/exthandler/tests/mochitest/script_redirect.html5
-rw-r--r--uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml65
-rw-r--r--uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml67
-rw-r--r--uriloader/exthandler/tests/mochitest/test_spammy_page.html27
-rw-r--r--uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html28
-rw-r--r--uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml89
-rw-r--r--uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs18
-rw-r--r--uriloader/exthandler/tests/moz.build35
-rw-r--r--uriloader/exthandler/tests/unit/handlers.json83
-rw-r--r--uriloader/exthandler/tests/unit/head.js84
-rw-r--r--uriloader/exthandler/tests/unit/mailcap2
-rw-r--r--uriloader/exthandler/tests/unit/test_badMIMEType.js29
-rw-r--r--uriloader/exthandler/tests/unit/test_defaults_handlerService.js202
-rw-r--r--uriloader/exthandler/tests/unit/test_filename_sanitize.js398
-rw-r--r--uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js17
-rw-r--r--uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js30
-rw-r--r--uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js32
-rw-r--r--uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js65
-rw-r--r--uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js218
-rw-r--r--uriloader/exthandler/tests/unit/test_handlerService.js467
-rw-r--r--uriloader/exthandler/tests/unit/test_handlerService_store.js757
-rw-r--r--uriloader/exthandler/tests/unit/test_punycodeURIs.js126
-rw-r--r--uriloader/exthandler/tests/unit/xpcshell.toml43
-rw-r--r--uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h27
-rw-r--r--uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm16
-rw-r--r--uriloader/exthandler/uikit/nsMIMEInfoUIKit.h31
-rw-r--r--uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm16
-rw-r--r--uriloader/exthandler/uikit/nsOSHelperAppService.h54
-rw-r--r--uriloader/exthandler/uikit/nsOSHelperAppService.mm58
-rw-r--r--uriloader/exthandler/unix/nsGNOMERegistry.cpp101
-rw-r--r--uriloader/exthandler/unix/nsGNOMERegistry.h28
-rw-r--r--uriloader/exthandler/unix/nsMIMEInfoUnix.cpp84
-rw-r--r--uriloader/exthandler/unix/nsMIMEInfoUnix.h30
-rw-r--r--uriloader/exthandler/unix/nsOSHelperAppService.cpp1498
-rw-r--r--uriloader/exthandler/unix/nsOSHelperAppService.h122
-rw-r--r--uriloader/exthandler/win/nsMIMEInfoWin.cpp913
-rw-r--r--uriloader/exthandler/win/nsMIMEInfoWin.h79
-rw-r--r--uriloader/exthandler/win/nsOSHelperAppService.cpp609
-rw-r--r--uriloader/exthandler/win/nsOSHelperAppService.h76
169 files changed, 27906 insertions, 0 deletions
diff --git a/uriloader/exthandler/ContentHandlerService.cpp b/uriloader/exthandler/ContentHandlerService.cpp
new file mode 100644
index 0000000000..599185bcd0
--- /dev/null
+++ b/uriloader/exthandler/ContentHandlerService.cpp
@@ -0,0 +1,263 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContentHandlerService.h"
+#include "HandlerServiceChild.h"
+#include "ContentChild.h"
+#include "nsIMutableArray.h"
+#include "nsIMIMEInfo.h"
+#include "nsIStringEnumerator.h"
+#include "nsReadableUtils.h"
+#include "nsMIMEInfoImpl.h"
+#include "nsMIMEInfoChild.h"
+
+using mozilla::dom::ContentChild;
+using mozilla::dom::HandlerInfo;
+using mozilla::dom::PHandlerServiceChild;
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(ContentHandlerService, nsIHandlerService)
+
+ContentHandlerService::ContentHandlerService() {}
+
+/* static */ already_AddRefed<nsIHandlerService>
+ContentHandlerService::Create() {
+ if (XRE_IsContentProcess()) {
+ RefPtr service = new ContentHandlerService();
+ if (NS_SUCCEEDED(service->Init())) {
+ return service.forget();
+ }
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIHandlerService> service =
+ do_GetService("@mozilla.org/uriloader/handler-service-parent;1");
+ return service.forget();
+}
+
+nsresult ContentHandlerService::Init() {
+ if (!XRE_IsContentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+ ContentChild* cpc = ContentChild::GetSingleton();
+
+ mHandlerServiceChild = new HandlerServiceChild();
+ if (!cpc->SendPHandlerServiceConstructor(mHandlerServiceChild)) {
+ mHandlerServiceChild = nullptr;
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+void ContentHandlerService::nsIHandlerInfoToHandlerInfo(
+ nsIHandlerInfo* aInfo, HandlerInfo* aHandlerInfo) {
+ nsCString type;
+ aInfo->GetType(type);
+ nsCOMPtr<nsIMIMEInfo> mimeInfo = do_QueryInterface(aInfo);
+ bool isMIMEInfo = !!mimeInfo;
+ nsString description;
+ aInfo->GetDescription(description);
+ bool alwaysAskBeforeHandling;
+ aInfo->GetAlwaysAskBeforeHandling(&alwaysAskBeforeHandling);
+ nsCOMPtr<nsIHandlerApp> app;
+ aInfo->GetPreferredApplicationHandler(getter_AddRefs(app));
+ nsString name;
+ nsString detailedDescription;
+ if (app) {
+ app->GetName(name);
+ app->GetDetailedDescription(detailedDescription);
+ }
+ HandlerApp happ(name, detailedDescription);
+ nsTArray<HandlerApp> happs;
+ nsCOMPtr<nsIMutableArray> apps;
+ aInfo->GetPossibleApplicationHandlers(getter_AddRefs(apps));
+ if (apps) {
+ unsigned int length;
+ apps->GetLength(&length);
+ for (unsigned int i = 0; i < length; i++) {
+ apps->QueryElementAt(i, NS_GET_IID(nsIHandlerApp), getter_AddRefs(app));
+ app->GetName(name);
+ app->GetDetailedDescription(detailedDescription);
+ happs.AppendElement(HandlerApp(name, detailedDescription));
+ }
+ }
+
+ nsTArray<nsCString> extensions;
+
+ if (isMIMEInfo) {
+ nsCOMPtr<nsIUTF8StringEnumerator> extensionsIter;
+ mimeInfo->GetFileExtensions(getter_AddRefs(extensionsIter));
+ if (extensionsIter) {
+ bool hasMore = false;
+ while (NS_SUCCEEDED(extensionsIter->HasMore(&hasMore)) && hasMore) {
+ nsAutoCString extension;
+ if (NS_SUCCEEDED(extensionsIter->GetNext(extension))) {
+ extensions.AppendElement(std::move(extension));
+ }
+ }
+ }
+ }
+
+ nsHandlerInfoAction action;
+ aInfo->GetPreferredAction(&action);
+ HandlerInfo info(type, isMIMEInfo, description, alwaysAskBeforeHandling,
+ std::move(extensions), happ, happs, action);
+ *aHandlerInfo = info;
+}
+
+NS_IMETHODIMP RemoteHandlerApp::GetName(nsAString& aName) {
+ aName.Assign(mAppChild.name());
+ return NS_OK;
+}
+
+NS_IMETHODIMP RemoteHandlerApp::SetName(const nsAString& aName) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP RemoteHandlerApp::GetDetailedDescription(
+ nsAString& aDetailedDescription) {
+ aDetailedDescription.Assign(mAppChild.detailedDescription());
+ return NS_OK;
+}
+
+NS_IMETHODIMP RemoteHandlerApp::SetDetailedDescription(
+ const nsAString& aDetailedDescription) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP RemoteHandlerApp::Equals(nsIHandlerApp* aHandlerApp,
+ bool* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP RemoteHandlerApp::LaunchWithURI(
+ nsIURI* aURI, BrowsingContext* aBrowsingContext) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(RemoteHandlerApp, nsIHandlerApp)
+
+static inline void CopyHandlerInfoTonsIHandlerInfo(
+ const HandlerInfo& info, nsIHandlerInfo* aHandlerInfo) {
+ HandlerApp preferredApplicationHandler = info.preferredApplicationHandler();
+ nsCOMPtr<nsIHandlerApp> preferredApp(
+ new RemoteHandlerApp(preferredApplicationHandler));
+ aHandlerInfo->SetPreferredApplicationHandler(preferredApp);
+ nsCOMPtr<nsIMutableArray> possibleHandlers;
+ aHandlerInfo->GetPossibleApplicationHandlers(
+ getter_AddRefs(possibleHandlers));
+ possibleHandlers->AppendElement(preferredApp);
+
+ aHandlerInfo->SetPreferredAction(info.preferredAction());
+ aHandlerInfo->SetAlwaysAskBeforeHandling(info.alwaysAskBeforeHandling());
+
+ if (info.isMIMEInfo()) {
+ nsCOMPtr<nsIMIMEInfo> mimeInfo(do_QueryInterface(aHandlerInfo));
+ MOZ_ASSERT(mimeInfo,
+ "parent and child don't agree on whether this is a MIME info");
+ mimeInfo->SetFileExtensions(StringJoin(","_ns, info.extensions()));
+ }
+}
+
+ContentHandlerService::~ContentHandlerService() {}
+
+NS_IMETHODIMP ContentHandlerService::AsyncInit() {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP ContentHandlerService::Enumerate(nsISimpleEnumerator** _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP ContentHandlerService::FillHandlerInfo(
+ nsIHandlerInfo* aHandlerInfo, const nsACString& aOverrideType) {
+ HandlerInfo info, returnedInfo;
+ nsIHandlerInfoToHandlerInfo(aHandlerInfo, &info);
+ mHandlerServiceChild->SendFillHandlerInfo(info, aOverrideType, &returnedInfo);
+ CopyHandlerInfoTonsIHandlerInfo(returnedInfo, aHandlerInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ContentHandlerService::GetMIMEInfoFromOS(
+ const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) {
+ nsresult rv = NS_ERROR_FAILURE;
+ HandlerInfo returnedInfo;
+ if (!mHandlerServiceChild->SendGetMIMEInfoFromOS(aMIMEType, aFileExt, &rv,
+ &returnedInfo, aFound)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RefPtr<nsChildProcessMIMEInfo> mimeInfo =
+ new nsChildProcessMIMEInfo(returnedInfo.type());
+ CopyHandlerInfoTonsIHandlerInfo(returnedInfo, mimeInfo);
+ mimeInfo.forget(aMIMEInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ContentHandlerService::Store(nsIHandlerInfo* aHandlerInfo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP ContentHandlerService::Exists(nsIHandlerInfo* aHandlerInfo,
+ bool* _retval) {
+ HandlerInfo info;
+ nsIHandlerInfoToHandlerInfo(aHandlerInfo, &info);
+ mHandlerServiceChild->SendExists(info, _retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ContentHandlerService::Remove(nsIHandlerInfo* aHandlerInfo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ContentHandlerService::ExistsForProtocolOS(const nsACString& aProtocolScheme,
+ bool* aRetval) {
+ if (!mHandlerServiceChild->SendExistsForProtocolOS(aProtocolScheme,
+ aRetval)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ContentHandlerService::ExistsForProtocol(const nsACString& aProtocolScheme,
+ bool* aRetval) {
+ if (!mHandlerServiceChild->SendExistsForProtocol(aProtocolScheme, aRetval)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP ContentHandlerService::GetTypeFromExtension(
+ const nsACString& aFileExtension, nsACString& _retval) {
+ _retval.Assign(*mExtToTypeMap.LookupOrInsertWith(aFileExtension, [&] {
+ nsCString type;
+ mHandlerServiceChild->SendGetTypeFromExtension(aFileExtension, &type);
+ return MakeUnique<nsCString>(type);
+ }));
+ return NS_OK;
+}
+
+NS_IMETHODIMP ContentHandlerService::GetApplicationDescription(
+ const nsACString& aProtocolScheme, nsAString& aRetVal) {
+ nsresult rv = NS_ERROR_FAILURE;
+ nsAutoCString scheme(aProtocolScheme);
+ nsAutoString desc;
+ mHandlerServiceChild->SendGetApplicationDescription(scheme, &rv, &desc);
+ aRetVal.Assign(desc);
+ return rv;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/uriloader/exthandler/ContentHandlerService.h b/uriloader/exthandler/ContentHandlerService.h
new file mode 100644
index 0000000000..650dbd9115
--- /dev/null
+++ b/uriloader/exthandler/ContentHandlerService.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ContentHandlerService_h
+#define ContentHandlerService_h
+
+#include "mozilla/dom/PHandlerService.h"
+#include "nsIHandlerService.h"
+#include "nsClassHashtable.h"
+#include "nsIMIMEInfo.h"
+
+namespace mozilla {
+
+class HandlerServiceChild;
+
+namespace dom {
+
+class PHandlerServiceChild;
+
+class ContentHandlerService : public nsIHandlerService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHANDLERSERVICE
+
+ ContentHandlerService();
+ [[nodiscard]] nsresult Init();
+ static void nsIHandlerInfoToHandlerInfo(nsIHandlerInfo* aInfo,
+ HandlerInfo* aHandlerInfo);
+
+ static already_AddRefed<nsIHandlerService> Create();
+
+ private:
+ virtual ~ContentHandlerService();
+ RefPtr<HandlerServiceChild> mHandlerServiceChild;
+ nsClassHashtable<nsCStringHashKey, nsCString> mExtToTypeMap;
+};
+
+class RemoteHandlerApp : public nsIHandlerApp {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHANDLERAPP
+
+ explicit RemoteHandlerApp(HandlerApp aAppChild) : mAppChild(aAppChild) {}
+
+ private:
+ virtual ~RemoteHandlerApp() {}
+ HandlerApp mAppChild;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif
diff --git a/uriloader/exthandler/DBusHelpers.h b/uriloader/exthandler/DBusHelpers.h
new file mode 100644
index 0000000000..4f4f64309d
--- /dev/null
+++ b/uriloader/exthandler/DBusHelpers.h
@@ -0,0 +1,84 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_DBusHelpers_h
+#define mozilla_DBusHelpers_h
+
+#include <dbus/dbus.h>
+#include "mozilla/UniquePtr.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+template <>
+struct RefPtrTraits<DBusMessage> {
+ static void AddRef(DBusMessage* aMessage) {
+ MOZ_ASSERT(aMessage);
+ dbus_message_ref(aMessage);
+ }
+ static void Release(DBusMessage* aMessage) {
+ MOZ_ASSERT(aMessage);
+ dbus_message_unref(aMessage);
+ }
+};
+
+template <>
+struct RefPtrTraits<DBusPendingCall> {
+ static void AddRef(DBusPendingCall* aPendingCall) {
+ MOZ_ASSERT(aPendingCall);
+ dbus_pending_call_ref(aPendingCall);
+ }
+ static void Release(DBusPendingCall* aPendingCall) {
+ MOZ_ASSERT(aPendingCall);
+ dbus_pending_call_unref(aPendingCall);
+ }
+};
+
+/*
+ * |RefPtrTraits<DBusConnection>| specializes |RefPtrTraits<>|
+ * for managing |DBusConnection| with |RefPtr|.
+ *
+ * |RefPtrTraits<DBusConnection>| will _not_ close the DBus
+ * connection upon the final unref. The caller is responsible
+ * for closing the connection.
+ */
+template <>
+struct RefPtrTraits<DBusConnection> {
+ static void AddRef(DBusConnection* aConnection) {
+ MOZ_ASSERT(aConnection);
+ dbus_connection_ref(aConnection);
+ }
+ static void Release(DBusConnection* aConnection) {
+ MOZ_ASSERT(aConnection);
+ dbus_connection_unref(aConnection);
+ }
+};
+
+/*
+ * |DBusConnectionDelete| is a deleter for managing instances
+ * of |DBusConnection| in |UniquePtr|. Upon destruction, it
+ * will close an open connection before unref'ing the data
+ * structure.
+ *
+ * Do not use |UniquePtr| with shared DBus connections. For
+ * shared connections, use |RefPtr|.
+ */
+class DBusConnectionDelete {
+ public:
+ constexpr DBusConnectionDelete() {}
+
+ void operator()(DBusConnection* aConnection) const {
+ MOZ_ASSERT(aConnection);
+ if (dbus_connection_get_is_connected(aConnection)) {
+ dbus_connection_close(aConnection);
+ }
+ dbus_connection_unref(aConnection);
+ }
+};
+
+} // namespace mozilla
+
+#endif // mozilla_DBusHelpers_h
diff --git a/uriloader/exthandler/ExtHandlerService.sys.mjs b/uriloader/exthandler/ExtHandlerService.sys.mjs
new file mode 100644
index 0000000000..aad2c0b8d9
--- /dev/null
+++ b/uriloader/exthandler/ExtHandlerService.sys.mjs
@@ -0,0 +1,757 @@
+/* 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";
+
+const {
+ saveToDisk,
+ alwaysAsk,
+ useHelperApp,
+ handleInternally,
+ useSystemDefault,
+} = Ci.nsIHandlerInfo;
+
+const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ kHandlerList: "resource://gre/modules/handlers/HandlerList.sys.mjs",
+ kHandlerListVersion: "resource://gre/modules/handlers/HandlerList.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "externalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "MIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+
+export function HandlerService() {
+ // Observe handlersvc-json-replace so we can switch to the datasource
+ Services.obs.addObserver(this, "handlersvc-json-replace", true);
+}
+
+HandlerService.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISupportsWeakReference",
+ "nsIHandlerService",
+ "nsIObserver",
+ ]),
+
+ __store: null,
+ get _store() {
+ if (!this.__store) {
+ this.__store = new lazy.JSONFile({
+ path: PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ "handlers.json"
+ ),
+ dataPostProcessor: this._dataPostProcessor.bind(this),
+ });
+ }
+
+ // Always call this even if this.__store was set, since it may have been
+ // set by asyncInit, which might not have completed yet.
+ this._ensureStoreInitialized();
+ return this.__store;
+ },
+
+ __storeInitialized: false,
+ _ensureStoreInitialized() {
+ if (!this.__storeInitialized) {
+ this.__storeInitialized = true;
+ this.__store.ensureDataReady();
+
+ this._injectDefaultProtocolHandlersIfNeeded();
+ this._migrateProtocolHandlersIfNeeded();
+
+ Services.obs.notifyObservers(null, "handlersvc-store-initialized");
+ }
+ },
+
+ _dataPostProcessor(data) {
+ return data.defaultHandlersVersion
+ ? data
+ : {
+ defaultHandlersVersion: {},
+ mimeTypes: {},
+ schemes: {},
+ isDownloadsImprovementsAlreadyMigrated: false,
+ };
+ },
+
+ /**
+ * Injects new default protocol handlers if the version in the preferences is
+ * newer than the one in the data store.
+ */
+ _injectDefaultProtocolHandlersIfNeeded() {
+ try {
+ let defaultHandlersVersion = Services.prefs.getIntPref(
+ "gecko.handlerService.defaultHandlersVersion",
+ 0
+ );
+ if (defaultHandlersVersion < lazy.kHandlerListVersion) {
+ this._injectDefaultProtocolHandlers();
+ Services.prefs.setIntPref(
+ "gecko.handlerService.defaultHandlersVersion",
+ lazy.kHandlerListVersion
+ );
+ // Now save the result:
+ this._store.saveSoon();
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ },
+
+ _injectDefaultProtocolHandlers() {
+ let locale = Services.locale.appLocaleAsBCP47;
+
+ // Initialize handlers to default and update based on locale.
+ let localeHandlers = lazy.kHandlerList.default;
+ if (lazy.kHandlerList[locale]) {
+ for (let scheme in lazy.kHandlerList[locale].schemes) {
+ localeHandlers.schemes[scheme] =
+ lazy.kHandlerList[locale].schemes[scheme];
+ }
+ }
+
+ // Now, we're going to cheat. Terribly. The idiologically correct way
+ // of implementing the following bit of code would be to fetch the
+ // handler info objects from the protocol service, manipulate those,
+ // and then store each of them.
+ // However, that's expensive. It causes us to talk to the OS about
+ // default apps, which causes the OS to go hit the disk.
+ // All we're trying to do is insert some web apps into the list. We
+ // don't care what's already in the file, we just want to do the
+ // equivalent of appending into the database. So let's just go do that:
+ for (let scheme of Object.keys(localeHandlers.schemes)) {
+ if (scheme == "mailto" && AppConstants.MOZ_APP_NAME == "thunderbird") {
+ // Thunderbird IS a mailto handler, it doesn't need handlers added.
+ continue;
+ }
+
+ let existingSchemeInfo = this._store.data.schemes[scheme];
+ if (!existingSchemeInfo) {
+ // Haven't seen this scheme before. Default to asking which app the
+ // user wants to use:
+ existingSchemeInfo = {
+ // Signal to future readers that we didn't ask the OS anything.
+ // When the entry is first used, get the info from the OS.
+ stubEntry: true,
+ // The first item in the list is the preferred handler, and
+ // there isn't one, so we fill in null:
+ handlers: [null],
+ };
+ this._store.data.schemes[scheme] = existingSchemeInfo;
+ }
+ let { handlers } = existingSchemeInfo;
+ for (let newHandler of localeHandlers.schemes[scheme].handlers) {
+ if (!newHandler.uriTemplate) {
+ console.error(
+ `Ignoring protocol handler for ${scheme} without a uriTemplate!`
+ );
+ continue;
+ }
+ if (!newHandler.uriTemplate.startsWith("https://")) {
+ console.error(
+ `Ignoring protocol handler for ${scheme} with insecure template URL ${newHandler.uriTemplate}.`
+ );
+ continue;
+ }
+ if (!newHandler.uriTemplate.toLowerCase().includes("%s")) {
+ console.error(
+ `Ignoring protocol handler for ${scheme} with invalid template URL ${newHandler.uriTemplate}.`
+ );
+ continue;
+ }
+ // If there is already a handler registered with the same template
+ // URL, ignore the new one:
+ let matchingTemplate = handler =>
+ handler && handler.uriTemplate == newHandler.uriTemplate;
+ if (!handlers.some(matchingTemplate)) {
+ handlers.push(newHandler);
+ }
+ }
+ }
+ },
+
+ /**
+ * Execute any migrations. Migrations are defined here for any changes or removals for
+ * existing handlers. Additions are still handled via the localized prefs infrastructure.
+ *
+ * This depends on the browser.handlers.migrations pref being set by migrateUI in
+ * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products.
+ * This is a comma-separated list of identifiers of migrations that need running.
+ * This avoids both re-running older migrations and keeping an additional
+ * pref around permanently.
+ */
+ _migrateProtocolHandlersIfNeeded() {
+ const kMigrations = {
+ "30boxes": () => {
+ const k30BoxesRegex =
+ /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i;
+ let webcalHandler =
+ lazy.externalProtocolService.getProtocolHandlerInfo("webcal");
+ if (this.exists(webcalHandler)) {
+ this.fillHandlerInfo(webcalHandler, "");
+ let shouldStore = false;
+ // First remove 30boxes from possible handlers.
+ let handlers = webcalHandler.possibleApplicationHandlers;
+ for (let i = handlers.length - 1; i >= 0; i--) {
+ let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
+ if (
+ app instanceof Ci.nsIWebHandlerApp &&
+ k30BoxesRegex.test(app.uriTemplate)
+ ) {
+ shouldStore = true;
+ handlers.removeElementAt(i);
+ }
+ }
+ // Then remove as a preferred handler.
+ if (webcalHandler.preferredApplicationHandler) {
+ let app = webcalHandler.preferredApplicationHandler;
+ if (
+ app instanceof Ci.nsIWebHandlerApp &&
+ k30BoxesRegex.test(app.uriTemplate)
+ ) {
+ webcalHandler.preferredApplicationHandler = null;
+ shouldStore = true;
+ }
+ }
+ // Then store, if we changed anything.
+ if (shouldStore) {
+ this.store(webcalHandler);
+ }
+ }
+ },
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1526890 for context.
+ "secure-mail": () => {
+ const kSubstitutions = new Map([
+ [
+ "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s",
+ "https://mail.yahoo.co.jp/compose/?To=%s",
+ ],
+ [
+ "http://www.inbox.lv/rfc2368/?value=%s",
+ "https://mail.inbox.lv/compose?to=%s",
+ ],
+ [
+ "http://poczta.interia.pl/mh/?mailto=%s",
+ "https://poczta.interia.pl/mh/?mailto=%s",
+ ],
+ [
+ "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s",
+ "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
+ ],
+ ]);
+
+ function maybeReplaceURL(app) {
+ if (app instanceof Ci.nsIWebHandlerApp) {
+ let { uriTemplate } = app;
+ let sub = kSubstitutions.get(uriTemplate);
+ if (sub) {
+ app.uriTemplate = sub;
+ return true;
+ }
+ }
+ return false;
+ }
+ let mailHandler =
+ lazy.externalProtocolService.getProtocolHandlerInfo("mailto");
+ if (this.exists(mailHandler)) {
+ this.fillHandlerInfo(mailHandler, "");
+ let handlers = mailHandler.possibleApplicationHandlers;
+ let shouldStore = false;
+ for (let i = handlers.length - 1; i >= 0; i--) {
+ let app = handlers.queryElementAt(i, Ci.nsIHandlerApp);
+ // Note: will evaluate the RHS because it's a binary rather than
+ // logical or.
+ shouldStore |= maybeReplaceURL(app);
+ }
+ // Then check the preferred handler.
+ if (mailHandler.preferredApplicationHandler) {
+ shouldStore |= maybeReplaceURL(
+ mailHandler.preferredApplicationHandler
+ );
+ }
+ // Then store, if we changed anything. Note that store() handles
+ // duplicates, so we don't have to.
+ if (shouldStore) {
+ this.store(mailHandler);
+ }
+ }
+ },
+ };
+ let migrationsToRun = Services.prefs.getCharPref(
+ "browser.handlers.migrations",
+ ""
+ );
+ migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
+ for (let migration of migrationsToRun) {
+ migration.trim();
+ try {
+ kMigrations[migration]();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ if (migrationsToRun.length) {
+ Services.prefs.clearUserPref("browser.handlers.migrations");
+ }
+ },
+
+ _onDBChange() {
+ return (async () => {
+ if (this.__store) {
+ await this.__store.finalize();
+ }
+ this.__store = null;
+ this.__storeInitialized = false;
+ })().catch(console.error);
+ },
+
+ // nsIObserver
+ observe(subject, topic, data) {
+ if (topic != "handlersvc-json-replace") {
+ return;
+ }
+ let promise = this._onDBChange();
+ promise.then(() => {
+ Services.obs.notifyObservers(null, "handlersvc-json-replace-complete");
+ });
+ },
+
+ // nsIHandlerService
+ asyncInit() {
+ if (!this.__store) {
+ this.__store = new lazy.JSONFile({
+ path: PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ "handlers.json"
+ ),
+ dataPostProcessor: this._dataPostProcessor.bind(this),
+ });
+ this.__store
+ .load()
+ .then(() => {
+ // __store can be null if we called _onDBChange in the mean time.
+ if (this.__store) {
+ this._ensureStoreInitialized();
+ }
+ })
+ .catch(console.error);
+ }
+ },
+
+ // nsIHandlerService
+ enumerate() {
+ let handlers = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ for (let [type, typeInfo] of Object.entries(this._store.data.mimeTypes)) {
+ let primaryExtension = typeInfo.extensions?.[0] ?? null;
+ let handler = lazy.MIMEService.getFromTypeAndExtension(
+ type,
+ primaryExtension
+ );
+ handlers.appendElement(handler);
+ }
+ for (let type of Object.keys(this._store.data.schemes)) {
+ // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive
+ // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo
+ // until one of its properties is accessed.
+ //
+ // Note: our caller still needs to yield periodically when iterating
+ // the enumerator and accessing handler properties to avoid monopolizing
+ // the main thread.
+ //
+ let handler = new Proxy(
+ {
+ QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]),
+ type,
+ get _handlerInfo() {
+ delete this._handlerInfo;
+ return (this._handlerInfo =
+ lazy.externalProtocolService.getProtocolHandlerInfo(type));
+ },
+ },
+ {
+ get(target, name) {
+ return target[name] || target._handlerInfo[name];
+ },
+ set(target, name, value) {
+ target._handlerInfo[name] = value;
+ },
+ }
+ );
+ handlers.appendElement(handler);
+ }
+ return handlers.enumerate(Ci.nsIHandlerInfo);
+ },
+
+ // nsIHandlerService
+ store(handlerInfo) {
+ let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo);
+
+ // Retrieve an existing entry if present, instead of creating a new one, so
+ // that we preserve unknown properties for forward compatibility.
+ let storedHandlerInfo = handlerList[handlerInfo.type];
+ if (!storedHandlerInfo) {
+ storedHandlerInfo = {};
+ handlerList[handlerInfo.type] = storedHandlerInfo;
+ }
+
+ // Only a limited number of preferredAction values is allowed.
+ if (
+ handlerInfo.preferredAction == saveToDisk ||
+ handlerInfo.preferredAction == useSystemDefault ||
+ handlerInfo.preferredAction == handleInternally ||
+ // For files (ie mimetype rather than protocol handling info), ensure
+ // we can store the "always ask" state, too:
+ (handlerInfo.preferredAction == alwaysAsk &&
+ this._isMIMEInfo(handlerInfo))
+ ) {
+ storedHandlerInfo.action = handlerInfo.preferredAction;
+ } else {
+ storedHandlerInfo.action = useHelperApp;
+ }
+
+ if (handlerInfo.alwaysAskBeforeHandling) {
+ storedHandlerInfo.ask = true;
+ } else {
+ delete storedHandlerInfo.ask;
+ }
+
+ // Build a list of unique nsIHandlerInfo instances to process later.
+ let handlers = [];
+ if (handlerInfo.preferredApplicationHandler) {
+ handlers.push(handlerInfo.preferredApplicationHandler);
+ }
+ for (let handler of handlerInfo.possibleApplicationHandlers.enumerate(
+ Ci.nsIHandlerApp
+ )) {
+ // If the caller stored duplicate handlers, we save them only once.
+ if (!handlers.some(h => h.equals(handler))) {
+ handlers.push(handler);
+ }
+ }
+
+ // If any of the nsIHandlerInfo instances cannot be serialized, it is not
+ // included in the final list. The first element is always the preferred
+ // handler, or null if there is none.
+ let serializableHandlers = handlers
+ .map(h => this.handlerAppToSerializable(h))
+ .filter(h => h);
+ if (serializableHandlers.length) {
+ if (!handlerInfo.preferredApplicationHandler) {
+ serializableHandlers.unshift(null);
+ }
+ storedHandlerInfo.handlers = serializableHandlers;
+ } else {
+ delete storedHandlerInfo.handlers;
+ }
+
+ if (this._isMIMEInfo(handlerInfo)) {
+ let extensions = storedHandlerInfo.extensions || [];
+ for (let extension of handlerInfo.getFileExtensions()) {
+ extension = extension.toLowerCase();
+ // If the caller stored duplicate extensions, we save them only once.
+ if (!extensions.includes(extension)) {
+ extensions.push(extension);
+ }
+ }
+ if (extensions.length) {
+ storedHandlerInfo.extensions = extensions;
+ } else {
+ delete storedHandlerInfo.extensions;
+ }
+ }
+
+ // If we're saving *anything*, it stops being a stub:
+ delete storedHandlerInfo.stubEntry;
+
+ this._store.saveSoon();
+
+ // Now notify PDF.js. This is hacky, but a lot better than expecting all
+ // the consumers to do it...
+ if (handlerInfo.type == "application/pdf") {
+ Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED);
+ }
+ },
+
+ // nsIHandlerService
+ fillHandlerInfo(handlerInfo, overrideType) {
+ let type = overrideType || handlerInfo.type;
+ let storedHandlerInfo =
+ this._getHandlerListByHandlerInfoType(handlerInfo)[type];
+ if (!storedHandlerInfo) {
+ throw new Components.Exception(
+ "handlerSvc fillHandlerInfo: don't know this type",
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+
+ let isStub = !!storedHandlerInfo.stubEntry;
+ // In the normal case, this is not a stub, so we can just read stored info
+ // and write to the handlerInfo object we were passed.
+ if (!isStub) {
+ handlerInfo.preferredAction = storedHandlerInfo.action;
+ handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask;
+ } else {
+ // If we've got a stub, ensure the defaults are still set:
+ lazy.externalProtocolService.setProtocolHandlerDefaults(
+ handlerInfo,
+ handlerInfo.hasDefaultHandler
+ );
+ if (
+ handlerInfo.preferredAction == alwaysAsk &&
+ handlerInfo.alwaysAskBeforeHandling
+ ) {
+ // `store` will default to `useHelperApp` because `alwaysAsk` is
+ // not one of the 3 recognized options; for compatibility, do
+ // the same here.
+ handlerInfo.preferredAction = useHelperApp;
+ }
+ }
+ // If it *is* a stub, don't override alwaysAskBeforeHandling or the
+ // preferred actions. Instead, just append the stored handlers, without
+ // overriding the preferred app, and then schedule a task to store proper
+ // info for this handler.
+ this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub);
+
+ if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) {
+ for (let extension of storedHandlerInfo.extensions) {
+ handlerInfo.appendExtension(extension);
+ }
+ } else if (this._mockedHandler) {
+ this._insertMockedHandler(handlerInfo);
+ }
+ },
+
+ /**
+ * Private method to inject stored handler information into an nsIHandlerInfo
+ * instance.
+ * @param handlerInfo the nsIHandlerInfo instance to write to
+ * @param storedHandlers the stored handlers
+ * @param keepPreferredApp whether to keep the handlerInfo's
+ * preferredApplicationHandler or override it
+ * (default: false, ie override it)
+ */
+ _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) {
+ // If the first item is not null, it is also the preferred handler. Since
+ // we cannot modify the stored array, use a boolean to keep track of this.
+ let isFirstItem = true;
+ for (let handler of storedHandlers || [null]) {
+ let handlerApp = this.handlerAppFromSerializable(handler || {});
+ if (isFirstItem) {
+ isFirstItem = false;
+ // Do not overwrite the preferred app unless that's allowed
+ if (!keepPreferredApp) {
+ handlerInfo.preferredApplicationHandler = handlerApp;
+ }
+ }
+ if (handlerApp) {
+ handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
+ }
+ }
+ },
+
+ /**
+ * @param handler
+ * A nsIHandlerApp handler app
+ * @returns Serializable representation of a handler app object.
+ */
+ handlerAppToSerializable(handler) {
+ if (handler instanceof Ci.nsILocalHandlerApp) {
+ return {
+ name: handler.name,
+ path: handler.executable.path,
+ };
+ } else if (handler instanceof Ci.nsIWebHandlerApp) {
+ return {
+ name: handler.name,
+ uriTemplate: handler.uriTemplate,
+ };
+ } else if (handler instanceof Ci.nsIDBusHandlerApp) {
+ return {
+ name: handler.name,
+ service: handler.service,
+ method: handler.method,
+ objectPath: handler.objectPath,
+ dBusInterface: handler.dBusInterface,
+ };
+ } else if (handler instanceof Ci.nsIGIOMimeApp) {
+ return {
+ name: handler.name,
+ command: handler.command,
+ };
+ }
+ // If the handler is an unknown handler type, return null.
+ // Android default application handler is the case.
+ return null;
+ },
+
+ /**
+ * @param handlerObj
+ * Serializable representation of a handler object.
+ * @returns {nsIHandlerApp} the handler app, if any; otherwise null
+ */
+ handlerAppFromSerializable(handlerObj) {
+ let handlerApp;
+ if ("path" in handlerObj) {
+ try {
+ let file = new lazy.FileUtils.File(handlerObj.path);
+ if (!file.exists()) {
+ return null;
+ }
+ handlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ handlerApp.executable = file;
+ } catch (ex) {
+ return null;
+ }
+ } else if ("uriTemplate" in handlerObj) {
+ handlerApp = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(Ci.nsIWebHandlerApp);
+ handlerApp.uriTemplate = handlerObj.uriTemplate;
+ } else if ("service" in handlerObj) {
+ handlerApp = Cc[
+ "@mozilla.org/uriloader/dbus-handler-app;1"
+ ].createInstance(Ci.nsIDBusHandlerApp);
+ handlerApp.service = handlerObj.service;
+ handlerApp.method = handlerObj.method;
+ handlerApp.objectPath = handlerObj.objectPath;
+ handlerApp.dBusInterface = handlerObj.dBusInterface;
+ } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) {
+ try {
+ handlerApp = Cc["@mozilla.org/gio-service;1"]
+ .getService(Ci.nsIGIOService)
+ .createAppFromCommand(handlerObj.command, handlerObj.name);
+ } catch (ex) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+
+ handlerApp.name = handlerObj.name;
+ return handlerApp;
+ },
+
+ /**
+ * The function returns a reference to the "mimeTypes" or "schemes" object
+ * based on which type of handlerInfo is provided.
+ */
+ _getHandlerListByHandlerInfoType(handlerInfo) {
+ return this._isMIMEInfo(handlerInfo)
+ ? this._store.data.mimeTypes
+ : this._store.data.schemes;
+ },
+
+ /**
+ * Determines whether an nsIHandlerInfo instance represents a MIME type.
+ */
+ _isMIMEInfo(handlerInfo) {
+ // We cannot rely only on the instanceof check because on Android both MIME
+ // types and protocols are instances of nsIMIMEInfo. We still do the check
+ // so that properties of nsIMIMEInfo become available to the callers.
+ return (
+ handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/")
+ );
+ },
+
+ // nsIHandlerService
+ exists(handlerInfo) {
+ return (
+ handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo)
+ );
+ },
+
+ // nsIHandlerService
+ remove(handlerInfo) {
+ delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type];
+ this._store.saveSoon();
+ },
+
+ // nsIHandlerService
+ getTypeFromExtension(fileExtension) {
+ let extension = fileExtension.toLowerCase();
+ let mimeTypes = this._store.data.mimeTypes;
+ for (let type of Object.keys(mimeTypes)) {
+ if (
+ mimeTypes[type].extensions &&
+ mimeTypes[type].extensions.includes(extension)
+ ) {
+ return type;
+ }
+ }
+ return "";
+ },
+
+ _mockedHandler: null,
+ _mockedProtocol: null,
+
+ _insertMockedHandler(handlerInfo) {
+ if (handlerInfo.type == this._mockedProtocol) {
+ handlerInfo.preferredApplicationHandler = this._mockedHandler;
+ handlerInfo.possibleApplicationHandlers.insertElementAt(
+ this._mockedHandler,
+ 0
+ );
+ }
+ },
+
+ // test-only: mock the handler instance for a particular protocol/scheme
+ mockProtocolHandler(protocol) {
+ if (!protocol) {
+ this._mockedProtocol = null;
+ this._mockedHandler = null;
+ return;
+ }
+ this._mockedProtocol = protocol;
+ this._mockedHandler = {
+ QueryInterface: ChromeUtils.generateQI([Ci.nsILocalHandlerApp]),
+ launchWithURI(uri, context) {
+ Services.obs.notifyObservers(uri, "mocked-protocol-handler");
+ },
+ name: "Mocked handler",
+ detailedDescription: "Mocked handler for tests",
+ equals(x) {
+ return x == this;
+ },
+ get executable() {
+ if (AppConstants.platform == "macosx") {
+ // We need an app path that isn't us, nor in our app bundle, and
+ // Apple no longer allows us to read the default-shipped apps
+ // in /Applications/ - except for Safari, it would appear!
+ let f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ f.initWithPath("/Applications/Safari.app");
+ return f;
+ }
+ return Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ },
+ parameterCount: 0,
+ clearParameters() {},
+ appendParameter() {},
+ getParameter() {},
+ parameterExists() {
+ return false;
+ },
+ };
+ },
+};
diff --git a/uriloader/exthandler/ExternalHelperAppChild.cpp b/uriloader/exthandler/ExternalHelperAppChild.cpp
new file mode 100644
index 0000000000..569d42e7a4
--- /dev/null
+++ b/uriloader/exthandler/ExternalHelperAppChild.cpp
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ExternalHelperAppChild.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "nsIInputStream.h"
+#include "nsIRequest.h"
+#include "nsIResumableChannel.h"
+#include "nsIPropertyBag2.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(ExternalHelperAppChild, nsIStreamListener, nsIRequestObserver)
+
+ExternalHelperAppChild::ExternalHelperAppChild() : mStatus(NS_OK) {}
+
+ExternalHelperAppChild::~ExternalHelperAppChild() {}
+
+//-----------------------------------------------------------------------------
+// nsIStreamListener
+//-----------------------------------------------------------------------------
+NS_IMETHODIMP
+ExternalHelperAppChild::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* input, uint64_t offset,
+ uint32_t count) {
+ if (NS_FAILED(mStatus)) return mStatus;
+
+ static uint32_t const kCopyChunkSize = 128 * 1024;
+ uint32_t toRead = std::min<uint32_t>(count, kCopyChunkSize);
+
+ nsCString data;
+
+ while (count) {
+ nsresult rv = NS_ReadInputStreamToString(input, data, toRead);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!SendOnDataAvailable(data, offset, toRead))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ count -= toRead;
+ offset += toRead;
+ toRead = std::min<uint32_t>(count, kCopyChunkSize);
+ }
+
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// nsIRequestObserver
+//////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+ExternalHelperAppChild::OnStartRequest(nsIRequest* request) {
+ nsresult rv = mHandler->OnStartRequest(request);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED);
+
+ nsCString entityID;
+ nsCOMPtr<nsIResumableChannel> resumable(do_QueryInterface(request));
+ if (resumable) {
+ resumable->GetEntityID(entityID);
+ }
+ SendOnStartRequest(entityID);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppChild::OnStopRequest(nsIRequest* request, nsresult status) {
+ // mHandler can be null if we diverted the request to the parent
+ if (mHandler) {
+ nsresult rv = mHandler->OnStopRequest(request, status);
+ SendOnStopRequest(status);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED);
+ }
+
+ return NS_OK;
+}
+
+mozilla::ipc::IPCResult ExternalHelperAppChild::RecvCancel(
+ const nsresult& aStatus) {
+ mStatus = aStatus;
+ return IPC_OK();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/uriloader/exthandler/ExternalHelperAppChild.h b/uriloader/exthandler/ExternalHelperAppChild.h
new file mode 100644
index 0000000000..2b189499fe
--- /dev/null
+++ b/uriloader/exthandler/ExternalHelperAppChild.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ExternalHelperAppChild_h
+#define mozilla_dom_ExternalHelperAppChild_h
+
+#include "mozilla/dom/PExternalHelperAppChild.h"
+#include "nsExternalHelperAppService.h"
+#include "nsIStreamListener.h"
+
+class nsIDivertableChannel;
+
+namespace mozilla::dom {
+
+class BrowserChild;
+
+class ExternalHelperAppChild : public PExternalHelperAppChild,
+ public nsIStreamListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+
+ ExternalHelperAppChild();
+
+ // Give the listener a real nsExternalAppHandler to complete processing on
+ // the child.
+ void SetHandler(nsExternalAppHandler* handler) { mHandler = handler; }
+
+ mozilla::ipc::IPCResult RecvCancel(const nsresult& aStatus);
+
+ private:
+ virtual ~ExternalHelperAppChild();
+
+ RefPtr<nsExternalAppHandler> mHandler;
+ nsresult mStatus;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_ExternalHelperAppChild_h
diff --git a/uriloader/exthandler/ExternalHelperAppParent.cpp b/uriloader/exthandler/ExternalHelperAppParent.cpp
new file mode 100644
index 0000000000..96c7444aba
--- /dev/null
+++ b/uriloader/exthandler/ExternalHelperAppParent.cpp
@@ -0,0 +1,477 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/DebugOnly.h"
+
+#include "ExternalHelperAppParent.h"
+#include "nsExternalHelperAppService.h"
+#include "nsIContent.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIExternalHelperAppService.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "nsStringStream.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "nsNetUtil.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "nsQueryObject.h"
+
+#include "mozilla/Unused.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS_INHERITED(ExternalHelperAppParent, nsHashPropertyBag,
+ nsIRequest, nsIChannel, nsIMultiPartChannel,
+ nsIPrivateBrowsingChannel, nsIResumableChannel,
+ nsIStreamListener, nsIExternalHelperAppParent)
+
+ExternalHelperAppParent::ExternalHelperAppParent(
+ nsIURI* uri, const int64_t& aContentLength, const bool& aWasFileChannel,
+ const nsACString& aContentDispositionHeader,
+ const uint32_t& aContentDispositionHint,
+ const nsAString& aContentDispositionFilename)
+ : mURI(uri),
+ mPending(false),
+ mIPCClosed(false),
+ mLoadFlags(0),
+ mStatus(NS_OK),
+ mCanceled(false),
+ mContentLength(aContentLength),
+ mWasFileChannel(aWasFileChannel) {
+ mContentDispositionHeader = aContentDispositionHeader;
+ if (!mContentDispositionHeader.IsEmpty()) {
+ NS_GetFilenameFromDisposition(mContentDispositionFilename,
+ mContentDispositionHeader);
+ mContentDisposition =
+ NS_GetContentDispositionFromHeader(mContentDispositionHeader, this);
+ } else {
+ mContentDisposition = aContentDispositionHint;
+ mContentDispositionFilename = aContentDispositionFilename;
+ }
+}
+
+bool ExternalHelperAppParent::Init(
+ const mozilla::net::LoadInfoArgs& aLoadInfoArgs,
+ const nsACString& aMimeContentType, const bool& aForceSave,
+ nsIURI* aReferrer, BrowsingContext* aContext,
+ const bool& aShouldCloseWindow) {
+ nsresult rv = mozilla::ipc::LoadInfoArgsToLoadInfo(
+ aLoadInfoArgs, ContentParent::Cast(Manager())->GetRemoteType(),
+ getter_AddRefs(mLoadInfo));
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsCOMPtr<nsIExternalHelperAppService> helperAppService =
+ do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID);
+ NS_ASSERTION(helperAppService, "No Helper App Service!");
+
+ if (aReferrer) {
+ SetPropertyAsInterface(u"docshell.internalReferrer"_ns, aReferrer);
+ }
+
+ if (aContext) {
+ WindowGlobalParent* parent =
+ aContext->Canonical()->GetCurrentWindowGlobal();
+ if (parent) {
+ RefPtr<BrowserParent> browser = parent->GetBrowserParent();
+ if (browser) {
+ bool isPrivate = false;
+ nsCOMPtr<nsILoadContext> loadContext = browser->GetLoadContext();
+ loadContext->GetUsePrivateBrowsing(&isPrivate);
+ SetPrivate(isPrivate);
+ }
+ }
+ }
+
+ helperAppService->CreateListener(aMimeContentType, this, aContext, aForceSave,
+ nullptr, getter_AddRefs(mListener));
+ if (!mListener) {
+ return false;
+ }
+
+ if (aShouldCloseWindow) {
+ RefPtr<nsExternalAppHandler> handler = do_QueryObject(mListener);
+ if (handler) {
+ handler->SetShouldCloseWindow();
+ }
+ }
+
+ return true;
+}
+
+void ExternalHelperAppParent::ActorDestroy(ActorDestroyReason why) {
+ mIPCClosed = true;
+}
+
+void ExternalHelperAppParent::Delete() {
+ if (!mIPCClosed) {
+ Unused << Send__delete__(this);
+ }
+}
+
+mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnStartRequest(
+ const nsACString& entityID) {
+ mEntityID = entityID;
+ mPending = true;
+ mStatus = mListener->OnStartRequest(this);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnDataAvailable(
+ const nsACString& data, const uint64_t& offset, const uint32_t& count) {
+ if (NS_FAILED(mStatus)) {
+ return IPC_OK();
+ }
+
+ MOZ_ASSERT(mPending, "must be pending!");
+
+ nsCOMPtr<nsIInputStream> stringStream;
+ DebugOnly<nsresult> rv = NS_NewByteInputStream(
+ getter_AddRefs(stringStream), Span(data).To(count), NS_ASSIGNMENT_DEPEND);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create dependent string!");
+ mStatus = mListener->OnDataAvailable(this, stringStream, offset, count);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnStopRequest(
+ const nsresult& code) {
+ mPending = false;
+ mListener->OnStopRequest(
+ this, (NS_SUCCEEDED(code) && NS_FAILED(mStatus)) ? mStatus : code);
+ Delete();
+ return IPC_OK();
+}
+
+//
+// nsIStreamListener
+//
+
+NS_IMETHODIMP
+ExternalHelperAppParent::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* input, uint64_t offset,
+ uint32_t count) {
+ return mListener->OnDataAvailable(request, input, offset, count);
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::OnStartRequest(nsIRequest* request) {
+ return mListener->OnStartRequest(request);
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::OnStopRequest(nsIRequest* request, nsresult status) {
+ nsresult rv = mListener->OnStopRequest(request, status);
+ Delete();
+ return rv;
+}
+
+ExternalHelperAppParent::~ExternalHelperAppParent() {}
+
+//
+// nsIRequest implementation...
+//
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetName(nsACString& aResult) {
+ if (!mURI) {
+ aResult.Truncate();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mURI->GetAsciiSpec(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::IsPending(bool* aResult) {
+ *aResult = mPending;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetStatus(nsresult* aResult) {
+ *aResult = mStatus;
+ return NS_OK;
+}
+
+NS_IMETHODIMP ExternalHelperAppParent::SetCanceledReason(
+ const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP ExternalHelperAppParent::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP ExternalHelperAppParent::CancelWithReason(
+ nsresult aStatus, const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::Cancel(nsresult aStatus) {
+ mCanceled = true;
+ mStatus = aStatus;
+ Unused << SendCancel(aStatus);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetCanceled(bool* aCanceled) {
+ *aCanceled = mCanceled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::Suspend() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+ExternalHelperAppParent::Resume() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+//
+// nsIChannel implementation
+//
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetOriginalURI(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = mURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetOriginalURI(nsIURI* aURI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetURI(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = mURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::Open(nsIInputStream** aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::AsyncOpen(nsIStreamListener* aListener) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = mLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ mLoadFlags = aLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetIsDocument(bool* aIsDocument) {
+ return NS_GetIsDocumentChannel(this, aIsDocument);
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ *aLoadGroup = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetOwner(nsISupports** aOwner) {
+ *aOwner = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetOwner(nsISupports* aOwner) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetLoadInfo(nsILoadInfo** aLoadInfo) {
+ NS_IF_ADDREF(*aLoadInfo = mLoadInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetLoadInfo(nsILoadInfo* aLoadInfo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aCallbacks) {
+ *aCallbacks = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aCallbacks) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetSecurityInfo(
+ nsITransportSecurityInfo** aSecurityInfo) {
+ *aSecurityInfo = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetContentType(nsACString& aContentType) {
+ aContentType.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetContentType(const nsACString& aContentType) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetContentCharset(nsACString& aContentCharset) {
+ aContentCharset.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetContentCharset(const nsACString& aContentCharset) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetContentDisposition(uint32_t* aContentDisposition) {
+ // NB: mContentDisposition may or may not be set to a non UINT32_MAX value in
+ // nsExternalHelperAppService::DoContentContentProcessHelper
+ if (mContentDispositionHeader.IsEmpty() && mContentDisposition == UINT32_MAX)
+ return NS_ERROR_NOT_AVAILABLE;
+
+ *aContentDisposition = mContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetContentDisposition(uint32_t aContentDisposition) {
+ mContentDisposition = aContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetContentDispositionFilename(
+ nsAString& aContentDispositionFilename) {
+ if (mContentDispositionFilename.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aContentDispositionFilename = mContentDispositionFilename;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetContentDispositionFilename(
+ const nsAString& aContentDispositionFilename) {
+ mContentDispositionFilename = aContentDispositionFilename;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetContentDispositionHeader(
+ nsACString& aContentDispositionHeader) {
+ if (mContentDispositionHeader.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aContentDispositionHeader = mContentDispositionHeader;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetContentLength(int64_t* aContentLength) {
+ if (mContentLength < 0) {
+ *aContentLength = -1;
+ } else {
+ *aContentLength = mContentLength;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::SetContentLength(int64_t aContentLength) {
+ mContentLength = aContentLength;
+ return NS_OK;
+}
+
+//
+// nsIResumableChannel implementation
+//
+
+NS_IMETHODIMP
+ExternalHelperAppParent::ResumeAt(uint64_t startPos,
+ const nsACString& entityID) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetEntityID(nsACString& aEntityID) {
+ aEntityID = mEntityID;
+ return NS_OK;
+}
+
+//
+// nsIMultiPartChannel implementation
+//
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetBaseChannel(nsIChannel** aChannel) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetPartID(uint32_t* aPartID) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetIsFirstPart(bool* aIsLastPart) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ExternalHelperAppParent::GetIsLastPart(bool* aIsLastPart) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/uriloader/exthandler/ExternalHelperAppParent.h b/uriloader/exthandler/ExternalHelperAppParent.h
new file mode 100644
index 0000000000..540889e648
--- /dev/null
+++ b/uriloader/exthandler/ExternalHelperAppParent.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ExternalHelperAppParent_h
+#define mozilla_dom_ExternalHelperAppParent_h
+
+#include "mozilla/dom/PExternalHelperAppParent.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "nsIChannel.h"
+#include "nsIMultiPartChannel.h"
+#include "nsIResumableChannel.h"
+#include "nsIStreamListener.h"
+#include "nsHashPropertyBag.h"
+#include "mozilla/net/PrivateBrowsingChannel.h"
+
+namespace IPC {
+class URI;
+} // namespace IPC
+
+class nsExternalAppHandler;
+
+namespace mozilla {
+
+namespace net {
+class PChannelDiverterParent;
+} // namespace net
+
+namespace dom {
+
+#define NS_IEXTERNALHELPERAPPPARENT_IID \
+ { \
+ 0x127a01bc, 0x2a49, 0x46a8, { \
+ 0x8c, 0x63, 0x4b, 0x5d, 0x3c, 0xa4, 0x07, 0x9c \
+ } \
+ }
+
+class nsIExternalHelperAppParent : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IEXTERNALHELPERAPPPARENT_IID)
+
+ /**
+ * Returns true if this fake channel represented a file channel in the child.
+ */
+ virtual bool WasFileChannel() = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIExternalHelperAppParent,
+ NS_IEXTERNALHELPERAPPPARENT_IID)
+
+class ContentParent;
+class PBrowserParent;
+
+class ExternalHelperAppParent
+ : public PExternalHelperAppParent,
+ public nsHashPropertyBag,
+ public nsIChannel,
+ public nsIMultiPartChannel,
+ public nsIResumableChannel,
+ public nsIStreamListener,
+ public net::PrivateBrowsingChannel<ExternalHelperAppParent>,
+ public nsIExternalHelperAppParent {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIREQUEST
+ NS_DECL_NSICHANNEL
+ NS_DECL_NSIMULTIPARTCHANNEL
+ NS_DECL_NSIRESUMABLECHANNEL
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+
+ mozilla::ipc::IPCResult RecvOnStartRequest(
+ const nsACString& entityID) override;
+ mozilla::ipc::IPCResult RecvOnDataAvailable(const nsACString& data,
+ const uint64_t& offset,
+ const uint32_t& count) override;
+ mozilla::ipc::IPCResult RecvOnStopRequest(const nsresult& code) override;
+
+ bool WasFileChannel() override { return mWasFileChannel; }
+
+ ExternalHelperAppParent(nsIURI* uri, const int64_t& contentLength,
+ const bool& wasFileChannel,
+ const nsACString& aContentDispositionHeader,
+ const uint32_t& aContentDispositionHint,
+ const nsAString& aContentDispositionFilename);
+ bool Init(const mozilla::net::LoadInfoArgs& aLoadInfoArgs,
+ const nsACString& aMimeContentType, const bool& aForceSave,
+ nsIURI* aReferrer, BrowsingContext* aContext,
+ const bool& aShouldCloseWindow);
+
+ protected:
+ virtual ~ExternalHelperAppParent();
+
+ virtual void ActorDestroy(ActorDestroyReason why) override;
+ void Delete();
+
+ private:
+ RefPtr<nsIStreamListener> mListener;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsILoadInfo> mLoadInfo;
+ bool mPending;
+ bool mIPCClosed;
+ nsLoadFlags mLoadFlags;
+ nsresult mStatus;
+ bool mCanceled;
+ int64_t mContentLength;
+ bool mWasFileChannel;
+ uint32_t mContentDisposition;
+ nsString mContentDispositionFilename;
+ nsCString mContentDispositionHeader;
+ nsCString mEntityID;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif
diff --git a/uriloader/exthandler/HandlerList.sys.mjs b/uriloader/exthandler/HandlerList.sys.mjs
new file mode 100644
index 0000000000..e95d627c07
--- /dev/null
+++ b/uriloader/exthandler/HandlerList.sys.mjs
@@ -0,0 +1,206 @@
+/* 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/. */
+
+export const kHandlerListVersion = 1;
+
+export const kHandlerList = {
+ default: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ },
+ },
+ },
+ cs: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Seznam",
+ uriTemplate: "https://email.seznam.cz/newMessageScreen?mailto=%s",
+ },
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ },
+ },
+ },
+ "es-CL": {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ {
+ name: "Outlook",
+ uriTemplate:
+ "https://outlook.live.com/default.aspx?rru=compose&to=%s",
+ },
+ ],
+ },
+ },
+ },
+ "ja-JP-mac": {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Yahoo!メール",
+ uriTemplate: "https://mail.yahoo.co.jp/compose/?To=%s",
+ },
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ },
+ },
+ },
+ ja: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Yahoo!メール",
+ uriTemplate: "https://mail.yahoo.co.jp/compose/?To=%s",
+ },
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ },
+ },
+ },
+ kk: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Яндекс.Почта",
+ uriTemplate: "https://mail.yandex.ru/compose?mailto=%s",
+ },
+ {
+ name: "Mail.Ru",
+ uriTemplate: "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
+ },
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ },
+ },
+ },
+ ltg: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ {
+ name: "inbox.lv mail",
+ uriTemplate: "https://mail.inbox.lv/compose?to=%s",
+ },
+ ],
+ },
+ },
+ },
+ lv: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ {
+ name: "inbox.lv mail",
+ uriTemplate: "https://mail.inbox.lv/compose?to=%s",
+ },
+ ],
+ },
+ },
+ },
+ pl: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Poczta Interia.pl",
+ uriTemplate: "https://poczta.interia.pl/mh/?mailto=%s",
+ },
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ },
+ },
+ },
+ ru: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Яндекс.Почту",
+ uriTemplate: "https://mail.yandex.ru/compose?mailto=%s",
+ },
+ {
+ name: "Mail.Ru",
+ uriTemplate: "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
+ },
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ },
+ },
+ },
+ uk: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ {
+ name: "Outlook",
+ uriTemplate:
+ "https://outlook.live.com/default.aspx?rru=compose&to=%s",
+ },
+ ],
+ },
+ },
+ },
+ uz: {
+ schemes: {
+ mailto: {
+ handlers: [
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ {
+ name: "Mail.Ru",
+ uriTemplate: "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/uriloader/exthandler/HandlerServiceChild.h b/uriloader/exthandler/HandlerServiceChild.h
new file mode 100644
index 0000000000..5c58a3c230
--- /dev/null
+++ b/uriloader/exthandler/HandlerServiceChild.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef handler_service_child_h
+#define handler_service_child_h
+
+#include "mozilla/dom/PHandlerServiceChild.h"
+
+namespace mozilla {
+
+class HandlerServiceChild final : public mozilla::dom::PHandlerServiceChild {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(HandlerServiceChild, final)
+ HandlerServiceChild() {}
+
+ private:
+ virtual ~HandlerServiceChild() {}
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/exthandler/HandlerServiceParent.cpp b/uriloader/exthandler/HandlerServiceParent.cpp
new file mode 100644
index 0000000000..ab77657dd5
--- /dev/null
+++ b/uriloader/exthandler/HandlerServiceParent.cpp
@@ -0,0 +1,381 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/Logging.h"
+#include "HandlerServiceParent.h"
+#include "nsIHandlerService.h"
+#include "nsIMIMEInfo.h"
+#include "nsIMIMEService.h"
+#include "ContentHandlerService.h"
+#include "nsIExternalProtocolService.h"
+#include "nsStringEnumerator.h"
+#include "nsIMutableArray.h"
+#include "nsCExternalHandlerService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#ifdef MOZ_WIDGET_GTK
+# include "unix/nsGNOMERegistry.h"
+#endif
+
+using mozilla::dom::ContentHandlerService;
+using mozilla::dom::HandlerApp;
+using mozilla::dom::HandlerInfo;
+using mozilla::dom::RemoteHandlerApp;
+
+namespace {
+
+class ProxyHandlerInfo final : public nsIHandlerInfo {
+ public:
+ explicit ProxyHandlerInfo(const HandlerInfo& aHandlerInfo);
+ NS_DECL_ISUPPORTS;
+ NS_DECL_NSIHANDLERINFO;
+
+ nsTArray<nsCString>& Extensions() { return mHandlerInfo.extensions(); }
+
+ protected:
+ ~ProxyHandlerInfo() {}
+ HandlerInfo mHandlerInfo;
+ nsHandlerInfoAction mPrefAction;
+ nsCOMPtr<nsIMutableArray> mPossibleApps;
+};
+
+NS_IMPL_ISUPPORTS(ProxyHandlerInfo, nsIHandlerInfo)
+
+ProxyHandlerInfo::ProxyHandlerInfo(const HandlerInfo& aHandlerInfo)
+ : mHandlerInfo(aHandlerInfo),
+ mPrefAction(nsIHandlerInfo::alwaysAsk),
+ mPossibleApps(do_CreateInstance(NS_ARRAY_CONTRACTID)) {
+ for (auto& happ : aHandlerInfo.possibleApplicationHandlers()) {
+ mPossibleApps->AppendElement(new RemoteHandlerApp(happ));
+ }
+}
+
+/* readonly attribute ACString type; */
+NS_IMETHODIMP ProxyHandlerInfo::GetType(nsACString& aType) {
+ aType.Assign(mHandlerInfo.type());
+ return NS_OK;
+}
+
+/* attribute AString description; */
+NS_IMETHODIMP ProxyHandlerInfo::GetDescription(nsAString& aDescription) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+NS_IMETHODIMP ProxyHandlerInfo::SetDescription(const nsAString& aDescription) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* attribute nsIHandlerApp preferredApplicationHandler; */
+NS_IMETHODIMP ProxyHandlerInfo::GetPreferredApplicationHandler(
+ nsIHandlerApp** aPreferredApplicationHandler) {
+ *aPreferredApplicationHandler =
+ new RemoteHandlerApp(mHandlerInfo.preferredApplicationHandler());
+ NS_IF_ADDREF(*aPreferredApplicationHandler);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ProxyHandlerInfo::SetPreferredApplicationHandler(
+ nsIHandlerApp* aApp) {
+ nsString name;
+ nsString detailedDescription;
+ if (aApp) {
+ aApp->GetName(name);
+ aApp->GetDetailedDescription(detailedDescription);
+ }
+
+ mHandlerInfo.preferredApplicationHandler() =
+ HandlerApp(name, detailedDescription);
+ return NS_OK;
+}
+
+/* readonly attribute nsIMutableArray possibleApplicationHandlers; */
+NS_IMETHODIMP ProxyHandlerInfo::GetPossibleApplicationHandlers(
+ nsIMutableArray** aPossibleApplicationHandlers) {
+ *aPossibleApplicationHandlers = mPossibleApps;
+ NS_IF_ADDREF(*aPossibleApplicationHandlers);
+ return NS_OK;
+}
+
+/* readonly attribute boolean hasDefaultHandler; */
+NS_IMETHODIMP ProxyHandlerInfo::GetHasDefaultHandler(bool* aHasDefaultHandler) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* readonly attribute AString defaultDescription; */
+NS_IMETHODIMP ProxyHandlerInfo::GetDefaultDescription(
+ nsAString& aDefaultDescription) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void launchWithURI (in nsIURI aURI,
+ [optional] in BrowsingContext aBrowsingContext); */
+NS_IMETHODIMP ProxyHandlerInfo::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* attribute ProxyHandlerInfoAction preferredAction; */
+NS_IMETHODIMP ProxyHandlerInfo::GetPreferredAction(
+ nsHandlerInfoAction* aPreferredAction) {
+ *aPreferredAction = mPrefAction;
+ return NS_OK;
+}
+NS_IMETHODIMP ProxyHandlerInfo::SetPreferredAction(
+ nsHandlerInfoAction aPreferredAction) {
+ mHandlerInfo.preferredAction() = aPreferredAction;
+ mPrefAction = aPreferredAction;
+ return NS_OK;
+}
+
+/* attribute boolean alwaysAskBeforeHandling; */
+NS_IMETHODIMP ProxyHandlerInfo::GetAlwaysAskBeforeHandling(
+ bool* aAlwaysAskBeforeHandling) {
+ *aAlwaysAskBeforeHandling = mHandlerInfo.alwaysAskBeforeHandling();
+ return NS_OK;
+}
+NS_IMETHODIMP ProxyHandlerInfo::SetAlwaysAskBeforeHandling(
+ bool aAlwaysAskBeforeHandling) {
+ mHandlerInfo.alwaysAskBeforeHandling() = aAlwaysAskBeforeHandling;
+ return NS_OK;
+}
+
+class ProxyMIMEInfo : public nsIMIMEInfo {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMIMEINFO
+ NS_FORWARD_NSIHANDLERINFO(mProxyHandlerInfo->);
+
+ explicit ProxyMIMEInfo(const HandlerInfo& aHandlerInfo)
+ : mProxyHandlerInfo(new ProxyHandlerInfo(aHandlerInfo)) {}
+
+ private:
+ virtual ~ProxyMIMEInfo() {}
+ RefPtr<ProxyHandlerInfo> mProxyHandlerInfo;
+
+ protected:
+ /* additional members */
+};
+
+NS_IMPL_ISUPPORTS(ProxyMIMEInfo, nsIMIMEInfo, nsIHandlerInfo)
+
+/* nsIUTF8StringEnumerator getFileExtensions (); */
+NS_IMETHODIMP ProxyMIMEInfo::GetFileExtensions(
+ nsIUTF8StringEnumerator** _retval) {
+ return NS_NewUTF8StringEnumerator(_retval, &mProxyHandlerInfo->Extensions(),
+ this);
+}
+
+/* void setFileExtensions (in AUTF8String aExtensions); */
+NS_IMETHODIMP ProxyMIMEInfo::SetFileExtensions(const nsACString& aExtensions) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* boolean extensionExists (in AUTF8String aExtension); */
+NS_IMETHODIMP ProxyMIMEInfo::ExtensionExists(const nsACString& aExtension,
+ bool* _retval) {
+ *_retval = mProxyHandlerInfo->Extensions().Contains(
+ aExtension, nsCaseInsensitiveCStringArrayComparator());
+ return NS_OK;
+}
+
+/* void appendExtension (in AUTF8String aExtension); */
+NS_IMETHODIMP ProxyMIMEInfo::AppendExtension(const nsACString& aExtension) {
+ if (!aExtension.IsEmpty() &&
+ !mProxyHandlerInfo->Extensions().Contains(
+ aExtension, nsCaseInsensitiveCStringArrayComparator())) {
+ mProxyHandlerInfo->Extensions().AppendElement(aExtension);
+ }
+ return NS_OK;
+}
+
+/* attribute AUTF8String primaryExtension; */
+NS_IMETHODIMP ProxyMIMEInfo::GetPrimaryExtension(
+ nsACString& aPrimaryExtension) {
+ const auto& extensions = mProxyHandlerInfo->Extensions();
+ if (extensions.IsEmpty()) {
+ aPrimaryExtension.Truncate();
+ return NS_ERROR_FAILURE;
+ }
+ aPrimaryExtension = extensions[0];
+ return NS_OK;
+}
+
+NS_IMETHODIMP ProxyMIMEInfo::SetPrimaryExtension(
+ const nsACString& aPrimaryExtension) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* readonly attribute ACString MIMEType; */
+NS_IMETHODIMP ProxyMIMEInfo::GetMIMEType(nsACString& aMIMEType) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* boolean equals (in nsIMIMEInfo aMIMEInfo); */
+NS_IMETHODIMP ProxyMIMEInfo::Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* readonly attribute nsIArray possibleLocalHandlers; */
+NS_IMETHODIMP ProxyMIMEInfo::GetPossibleLocalHandlers(
+ nsIArray** aPossibleLocalHandlers) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void launchWithFile (in nsIFile aFile); */
+NS_IMETHODIMP ProxyMIMEInfo::LaunchWithFile(nsIFile* aFile) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* boolean isCurrentAppOSDefault(); */
+NS_IMETHODIMP ProxyMIMEInfo::IsCurrentAppOSDefault(bool* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+static already_AddRefed<nsIHandlerInfo> WrapHandlerInfo(
+ const HandlerInfo& aHandlerInfo) {
+ nsCOMPtr<nsIHandlerInfo> info;
+ if (aHandlerInfo.isMIMEInfo()) {
+ info = new ProxyMIMEInfo(aHandlerInfo);
+ } else {
+ info = new ProxyHandlerInfo(aHandlerInfo);
+ }
+ return info.forget();
+}
+
+} // anonymous namespace
+
+HandlerServiceParent::HandlerServiceParent() {}
+
+HandlerServiceParent::~HandlerServiceParent() {}
+
+mozilla::ipc::IPCResult HandlerServiceParent::RecvFillHandlerInfo(
+ const HandlerInfo& aHandlerInfoData, const nsACString& aOverrideType,
+ HandlerInfo* handlerInfoData) {
+ nsCOMPtr<nsIHandlerInfo> info(WrapHandlerInfo(aHandlerInfoData));
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID);
+ handlerSvc->FillHandlerInfo(info, aOverrideType);
+ ContentHandlerService::nsIHandlerInfoToHandlerInfo(info, handlerInfoData);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult HandlerServiceParent::RecvGetMIMEInfoFromOS(
+ const nsACString& aMIMEType, const nsACString& aExtension, nsresult* aRv,
+ HandlerInfo* aHandlerInfoData, bool* aFound) {
+ *aFound = false;
+ if (aMIMEType.Length() > MAX_MIMETYPE_LENGTH ||
+ aExtension.Length() > MAX_EXT_LENGTH) {
+ *aRv = NS_OK;
+ return IPC_OK();
+ }
+
+ nsCOMPtr<nsIMIMEService> mimeService =
+ do_GetService(NS_MIMESERVICE_CONTRACTID, aRv);
+ if (NS_WARN_IF(NS_FAILED(*aRv))) {
+ return IPC_OK();
+ }
+
+ nsCOMPtr<nsIMIMEInfo> mimeInfo;
+ *aRv = mimeService->GetMIMEInfoFromOS(aMIMEType, aExtension, aFound,
+ getter_AddRefs(mimeInfo));
+ if (NS_WARN_IF(NS_FAILED(*aRv))) {
+ return IPC_OK();
+ }
+
+ if (mimeInfo) {
+ ContentHandlerService::nsIHandlerInfoToHandlerInfo(mimeInfo,
+ aHandlerInfoData);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult HandlerServiceParent::RecvExists(
+ const HandlerInfo& aHandlerInfo, bool* exists) {
+ nsCOMPtr<nsIHandlerInfo> info(WrapHandlerInfo(aHandlerInfo));
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID);
+ handlerSvc->Exists(info, exists);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult HandlerServiceParent::RecvExistsForProtocolOS(
+ const nsACString& aProtocolScheme, bool* aHandlerExists) {
+ if (aProtocolScheme.Length() > MAX_SCHEME_LENGTH) {
+ *aHandlerExists = false;
+ return IPC_OK();
+ }
+#ifdef MOZ_WIDGET_GTK
+ // Check the GNOME registry for a protocol handler
+ *aHandlerExists =
+ nsGNOMERegistry::HandlerExists(PromiseFlatCString(aProtocolScheme).get());
+#else
+ *aHandlerExists = false;
+#endif
+ return IPC_OK();
+}
+
+/*
+ * Check if a handler exists for the provided protocol. Check the datastore
+ * first and then fallback to checking the OS for a handler.
+ */
+mozilla::ipc::IPCResult HandlerServiceParent::RecvExistsForProtocol(
+ const nsACString& aProtocolScheme, bool* aHandlerExists) {
+ if (aProtocolScheme.Length() > MAX_SCHEME_LENGTH) {
+ *aHandlerExists = false;
+ return IPC_OK();
+ }
+ // Check the datastore and fallback to an OS check.
+ // ExternalProcotolHandlerExists() does the fallback.
+ nsresult rv;
+ nsCOMPtr<nsIExternalProtocolService> protoSvc =
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aHandlerExists = false;
+ return IPC_OK();
+ }
+ rv = protoSvc->ExternalProtocolHandlerExists(
+ PromiseFlatCString(aProtocolScheme).get(), aHandlerExists);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aHandlerExists = false;
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult HandlerServiceParent::RecvGetTypeFromExtension(
+ const nsACString& aFileExtension, nsCString* type) {
+ if (aFileExtension.Length() > MAX_EXT_LENGTH) {
+ return IPC_OK();
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IPC_OK();
+ }
+
+ rv = handlerSvc->GetTypeFromExtension(aFileExtension, *type);
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult HandlerServiceParent::RecvGetApplicationDescription(
+ const nsACString& aScheme, nsresult* aRv, nsString* aDescription) {
+ if (aScheme.Length() > MAX_SCHEME_LENGTH) {
+ *aRv = NS_ERROR_NOT_AVAILABLE;
+ return IPC_OK();
+ }
+ nsCOMPtr<nsIExternalProtocolService> protoSvc =
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID);
+ NS_ASSERTION(protoSvc, "No Helper App Service!");
+ *aRv = protoSvc->GetApplicationDescription(aScheme, *aDescription);
+ return IPC_OK();
+}
+
+void HandlerServiceParent::ActorDestroy(ActorDestroyReason aWhy) {}
diff --git a/uriloader/exthandler/HandlerServiceParent.h b/uriloader/exthandler/HandlerServiceParent.h
new file mode 100644
index 0000000000..a8952f743e
--- /dev/null
+++ b/uriloader/exthandler/HandlerServiceParent.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef handler_service_parent_h
+#define handler_service_parent_h
+
+#include "mozilla/dom/PHandlerServiceParent.h"
+#include "nsIMIMEInfo.h"
+
+class nsIHandlerApp;
+
+class HandlerServiceParent final : public mozilla::dom::PHandlerServiceParent {
+ public:
+ HandlerServiceParent();
+ NS_INLINE_DECL_REFCOUNTING(HandlerServiceParent, final)
+
+ private:
+ virtual ~HandlerServiceParent();
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvFillHandlerInfo(
+ const HandlerInfo& aHandlerInfoData, const nsACString& aOverrideType,
+ HandlerInfo* handlerInfoData) override;
+
+ mozilla::ipc::IPCResult RecvGetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aExtension,
+ nsresult* aRv,
+ HandlerInfo* aHandlerInfoData,
+ bool* aFound) override;
+
+ mozilla::ipc::IPCResult RecvExists(const HandlerInfo& aHandlerInfo,
+ bool* exists) override;
+
+ mozilla::ipc::IPCResult RecvGetTypeFromExtension(
+ const nsACString& aFileExtension, nsCString* type) override;
+
+ mozilla::ipc::IPCResult RecvExistsForProtocolOS(
+ const nsACString& aProtocolScheme, bool* aHandlerExists) override;
+
+ mozilla::ipc::IPCResult RecvExistsForProtocol(
+ const nsACString& aProtocolScheme, bool* aHandlerExists) override;
+
+ mozilla::ipc::IPCResult RecvGetApplicationDescription(
+ const nsACString& aScheme, nsresult* aRv,
+ nsString* aDescription) override;
+
+ /*
+ * Limit the length of MIME types, filename extensions, and protocol
+ * schemes we'll consider.
+ */
+ static const size_t MAX_MIMETYPE_LENGTH = 129; /* Per RFC 6838, type and
+ subtype should be limited
+ to 64 characters. We add
+ one more to account for
+ a '/' separator. */
+ static const size_t MAX_EXT_LENGTH = 64; /* Arbitratily chosen to be
+ longer than any known
+ extension */
+ static const size_t MAX_SCHEME_LENGTH = 1024; /* Arbitratily chosen to be
+ longer than any known
+ protocol scheme */
+};
+
+#endif
diff --git a/uriloader/exthandler/PExternalHelperApp.ipdl b/uriloader/exthandler/PExternalHelperApp.ipdl
new file mode 100644
index 0000000000..1a57de7cc0
--- /dev/null
+++ b/uriloader/exthandler/PExternalHelperApp.ipdl
@@ -0,0 +1,29 @@
+/* vim: set ft=cpp: */
+/* 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 protocol PBrowser;
+include protocol PContent;
+
+namespace mozilla {
+namespace dom {
+
+[ParentImpl=virtual]
+protocol PExternalHelperApp
+{
+ manager PContent;
+
+parent:
+ async OnStartRequest(nsCString entityID);
+ async OnDataAvailable(nsCString data, uint64_t offset, uint32_t count);
+ async OnStopRequest(nsresult code);
+
+child:
+ async Cancel(nsresult aStatus);
+ async __delete__();
+};
+
+
+} // namespace dom
+} // namespace mozilla
diff --git a/uriloader/exthandler/PHandlerService.ipdl b/uriloader/exthandler/PHandlerService.ipdl
new file mode 100644
index 0000000000..bcd1e74668
--- /dev/null
+++ b/uriloader/exthandler/PHandlerService.ipdl
@@ -0,0 +1,62 @@
+/* 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 protocol PContent;
+
+namespace mozilla {
+namespace dom {
+
+struct HandlerApp {
+ nsString name;
+ nsString detailedDescription;
+};
+
+struct HandlerInfo {
+ nsCString type;
+ bool isMIMEInfo;
+ nsString description;
+ bool alwaysAskBeforeHandling;
+ nsCString[] extensions;
+ HandlerApp preferredApplicationHandler;
+ HandlerApp[] possibleApplicationHandlers;
+ int32_t preferredAction;
+};
+
+[ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PHandlerService
+{
+ manager PContent;
+
+parent:
+ sync FillHandlerInfo(HandlerInfo aHandlerInfoData,
+ nsCString aOverrideType)
+ returns (HandlerInfo handlerInfoData);
+
+ /*
+ * Check if an OS handler exists for the given protocol scheme.
+ */
+ sync ExistsForProtocolOS(nsCString aProtocolScheme)
+ returns (bool exists);
+
+ /*
+ * Check if a handler exists for the given protocol scheme. Check
+ * the datastore first and then fallback to an OS handler check.
+ */
+ sync ExistsForProtocol(nsCString aProtocolScheme)
+ returns (bool exists);
+
+ sync Exists(HandlerInfo aHandlerInfo)
+ returns (bool exists);
+ sync GetTypeFromExtension(nsCString aFileExtension)
+ returns (nsCString type);
+ sync GetMIMEInfoFromOS(nsCString aMIMEType, nsCString aExtension)
+ returns (nsresult rv, HandlerInfo handlerInfoData, bool found);
+ sync GetApplicationDescription(nsCString aScheme)
+ returns (nsresult rv, nsString description);
+ async __delete__();
+};
+
+
+} // namespace dom
+} // namespace mozilla
diff --git a/uriloader/exthandler/WebHandlerApp.sys.mjs b/uriloader/exthandler/WebHandlerApp.sys.mjs
new file mode 100644
index 0000000000..229561c64d
--- /dev/null
+++ b/uriloader/exthandler/WebHandlerApp.sys.mjs
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+export function nsWebHandlerApp() {}
+
+nsWebHandlerApp.prototype = {
+ classDescription: "A web handler for protocols and content",
+ classID: Components.ID("8b1ae382-51a9-4972-b930-56977a57919d"),
+ contractID: "@mozilla.org/uriloader/web-handler-app;1",
+ QueryInterface: ChromeUtils.generateQI(["nsIWebHandlerApp", "nsIHandlerApp"]),
+
+ _name: null,
+ _detailedDescription: null,
+ _uriTemplate: null,
+
+ // nsIHandlerApp
+
+ get name() {
+ return this._name;
+ },
+
+ set name(aName) {
+ this._name = aName;
+ },
+
+ get detailedDescription() {
+ return this._detailedDescription;
+ },
+
+ set detailedDescription(aDesc) {
+ this._detailedDescription = aDesc;
+ },
+
+ equals(aHandlerApp) {
+ if (!aHandlerApp) {
+ throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ if (
+ aHandlerApp instanceof Ci.nsIWebHandlerApp &&
+ aHandlerApp.uriTemplate &&
+ this.uriTemplate &&
+ aHandlerApp.uriTemplate == this.uriTemplate
+ ) {
+ return true;
+ }
+ return false;
+ },
+
+ launchWithURI(aURI, aBrowsingContext) {
+ // XXX need to strip passwd & username from URI to handle, as per the
+ // WhatWG HTML5 draft. nsSimpleURL, which is what we're going to get,
+ // can't do this directly. Ideally, we'd fix nsStandardURL to make it
+ // possible to turn off all of its quirks handling, and use that...
+
+ let { scheme } = aURI;
+ if (scheme == "ftp" || scheme == "ftps" || scheme == "sftp") {
+ // FTP URLs are parsed by nsStandardURL, so clearing the username and
+ // password does not throw.
+ aURI = aURI.mutate().setUserPass("").finalize();
+ }
+
+ // encode the URI to be handled
+ var escapedUriSpecToHandle = encodeURIComponent(aURI.spec);
+
+ // insert the encoded URI and create the object version.
+ var uriToSend = Services.io.newURI(
+ this.uriTemplate.replace("%s", escapedUriSpecToHandle)
+ );
+
+ let policy = WebExtensionPolicy.getByURI(uriToSend);
+ let privateAllowed = !policy || policy.privateBrowsingAllowed;
+
+ if (!scheme.startsWith("web+") && !scheme.startsWith("ext+")) {
+ // If we're in a frame, check if we're a built-in scheme, in which case,
+ // override the target browsingcontext. It's not a good idea to try to
+ // load mail clients or other apps with potential for logged in data into
+ // iframes, and in any case it's unlikely to work due to framing
+ // restrictions employed by the target site.
+ if (aBrowsingContext && aBrowsingContext != aBrowsingContext.top) {
+ aBrowsingContext = null;
+ }
+
+ // Unset the browsing context when in a pinned tab and changing hosts
+ // to force loading the mail handler in a new unpinned tab.
+ if (aBrowsingContext?.top.isAppTab) {
+ let docURI = aBrowsingContext.currentWindowGlobal.documentURI;
+ let docHost, sendHost;
+
+ try {
+ docHost = docURI?.host;
+ sendHost = uriToSend?.host;
+ } catch (e) {}
+
+ // Special case: ignore "www" prefix if it is part of host string
+ if (docHost?.startsWith("www.")) {
+ docHost = docHost.replace(/^www\./, "");
+ }
+ if (sendHost?.startsWith("www.")) {
+ sendHost = sendHost.replace(/^www\./, "");
+ }
+
+ if (docHost != sendHost) {
+ aBrowsingContext = null;
+ }
+ }
+ }
+
+ // if we have a context, use the URI loader to load there
+ if (aBrowsingContext) {
+ if (aBrowsingContext.usePrivateBrowsing && !privateAllowed) {
+ throw Components.Exception(
+ "Extension not allowed in private windows.",
+ Cr.NS_ERROR_FILE_NOT_FOUND
+ );
+ }
+
+ let triggeringPrincipal =
+ Services.scriptSecurityManager.getSystemPrincipal();
+ Services.tm.dispatchToMainThread(() =>
+ aBrowsingContext.loadURI(uriToSend, { triggeringPrincipal })
+ );
+ return;
+ }
+
+ // The window type depends on the app.
+ const windowType =
+ AppConstants.MOZ_APP_NAME == "thunderbird"
+ ? "mail:3pane"
+ : "navigator:browser";
+ let win = Services.wm.getMostRecentWindow(windowType);
+
+ // If this is an extension handler, check private browsing access.
+ if (!privateAllowed && lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
+ throw Components.Exception(
+ "Extension not allowed in private windows.",
+ Cr.NS_ERROR_FILE_NOT_FOUND
+ );
+ }
+
+ // If we get an exception, there are several possible reasons why:
+ // a) this gecko embedding doesn't provide an nsIBrowserDOMWindow
+ // implementation (i.e. doesn't support browser-style functionality),
+ // so we need to kick the URL out to the OS default browser. This is
+ // the subject of bug 394479.
+ // b) this embedding does provide an nsIBrowserDOMWindow impl, but
+ // there doesn't happen to be a browser window open at the moment; one
+ // should be opened. It's not clear whether this situation will really
+ // ever occur in real life. If it does, the only API that I can find
+ // that seems reasonably likely to work for most embedders is the
+ // command line handler.
+ // c) something else went wrong
+ //
+ // It's not clear how one would differentiate between the three cases
+ // above, so for now we don't catch the exception.
+
+ // openURI
+ win.browserDOMWindow.openURI(
+ uriToSend,
+ null, // no window.opener
+ Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
+ Ci.nsIBrowserDOMWindow.OPEN_NEW,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ },
+
+ // nsIWebHandlerApp
+
+ get uriTemplate() {
+ return this._uriTemplate;
+ },
+
+ set uriTemplate(aURITemplate) {
+ this._uriTemplate = aURITemplate;
+ },
+};
diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp
new file mode 100644
index 0000000000..86049a55fb
--- /dev/null
+++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp
@@ -0,0 +1,340 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; 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/. */
+#include "nsMIMEInfoAndroid.h"
+#include "nsArrayUtils.h"
+#include "nsISupportsUtils.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsMIMEInfoAndroid, nsIMIMEInfo, nsIHandlerInfo)
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::LaunchDefaultWithFile(nsIFile* aFile) {
+ return LaunchWithFile(aFile);
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::LoadUriInternal(nsIURI* aURI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+bool nsMIMEInfoAndroid::GetMimeInfoForMimeType(const nsACString& aMimeType,
+ nsMIMEInfoAndroid** aMimeInfo) {
+ RefPtr<nsMIMEInfoAndroid> info = new nsMIMEInfoAndroid(aMimeType);
+ info.forget(aMimeInfo);
+ return false;
+}
+
+bool nsMIMEInfoAndroid::GetMimeInfoForFileExt(const nsACString& aFileExt,
+ nsMIMEInfoAndroid** aMimeInfo) {
+ nsCString mimeType;
+ if (jni::IsAvailable()) {
+ auto javaString = java::GeckoAppShell::GetMimeTypeFromExtensions(aFileExt);
+ if (javaString) {
+ mimeType = javaString->ToCString();
+ }
+ }
+
+ // "*/*" means that the bridge didn't know.
+ if (mimeType.Equals(nsDependentCString("*/*"),
+ nsCaseInsensitiveCStringComparator)) {
+ return false;
+ }
+
+ bool found = GetMimeInfoForMimeType(mimeType, aMimeInfo);
+ (*aMimeInfo)->SetPrimaryExtension(aFileExt);
+ return found;
+}
+
+/**
+ * Returns MIME info for the aURL, which may contain the whole URL or only a
+ * protocol
+ */
+nsresult nsMIMEInfoAndroid::GetMimeInfoForURL(const nsACString& aURL,
+ bool* found,
+ nsIHandlerInfo** info) {
+ nsMIMEInfoAndroid* mimeinfo = new nsMIMEInfoAndroid(aURL);
+ NS_ADDREF(*info = mimeinfo);
+ *found = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetType(nsACString& aType) {
+ aType.Assign(mType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetDescription(nsAString& aDesc) {
+ aDesc.Assign(mDescription);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::SetDescription(const nsAString& aDesc) {
+ mDescription.Assign(aDesc);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetPreferredApplicationHandler(nsIHandlerApp** aApp) {
+ *aApp = mPrefApp;
+ NS_IF_ADDREF(*aApp);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::SetPreferredApplicationHandler(nsIHandlerApp* aApp) {
+ mPrefApp = aApp;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetPossibleApplicationHandlers(
+ nsIMutableArray** aHandlerApps) {
+ if (!mHandlerApps) mHandlerApps = do_CreateInstance(NS_ARRAY_CONTRACTID);
+
+ if (!mHandlerApps) return NS_ERROR_OUT_OF_MEMORY;
+
+ *aHandlerApps = mHandlerApps;
+ NS_IF_ADDREF(*aHandlerApps);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetHasDefaultHandler(bool* aHasDefault) {
+ uint32_t len;
+ *aHasDefault = false;
+ if (!mHandlerApps) return NS_OK;
+
+ if (NS_FAILED(mHandlerApps->GetLength(&len))) return NS_OK;
+
+ if (len == 0) return NS_OK;
+
+ *aHasDefault = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetDefaultDescription(nsAString& aDesc) {
+ aDesc.Assign(u""_ns);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) {
+ return mPrefApp->LaunchWithURI(aURI, aBrowsingContext);
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetPreferredAction(nsHandlerInfoAction* aPrefAction) {
+ *aPrefAction = mPrefAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::SetPreferredAction(nsHandlerInfoAction aPrefAction) {
+ mPrefAction = aPrefAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetAlwaysAskBeforeHandling(bool* aAlwaysAsk) {
+ *aAlwaysAsk = mAlwaysAsk;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::SetAlwaysAskBeforeHandling(bool aAlwaysAsk) {
+ mAlwaysAsk = aAlwaysAsk;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetFileExtensions(nsIUTF8StringEnumerator** aResult) {
+ return NS_NewUTF8StringEnumerator(aResult, &mExtensions, this);
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::SetFileExtensions(const nsACString& aExtensions) {
+ mExtensions.Clear();
+ nsACString::const_iterator start, end;
+ aExtensions.BeginReading(start);
+ aExtensions.EndReading(end);
+ while (start != end) {
+ nsACString::const_iterator cursor = start;
+ mozilla::Unused << FindCharInReadable(',', cursor, end);
+ AddUniqueExtension(Substring(start, cursor));
+ // If a comma was found, skip it for the next search.
+ start = cursor != end ? ++cursor : cursor;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::ExtensionExists(const nsACString& aExtension,
+ bool* aRetVal) {
+ NS_ASSERTION(!aExtension.IsEmpty(), "no extension");
+
+ nsCString mimeType;
+ if (jni::IsAvailable()) {
+ auto javaString =
+ java::GeckoAppShell::GetMimeTypeFromExtensions(aExtension);
+ if (javaString) {
+ mimeType = javaString->ToCString();
+ }
+ }
+
+ // "*/*" means the bridge didn't find anything (i.e., extension doesn't
+ // exist).
+ *aRetVal = !mimeType.Equals(nsDependentCString("*/*"),
+ nsCaseInsensitiveCStringComparator);
+ return NS_OK;
+}
+
+void nsMIMEInfoAndroid::AddUniqueExtension(const nsACString& aExtension) {
+ if (!aExtension.IsEmpty() &&
+ !mExtensions.Contains(aExtension,
+ nsCaseInsensitiveCStringArrayComparator())) {
+ mExtensions.AppendElement(aExtension);
+ }
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::AppendExtension(const nsACString& aExtension) {
+ MOZ_ASSERT(!aExtension.IsEmpty(), "No extension");
+ AddUniqueExtension(aExtension);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetPrimaryExtension(nsACString& aPrimaryExtension) {
+ if (!mExtensions.Length()) {
+ aPrimaryExtension.Truncate();
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ aPrimaryExtension = mExtensions[0];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::SetPrimaryExtension(const nsACString& aExtension) {
+ if (MOZ_UNLIKELY(aExtension.IsEmpty())) {
+ // Don't assert since Java may return an empty extension for unknown types.
+ return NS_ERROR_INVALID_ARG;
+ }
+ int32_t i = mExtensions.IndexOf(aExtension, 0,
+ nsCaseInsensitiveCStringArrayComparator());
+ if (i != -1) {
+ mExtensions.RemoveElementAt(i);
+ }
+ mExtensions.InsertElementAt(0, aExtension);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetMIMEType(nsACString& aMIMEType) {
+ aMIMEType.Assign(mType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::Equals(nsIMIMEInfo* aMIMEInfo, bool* aRetVal) {
+ if (!aMIMEInfo) return NS_ERROR_NULL_POINTER;
+
+ nsAutoCString type;
+ nsresult rv = aMIMEInfo->GetMIMEType(type);
+ if (NS_FAILED(rv)) return rv;
+
+ *aRetVal = mType.Equals(type);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::GetPossibleLocalHandlers(nsIArray** aPossibleLocalHandlers) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::LaunchWithFile(nsIFile* aFile) {
+ nsCOMPtr<nsIURI> uri;
+ NS_NewFileURI(getter_AddRefs(uri), aFile);
+ return LoadUriInternal(uri);
+}
+
+NS_IMETHODIMP
+nsMIMEInfoAndroid::IsCurrentAppOSDefault(bool* aRetVal) {
+ // FIXME: this should in theory be meaningfully implemented. However, android
+ // implements its own version of nsIHandlerApp instances which internally
+ // have package and class names - but do not expose those. So to meaningfully
+ // compare the handler app would require access to those and knowing what
+ // our own package/class names are, and it's not clear how to do that.
+ // It also seems less important to do this right on Android, given that
+ // Android UI normally limits what apps you can associate with what files, so
+ // it shouldn't be possible to get into the same kind of loop as on desktop.
+ *aRetVal = false;
+ return NS_OK;
+}
+
+nsMIMEInfoAndroid::nsMIMEInfoAndroid(const nsACString& aMIMEType)
+ : mType(aMIMEType),
+ mAlwaysAsk(true),
+ mPrefAction(nsIMIMEInfo::useHelperApp) {
+ mPrefApp = new nsMIMEInfoAndroid::SystemChooser(this);
+ nsresult rv;
+ mHandlerApps = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ mHandlerApps->AppendElement(mPrefApp);
+}
+
+#define SYSTEMCHOOSER_NAME u"Android chooser"
+#define SYSTEMCHOOSER_DESCRIPTION \
+ u"Android's default handler application chooser"
+
+NS_IMPL_ISUPPORTS(nsMIMEInfoAndroid::SystemChooser, nsIHandlerApp)
+
+nsresult nsMIMEInfoAndroid::SystemChooser::GetName(nsAString& aName) {
+ aName.AssignLiteral(SYSTEMCHOOSER_NAME);
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoAndroid::SystemChooser::SetName(const nsAString&) {
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoAndroid::SystemChooser::GetDetailedDescription(
+ nsAString& aDesc) {
+ aDesc.AssignLiteral(SYSTEMCHOOSER_DESCRIPTION);
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoAndroid::SystemChooser::SetDetailedDescription(
+ const nsAString&) {
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoAndroid::SystemChooser::Equals(nsIHandlerApp* aHandlerApp,
+ bool* aRetVal) {
+ *aRetVal = false;
+ if (!aHandlerApp) {
+ return NS_OK;
+ }
+
+ nsAutoString name;
+ nsAutoString detailedDescription;
+ aHandlerApp->GetName(name);
+ aHandlerApp->GetDetailedDescription(detailedDescription);
+
+ *aRetVal = name.Equals(SYSTEMCHOOSER_NAME) &&
+ detailedDescription.Equals(SYSTEMCHOOSER_DESCRIPTION);
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoAndroid::SystemChooser::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext*) {
+ return mOuter->LoadUriInternal(aURI);
+}
diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.h b/uriloader/exthandler/android/nsMIMEInfoAndroid.h
new file mode 100644
index 0000000000..48693fd99c
--- /dev/null
+++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.h
@@ -0,0 +1,62 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; 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/. */
+
+#ifndef nsMIMEInfoAndroid_h
+#define nsMIMEInfoAndroid_h
+
+#include "nsMIMEInfoImpl.h"
+#include "nsIMutableArray.h"
+#include "mozilla/java/GeckoAppShellWrappers.h"
+
+class nsMIMEInfoAndroid final : public nsIMIMEInfo {
+ public:
+ [[nodiscard]] static bool GetMimeInfoForMimeType(
+ const nsACString& aMimeType, nsMIMEInfoAndroid** aMimeInfo);
+ [[nodiscard]] static bool GetMimeInfoForFileExt(
+ const nsACString& aFileExt, nsMIMEInfoAndroid** aMimeInfo);
+
+ [[nodiscard]] static nsresult GetMimeInfoForURL(const nsACString& aURL,
+ bool* found,
+ nsIHandlerInfo** info);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMIMEINFO
+ NS_DECL_NSIHANDLERINFO
+
+ explicit nsMIMEInfoAndroid(const nsACString& aMIMEType);
+
+ private:
+ ~nsMIMEInfoAndroid() {}
+
+ /**
+ * Internal helper to avoid adding duplicates.
+ */
+ void AddUniqueExtension(const nsACString& aExtension);
+
+ [[nodiscard]] virtual nsresult LaunchDefaultWithFile(nsIFile* aFile);
+ [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI);
+ nsCOMPtr<nsIMutableArray> mHandlerApps;
+ nsCString mType;
+ nsTArray<nsCString> mExtensions;
+ bool mAlwaysAsk;
+ nsHandlerInfoAction mPrefAction;
+ nsString mDescription;
+ nsCOMPtr<nsIHandlerApp> mPrefApp;
+
+ public:
+ class SystemChooser final : public nsIHandlerApp {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHANDLERAPP
+ explicit SystemChooser(nsMIMEInfoAndroid* aOuter) : mOuter(aOuter) {}
+
+ private:
+ ~SystemChooser() {}
+
+ nsMIMEInfoAndroid* mOuter;
+ };
+};
+
+#endif /* nsMIMEInfoAndroid_h */
diff --git a/uriloader/exthandler/android/nsOSHelperAppService.cpp b/uriloader/exthandler/android/nsOSHelperAppService.cpp
new file mode 100644
index 0000000000..29634af7b6
--- /dev/null
+++ b/uriloader/exthandler/android/nsOSHelperAppService.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; 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/. */
+
+#include "nsOSHelperAppService.h"
+#include "nsMIMEInfoAndroid.h"
+
+nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() {}
+
+nsOSHelperAppService::~nsOSHelperAppService() {}
+
+nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt,
+ bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) {
+ RefPtr<nsMIMEInfoAndroid> mimeInfo;
+ *aFound = false;
+ if (!aMIMEType.IsEmpty())
+ *aFound = nsMIMEInfoAndroid::GetMimeInfoForMimeType(
+ aMIMEType, getter_AddRefs(mimeInfo));
+ if (!*aFound)
+ *aFound = nsMIMEInfoAndroid::GetMimeInfoForFileExt(
+ aFileExt, getter_AddRefs(mimeInfo));
+
+ // Code that calls this requires an object regardless if the OS has
+ // something for us, so we return the empty object.
+ if (!*aFound) mimeInfo = new nsMIMEInfoAndroid(aMIMEType);
+
+ mimeInfo.forget(aMIMEInfo);
+ return NS_OK;
+}
+
+nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aScheme,
+ bool* aExists) {
+ // Support any URI barring a couple schemes we use in testing; let the
+ // app decide what to do with them.
+ nsAutoCString scheme(aScheme);
+ *aExists =
+ !scheme.Equals("unsupported"_ns) && !scheme.Equals("unknownextproto"_ns);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme,
+ nsAString& _retval) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
+ const nsACString& aScheme, bool* _retval) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult nsOSHelperAppService::GetProtocolHandlerInfoFromOS(
+ const nsACString& aScheme, bool* found, nsIHandlerInfo** info) {
+ // We don't want to get protocol handlers from the OS in GV; the app
+ // should take care of that in NavigationDelegate.onLoadRequest().
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/uriloader/exthandler/android/nsOSHelperAppService.h b/uriloader/exthandler/android/nsOSHelperAppService.h
new file mode 100644
index 0000000000..a333c4bcd3
--- /dev/null
+++ b/uriloader/exthandler/android/nsOSHelperAppService.h
@@ -0,0 +1,38 @@
+/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; 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/. */
+
+#ifndef nsOSHelperAppService_h
+#define nsOSHelperAppService_h
+
+#include "nsCExternalHandlerService.h"
+#include "nsExternalHelperAppService.h"
+
+class nsOSHelperAppService : public nsExternalHelperAppService {
+ public:
+ nsOSHelperAppService();
+ virtual ~nsOSHelperAppService();
+
+ nsresult GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) override;
+
+ [[nodiscard]] nsresult OSProtocolHandlerExists(const char* aScheme,
+ bool* aExists) override;
+
+ NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) override;
+ NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
+ nsAString& _retval) override;
+ NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval) override;
+
+ static nsIHandlerApp* CreateAndroidHandlerApp(
+ const nsAString& aName, const nsAString& aDescription,
+ const nsAString& aPackageName, const nsAString& aClassName,
+ const nsACString& aMimeType, const nsAString& aAction = u""_ns);
+};
+
+#endif /* nsOSHelperAppService_h */
diff --git a/uriloader/exthandler/components.conf b/uriloader/exthandler/components.conf
new file mode 100644
index 0000000000..5c3b738c10
--- /dev/null
+++ b/uriloader/exthandler/components.conf
@@ -0,0 +1,20 @@
+# -*- 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': '{220cc253-b60f-41f6-b9cf-fdcb325f970f}',
+ 'contract_ids': ['@mozilla.org/uriloader/handler-service-parent;1'],
+ 'esModule': 'resource://gre/modules/ExtHandlerService.sys.mjs',
+ 'constructor': 'HandlerService',
+ },
+ {
+ 'cid': '{8b1ae382-51a9-4972-b930-56977a57919d}',
+ 'contract_ids': ['@mozilla.org/uriloader/web-handler-app;1'],
+ 'esModule': 'resource://gre/modules/WebHandlerApp.sys.mjs',
+ 'constructor': 'nsWebHandlerApp',
+ },
+]
diff --git a/uriloader/exthandler/docs/index.rst b/uriloader/exthandler/docs/index.rst
new file mode 100644
index 0000000000..40088ab6d3
--- /dev/null
+++ b/uriloader/exthandler/docs/index.rst
@@ -0,0 +1,76 @@
+.. _external_helper_app_service:
+
+External Helper App Service
+===========================
+
+The external helper app service is responsible for deciding how to handle an
+attempt to load come content that cannot be loaded by the browser itself.
+
+Part of this involves using the Handler Service which manages the users
+preferences for what to do by default with different content.
+
+When a Link is Clicked
+----------------------
+
+When a link in a page is clicked (or a form submitted) ``nsDocShell`` tests
+whether the target protocol can be loaded by the browser itself, this is based
+on the preferences under ``network.protocol-handler``. When the browser cannot
+load the protocol it calls into ``nsExternalHelperAppService::LoadURI``.
+
+Some validation checks are performed but ultimateley we look for a registered
+protocol handler. First the OS is queried for an app registration for the
+protocol and then the handler server is asked to fill in any user settings from
+the internal database. If there were no settings from the handler service then
+some defaults are applied in ``nsExternalHelperAppService::SetProtocolHandlerDefaults``.
+
+If there is a default handler app chosen and the settings say to use it without
+asking then that happens. If not a dialog s shown asking the user what they
+want to do.
+
+During a Load
+-------------
+
+When content is already being loaded the :ref:`URI Loader Service <uri_loader_service>`
+determines whether the browser can handle the content or not. If not it calls
+into the external helper app server through ``nsExternalHelperAppService::DoContent``.
+
+The content type of the loading content is retrieved from the channel. A file
+extension is also generated using the Content-Disposition header or if the load
+is not a HTTP POST request the file extension is generated from the requested URL.
+
+We then query the MIME Service for an nsIMIMEInfo to find information about
+apps that can handle the content type or file extension based on OS and user
+settings, :ref:`see below <mime_service>` for further details. The result is
+used to create a ``nsExternalAppHandler`` which is then used as a stream listener
+for the content.
+
+The MIME info object contains settings that control whether to prompt the user
+before doing anything and what the default action should be. If we need to ask
+the user then a dialog is shown offering to let users cancel the load, save the
+content to disk or send it to a registered application handler.
+
+Assuming the load isn't canceled the content is streamed to disk using a background
+file saver with a target ``nsITransfer``. The ``nsITransfer`` is responsible for
+showing the download in the UI.
+
+If the user opted to open the file with an application then once the transfer is
+complete then ``nsIMIMEInfo::LaunchWithFile`` is used to
+`launch the application <https://searchfox.org/mozilla-central/search?q=nsIMIMEInfo%3A%3ALaunchWithFile&path=>`_.
+
+.. _mime_service:
+
+MIME Service
+------------
+
+The MIME service is responsible for getting an ``nsIMIMEInfo`` object for a
+content type or file extension:
+
+1. Fills out an ``nsIMIMEInfo`` based on OS provided information. This is platform
+ specific but should try to find the default application registered to handle
+ the content.
+2. Ask the handler service to fill out the ``nsIMIMEInfo`` with information held
+ in browser settings. This will not overwrite a any application found from
+ the OS.
+3. If one has not been found already then try to find a type description from
+ a `lookup table <https://searchfox.org/mozilla-central/search?q=extraMimeEntries[]&path=>`_
+ or just by appending " File" to the file extension.
diff --git a/uriloader/exthandler/mac/nsDecodeAppleFile.cpp b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp
new file mode 100644
index 0000000000..69f13ab8c8
--- /dev/null
+++ b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp
@@ -0,0 +1,361 @@
+/* -*- Mode: C++; tab-width: 4; 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 "nsDecodeAppleFile.h"
+#include "nsCRT.h"
+
+NS_IMPL_ADDREF(nsDecodeAppleFile)
+NS_IMPL_RELEASE(nsDecodeAppleFile)
+
+NS_INTERFACE_MAP_BEGIN(nsDecodeAppleFile)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIOutputStream)
+NS_INTERFACE_MAP_END
+
+nsDecodeAppleFile::nsDecodeAppleFile() {
+ m_state = parseHeaders;
+ m_dataBufferLength = 0;
+ m_dataBuffer = (unsigned char*)malloc(MAX_BUFFERSIZE);
+ m_entries = nullptr;
+ m_rfRefNum = -1;
+ m_totalDataForkWritten = 0;
+ m_totalResourceForkWritten = 0;
+ m_headerOk = false;
+
+ m_comment[0] = 0;
+ memset(&m_dates, 0, sizeof(m_dates));
+ memset(&m_finderInfo, 0, sizeof(m_dates));
+ memset(&m_finderExtraInfo, 0, sizeof(m_dates));
+}
+
+nsDecodeAppleFile::~nsDecodeAppleFile() {
+ free(m_dataBuffer);
+ m_dataBuffer = nullptr;
+ if (m_entries) delete[] m_entries;
+}
+
+NS_IMETHODIMP nsDecodeAppleFile::Initialize(nsIOutputStream* output,
+ nsIFile* file) {
+ m_output = output;
+
+ nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(file);
+ macFile->GetTargetFSSpec(&m_fsFileSpec);
+
+ m_offset = 0;
+ m_dataForkOffset = 0;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDecodeAppleFile::Close(void) {
+ nsresult rv;
+ rv = m_output->Close();
+
+ int32_t i;
+
+ if (m_rfRefNum != -1) FSClose(m_rfRefNum);
+
+ /* Check if the file is complete and if it's the case, write file attributes
+ */
+ if (m_headerOk) {
+ bool dataOk = true; /* It's ok if the file doesn't have a datafork,
+ therefore set it to true by default. */
+ if (m_headers.magic == APPLESINGLE_MAGIC) {
+ for (i = 0; i < m_headers.entriesCount; i++)
+ if (ENT_DFORK == m_entries[i].id) {
+ dataOk = (bool)(m_totalDataForkWritten == m_entries[i].length);
+ break;
+ }
+ }
+
+ bool resourceOk = FALSE;
+ for (i = 0; i < m_headers.entriesCount; i++)
+ if (ENT_RFORK == m_entries[i].id) {
+ resourceOk = (bool)(m_totalResourceForkWritten == m_entries[i].length);
+ break;
+ }
+
+ if (dataOk && resourceOk) {
+ HFileInfo* fpb;
+ CInfoPBRec cipbr;
+
+ fpb = (HFileInfo*)&cipbr;
+ fpb->ioVRefNum = m_fsFileSpec.vRefNum;
+ fpb->ioDirID = m_fsFileSpec.parID;
+ fpb->ioNamePtr = m_fsFileSpec.name;
+ fpb->ioFDirIndex = 0;
+ PBGetCatInfoSync(&cipbr);
+
+ /* set finder info */
+ memcpy(&fpb->ioFlFndrInfo, &m_finderInfo, sizeof(FInfo));
+ memcpy(&fpb->ioFlXFndrInfo, &m_finderExtraInfo, sizeof(FXInfo));
+ fpb->ioFlFndrInfo.fdFlags &=
+ 0xfc00; /* clear flags maintained by finder */
+
+ /* set file dates */
+ fpb->ioFlCrDat = m_dates.create - CONVERT_TIME;
+ fpb->ioFlMdDat = m_dates.modify - CONVERT_TIME;
+ fpb->ioFlBkDat = m_dates.backup - CONVERT_TIME;
+
+ /* update file info */
+ fpb->ioDirID = fpb->ioFlParID;
+ PBSetCatInfoSync(&cipbr);
+
+ /* set comment */
+ IOParam vinfo;
+ GetVolParmsInfoBuffer vp;
+ DTPBRec dtp;
+
+ memset((void*)&vinfo, 0, sizeof(vinfo));
+ vinfo.ioVRefNum = fpb->ioVRefNum;
+ vinfo.ioBuffer = (Ptr)&vp;
+ vinfo.ioReqCount = sizeof(vp);
+ if (PBHGetVolParmsSync((HParmBlkPtr)&vinfo) == noErr &&
+ ((vp.vMAttrib >> bHasDesktopMgr) & 1)) {
+ memset((void*)&dtp, 0, sizeof(dtp));
+ dtp.ioVRefNum = fpb->ioVRefNum;
+ if (PBDTGetPath(&dtp) == noErr) {
+ dtp.ioDTBuffer = (Ptr)&m_comment[1];
+ dtp.ioNamePtr = fpb->ioNamePtr;
+ dtp.ioDirID = fpb->ioDirID;
+ dtp.ioDTReqCount = m_comment[0];
+ if (PBDTSetCommentSync(&dtp) == noErr) PBDTFlushSync(&dtp);
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsDecodeAppleFile::Flush(void) { return m_output->Flush(); }
+
+NS_IMETHODIMP nsDecodeAppleFile::StreamStatus(void) {
+ return m_output->StreamStatus();
+}
+
+NS_IMETHODIMP nsDecodeAppleFile::WriteFrom(nsIInputStream* inStr,
+ uint32_t count, uint32_t* _retval) {
+ return m_output->WriteFrom(inStr, count, _retval);
+}
+
+NS_IMETHODIMP nsDecodeAppleFile::WriteSegments(nsReadSegmentFun reader,
+ void* closure, uint32_t count,
+ uint32_t* _retval) {
+ return m_output->WriteSegments(reader, closure, count, _retval);
+}
+
+NS_IMETHODIMP nsDecodeAppleFile::IsNonBlocking(bool* aNonBlocking) {
+ return m_output->IsNonBlocking(aNonBlocking);
+}
+
+NS_IMETHODIMP nsDecodeAppleFile::Write(const char* buffer, uint32_t bufferSize,
+ uint32_t* writeCount) {
+ /* WARNING: to simplify my life, I presume that I should get all appledouble
+ headers in the first block, else I would have to implement a buffer */
+
+ const char* buffPtr = buffer;
+ uint32_t dataCount;
+ int32_t i;
+ nsresult rv = NS_OK;
+
+ *writeCount = 0;
+
+ while (bufferSize > 0 && NS_SUCCEEDED(rv)) {
+ switch (m_state) {
+ case parseHeaders:
+ dataCount = sizeof(ap_header) - m_dataBufferLength;
+ if (dataCount > bufferSize) dataCount = bufferSize;
+ memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount);
+ m_dataBufferLength += dataCount;
+
+ if (m_dataBufferLength == sizeof(ap_header)) {
+ memcpy(&m_headers, m_dataBuffer, sizeof(ap_header));
+
+ /* Check header to be sure we are dealing with the right kind of data,
+ * else just write it to the data fork. */
+ if ((m_headers.magic == APPLEDOUBLE_MAGIC ||
+ m_headers.magic == APPLESINGLE_MAGIC) &&
+ m_headers.version == VERSION && m_headers.entriesCount) {
+ /* Just to be sure, the filler must contains only 0 */
+ for (i = 0; i < 4 && m_headers.fill[i] == 0L; i++)
+ ;
+ if (i == 4) m_state = parseEntries;
+ }
+ m_dataBufferLength = 0;
+
+ if (m_state == parseHeaders) {
+ dataCount = 0;
+ m_state = parseWriteThrough;
+ }
+ }
+ break;
+
+ case parseEntries:
+ if (!m_entries) {
+ m_entries = new ap_entry[m_headers.entriesCount];
+ if (!m_entries) return NS_ERROR_OUT_OF_MEMORY;
+ }
+ uint32_t entriesSize = sizeof(ap_entry) * m_headers.entriesCount;
+ dataCount = entriesSize - m_dataBufferLength;
+ if (dataCount > bufferSize) dataCount = bufferSize;
+ memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount);
+ m_dataBufferLength += dataCount;
+
+ if (m_dataBufferLength == entriesSize) {
+ for (i = 0; i < m_headers.entriesCount; i++) {
+ memcpy(&m_entries[i], &m_dataBuffer[i * sizeof(ap_entry)],
+ sizeof(ap_entry));
+ if (m_headers.magic == APPLEDOUBLE_MAGIC) {
+ uint32_t offset = m_entries[i].offset + m_entries[i].length;
+ if (offset > m_dataForkOffset) m_dataForkOffset = offset;
+ }
+ }
+ m_headerOk = true;
+ m_state = parseLookupPart;
+ }
+ break;
+
+ case parseLookupPart:
+ /* which part are we parsing? */
+ m_currentPartID = -1;
+ for (i = 0; i < m_headers.entriesCount; i++)
+ if (m_offset == m_entries[i].offset && m_entries[i].length) {
+ m_currentPartID = m_entries[i].id;
+ m_currentPartLength = m_entries[i].length;
+ m_currentPartCount = 0;
+
+ switch (m_currentPartID) {
+ case ENT_DFORK:
+ m_state = parseDataFork;
+ break;
+ case ENT_RFORK:
+ m_state = parseResourceFork;
+ break;
+
+ case ENT_COMMENT:
+ case ENT_DATES:
+ case ENT_FINFO:
+ m_dataBufferLength = 0;
+ m_state = parsePart;
+ break;
+
+ default:
+ m_state = parseSkipPart;
+ break;
+ }
+ break;
+ }
+
+ if (m_currentPartID == -1) {
+ /* maybe is the datafork of an appledouble file? */
+ if (m_offset == m_dataForkOffset) {
+ m_currentPartID = ENT_DFORK;
+ m_currentPartLength = -1;
+ m_currentPartCount = 0;
+ m_state = parseDataFork;
+ } else
+ dataCount = 1;
+ }
+ break;
+
+ case parsePart:
+ dataCount = m_currentPartLength - m_dataBufferLength;
+ if (dataCount > bufferSize) dataCount = bufferSize;
+ memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount);
+ m_dataBufferLength += dataCount;
+
+ if (m_dataBufferLength == m_currentPartLength) {
+ switch (m_currentPartID) {
+ case ENT_COMMENT:
+ m_comment[0] =
+ m_currentPartLength > 255 ? 255 : m_currentPartLength;
+ memcpy(&m_comment[1], buffPtr, m_comment[0]);
+ break;
+ case ENT_DATES:
+ if (m_currentPartLength == sizeof(m_dates))
+ memcpy(&m_dates, buffPtr, m_currentPartLength);
+ break;
+ case ENT_FINFO:
+ if (m_currentPartLength ==
+ (sizeof(m_finderInfo) + sizeof(m_finderExtraInfo))) {
+ memcpy(&m_finderInfo, buffPtr, sizeof(m_finderInfo));
+ memcpy(&m_finderExtraInfo, buffPtr + sizeof(m_finderInfo),
+ sizeof(m_finderExtraInfo));
+ }
+ break;
+ }
+ m_state = parseLookupPart;
+ }
+ break;
+
+ case parseSkipPart:
+ dataCount = m_currentPartLength - m_currentPartCount;
+ if (dataCount > bufferSize)
+ dataCount = bufferSize;
+ else
+ m_state = parseLookupPart;
+ break;
+
+ case parseDataFork:
+ if (m_headers.magic == APPLEDOUBLE_MAGIC)
+ dataCount = bufferSize;
+ else {
+ dataCount = m_currentPartLength - m_currentPartCount;
+ if (dataCount > bufferSize)
+ dataCount = bufferSize;
+ else
+ m_state = parseLookupPart;
+ }
+
+ if (m_output) {
+ uint32_t writeCount;
+ rv = m_output->Write((const char*)buffPtr, dataCount, &writeCount);
+ if (dataCount != writeCount) rv = NS_ERROR_FAILURE;
+ m_totalDataForkWritten += dataCount;
+ }
+
+ break;
+
+ case parseResourceFork:
+ dataCount = m_currentPartLength - m_currentPartCount;
+ if (dataCount > bufferSize)
+ dataCount = bufferSize;
+ else
+ m_state = parseLookupPart;
+
+ if (m_rfRefNum == -1) {
+ if (noErr != FSpOpenRF(&m_fsFileSpec, fsWrPerm, &m_rfRefNum))
+ return NS_ERROR_FAILURE;
+ }
+
+ long count = dataCount;
+ if (noErr != FSWrite(m_rfRefNum, &count, buffPtr) || count != dataCount)
+ return NS_ERROR_FAILURE;
+ m_totalResourceForkWritten += dataCount;
+ break;
+
+ case parseWriteThrough:
+ dataCount = bufferSize;
+ if (m_output) {
+ uint32_t writeCount;
+ rv = m_output->Write((const char*)buffPtr, dataCount, &writeCount);
+ if (dataCount != writeCount) rv = NS_ERROR_FAILURE;
+ }
+ break;
+ }
+
+ if (dataCount) {
+ *writeCount += dataCount;
+ bufferSize -= dataCount;
+ buffPtr += dataCount;
+ m_currentPartCount += dataCount;
+ m_offset += dataCount;
+ dataCount = 0;
+ }
+ }
+
+ return rv;
+}
diff --git a/uriloader/exthandler/mac/nsDecodeAppleFile.h b/uriloader/exthandler/mac/nsDecodeAppleFile.h
new file mode 100644
index 0000000000..22279a8a49
--- /dev/null
+++ b/uriloader/exthandler/mac/nsDecodeAppleFile.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 4; 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 nsDecodeAppleFile_h__
+#define nsDecodeAppleFile_h__
+
+#include "nscore.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+#include "nsIOutputStream.h"
+
+/*
+** applefile definitions used
+*/
+#if PRAGMA_STRUCT_ALIGN
+# pragma options align = mac68k
+#endif
+
+#define APPLESINGLE_MAGIC 0x00051600L
+#define APPLEDOUBLE_MAGIC 0x00051607L
+#define VERSION 0x00020000
+
+#define NUM_ENTRIES 6
+
+#define ENT_DFORK 1L
+#define ENT_RFORK 2L
+#define ENT_NAME 3L
+#define ENT_COMMENT 4L
+#define ENT_DATES 8L
+#define ENT_FINFO 9L
+
+#define CONVERT_TIME 1265437696L
+
+/*
+** data type used in the header decoder.
+*/
+typedef struct ap_header {
+ int32_t magic;
+ int32_t version;
+ int32_t fill[4];
+ int16_t entriesCount;
+
+} ap_header;
+
+typedef struct ap_entry {
+ int32_t id;
+ int32_t offset;
+ int32_t length;
+
+} ap_entry;
+
+typedef struct ap_dates {
+ int32_t create, modify, backup, access;
+
+} ap_dates;
+
+#if PRAGMA_STRUCT_ALIGN
+# pragma options align = reset
+#endif
+
+/*
+**Error codes
+*/
+enum { errADNotEnoughData = -12099, errADNotSupported, errADBadVersion };
+
+class nsDecodeAppleFile : public nsIOutputStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOUTPUTSTREAM
+
+ nsDecodeAppleFile();
+ virtual ~nsDecodeAppleFile();
+
+ [[nodiscard]] nsresult Initialize(nsIOutputStream* output, nsIFile* file);
+
+ private:
+#define MAX_BUFFERSIZE 1024
+ enum ParserState {
+ parseHeaders,
+ parseEntries,
+ parseLookupPart,
+ parsePart,
+ parseSkipPart,
+ parseDataFork,
+ parseResourceFork,
+ parseWriteThrough
+ };
+
+ nsCOMPtr<nsIOutputStream> m_output;
+ FSSpec m_fsFileSpec;
+ SInt16 m_rfRefNum;
+
+ unsigned char* m_dataBuffer;
+ int32_t m_dataBufferLength;
+ ParserState m_state;
+ ap_header m_headers;
+ ap_entry* m_entries;
+ int32_t m_offset;
+ int32_t m_dataForkOffset;
+ int32_t m_totalDataForkWritten;
+ int32_t m_totalResourceForkWritten;
+ bool m_headerOk;
+
+ int32_t m_currentPartID;
+ int32_t m_currentPartLength;
+ int32_t m_currentPartCount;
+
+ Str255 m_comment;
+ ap_dates m_dates;
+ FInfo m_finderInfo;
+ FXInfo m_finderExtraInfo;
+};
+
+#endif
diff --git a/uriloader/exthandler/mac/nsLocalHandlerAppMac.h b/uriloader/exthandler/mac/nsLocalHandlerAppMac.h
new file mode 100644
index 0000000000..7c77091b42
--- /dev/null
+++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.h
@@ -0,0 +1,26 @@
+/* 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 NSLOCALHANDLERAPPMAC_H_
+#define NSLOCALHANDLERAPPMAC_H_
+
+#include "nsLocalHandlerApp.h"
+
+class nsLocalHandlerAppMac : public nsLocalHandlerApp {
+ public:
+ nsLocalHandlerAppMac() {}
+
+ nsLocalHandlerAppMac(const char16_t* aName, nsIFile* aExecutable)
+ : nsLocalHandlerApp(aName, aExecutable) {}
+
+ nsLocalHandlerAppMac(const nsAString& aName, nsIFile* aExecutable)
+ : nsLocalHandlerApp(aName, aExecutable) {}
+ virtual ~nsLocalHandlerAppMac() {}
+
+ NS_IMETHOD LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override;
+ NS_IMETHOD GetName(nsAString& aName) override;
+};
+
+#endif /*NSLOCALHANDLERAPPMAC_H_*/
diff --git a/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm
new file mode 100644
index 0000000000..07f0276666
--- /dev/null
+++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <CoreFoundation/CoreFoundation.h>
+#import <ApplicationServices/ApplicationServices.h>
+
+#include "nsObjCExceptions.h"
+#include "nsLocalHandlerAppMac.h"
+#include "nsILocalFileMac.h"
+#include "nsIURI.h"
+
+// We override this to make sure app bundles display their pretty name (without
+// .app suffix)
+NS_IMETHODIMP nsLocalHandlerAppMac::GetName(nsAString& aName) {
+ if (mExecutable) {
+ nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(mExecutable);
+ if (macFile) {
+ bool isPackage;
+ (void)macFile->IsPackage(&isPackage);
+ if (isPackage) return macFile->GetBundleDisplayName(aName);
+ }
+ }
+
+ return nsLocalHandlerApp::GetName(aName);
+}
+
+/**
+ * mostly copy/pasted from nsMacShellService.cpp (which is in browser/,
+ * so we can't depend on it here). This code probably really wants to live
+ * somewhere more central (see bug 389922).
+ */
+NS_IMETHODIMP
+nsLocalHandlerAppMac::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv;
+ nsCOMPtr<nsILocalFileMac> lfm(do_QueryInterface(mExecutable, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ CFURLRef appURL;
+ rv = lfm->GetCFURL(&appURL);
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString uriSpec;
+ aURI->GetAsciiSpec(uriSpec);
+
+ const UInt8* uriString = reinterpret_cast<const UInt8*>(uriSpec.get());
+ CFURLRef uri = ::CFURLCreateWithBytes(NULL, uriString, uriSpec.Length(),
+ kCFStringEncodingUTF8, NULL);
+ if (!uri) {
+ ::CFRelease(appURL);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ CFArrayRef uris =
+ ::CFArrayCreate(NULL, reinterpret_cast<const void**>(&uri), 1, NULL);
+ if (!uris) {
+ ::CFRelease(uri);
+ ::CFRelease(appURL);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ LSLaunchURLSpec launchSpec;
+ launchSpec.appURL = appURL;
+ launchSpec.itemURLs = uris;
+ launchSpec.passThruParams = NULL;
+ launchSpec.launchFlags = kLSLaunchDefaults;
+ launchSpec.asyncRefCon = NULL;
+
+ OSErr err = ::LSOpenFromURLSpec(&launchSpec, NULL);
+
+ ::CFRelease(uris);
+ ::CFRelease(uri);
+ ::CFRelease(appURL);
+
+ return err != noErr ? NS_ERROR_FAILURE : NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/uriloader/exthandler/mac/nsMIMEInfoMac.h b/uriloader/exthandler/mac/nsMIMEInfoMac.h
new file mode 100644
index 0000000000..2b87601573
--- /dev/null
+++ b/uriloader/exthandler/mac/nsMIMEInfoMac.h
@@ -0,0 +1,33 @@
+/* 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 nsMIMEInfoMac_h_
+#define nsMIMEInfoMac_h_
+
+#include "nsMIMEInfoImpl.h"
+
+class nsMIMEInfoMac : public nsMIMEInfoImpl {
+ public:
+ explicit nsMIMEInfoMac(const char* aMIMEType = "")
+ : nsMIMEInfoImpl(aMIMEType) {}
+ explicit nsMIMEInfoMac(const nsACString& aMIMEType)
+ : nsMIMEInfoImpl(aMIMEType) {}
+ nsMIMEInfoMac(const nsACString& aType, HandlerClass aClass)
+ : nsMIMEInfoImpl(aType, aClass) {}
+
+ NS_IMETHOD LaunchWithFile(nsIFile* aFile) override;
+
+ protected:
+ [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI) override;
+#ifdef DEBUG
+ [[nodiscard]] virtual nsresult LaunchDefaultWithFile(
+ nsIFile* aFile) override {
+ MOZ_ASSERT_UNREACHABLE("do not call this method, use LaunchWithFile");
+ return NS_ERROR_UNEXPECTED;
+ }
+#endif
+ NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override;
+};
+
+#endif
diff --git a/uriloader/exthandler/mac/nsMIMEInfoMac.mm b/uriloader/exthandler/mac/nsMIMEInfoMac.mm
new file mode 100644
index 0000000000..33dd9d1ee0
--- /dev/null
+++ b/uriloader/exthandler/mac/nsMIMEInfoMac.mm
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 3; 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/. */
+
+#import <ApplicationServices/ApplicationServices.h>
+
+#include "nsComponentManagerUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsMIMEInfoMac.h"
+#include "nsILocalFileMac.h"
+
+// We override this to make sure app bundles display their pretty name (without
+// .app suffix)
+NS_IMETHODIMP nsMIMEInfoMac::GetDefaultDescription(
+ nsAString& aDefaultDescription) {
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (defaultApp) {
+ nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(defaultApp);
+ if (macFile) {
+ bool isPackage;
+ (void)macFile->IsPackage(&isPackage);
+ if (isPackage) return macFile->GetBundleDisplayName(aDefaultDescription);
+ }
+ }
+
+ return nsMIMEInfoImpl::GetDefaultDescription(aDefaultDescription);
+}
+
+NS_IMETHODIMP
+nsMIMEInfoMac::LaunchWithFile(nsIFile* aFile) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsCOMPtr<nsIFile> application;
+ nsresult rv;
+
+ NS_ASSERTION(mClass == eMIMEInfo, "only MIME infos are currently allowed"
+ "to pass content by value");
+
+ if (AutomationOnlyCheckIfLaunchStubbed(aFile)) {
+ return NS_OK;
+ }
+
+ if (mPreferredAction == useHelperApp) {
+ // we don't yet support passing content by value (rather than reference)
+ // to web apps. at some point, we will probably want to.
+ nsCOMPtr<nsILocalHandlerApp> localHandlerApp =
+ do_QueryInterface(mPreferredApplication, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = localHandlerApp->GetExecutable(getter_AddRefs(application));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ } else if (mPreferredAction == useSystemDefault) {
+ application = GetDefaultApplication();
+ } else
+ return NS_ERROR_INVALID_ARG;
+
+ // if we've already got an app, just QI so we have the launchWithDoc method
+ nsCOMPtr<nsILocalFileMac> app;
+ if (application) {
+ app = do_QueryInterface(application, &rv);
+ if (NS_FAILED(rv)) return rv;
+ } else {
+ // otherwise ask LaunchServices for an app directly
+ nsCOMPtr<nsILocalFileMac> tempFile = do_QueryInterface(aFile, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ FSRef tempFileRef;
+ tempFile->GetFSRef(&tempFileRef);
+
+ FSRef appFSRef;
+ if (::LSGetApplicationForItem(&tempFileRef, kLSRolesAll, &appFSRef,
+ nullptr) == noErr) {
+ app = (do_CreateInstance("@mozilla.org/file/local;1"));
+ if (!app) return NS_ERROR_FAILURE;
+ app->InitWithFSRef(&appFSRef);
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return app->LaunchWithDoc(aFile, false);
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsMIMEInfoMac::LoadUriInternal(nsIURI* aURI) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsAutoCString uri;
+ aURI->GetSpec(uri);
+ if (!uri.IsEmpty()) {
+ CFURLRef myURLRef =
+ ::CFURLCreateWithBytes(kCFAllocatorDefault, (const UInt8*)uri.get(),
+ strlen(uri.get()), kCFStringEncodingUTF8, NULL);
+ if (myURLRef) {
+ OSStatus status = ::LSOpenCFURLRef(myURLRef, NULL);
+ if (status == noErr) rv = NS_OK;
+ ::CFRelease(myURLRef);
+ }
+ }
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
diff --git a/uriloader/exthandler/mac/nsOSHelperAppService.h b/uriloader/exthandler/mac/nsOSHelperAppService.h
new file mode 100644
index 0000000000..aa97fee5b4
--- /dev/null
+++ b/uriloader/exthandler/mac/nsOSHelperAppService.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 3; 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 nsOSHelperAppService_h__
+#define nsOSHelperAppService_h__
+
+// The OS helper app service is a subclass of nsExternalHelperAppService and is
+// implemented on each platform. It contains platform specific code for finding
+// helper applications for a given mime type in addition to launching those
+// applications. This is the Mac version.
+
+#include "nsExternalHelperAppService.h"
+#include "nsCExternalHandlerService.h"
+#include "nsCOMPtr.h"
+
+class nsIMimeInfo;
+
+class nsOSHelperAppService : public nsExternalHelperAppService {
+ public:
+ virtual ~nsOSHelperAppService();
+
+ // override nsIExternalProtocolService methods
+ NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
+ nsAString& _retval) override;
+
+ NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval) override;
+
+ nsresult GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) override;
+
+ NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) override;
+
+ // GetFileTokenForPath must be implemented by each platform.
+ // platformAppPath --> a platform specific path to an application that we got
+ // out of the rdf data source. This can be a mac file
+ // spec, a unix path or a windows path depending on the
+ // platform
+ // aFile --> an nsIFile representation of that platform application path.
+ [[nodiscard]] nsresult GetFileTokenForPath(const char16_t* platformAppPath,
+ nsIFile** aFile) override;
+
+ [[nodiscard]] nsresult OSProtocolHandlerExists(const char* aScheme,
+ bool* aHandlerExists) override;
+};
+
+#endif // nsOSHelperAppService_h__
diff --git a/uriloader/exthandler/mac/nsOSHelperAppService.mm b/uriloader/exthandler/mac/nsOSHelperAppService.mm
new file mode 100644
index 0000000000..602c2a89d5
--- /dev/null
+++ b/uriloader/exthandler/mac/nsOSHelperAppService.mm
@@ -0,0 +1,613 @@
+/* -*- Mode: C++; tab-width: 3; 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 <sys/types.h>
+#include <sys/stat.h>
+#include "mozilla/net/NeckoCommon.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "nsComponentManagerUtils.h"
+#include "nsOSHelperAppService.h"
+#include "nsObjCExceptions.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsIFile.h"
+#include "nsILocalFileMac.h"
+#include "nsMimeTypes.h"
+#include "nsCRT.h"
+#include "nsMIMEInfoMac.h"
+#include "nsEmbedCID.h"
+
+#import <CoreFoundation/CoreFoundation.h>
+#import <ApplicationServices/ApplicationServices.h>
+
+// chrome URL's
+#define HELPERAPPLAUNCHER_BUNDLE_URL \
+ "chrome://global/locale/helperAppLauncher.properties"
+#define BRAND_BUNDLE_URL "chrome://branding/locale/brand.properties"
+
+nsresult GetDefaultBundleURL(const nsACString& aScheme, CFURLRef* aBundleURL) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+
+ CFStringRef schemeCFString = ::CFStringCreateWithBytes(
+ kCFAllocatorDefault, (const UInt8*)PromiseFlatCString(aScheme).get(),
+ aScheme.Length(), kCFStringEncodingUTF8, false);
+
+ if (schemeCFString) {
+ CFStringRef lookupCFString =
+ ::CFStringCreateWithFormat(NULL, NULL, CFSTR("%@:"), schemeCFString);
+
+ if (lookupCFString) {
+ CFURLRef lookupCFURL =
+ ::CFURLCreateWithString(NULL, lookupCFString, NULL);
+
+ if (lookupCFURL) {
+ *aBundleURL =
+ ::LSCopyDefaultApplicationURLForURL(lookupCFURL, kLSRolesAll, NULL);
+ if (*aBundleURL) {
+ rv = NS_OK;
+ }
+
+ ::CFRelease(lookupCFURL);
+ }
+
+ ::CFRelease(lookupCFString);
+ }
+
+ ::CFRelease(schemeCFString);
+ }
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+using mozilla::LogLevel;
+
+/* This is an undocumented interface (in the Foundation framework) that has
+ * been stable since at least 10.2.8 and is still present on SnowLeopard.
+ * Furthermore WebKit has three public methods (in WebKitSystemInterface.h)
+ * that are thin wrappers around this interface's last three methods. So
+ * it's unlikely to change anytime soon. Now that we're no longer using
+ * Internet Config Services, this is the only way to look up a MIME type
+ * from an extension, or vice versa.
+ */
+@class NSURLFileTypeMappingsInternal;
+
+@interface NSURLFileTypeMappings : NSObject {
+ NSURLFileTypeMappingsInternal* _internal;
+}
+
++ (NSURLFileTypeMappings*)sharedMappings;
+- (NSString*)MIMETypeForExtension:(NSString*)aString;
+- (NSString*)preferredExtensionForMIMEType:(NSString*)aString;
+- (NSArray*)extensionsForMIMEType:(NSString*)aString;
+@end
+
+nsOSHelperAppService::~nsOSHelperAppService() {}
+
+nsresult nsOSHelperAppService::OSProtocolHandlerExists(
+ const char* aProtocolScheme, bool* aHandlerExists) {
+ // CFStringCreateWithBytes() can fail even if we're not out of memory --
+ // for example if the 'bytes' parameter is something very weird (like
+ // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's
+ // specified in the 'encoding' parameter. See bug 548719.
+ CFStringRef schemeString = ::CFStringCreateWithBytes(
+ kCFAllocatorDefault, (const UInt8*)aProtocolScheme,
+ strlen(aProtocolScheme), kCFStringEncodingUTF8, false);
+ if (schemeString) {
+ // LSCopyDefaultHandlerForURLScheme() can fail to find the default handler
+ // for aProtocolScheme when it's never been explicitly set (using
+ // LSSetDefaultHandlerForURLScheme()). For example, Safari is the default
+ // handler for the "http" scheme on a newly installed copy of OS X. But
+ // this (presumably) wasn't done using LSSetDefaultHandlerForURLScheme(),
+ // so LSCopyDefaultHandlerForURLScheme() will fail to find Safari. To get
+ // around this we use LSCopyAllHandlersForURLScheme() instead -- which seems
+ // never to fail.
+ // http://lists.apple.com/archives/Carbon-dev/2007/May/msg00349.html
+ // http://www.realsoftware.com/listarchives/realbasic-nug/2008-02/msg00119.html
+ CFArrayRef handlerArray = ::LSCopyAllHandlersForURLScheme(schemeString);
+ *aHandlerExists = !!handlerArray;
+ if (handlerArray) ::CFRelease(handlerArray);
+ ::CFRelease(schemeString);
+ } else {
+ *aHandlerExists = false;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(
+ const nsACString& aScheme, nsAString& _retval) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+
+ CFURLRef handlerBundleURL;
+ rv = GetDefaultBundleURL(aScheme, &handlerBundleURL);
+
+ if (NS_SUCCEEDED(rv) && handlerBundleURL) {
+ CFBundleRef handlerBundle = CFBundleCreate(NULL, handlerBundleURL);
+ if (!handlerBundle) {
+ ::CFRelease(handlerBundleURL);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Get the human-readable name of the bundle
+ CFStringRef bundleName =
+ (CFStringRef)::CFBundleGetValueForInfoDictionaryKey(handlerBundle,
+ kCFBundleNameKey);
+
+ if (bundleName) {
+ AutoTArray<UniChar, 255> buffer;
+ CFIndex bundleNameLength = ::CFStringGetLength(bundleName);
+ buffer.SetLength(bundleNameLength);
+ ::CFStringGetCharacters(bundleName, CFRangeMake(0, bundleNameLength),
+ buffer.Elements());
+ _retval.Assign(reinterpret_cast<char16_t*>(buffer.Elements()),
+ bundleNameLength);
+ rv = NS_OK;
+ }
+ ::CFRelease(handlerBundle);
+ ::CFRelease(handlerBundleURL);
+ }
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
+ const nsACString& aScheme, bool* _retval) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+
+ CFURLRef handlerBundleURL;
+ rv = GetDefaultBundleURL(aScheme, &handlerBundleURL);
+ if (NS_SUCCEEDED(rv) && handlerBundleURL) {
+ // Ensure we don't accidentally return success if we can't get an app
+ // bundle.
+ rv = NS_ERROR_NOT_AVAILABLE;
+ CFBundleRef appBundle = ::CFBundleGetMainBundle();
+ if (appBundle) {
+ CFURLRef selfURL = ::CFBundleCopyBundleURL(appBundle);
+ *_retval = ::CFEqual(selfURL, handlerBundleURL);
+ rv = NS_OK;
+ ::CFRelease(selfURL);
+ }
+ ::CFRelease(handlerBundleURL);
+ }
+
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+nsresult nsOSHelperAppService::GetFileTokenForPath(
+ const char16_t* aPlatformAppPath, nsIFile** aFile) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ nsresult rv;
+ nsCOMPtr<nsILocalFileMac> localFile(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ CFURLRef pathAsCFURL;
+ CFStringRef pathAsCFString = ::CFStringCreateWithCharacters(
+ NULL, reinterpret_cast<const UniChar*>(aPlatformAppPath),
+ NS_strlen(aPlatformAppPath));
+ if (!pathAsCFString) return NS_ERROR_OUT_OF_MEMORY;
+
+ if (::CFStringGetCharacterAtIndex(pathAsCFString, 0) == '/') {
+ // we have a Posix path
+ pathAsCFURL = ::CFURLCreateWithFileSystemPath(nullptr, pathAsCFString,
+ kCFURLPOSIXPathStyle, false);
+ if (!pathAsCFURL) {
+ ::CFRelease(pathAsCFString);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ } else {
+ // if it doesn't start with a / it's not an absolute Posix path
+ // let's check if it's a HFS path left over from old preferences
+
+ // If it starts with a ':' char, it's not an absolute HFS path
+ // so bail for that, and also if it's empty
+ if (::CFStringGetLength(pathAsCFString) == 0 ||
+ ::CFStringGetCharacterAtIndex(pathAsCFString, 0) == ':') {
+ ::CFRelease(pathAsCFString);
+ return NS_ERROR_FILE_UNRECOGNIZED_PATH;
+ }
+
+ pathAsCFURL = ::CFURLCreateWithFileSystemPath(nullptr, pathAsCFString,
+ kCFURLHFSPathStyle, false);
+ if (!pathAsCFURL) {
+ ::CFRelease(pathAsCFString);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ rv = localFile->InitWithCFURL(pathAsCFURL);
+ ::CFRelease(pathAsCFString);
+ ::CFRelease(pathAsCFURL);
+ if (NS_FAILED(rv)) return rv;
+ *aFile = localFile;
+ NS_IF_ADDREF(*aFile);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+// Returns the MIME types an application bundle explicitly claims to handle.
+// Returns NULL if aAppRef doesn't explicitly claim to handle any MIME types.
+// If the return value is non-NULL, the caller is responsible for freeing it.
+// This isn't necessarily the same as the MIME types the application bundle
+// is registered to handle in the Launch Services database. (For example
+// the Preview application is normally registered to handle the application/pdf
+// MIME type, even though it doesn't explicitly claim to handle *any* MIME
+// types in its Info.plist. This is probably because Preview does explicitly
+// claim to handle the com.adobe.pdf UTI, and Launch Services somehow
+// translates this into a claim to support the application/pdf MIME type.
+// Launch Services doesn't provide any APIs (documented or undocumented) to
+// query which MIME types a given application is registered to handle. So any
+// app that wants this information (e.g. the Default Apps pref pane) needs to
+// iterate through the entire Launch Services database -- a process which can
+// take several seconds.)
+static CFArrayRef GetMIMETypesHandledByApp(FSRef* aAppRef) {
+ CFURLRef appURL = ::CFURLCreateFromFSRef(kCFAllocatorDefault, aAppRef);
+ if (!appURL) {
+ return NULL;
+ }
+ CFDictionaryRef infoDict = ::CFBundleCopyInfoDictionaryForURL(appURL);
+ ::CFRelease(appURL);
+ if (!infoDict) {
+ return NULL;
+ }
+ CFTypeRef cfObject =
+ ::CFDictionaryGetValue(infoDict, CFSTR("CFBundleDocumentTypes"));
+ if (!cfObject || (::CFGetTypeID(cfObject) != ::CFArrayGetTypeID())) {
+ ::CFRelease(infoDict);
+ return NULL;
+ }
+
+ CFArrayRef docTypes = static_cast<CFArrayRef>(cfObject);
+ CFIndex docTypesCount = ::CFArrayGetCount(docTypes);
+ if (docTypesCount == 0) {
+ ::CFRelease(infoDict);
+ return NULL;
+ }
+
+ CFMutableArrayRef mimeTypes =
+ ::CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
+ for (CFIndex i = 0; i < docTypesCount; ++i) {
+ cfObject = ::CFArrayGetValueAtIndex(docTypes, i);
+ if (!cfObject || (::CFGetTypeID(cfObject) != ::CFDictionaryGetTypeID())) {
+ continue;
+ }
+ CFDictionaryRef typeDict = static_cast<CFDictionaryRef>(cfObject);
+
+ // When this key is present (on OS X 10.5 and later), its contents
+ // take precedence over CFBundleTypeMIMETypes (and CFBundleTypeExtensions
+ // and CFBundleTypeOSTypes).
+ cfObject = ::CFDictionaryGetValue(typeDict, CFSTR("LSItemContentTypes"));
+ if (cfObject && (::CFGetTypeID(cfObject) == ::CFArrayGetTypeID())) {
+ continue;
+ }
+
+ cfObject = ::CFDictionaryGetValue(typeDict, CFSTR("CFBundleTypeMIMETypes"));
+ if (!cfObject || (::CFGetTypeID(cfObject) != ::CFArrayGetTypeID())) {
+ continue;
+ }
+ CFArrayRef mimeTypeHolder = static_cast<CFArrayRef>(cfObject);
+ CFArrayAppendArray(mimeTypes, mimeTypeHolder,
+ ::CFRangeMake(0, ::CFArrayGetCount(mimeTypeHolder)));
+ }
+
+ ::CFRelease(infoDict);
+ if (!::CFArrayGetCount(mimeTypes)) {
+ ::CFRelease(mimeTypes);
+ mimeTypes = NULL;
+ }
+ return mimeTypes;
+}
+
+nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt,
+ bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ *aFound = false;
+
+ const nsCString& flatType = PromiseFlatCString(aMIMEType);
+ const nsCString& flatExt = PromiseFlatCString(aFileExt);
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("Mac: HelperAppService lookup for type '%s' ext '%s'\n",
+ flatType.get(), flatExt.get()));
+
+ // Create a Mac-specific MIME info so we can use Mac-specific members.
+ RefPtr<nsMIMEInfoMac> mimeInfoMac = new nsMIMEInfoMac(aMIMEType);
+
+ NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init];
+
+ OSStatus err;
+ bool haveAppForType = false;
+ bool haveAppForExt = false;
+ bool typeIsOctetStream = false;
+ bool typeAppIsDefault = false;
+ bool extAppIsDefault = false;
+ FSRef typeAppFSRef;
+ FSRef extAppFSRef;
+
+ CFStringRef cfMIMEType = NULL;
+
+ if (!aMIMEType.IsEmpty()) {
+ typeIsOctetStream =
+ aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM);
+ CFURLRef appURL = NULL;
+ // CFStringCreateWithCString() can fail even if we're not out of memory --
+ // for example if the 'cStr' parameter is something very weird (like
+ // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's
+ // specified in the 'encoding' parameter. See bug 548719.
+ cfMIMEType = ::CFStringCreateWithCString(NULL, flatType.get(),
+ kCFStringEncodingUTF8);
+ if (cfMIMEType) {
+ err = ::LSCopyApplicationForMIMEType(cfMIMEType, kLSRolesAll, &appURL);
+ if ((err == noErr) && appURL && ::CFURLGetFSRef(appURL, &typeAppFSRef)) {
+ haveAppForType = true;
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("LSCopyApplicationForMIMEType found a default application\n"));
+ }
+ if (appURL) {
+ ::CFRelease(appURL);
+ }
+ }
+ }
+ if (!aFileExt.IsEmpty()) {
+ // CFStringCreateWithCString() can fail even if we're not out of memory --
+ // for example if the 'cStr' parameter is something very weird (like
+ // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's
+ // specified in the 'encoding' parameter. See bug 548719.
+ CFStringRef cfExt =
+ ::CFStringCreateWithCString(NULL, flatExt.get(), kCFStringEncodingUTF8);
+ if (cfExt) {
+ err = ::LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, cfExt,
+ kLSRolesAll, &extAppFSRef, nullptr);
+ if (err == noErr) {
+ haveAppForExt = true;
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("LSGetApplicationForInfo found a default application\n"));
+ }
+ ::CFRelease(cfExt);
+ }
+ }
+
+ if (haveAppForType && haveAppForExt) {
+ // Do aMIMEType and aFileExt match?
+ if (::FSCompareFSRefs((const FSRef*)&typeAppFSRef,
+ (const FSRef*)&extAppFSRef) == noErr) {
+ typeAppIsDefault = true;
+ *aFound = true;
+ }
+ } else if (haveAppForType) {
+ // If aFileExt isn't empty, it doesn't match aMIMEType.
+ if (aFileExt.IsEmpty()) {
+ typeAppIsDefault = true;
+ *aFound = true;
+ }
+ }
+
+ // If we have an app for the extension, and either don't have one for the
+ // type, or the type is application/octet-stream (ie "binary blob"), rely
+ // on the file extension.
+ if ((!haveAppForType || (!*aFound && typeIsOctetStream)) && haveAppForExt) {
+ // If aMIMEType isn't empty, it doesn't match aFileExt, which should mean
+ // that we haven't found a matching app. But make an exception for an app
+ // that also explicitly claims to handle aMIMEType, or which doesn't claim
+ // to handle any MIME types. This helps work around the following Apple
+ // design flaw:
+ //
+ // Launch Services is somewhat unreliable about registering Apple apps to
+ // handle MIME types. Probably this is because Apple has officially
+ // deprecated support for MIME types (in favor of UTIs). As a result,
+ // most of Apple's own apps don't explicitly claim to handle any MIME
+ // types (instead they claim to handle one or more UTIs). So Launch
+ // Services must contain logic to translate support for a given UTI into
+ // support for one or more MIME types, and it doesn't always do this
+ // correctly. For example DiskImageMounter isn't (by default) registered
+ // to handle the application/x-apple-diskimage MIME type. See bug 675356.
+ //
+ // Apple has also deprecated support for file extensions, and Apple apps
+ // also don't register to handle them. But for some reason Launch Services
+ // is (apparently) better about translating support for a given UTI into
+ // support for one or more file extensions. It's not at all clear why.
+ if (aMIMEType.IsEmpty() || typeIsOctetStream) {
+ extAppIsDefault = true;
+ *aFound = true;
+ } else {
+ CFArrayRef extAppMIMETypes = GetMIMETypesHandledByApp(&extAppFSRef);
+ if (extAppMIMETypes) {
+ if (cfMIMEType) {
+ if (::CFArrayContainsValue(
+ extAppMIMETypes,
+ ::CFRangeMake(0, ::CFArrayGetCount(extAppMIMETypes)),
+ cfMIMEType)) {
+ extAppIsDefault = true;
+ *aFound = true;
+ }
+ }
+ ::CFRelease(extAppMIMETypes);
+ } else {
+ extAppIsDefault = true;
+ *aFound = true;
+ }
+ }
+ }
+
+ if (cfMIMEType) {
+ ::CFRelease(cfMIMEType);
+ }
+
+ if (aMIMEType.IsEmpty()) {
+ if (haveAppForExt) {
+ // If aMIMEType is empty and we've found a default app for aFileExt, try
+ // to get the MIME type from aFileExt. (It might also be worth doing
+ // this when aMIMEType isn't empty but haveAppForType is false -- but
+ // the doc for this method says that if we have a MIME type (in
+ // aMIMEType), we need to give it preference.)
+ NSURLFileTypeMappings* map = [NSURLFileTypeMappings sharedMappings];
+ NSString* extStr = [NSString stringWithCString:flatExt.get()
+ encoding:NSASCIIStringEncoding];
+ NSString* typeStr = map ? [map MIMETypeForExtension:extStr] : NULL;
+ if (typeStr) {
+ nsAutoCString mimeType;
+ mimeType.Assign(
+ (char*)[typeStr cStringUsingEncoding:NSASCIIStringEncoding]);
+ mimeInfoMac->SetMIMEType(mimeType);
+ haveAppForType = true;
+ } else {
+ // Sometimes the OS won't give us a MIME type for an extension that's
+ // registered with Launch Services and has a default app: For example
+ // Real Player registers itself for the "ogg" extension and for the
+ // audio/x-ogg and application/x-ogg MIME types, but
+ // MIMETypeForExtension returns nil for the "ogg" extension even on
+ // systems where Real Player is installed. This is probably an Apple
+ // bug. But bad things happen if we return an nsIMIMEInfo structure
+ // with an empty MIME type and set *aFound to true. So in this
+ // case we need to set it to false here.
+ haveAppForExt = false;
+ extAppIsDefault = false;
+ *aFound = false;
+ }
+ } else {
+ // Otherwise set the MIME type to a reasonable fallback.
+ mimeInfoMac->SetMIMEType(nsLiteralCString(APPLICATION_OCTET_STREAM));
+ }
+ }
+
+ if (typeAppIsDefault || extAppIsDefault) {
+ if (haveAppForExt) mimeInfoMac->AppendExtension(aFileExt);
+
+ nsresult rv;
+ nsCOMPtr<nsILocalFileMac> app(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ [localPool release];
+ return rv;
+ }
+
+ CFStringRef cfAppName = NULL;
+ if (typeAppIsDefault) {
+ app->InitWithFSRef(&typeAppFSRef);
+ ::LSCopyItemAttribute((const FSRef*)&typeAppFSRef, kLSRolesAll,
+ kLSItemDisplayName, (CFTypeRef*)&cfAppName);
+ } else {
+ app->InitWithFSRef(&extAppFSRef);
+ ::LSCopyItemAttribute((const FSRef*)&extAppFSRef, kLSRolesAll,
+ kLSItemDisplayName, (CFTypeRef*)&cfAppName);
+ }
+ if (cfAppName) {
+ AutoTArray<UniChar, 255> buffer;
+ CFIndex appNameLength = ::CFStringGetLength(cfAppName);
+ buffer.SetLength(appNameLength);
+ ::CFStringGetCharacters(cfAppName, CFRangeMake(0, appNameLength),
+ buffer.Elements());
+ nsAutoString appName;
+ appName.Assign(reinterpret_cast<char16_t*>(buffer.Elements()),
+ appNameLength);
+ mimeInfoMac->SetDefaultDescription(appName);
+ ::CFRelease(cfAppName);
+ }
+
+ mimeInfoMac->SetDefaultApplication(app);
+ mimeInfoMac->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+
+ } else {
+ mimeInfoMac->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ }
+
+ nsAutoCString mimeType;
+ mimeInfoMac->GetMIMEType(mimeType);
+ if (*aFound && !mimeType.IsEmpty()) {
+ // If we have a MIME type, make sure its extension list is included in our
+ // list.
+ NSURLFileTypeMappings* map = [NSURLFileTypeMappings sharedMappings];
+ NSString* typeStr = [NSString stringWithCString:mimeType.get()
+ encoding:NSASCIIStringEncoding];
+ NSArray* extensionsList = map ? [map extensionsForMIMEType:typeStr] : NULL;
+ if (extensionsList) {
+ for (NSString* extension in extensionsList) {
+ nsAutoCString ext;
+ ext.Assign(
+ (char*)[extension cStringUsingEncoding:NSASCIIStringEncoding]);
+ mimeInfoMac->AppendExtension(ext);
+ }
+ }
+
+ if (CFStringRef cfType = ::CFStringCreateWithCString(
+ NULL, mimeType.get(), kCFStringEncodingUTF8)) {
+ if (CFStringRef cfTypeDesc = ::UTTypeCopyDescription(cfType)) {
+ AutoTArray<UniChar, 255> buffer;
+ CFIndex typeDescLength = ::CFStringGetLength(cfTypeDesc);
+ buffer.SetLength(typeDescLength);
+ ::CFStringGetCharacters(cfTypeDesc, CFRangeMake(0, typeDescLength),
+ buffer.Elements());
+ nsAutoString typeDesc;
+ typeDesc.Assign(reinterpret_cast<char16_t*>(buffer.Elements()),
+ typeDescLength);
+ mimeInfoMac->SetDescription(typeDesc);
+ ::CFRelease(cfTypeDesc);
+ }
+ ::CFRelease(cfType);
+ }
+ }
+
+ MOZ_LOG(sLog, LogLevel::Debug,
+ ("OS gave us: type '%s' found '%i'\n", mimeType.get(), *aFound));
+
+ [localPool release];
+ mimeInfoMac.forget(aMIMEInfo);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) {
+ NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!");
+
+ nsresult rv =
+ OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found);
+ if (NS_FAILED(rv)) return rv;
+
+ nsMIMEInfoMac* handlerInfo =
+ new nsMIMEInfoMac(aScheme, nsMIMEInfoBase::eProtocolInfo);
+ NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(*_retval = handlerInfo);
+
+ if (!*found) {
+ // Code that calls this requires an object regardless if the OS has
+ // something for us, so we return the empty object.
+ return NS_OK;
+ }
+
+ // As a workaround for the OS X problem described in bug 1391186, don't
+ // attempt to get/set the application description from the child process.
+ if (!mozilla::net::IsNeckoChild()) {
+ nsAutoString desc;
+ rv = GetApplicationDescription(aScheme, desc);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "GetApplicationDescription failed");
+ handlerInfo->SetDefaultDescription(desc);
+ }
+
+ return NS_OK;
+}
diff --git a/uriloader/exthandler/moz.build b/uriloader/exthandler/moz.build
new file mode 100644
index 0000000000..cb96c690a6
--- /dev/null
+++ b/uriloader/exthandler/moz.build
@@ -0,0 +1,143 @@
+# -*- 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["/uriloader/exthandler"] = "docs"
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "File Handling")
+
+TEST_DIRS += ["tests"]
+
+XPIDL_SOURCES += [
+ "nsCExternalHandlerService.idl",
+ "nsIContentDispatchChooser.idl",
+ "nsIExternalHelperAppService.idl",
+ "nsIExternalProtocolService.idl",
+ "nsIHandlerService.idl",
+ "nsIHelperAppLauncherDialog.idl",
+ "nsISharingHandlerApp.idl",
+]
+
+XPIDL_MODULE = "exthandler"
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ osdir = "win"
+ LOCAL_INCLUDES += ["win"]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ osdir = "mac"
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] in ("android", "uikit"):
+ osdir = CONFIG["MOZ_WIDGET_TOOLKIT"]
+else:
+ osdir = "unix"
+
+EXPORTS += [osdir + "/nsOSHelperAppService.h"]
+
+EXPORTS += [
+ "ContentHandlerService.h",
+ "nsExternalHelperAppService.h",
+ "nsMIMEInfoChild.h",
+ "nsOSHelperAppServiceChild.h",
+]
+
+EXPORTS += [
+ "nsLocalHandlerApp.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "ExternalHelperAppChild.h",
+ "ExternalHelperAppParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "ContentHandlerService.cpp",
+ "ExternalHelperAppChild.cpp",
+ "ExternalHelperAppParent.cpp",
+ "HandlerServiceParent.cpp",
+ "nsExternalHelperAppService.cpp",
+ "nsExternalProtocolHandler.cpp",
+ "nsLocalHandlerApp.cpp",
+ "nsMIMEInfoImpl.cpp",
+ "nsOSHelperAppServiceChild.cpp",
+]
+
+if CONFIG["MOZ_BUILD_APP"] == "browser":
+ DEFINES["MOZ_BUILD_APP_IS_BROWSER"] = True
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ UNIFIED_SOURCES += [
+ "mac/nsLocalHandlerAppMac.mm",
+ "mac/nsMIMEInfoMac.mm",
+ "mac/nsOSHelperAppService.mm",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "uikit":
+ UNIFIED_SOURCES += [
+ "uikit/nsLocalHandlerAppUIKit.mm",
+ "uikit/nsMIMEInfoUIKit.mm",
+ "uikit/nsOSHelperAppService.mm",
+ ]
+else:
+ # These files can't be built in unified mode because they redefine LOG.
+ SOURCES += [
+ osdir + "/nsOSHelperAppService.cpp",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ UNIFIED_SOURCES += [
+ "unix/nsGNOMERegistry.cpp",
+ "unix/nsMIMEInfoUnix.cpp",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ UNIFIED_SOURCES += [
+ "android/nsMIMEInfoAndroid.cpp",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ UNIFIED_SOURCES += [
+ "win/nsMIMEInfoWin.cpp",
+ ]
+
+if CONFIG["MOZ_ENABLE_DBUS"]:
+ UNIFIED_SOURCES += [
+ "nsDBusHandlerApp.cpp",
+ ]
+ EXPORTS.mozilla += [
+ "DBusHelpers.h",
+ ]
+
+EXTRA_JS_MODULES += [
+ "ExtHandlerService.sys.mjs",
+ "WebHandlerApp.sys.mjs",
+]
+
+EXTRA_JS_MODULES.handlers += [
+ "HandlerList.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+IPDL_SOURCES += [
+ "PExternalHelperApp.ipdl",
+ "PHandlerService.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/docshell/base",
+ "/dom/base",
+ "/dom/ipc",
+ "/netwerk/base",
+ "/netwerk/protocol/http",
+]
+
+if CONFIG["MOZ_ENABLE_DBUS"]:
+ CXXFLAGS += CONFIG["MOZ_DBUS_CFLAGS"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
diff --git a/uriloader/exthandler/nsCExternalHandlerService.idl b/uriloader/exthandler/nsCExternalHandlerService.idl
new file mode 100644
index 0000000000..b6aa83e285
--- /dev/null
+++ b/uriloader/exthandler/nsCExternalHandlerService.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: IDL; tab-width: 3; 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 "nsIExternalHelperAppService.idl"
+
+/*
+nsCExternalHelperApp implements:
+-------------------------
+nsIExternalHelperAppService
+*/
+
+%{ C++
+
+#define NS_EXTERNALHELPERAPPSERVICE_CONTRACTID \
+"@mozilla.org/uriloader/external-helper-app-service;1"
+
+#define NS_HANDLERSERVICE_CONTRACTID \
+"@mozilla.org/uriloader/handler-service;1"
+
+#define NS_EXTERNALPROTOCOLSERVICE_CONTRACTID \
+"@mozilla.org/uriloader/external-protocol-service;1"
+
+#define NS_MIMESERVICE_CONTRACTID \
+"@mozilla.org/mime;1"
+
+#define NS_LOCALHANDLERAPP_CONTRACTID \
+"@mozilla.org/uriloader/local-handler-app;1"
+
+%}
diff --git a/uriloader/exthandler/nsContentHandlerApp.h b/uriloader/exthandler/nsContentHandlerApp.h
new file mode 100644
index 0000000000..f372d8735e
--- /dev/null
+++ b/uriloader/exthandler/nsContentHandlerApp.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 __nsContentHandlerAppImpl_h__
+#define __nsContentHandlerAppImpl_h__
+
+#include <contentaction/contentaction.h>
+#include "nsString.h"
+#include "nsIMIMEInfo.h"
+
+class nsContentHandlerApp : public nsIHandlerApp {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHANDLERAPP
+
+ nsContentHandlerApp(nsString aName, nsCString aType,
+ ContentAction::Action& aAction);
+ virtual ~nsContentHandlerApp() {}
+
+ protected:
+ nsString mName;
+ nsCString mType;
+ nsString mDetailedDescription;
+
+ ContentAction::Action mAction;
+};
+#endif
diff --git a/uriloader/exthandler/nsDBusHandlerApp.cpp b/uriloader/exthandler/nsDBusHandlerApp.cpp
new file mode 100644
index 0000000000..6155a1a951
--- /dev/null
+++ b/uriloader/exthandler/nsDBusHandlerApp.cpp
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 <dbus/dbus.h>
+#include "mozilla/Components.h"
+#include "mozilla/DBusHelpers.h"
+#include "nsDBusHandlerApp.h"
+#include "nsIURI.h"
+#include "nsIClassInfoImpl.h"
+#include "nsCOMPtr.h"
+#include "nsCExternalHandlerService.h"
+
+using namespace mozilla;
+
+// XXX why does nsMIMEInfoImpl have a threadsafe nsISupports? do we need one
+// here too?
+NS_IMPL_CLASSINFO(nsDBusHandlerApp, nullptr, 0,
+ components::DBusHandlerApp::CID())
+NS_IMPL_ISUPPORTS_CI(nsDBusHandlerApp, nsIDBusHandlerApp, nsIHandlerApp)
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIHandlerApp
+
+NS_IMETHODIMP nsDBusHandlerApp::GetName(nsAString& aName) {
+ aName.Assign(mName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::SetName(const nsAString& aName) {
+ mName.Assign(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::SetDetailedDescription(
+ const nsAString& aDescription) {
+ mDetailedDescription.Assign(aDescription);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::GetDetailedDescription(
+ nsAString& aDescription) {
+ aDescription.Assign(mDetailedDescription);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDBusHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(aHandlerApp);
+
+ // If the handler app isn't a dbus handler app, then it's not the same app.
+ nsCOMPtr<nsIDBusHandlerApp> dbusHandlerApp = do_QueryInterface(aHandlerApp);
+ if (!dbusHandlerApp) {
+ *_retval = false;
+ return NS_OK;
+ }
+ nsAutoCString service;
+ nsAutoCString method;
+
+ nsresult rv = dbusHandlerApp->GetService(service);
+ if (NS_FAILED(rv)) {
+ *_retval = false;
+ return NS_OK;
+ }
+ rv = dbusHandlerApp->GetMethod(method);
+ if (NS_FAILED(rv)) {
+ *_retval = false;
+ return NS_OK;
+ }
+
+ *_retval = service.Equals(mService) && method.Equals(mMethod);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDBusHandlerApp::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) {
+ nsAutoCString spec;
+ nsresult rv = aURI->GetAsciiSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ const char* uri = spec.get();
+
+ DBusError err;
+ dbus_error_init(&err);
+
+ mozilla::UniquePtr<DBusConnection, mozilla::DBusConnectionDelete> connection(
+ dbus_bus_get_private(DBUS_BUS_SESSION, &err));
+
+ if (dbus_error_is_set(&err)) {
+ dbus_error_free(&err);
+ return NS_ERROR_FAILURE;
+ }
+ if (nullptr == connection) {
+ return NS_ERROR_FAILURE;
+ }
+ dbus_connection_set_exit_on_disconnect(connection.get(), false);
+
+ RefPtr<DBusMessage> msg =
+ already_AddRefed<DBusMessage>(dbus_message_new_method_call(
+ mService.get(), mObjpath.get(), mInterface.get(), mMethod.get()));
+
+ if (!msg) {
+ return NS_ERROR_FAILURE;
+ }
+ dbus_message_set_no_reply(msg, true);
+
+ DBusMessageIter iter;
+ dbus_message_iter_init_append(msg, &iter);
+ dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &uri);
+
+ if (dbus_connection_send(connection.get(), msg, nullptr)) {
+ dbus_connection_flush(connection.get());
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIDBusHandlerApp
+
+NS_IMETHODIMP nsDBusHandlerApp::GetService(nsACString& aService) {
+ aService.Assign(mService);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::SetService(const nsACString& aService) {
+ mService.Assign(aService);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::GetMethod(nsACString& aMethod) {
+ aMethod.Assign(mMethod);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::SetMethod(const nsACString& aMethod) {
+ mMethod.Assign(aMethod);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::GetDBusInterface(nsACString& aInterface) {
+ aInterface.Assign(mInterface);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::SetDBusInterface(const nsACString& aInterface) {
+ mInterface.Assign(aInterface);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::GetObjectPath(nsACString& aObjpath) {
+ aObjpath.Assign(mObjpath);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDBusHandlerApp::SetObjectPath(const nsACString& aObjpath) {
+ mObjpath.Assign(aObjpath);
+ return NS_OK;
+}
diff --git a/uriloader/exthandler/nsDBusHandlerApp.h b/uriloader/exthandler/nsDBusHandlerApp.h
new file mode 100644
index 0000000000..f45f917446
--- /dev/null
+++ b/uriloader/exthandler/nsDBusHandlerApp.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 __nsDBusHandlerAppImpl_h__
+#define __nsDBusHandlerAppImpl_h__
+
+#include "nsString.h"
+#include "nsIMIMEInfo.h"
+
+class nsDBusHandlerApp : public nsIDBusHandlerApp {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHANDLERAPP
+ NS_DECL_NSIDBUSHANDLERAPP
+
+ nsDBusHandlerApp() {}
+
+ protected:
+ virtual ~nsDBusHandlerApp() {}
+
+ nsString mName;
+ nsString mDetailedDescription;
+ nsCString mService;
+ nsCString mMethod;
+ nsCString mInterface;
+ nsCString mObjpath;
+};
+#endif
diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp
new file mode 100644
index 0000000000..4573e28470
--- /dev/null
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -0,0 +1,3745 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 "base/basictypes.h"
+
+/* This must occur *after* base/basictypes.h to avoid typedefs conflicts. */
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Base64.h"
+#include "mozilla/ResultExtensions.h"
+
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/RandomNum.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "mozilla/StaticPtr.h"
+#include "nsXULAppAPI.h"
+
+#include "ExternalHelperAppParent.h"
+#include "nsExternalHelperAppService.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIURI.h"
+#include "nsIURL.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsIChannel.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsICategoryManager.h"
+#include "nsDependentSubstring.h"
+#include "nsSandboxFlags.h"
+#include "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsIStringEnumerator.h"
+#include "nsIStreamListener.h"
+#include "nsIMIMEService.h"
+#include "nsILoadGroup.h"
+#include "nsIWebProgressListener.h"
+#include "nsITransfer.h"
+#include "nsReadableUtils.h"
+#include "nsIRequest.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsThreadUtils.h"
+#include "nsIMutableArray.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsOSHelperAppService.h"
+#include "nsOSHelperAppServiceChild.h"
+#include "nsContentSecurityUtils.h"
+#include "nsUTF8Utils.h"
+#include "nsUnicodeProperties.h"
+
+// used to access our datastore of user-configured helper applications
+#include "nsIHandlerService.h"
+#include "nsIMIMEInfo.h"
+#include "nsIHelperAppLauncherDialog.h"
+#include "nsIContentDispatchChooser.h"
+#include "nsNetUtil.h"
+#include "nsIPrivateBrowsingChannel.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+
+#include "nsIApplicationReputation.h"
+
+#include "nsDSURIContentListener.h"
+#include "nsMimeTypes.h"
+#include "nsMIMEInfoImpl.h"
+// used for header disposition information.
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIEncodedChannel.h"
+#include "nsIMultiPartChannel.h"
+#include "nsIFileChannel.h"
+#include "nsIObserverService.h" // so we can be a profile change observer
+#include "nsIPropertyBag2.h" // for the 64-bit content length
+
+#ifdef XP_MACOSX
+# include "nsILocalFileMac.h"
+#endif
+
+#include "nsEscape.h"
+
+#include "nsIStringBundle.h" // XXX needed to localize error msgs
+#include "nsIPrompt.h"
+
+#include "nsITextToSubURI.h" // to unescape the filename
+
+#include "nsDocShellCID.h"
+
+#include "nsCRT.h"
+#include "nsLocalHandlerApp.h"
+
+#include "nsIRandomGenerator.h"
+
+#include "ContentChild.h"
+#include "nsXULAppAPI.h"
+#include "nsPIDOMWindow.h"
+#include "ExternalHelperAppChild.h"
+
+#include "mozilla/dom/nsHTTPSOnlyUtils.h"
+
+#ifdef XP_WIN
+# include "nsWindowsHelpers.h"
+# include "nsLocalFile.h"
+#endif
+
+#include "mozilla/Components.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ipc/URIUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::ipc;
+using namespace mozilla::dom;
+
+#define kDefaultMaxFileNameLength 254
+
+// Download Folder location constants
+#define NS_PREF_DOWNLOAD_DIR "browser.download.dir"
+#define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
+enum {
+ NS_FOLDER_VALUE_DESKTOP = 0,
+ NS_FOLDER_VALUE_DOWNLOADS = 1,
+ NS_FOLDER_VALUE_CUSTOM = 2
+};
+
+LazyLogModule nsExternalHelperAppService::sLog("HelperAppService");
+
+// Using level 3 here because the OSHelperAppServices use a log level
+// of LogLevel::Debug (4), and we want less detailed output here
+// Using 3 instead of LogLevel::Warning because we don't output warnings
+#undef LOG
+#define LOG(...) \
+ MOZ_LOG(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info, \
+ (__VA_ARGS__))
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info)
+
+static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] =
+ "browser.helperApps.neverAsk.saveToDisk";
+static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] =
+ "browser.helperApps.neverAsk.openFile";
+
+StaticRefPtr<nsIFile> sFallbackDownloadDir;
+
+// Helper functions for Content-Disposition headers
+
+/**
+ * Given a URI fragment, unescape it
+ * @param aFragment The string to unescape
+ * @param aURI The URI from which this fragment is taken. Only its character set
+ * will be used.
+ * @param aResult [out] Unescaped string.
+ */
+static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
+ nsAString& aResult) {
+ // We need the unescaper
+ nsresult rv;
+ nsCOMPtr<nsITextToSubURI> textToSubURI =
+ do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return textToSubURI->UnEscapeURIForUI(aFragment, /* aDontEscape = */ true,
+ aResult);
+}
+
+/**
+ * UTF-8 version of UnescapeFragment.
+ * @param aFragment The string to unescape
+ * @param aURI The URI from which this fragment is taken. Only its character set
+ * will be used.
+ * @param aResult [out] Unescaped string, UTF-8 encoded.
+ * @note It is safe to pass the same string for aFragment and aResult.
+ * @note When this function fails, aResult will not be modified.
+ */
+static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI,
+ nsACString& aResult) {
+ nsAutoString result;
+ nsresult rv = UnescapeFragment(aFragment, aURI, result);
+ if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult);
+ return rv;
+}
+
+/**
+ * Obtains the directory to use. This tends to vary per platform, and
+ * needs to be consistent throughout our codepaths. For platforms where
+ * helper apps use the downloads directory, this should be kept in
+ * sync with DownloadIntegration.sys.mjs.
+ *
+ * Optionally skip availability of the directory and storage.
+ */
+static nsresult GetDownloadDirectory(nsIFile** _directory,
+ bool aSkipChecks = false) {
+#if defined(ANDROID)
+ return NS_ERROR_FAILURE;
+#endif
+
+ bool usePrefDir = !StaticPrefs::browser_download_start_downloads_in_tmp_dir();
+
+ nsCOMPtr<nsIFile> dir;
+ nsresult rv;
+ if (usePrefDir) {
+ // Try to get the users download location, if it's set.
+ switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) {
+ case NS_FOLDER_VALUE_DESKTOP:
+ (void)NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir));
+ break;
+ case NS_FOLDER_VALUE_CUSTOM: {
+ Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(dir));
+ if (!dir) break;
+
+ // If we're not checking for availability we're done.
+ if (aSkipChecks) {
+ dir.forget(_directory);
+ return NS_OK;
+ }
+
+ // We have the directory, and now we need to make sure it exists
+ nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ // If we can't create this and it's not because the file already
+ // exists, clear out `dir` so we don't return it.
+ if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_FAILED(rv)) {
+ dir = nullptr;
+ }
+ } break;
+ case NS_FOLDER_VALUE_DOWNLOADS:
+ // This is just the OS default location, so fall out
+ break;
+ }
+ if (!dir) {
+ rv = NS_GetSpecialDirectory(NS_OS_DEFAULT_DOWNLOAD_DIR,
+ getter_AddRefs(dir));
+ if (NS_FAILED(rv)) {
+ // On some OSes, there is no guarantee this directory exists.
+ // Fall back to $HOME + Downloads.
+ if (sFallbackDownloadDir) {
+ sFallbackDownloadDir->Clone(getter_AddRefs(dir));
+ } else {
+ rv = NS_GetSpecialDirectory(NS_OS_HOME_DIR, getter_AddRefs(dir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString downloadLocalized;
+ nsCOMPtr<nsIStringBundle> downloadBundle;
+ rv = bundleService->CreateBundle(
+ "chrome://mozapps/locale/downloads/downloads.properties",
+ getter_AddRefs(downloadBundle));
+ if (NS_SUCCEEDED(rv)) {
+ rv = downloadBundle->GetStringFromName("downloadsFolder",
+ downloadLocalized);
+ }
+ if (NS_FAILED(rv)) {
+ downloadLocalized.AssignLiteral("Downloads");
+ }
+ rv = dir->Append(downloadLocalized);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Can't getter_AddRefs on StaticRefPtr, so do some copying.
+ nsCOMPtr<nsIFile> copy;
+ dir->Clone(getter_AddRefs(copy));
+ sFallbackDownloadDir = copy.forget();
+ ClearOnShutdown(&sFallbackDownloadDir);
+ }
+ if (aSkipChecks) {
+ dir.forget(_directory);
+ return NS_OK;
+ }
+
+ // We have the directory, and now we need to make sure it exists
+ rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (rv == NS_ERROR_FILE_ALREADY_EXISTS || NS_SUCCEEDED(rv)) {
+ dir.forget(_directory);
+ rv = NS_OK;
+ }
+ return rv;
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ } else {
+ rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#if !defined(XP_MACOSX) && defined(XP_UNIX)
+ // Ensuring that only the current user can read the file names we end up
+ // creating. Note that creating directories with a specified permission is
+ // only supported on Unix platform right now. That's why the above check
+ // exists.
+
+ uint32_t permissions;
+ rv = dir->GetPermissions(&permissions);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (permissions != PR_IRWXU) {
+ const char* userName = PR_GetEnv("USERNAME");
+ if (!userName || !*userName) {
+ userName = PR_GetEnv("USER");
+ }
+ if (!userName || !*userName) {
+ userName = PR_GetEnv("LOGNAME");
+ }
+ if (!userName || !*userName) {
+ userName = "mozillaUser";
+ }
+
+ nsAutoString userDir;
+ userDir.AssignLiteral("mozilla_");
+ userDir.AppendASCII(userName);
+ userDir.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
+
+ int counter = 0;
+ bool pathExists;
+ nsCOMPtr<nsIFile> finalPath;
+
+ while (true) {
+ nsAutoString countedUserDir(userDir);
+ countedUserDir.AppendInt(counter, 10);
+ dir->Clone(getter_AddRefs(finalPath));
+ finalPath->Append(countedUserDir);
+
+ rv = finalPath->Exists(&pathExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (pathExists) {
+ // If this path has the right permissions, use it.
+ rv = finalPath->GetPermissions(&permissions);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensuring the path is writable by the current user.
+ bool isWritable;
+ rv = finalPath->IsWritable(&isWritable);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (permissions == PR_IRWXU && isWritable) {
+ dir = finalPath;
+ break;
+ }
+ }
+
+ rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU);
+ if (NS_SUCCEEDED(rv)) {
+ dir = finalPath;
+ break;
+ }
+ if (rv != NS_ERROR_FILE_ALREADY_EXISTS) {
+ // Unexpected error.
+ return rv;
+ }
+ counter++;
+ }
+ }
+
+#endif
+ }
+
+ NS_ASSERTION(dir, "Somehow we didn't get a download directory!");
+ dir.forget(_directory);
+ return NS_OK;
+}
+
+/**
+ * Helper for random bytes for the filename of downloaded part files.
+ */
+nsresult GenerateRandomName(nsACString& result) {
+ // We will request raw random bytes, and transform that to a base64 string,
+ // using url-based base64 encoding so that all characters from the base64
+ // result will be acceptable for filenames.
+ // For each three bytes of random data, we will get four bytes of ASCII.
+ // Request a bit more, to be safe, then truncate in the end.
+
+ nsresult rv;
+ const uint32_t wantedFileNameLength = 8;
+ const uint32_t requiredBytesLength =
+ static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3);
+
+ uint8_t buffer[requiredBytesLength];
+ if (!mozilla::GenerateRandomBytesFromOS(buffer, requiredBytesLength)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString tempLeafName;
+ // We're forced to specify a padding policy, though this is guaranteed
+ // not to need padding due to requiredBytesLength being a multiple of 3.
+ rv = Base64URLEncode(requiredBytesLength, buffer,
+ Base64URLEncodePaddingPolicy::Omit, tempLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ tempLeafName.Truncate(wantedFileNameLength);
+
+ result.Assign(tempLeafName);
+ return NS_OK;
+}
+
+/**
+ * Structure for storing extension->type mappings.
+ * @see defaultMimeEntries
+ */
+struct nsDefaultMimeTypeEntry {
+ const char* mMimeType;
+ const char* mFileExtension;
+};
+
+/**
+ * Default extension->mimetype mappings. These are not overridable.
+ * If you add types here, make sure they are lowercase, or you'll regret it.
+ */
+static const nsDefaultMimeTypeEntry defaultMimeEntries[] = {
+ // The following are those extensions that we're asked about during startup,
+ // sorted by order used
+ {IMAGE_GIF, "gif"},
+ {TEXT_XML, "xml"},
+ {APPLICATION_RDF, "rdf"},
+ {IMAGE_PNG, "png"},
+ // -- end extensions used during startup
+ {TEXT_CSS, "css"},
+ {IMAGE_JPEG, "jpeg"},
+ {IMAGE_JPEG, "jpg"},
+ {IMAGE_SVG_XML, "svg"},
+ {TEXT_HTML, "html"},
+ {TEXT_HTML, "htm"},
+ {APPLICATION_XPINSTALL, "xpi"},
+ {"application/xhtml+xml", "xhtml"},
+ {"application/xhtml+xml", "xht"},
+ {TEXT_PLAIN, "txt"},
+ {APPLICATION_JSON, "json"},
+ {APPLICATION_XJAVASCRIPT, "mjs"},
+ {APPLICATION_XJAVASCRIPT, "js"},
+ {APPLICATION_XJAVASCRIPT, "jsm"},
+ {VIDEO_OGG, "ogv"},
+ {VIDEO_OGG, "ogg"},
+ {APPLICATION_OGG, "ogg"},
+ {AUDIO_OGG, "oga"},
+ {AUDIO_OGG, "opus"},
+ {APPLICATION_PDF, "pdf"},
+ {VIDEO_WEBM, "webm"},
+ {AUDIO_WEBM, "webm"},
+ {IMAGE_ICO, "ico"},
+ {TEXT_PLAIN, "properties"},
+ {TEXT_PLAIN, "locale"},
+ {TEXT_PLAIN, "ftl"},
+#if defined(MOZ_WMF)
+ {VIDEO_MP4, "mp4"},
+ {AUDIO_MP4, "m4a"},
+ {AUDIO_MP3, "mp3"},
+#endif
+#ifdef MOZ_RAW
+ {VIDEO_RAW, "yuv"}
+#endif
+};
+
+/**
+ * This is a small private struct used to help us initialize some
+ * default mime types.
+ */
+struct nsExtraMimeTypeEntry {
+ const char* mMimeType;
+ const char* mFileExtensions;
+ const char* mDescription;
+};
+
+/**
+ * This table lists all of the 'extra' content types that we can deduce from
+ * particular file extensions. These entries also ensure that we provide a good
+ * descriptive name when we encounter files with these content types and/or
+ * extensions. These can be overridden by user helper app prefs. If you add
+ * types here, make sure they are lowercase, or you'll regret it.
+ */
+static const nsExtraMimeTypeEntry extraMimeEntries[] = {
+#if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to
+ // look that up...
+ {APPLICATION_OCTET_STREAM, "exe,com", "Binary File"},
+#else
+ {APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"},
+#endif
+ {APPLICATION_GZIP2, "gz", "gzip"},
+ {"application/x-arj", "arj", "ARJ file"},
+ {"application/rtf", "rtf", "Rich Text Format File"},
+ {APPLICATION_ZIP, "zip", "ZIP Archive"},
+ {APPLICATION_XPINSTALL, "xpi", "XPInstall Install"},
+ {APPLICATION_PDF, "pdf", "Portable Document Format"},
+ {APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"},
+ {APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"},
+ {APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"},
+#ifdef MOZ_WIDGET_ANDROID
+ {"application/vnd.android.package-archive", "apk", "Android Package"},
+#endif
+
+ // OpenDocument formats
+ {"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"},
+ {"application/vnd.oasis.opendocument.presentation", "odp",
+ "OpenDocument Presentation"},
+ {"application/vnd.oasis.opendocument.spreadsheet", "ods",
+ "OpenDocument Spreadsheet"},
+ {"application/vnd.oasis.opendocument.graphics", "odg",
+ "OpenDocument Graphics"},
+
+ // Legacy Microsoft Office
+ {"application/msword", "doc", "Microsoft Word"},
+ {"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"},
+ {"application/vnd.ms-excel", "xls", "Microsoft Excel"},
+
+ // Office Open XML
+ {"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "docx", "Microsoft Word (Open XML)"},
+ {"application/"
+ "vnd.openxmlformats-officedocument.presentationml.presentation",
+ "pptx", "Microsoft PowerPoint (Open XML)"},
+ {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "xlsx", "Microsoft Excel (Open XML)"},
+
+ {IMAGE_ART, "art", "ART Image"},
+ {IMAGE_BMP, "bmp", "BMP Image"},
+ {IMAGE_GIF, "gif", "GIF Image"},
+ {IMAGE_ICO, "ico,cur", "ICO Image"},
+ {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"},
+ {IMAGE_PNG, "png", "PNG Image"},
+ {IMAGE_APNG, "apng", "APNG Image"},
+ {IMAGE_TIFF, "tiff,tif", "TIFF Image"},
+ {IMAGE_XBM, "xbm", "XBM Image"},
+ {IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"},
+ {IMAGE_WEBP, "webp", "WebP Image"},
+ {IMAGE_AVIF, "avif", "AV1 Image File"},
+ {IMAGE_JXL, "jxl", "JPEG XL Image File"},
+
+ {MESSAGE_RFC822, "eml", "RFC-822 data"},
+ {TEXT_PLAIN, "txt,text", "Text File"},
+ {APPLICATION_JSON, "json", "JavaScript Object Notation"},
+ {TEXT_VTT, "vtt", "Web Video Text Tracks"},
+ {TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"},
+ {TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"},
+ {"application/xhtml+xml", "xhtml,xht",
+ "Extensible HyperText Markup Language"},
+ {APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"},
+ {APPLICATION_RDF, "rdf", "Resource Description Framework"},
+ {"text/csv", "csv", "CSV File"},
+ {TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"},
+ {TEXT_CSS, "css", "Style Sheet"},
+ {TEXT_VCARD, "vcf,vcard", "Contact Information"},
+ {TEXT_CALENDAR, "ics,ical,ifb,icalendar", "iCalendar"},
+ {VIDEO_OGG, "ogv,ogg", "Ogg Video"},
+ {APPLICATION_OGG, "ogg", "Ogg Video"},
+ {AUDIO_OGG, "oga", "Ogg Audio"},
+ {AUDIO_OGG, "opus", "Opus Audio"},
+ {VIDEO_WEBM, "webm", "Web Media Video"},
+ {AUDIO_WEBM, "webm", "Web Media Audio"},
+ {AUDIO_MP3, "mp3,mpega,mp2", "MPEG Audio"},
+ {VIDEO_MP4, "mp4,m4a,m4b", "MPEG-4 Video"},
+ {AUDIO_MP4, "m4a,m4b", "MPEG-4 Audio"},
+ {VIDEO_RAW, "yuv", "Raw YUV Video"},
+ {AUDIO_WAV, "wav", "Waveform Audio"},
+ {VIDEO_3GPP, "3gpp,3gp", "3GPP Video"},
+ {VIDEO_3GPP2, "3g2", "3GPP2 Video"},
+ {AUDIO_AAC, "aac", "AAC Audio"},
+ {AUDIO_FLAC, "flac", "FLAC Audio"},
+ {AUDIO_MIDI, "mid", "Standard MIDI Audio"},
+ {APPLICATION_WASM, "wasm", "WebAssembly Module"}};
+
+static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = {
+ {IMAGE_JPEG, "jfif"}};
+
+/**
+ * File extensions for which decoding should be disabled.
+ * NOTE: These MUST be lower-case and ASCII.
+ */
+static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = {
+ {APPLICATION_GZIP, "gz"},
+ {APPLICATION_GZIP, "tgz"},
+ {APPLICATION_ZIP, "zip"},
+ {APPLICATION_COMPRESS, "z"},
+ {APPLICATION_GZIP, "svgz"}};
+
+/**
+ * Mimetypes for which we enforce using a known extension.
+ *
+ * In addition to this list, we do this for all audio/, video/ and
+ * image/ mimetypes.
+ */
+static const char* forcedExtensionMimetypes[] = {
+ APPLICATION_PDF, APPLICATION_OGG, APPLICATION_WASM,
+ TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD};
+
+/**
+ * Primary extensions of types whose descriptions should be overwritten.
+ * This extension is concatenated with "ExtHandlerDescription" to look up the
+ * description in unknownContentType.properties.
+ * NOTE: These MUST be lower-case and ASCII.
+ */
+static const char* descriptionOverwriteExtensions[] = {
+ "avif", "jxl", "pdf", "svg", "webp", "xml",
+};
+
+static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton;
+
+/**
+ * In child processes, return an nsOSHelperAppServiceChild for remoting
+ * OS calls to the parent process. In the parent process itself, use
+ * nsOSHelperAppService.
+ */
+/* static */
+already_AddRefed<nsExternalHelperAppService>
+nsExternalHelperAppService::GetSingleton() {
+ if (!sExtHelperAppSvcSingleton) {
+ if (XRE_IsParentProcess()) {
+ sExtHelperAppSvcSingleton = new nsOSHelperAppService();
+ } else {
+ sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild();
+ }
+ ClearOnShutdown(&sExtHelperAppSvcSingleton);
+ }
+
+ return do_AddRef(sExtHelperAppSvcSingleton);
+}
+
+NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService,
+ nsPIExternalAppLauncher, nsIExternalProtocolService,
+ nsIMIMEService, nsIObserver, nsISupportsWeakReference)
+
+nsExternalHelperAppService::nsExternalHelperAppService() {}
+nsresult nsExternalHelperAppService::Init() {
+ // Add an observer for profile change
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (!obs) return NS_ERROR_FAILURE;
+
+ nsresult rv = obs->AddObserver(this, "profile-before-change", true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return obs->AddObserver(this, "last-pb-context-exited", true);
+}
+
+nsExternalHelperAppService::~nsExternalHelperAppService() {}
+
+nsresult nsExternalHelperAppService::DoContentContentProcessHelper(
+ const nsACString& aMimeContentType, nsIChannel* aChannel,
+ BrowsingContext* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener) {
+ NS_ENSURE_ARG_POINTER(aChannel);
+
+ // We need to get a hold of a ContentChild so that we can begin forwarding
+ // this data to the parent. In the HTTP case, this is unfortunate, since
+ // we're actually passing data from parent->child->parent wastefully, but
+ // the Right Fix will eventually be to short-circuit those channels on the
+ // parent side based on some sort of subscription concept.
+ using mozilla::dom::ContentChild;
+ using mozilla::dom::ExternalHelperAppChild;
+ ContentChild* child = ContentChild::GetSingleton();
+ if (!child) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCString disp;
+ nsCOMPtr<nsIURI> uri;
+ int64_t contentLength = -1;
+ bool wasFileChannel = false;
+ uint32_t contentDisposition = -1;
+ nsAutoString fileName;
+ nsCOMPtr<nsILoadInfo> loadInfo;
+
+ aChannel->GetURI(getter_AddRefs(uri));
+ aChannel->GetContentLength(&contentLength);
+ aChannel->GetContentDisposition(&contentDisposition);
+ aChannel->GetContentDispositionFilename(fileName);
+ aChannel->GetContentDispositionHeader(disp);
+ loadInfo = aChannel->LoadInfo();
+
+ nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aChannel));
+ wasFileChannel = fileChan != nullptr;
+
+ nsCOMPtr<nsIURI> referrer;
+ NS_GetReferrerFromChannel(aChannel, getter_AddRefs(referrer));
+
+ mozilla::net::LoadInfoArgs loadInfoArgs;
+ MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
+
+ nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aChannel));
+ // Determine whether a new window was opened specifically for this request
+ bool shouldCloseWindow = false;
+ if (props) {
+ props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns,
+ &shouldCloseWindow);
+ }
+
+ // Now we build a protocol for forwarding our data to the parent. The
+ // protocol will act as a listener on the child-side and create a "real"
+ // helperAppService listener on the parent-side, via another call to
+ // DoContent.
+ RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild();
+ MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor(
+ childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp,
+ contentDisposition, fileName, aForceSave, contentLength, wasFileChannel,
+ referrer, aContentContext, shouldCloseWindow));
+
+ NS_ADDREF(*aStreamListener = childListener);
+
+ uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
+
+ SanitizeFileName(fileName, 0);
+
+ RefPtr<nsExternalAppHandler> handler =
+ new nsExternalAppHandler(nullptr, u""_ns, aContentContext, aWindowContext,
+ this, fileName, reason, aForceSave);
+ if (!handler) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ childListener->SetHandler(handler);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalHelperAppService::CreateListener(
+ const nsACString& aMimeContentType, nsIChannel* aChannel,
+ BrowsingContext* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener) {
+ MOZ_ASSERT(!XRE_IsContentProcess());
+ NS_ENSURE_ARG_POINTER(aChannel);
+
+ nsAutoString fileName;
+ nsAutoCString fileExtension;
+ uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
+
+ uint32_t contentDisposition = -1;
+ aChannel->GetContentDisposition(&contentDisposition);
+ if (contentDisposition == nsIChannel::DISPOSITION_ATTACHMENT) {
+ reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
+ }
+
+ *aStreamListener = nullptr;
+
+ // Get the file extension and name that we will need later
+ nsCOMPtr<nsIURI> uri;
+ bool allowURLExtension =
+ GetFileNameFromChannel(aChannel, fileName, getter_AddRefs(uri));
+
+ uint32_t flags = VALIDATE_ALLOW_EMPTY;
+ if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
+ nsCaseInsensitiveCStringComparator)) {
+ flags |= VALIDATE_GUESS_FROM_EXTENSION;
+ }
+
+ nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
+ fileName, aMimeContentType, uri, nullptr, flags, allowURLExtension);
+
+ LOG("Type/Ext lookup found 0x%p\n", mimeInfo.get());
+
+ // No mimeinfo -> we can't continue. probably OOM.
+ if (!mimeInfo) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (flags & VALIDATE_GUESS_FROM_EXTENSION) {
+ // Replace the content type with what was guessed.
+ nsAutoCString mimeType;
+ mimeInfo->GetMIMEType(mimeType);
+ aChannel->SetContentType(mimeType);
+
+ if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
+ reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
+ }
+ }
+
+ nsAutoString extension;
+ int32_t dotidx = fileName.RFind(u".");
+ if (dotidx != -1) {
+ extension = Substring(fileName, dotidx + 1);
+ }
+
+ // NB: ExternalHelperAppParent depends on this listener always being an
+ // nsExternalAppHandler. If this changes, make sure to update that code.
+ nsExternalAppHandler* handler = new nsExternalAppHandler(
+ mimeInfo, extension, aContentContext, aWindowContext, this, fileName,
+ reason, aForceSave);
+ if (!handler) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ NS_ADDREF(*aStreamListener = handler);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalHelperAppService::DoContent(
+ const nsACString& aMimeContentType, nsIChannel* aChannel,
+ nsIInterfaceRequestor* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener) {
+ // Scripted interface requestors cannot return an instance of the
+ // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so
+ // get to the window via `nsIDOMWindow`. Unfortunately, at that point we
+ // don't know whether the thing we got is an inner or outer window, so have to
+ // work with either one.
+ RefPtr<BrowsingContext> bc;
+ nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext);
+ if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) {
+ bc = outerWindow->GetBrowsingContext();
+ } else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow =
+ do_QueryInterface(domWindow)) {
+ bc = innerWindow->GetBrowsingContext();
+ }
+
+ if (XRE_IsContentProcess()) {
+ return DoContentContentProcessHelper(aMimeContentType, aChannel, bc,
+ aForceSave, aWindowContext,
+ aStreamListener);
+ }
+
+ nsresult rv = CreateListener(aMimeContentType, aChannel, bc, aForceSave,
+ aWindowContext, aStreamListener);
+ return rv;
+}
+
+NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
+ const nsACString& aExtension, const nsACString& aEncodingType,
+ bool* aApplyDecoding) {
+ *aApplyDecoding = true;
+ uint32_t i;
+ for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) {
+ if (aExtension.LowerCaseEqualsASCII(
+ nonDecodableExtensions[i].mFileExtension) &&
+ aEncodingType.LowerCaseEqualsASCII(
+ nonDecodableExtensions[i].mMimeType)) {
+ *aApplyDecoding = false;
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsExternalHelperAppService::GetFileTokenForPath(
+ const char16_t* aPlatformAppPath, nsIFile** aFile) {
+ nsDependentString platformAppPath(aPlatformAppPath);
+ // First, check if we have an absolute path
+ nsIFile* localFile = nullptr;
+ nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile);
+ if (NS_SUCCEEDED(rv)) {
+ *aFile = localFile;
+ bool exists;
+ if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) {
+ NS_RELEASE(*aFile);
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ return NS_OK;
+ }
+
+ // Second, check if file exists in mozilla program directory
+ rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile);
+ if (NS_SUCCEEDED(rv)) {
+ rv = (*aFile)->Append(platformAppPath);
+ if (NS_SUCCEEDED(rv)) {
+ bool exists = false;
+ rv = (*aFile)->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) return NS_OK;
+ }
+ NS_RELEASE(*aFile);
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+// begin external protocol service default implementation...
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists(
+ const char* aProtocolScheme, bool* aHandlerExists) {
+ nsCOMPtr<nsIHandlerInfo> handlerInfo;
+ nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme),
+ getter_AddRefs(handlerInfo));
+ if (NS_SUCCEEDED(rv)) {
+ // See if we have any known possible handler apps for this
+ nsCOMPtr<nsIMutableArray> possibleHandlers;
+ handlerInfo->GetPossibleApplicationHandlers(
+ getter_AddRefs(possibleHandlers));
+
+ uint32_t length;
+ possibleHandlers->GetLength(&length);
+ if (length) {
+ *aHandlerExists = true;
+ return NS_OK;
+ }
+ }
+
+ // if not, fall back on an os-based handler
+ return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists);
+}
+
+NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol(
+ const char* aProtocolScheme, bool* aResult) {
+ // check the per protocol setting first. it always takes precedence.
+ // if not set, then use the global setting.
+
+ nsAutoCString prefName("network.protocol-handler.expose.");
+ prefName += aProtocolScheme;
+ bool val;
+ if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) {
+ *aResult = val;
+ return NS_OK;
+ }
+
+ // by default, no protocol is exposed. i.e., by default all link clicks must
+ // go through the external protocol service. most applications override this
+ // default behavior.
+ *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false);
+
+ return NS_OK;
+}
+
+static const char kExternalProtocolPrefPrefix[] =
+ "network.protocol-handler.external.";
+static const char kExternalProtocolDefaultPref[] =
+ "network.protocol-handler.external-default";
+
+// static
+nsresult nsExternalHelperAppService::EscapeURI(nsIURI* aURI, nsIURI** aResult) {
+ MOZ_ASSERT(aURI);
+ MOZ_ASSERT(aResult);
+
+ nsAutoCString spec;
+ aURI->GetSpec(spec);
+
+ if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI;
+
+ nsAutoCString escapedSpec;
+ nsresult rv = NS_EscapeURL(spec, esc_AlwaysCopy | esc_ExtHandler, escapedSpec,
+ fallible);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIIOService> ios(do_GetIOService());
+ return ios->NewURI(escapedSpec, nullptr, nullptr, aResult);
+}
+
+bool ExternalProtocolIsBlockedBySandbox(
+ BrowsingContext* aBrowsingContext,
+ const bool aHasValidUserGestureActivation) {
+ if (!StaticPrefs::dom_block_external_protocol_navigation_from_sandbox()) {
+ return false;
+ }
+
+ if (!aBrowsingContext || aBrowsingContext->IsTop()) {
+ return false;
+ }
+
+ uint32_t sandboxFlags = aBrowsingContext->GetSandboxFlags();
+
+ if (sandboxFlags == SANDBOXED_NONE) {
+ return false;
+ }
+
+ if (!(sandboxFlags & SANDBOXED_AUXILIARY_NAVIGATION)) {
+ return false;
+ }
+
+ if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION)) {
+ return false;
+ }
+
+ if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_CUSTOM_PROTOCOLS)) {
+ return false;
+ }
+
+ if (!(sandboxFlags & SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION) &&
+ aHasValidUserGestureActivation) {
+ return false;
+ }
+
+ return true;
+}
+
+NS_IMETHODIMP
+nsExternalHelperAppService::LoadURI(nsIURI* aURI,
+ nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aRedirectPrincipal,
+ BrowsingContext* aBrowsingContext,
+ bool aTriggeredExternally,
+ bool aHasValidUserGestureActivation) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ if (XRE_IsContentProcess()) {
+ mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
+ aURI, aTriggeringPrincipal, aRedirectPrincipal, aBrowsingContext,
+ aTriggeredExternally, aHasValidUserGestureActivation);
+ return NS_OK;
+ }
+
+ // Prevent sandboxed BrowsingContexts from navigating to external protocols.
+ // This only uses the sandbox flags of the target BrowsingContext of the
+ // load. The navigating document's CSP sandbox flags do not apply.
+ if (aBrowsingContext &&
+ ExternalProtocolIsBlockedBySandbox(aBrowsingContext,
+ aHasValidUserGestureActivation)) {
+ // Log an error to the web console of the sandboxed BrowsingContext.
+ nsAutoString localizedMsg;
+ nsAutoCString spec;
+ aURI->GetSpec(spec);
+
+ AutoTArray<nsString, 1> params = {NS_ConvertUTF8toUTF16(spec)};
+ nsresult rv = nsContentUtils::FormatLocalizedString(
+ nsContentUtils::eSECURITY_PROPERTIES, "SandboxBlockedCustomProtocols",
+ params, localizedMsg);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Log to the the parent window of the iframe. If there is no parent, fall
+ // back to the iframe window itself.
+ WindowContext* windowContext = aBrowsingContext->GetParentWindowContext();
+ if (!windowContext) {
+ windowContext = aBrowsingContext->GetCurrentWindowContext();
+ }
+
+ // Skip logging if we still don't have a WindowContext.
+ NS_ENSURE_TRUE(windowContext, NS_ERROR_FAILURE);
+
+ nsContentUtils::ReportToConsoleByWindowID(
+ localizedMsg, nsIScriptError::errorFlag, "Security"_ns,
+ windowContext->InnerWindowId(),
+ windowContext->Canonical()->GetDocumentURI());
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> escapedURI;
+ nsresult rv = EscapeURI(aURI, getter_AddRefs(escapedURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString scheme;
+ escapedURI->GetScheme(scheme);
+ if (scheme.IsEmpty()) return NS_OK; // must have a scheme
+
+ // Deny load if the prefs say to do so
+ nsAutoCString externalPref(kExternalProtocolPrefPrefix);
+ externalPref += scheme;
+ bool allowLoad = false;
+ if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) {
+ // no scheme-specific value, check the default
+ if (NS_FAILED(
+ Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) {
+ return NS_OK; // missing default pref
+ }
+ }
+
+ if (!allowLoad) {
+ return NS_OK; // explicitly denied
+ }
+
+ // Now check if the principal is allowed to access the navigated context.
+ // We allow navigating subframes, even if not same-origin - non-external
+ // links can always navigate everywhere, so this is a minor additional
+ // restriction, only aiming to prevent some types of spoofing attacks
+ // from otherwise disjoint browsingcontext trees.
+ if (aBrowsingContext && aTriggeringPrincipal &&
+ !StaticPrefs::security_allow_disjointed_external_uri_loads() &&
+ // Add-on principals are always allowed:
+ !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() &&
+ // As is chrome code:
+ !aTriggeringPrincipal->IsSystemPrincipal()) {
+ RefPtr<BrowsingContext> bc = aBrowsingContext;
+ WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal();
+ bool foundAccessibleFrame = false;
+
+ // Also allow this load if the target is a toplevel BC and contains a
+ // non-web-controlled about:blank document
+ if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) {
+ RefPtr<nsIURI> uri = wgp->GetDocumentURI();
+ foundAccessibleFrame =
+ uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank");
+ }
+
+ while (!foundAccessibleFrame) {
+ if (wgp) {
+ foundAccessibleFrame =
+ aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal());
+ }
+ // We have to get the parent via the bc, because there may not
+ // be a window global for the innermost bc; see bug 1650162.
+ BrowsingContext* parent = bc->GetParent();
+ if (!parent) {
+ break;
+ }
+ bc = parent;
+ wgp = parent->Canonical()->GetCurrentWindowGlobal();
+ }
+
+ if (!foundAccessibleFrame) {
+ // See if this navigation could have come from a subframe.
+ nsTArray<RefPtr<BrowsingContext>> contexts;
+ aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts);
+ for (const auto& kid : contexts) {
+ wgp = kid->Canonical()->GetCurrentWindowGlobal();
+ if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) {
+ foundAccessibleFrame = true;
+ break;
+ }
+ }
+ }
+
+ if (!foundAccessibleFrame) {
+ return NS_OK; // deny the load.
+ }
+ }
+
+ nsCOMPtr<nsIHandlerInfo> handler;
+ rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIContentDispatchChooser> chooser =
+ do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return chooser->HandleURI(
+ handler, escapedURI,
+ aRedirectPrincipal ? aRedirectPrincipal : aTriggeringPrincipal,
+ aBrowsingContext, aTriggeredExternally);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+// Methods related to deleting temporary files on exit
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/* static */
+nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper(
+ nsIFile* aTemporaryFile, nsCOMArray<nsIFile>& aFileList) {
+ bool isFile = false;
+
+ // as a safety measure, make sure the nsIFile is really a file and not a
+ // directory object.
+ aTemporaryFile->IsFile(&isFile);
+ if (!isFile) return NS_OK;
+
+ aFileList.AppendObject(aTemporaryFile);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) {
+ return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList);
+}
+
+NS_IMETHODIMP
+nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible(
+ nsIFile* aTemporaryFile) {
+ return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList);
+}
+
+void nsExternalHelperAppService::ExpungeTemporaryFilesHelper(
+ nsCOMArray<nsIFile>& fileList) {
+ int32_t numEntries = fileList.Count();
+ nsIFile* localFile;
+ for (int32_t index = 0; index < numEntries; index++) {
+ localFile = fileList[index];
+ if (localFile) {
+ // First make the file writable, since the temp file is probably readonly.
+ localFile->SetPermissions(0600);
+ localFile->Remove(false);
+ }
+ }
+
+ fileList.Clear();
+}
+
+void nsExternalHelperAppService::ExpungeTemporaryFiles() {
+ ExpungeTemporaryFilesHelper(mTemporaryFilesList);
+}
+
+void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() {
+ ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList);
+}
+
+static const char kExternalWarningPrefPrefix[] =
+ "network.protocol-handler.warn-external.";
+static const char kExternalWarningDefaultPref[] =
+ "network.protocol-handler.warn-external-default";
+
+NS_IMETHODIMP
+nsExternalHelperAppService::GetProtocolHandlerInfo(
+ const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) {
+ // XXX enterprise customers should be able to turn this support off with a
+ // single master pref (maybe use one of the "exposed" prefs here?)
+
+ bool exists;
+ nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo);
+ if (NS_FAILED(rv)) {
+ // Either it knows nothing, or we ran out of memory
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID);
+ if (handlerSvc) {
+ bool hasHandler = false;
+ (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler);
+ if (hasHandler) {
+ rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns);
+ if (NS_SUCCEEDED(rv)) return NS_OK;
+ }
+ }
+
+ return SetProtocolHandlerDefaults(*aHandlerInfo, exists);
+}
+
+NS_IMETHODIMP
+nsExternalHelperAppService::SetProtocolHandlerDefaults(
+ nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) {
+ // this type isn't in our database, so we've only got an OS default handler,
+ // if one exists
+
+ if (aOSHandlerExists) {
+ // we've got a default, so use it
+ aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault);
+
+ // whether or not to ask the user depends on the warning preference
+ nsAutoCString scheme;
+ aHandlerInfo->GetType(scheme);
+
+ nsAutoCString warningPref(kExternalWarningPrefPrefix);
+ warningPref += scheme;
+ bool warn;
+ if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) {
+ // no scheme-specific value, check the default
+ warn = Preferences::GetBool(kExternalWarningDefaultPref, true);
+ }
+ aHandlerInfo->SetAlwaysAskBeforeHandling(warn);
+ } else {
+ // If no OS default existed, we set the preferred action to alwaysAsk.
+ // This really means not initialized (i.e. there's no available handler)
+ // to all the code...
+ aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk);
+ }
+
+ return NS_OK;
+}
+
+// XPCOM profile change observer
+NS_IMETHODIMP
+nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* someData) {
+ if (!strcmp(aTopic, "profile-before-change")) {
+ ExpungeTemporaryFiles();
+ } else if (!strcmp(aTopic, "last-pb-context-exited")) {
+ ExpungeTemporaryPrivateFiles();
+ }
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+// begin external app handler implementation
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMPL_ADDREF(nsExternalAppHandler)
+NS_IMPL_RELEASE(nsExternalAppHandler)
+
+NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener)
+ NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
+ NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher)
+ NS_INTERFACE_MAP_ENTRY(nsICancelable)
+ NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler)
+NS_INTERFACE_MAP_END
+
+nsExternalAppHandler::nsExternalAppHandler(
+ nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension,
+ BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
+ nsExternalHelperAppService* aExtProtSvc,
+ const nsAString& aSuggestedFileName, uint32_t aReason, bool aForceSave)
+ : mMimeInfo(aMIMEInfo),
+ mBrowsingContext(aBrowsingContext),
+ mWindowContext(aWindowContext),
+ mSuggestedFileName(aSuggestedFileName),
+ mForceSave(aForceSave),
+ mForceSaveInternallyHandled(false),
+ mCanceled(false),
+ mStopRequestIssued(false),
+ mIsFileChannel(false),
+ mShouldCloseWindow(false),
+ mHandleInternally(false),
+ mDialogShowing(false),
+ mReason(aReason),
+ mTempFileIsExecutable(false),
+ mTimeDownloadStarted(0),
+ mContentLength(-1),
+ mProgress(0),
+ mSaver(nullptr),
+ mDialogProgressListener(nullptr),
+ mTransfer(nullptr),
+ mRequest(nullptr),
+ mExtProtSvc(aExtProtSvc) {
+ // make sure the extention includes the '.'
+ if (!aFileExtension.IsEmpty() && aFileExtension.First() != '.') {
+ mFileExtension = char16_t('.');
+ }
+ mFileExtension.Append(aFileExtension);
+
+ mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
+}
+
+nsExternalAppHandler::~nsExternalAppHandler() {
+ MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
+}
+
+void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) {
+ MOZ_ASSERT(XRE_IsContentProcess(), "in child process");
+ // Remove our request from the child loadGroup
+ RetargetLoadNotifications(request);
+}
+
+NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener(
+ nsIWebProgressListener2* aWebProgressListener) {
+ // This is always called by nsHelperDlg.js. Go ahead and register the
+ // progress listener. At this point, we don't have mTransfer.
+ mDialogProgressListener = aWebProgressListener;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) {
+ if (mFinalFileDestination)
+ *aTarget = mFinalFileDestination;
+ else
+ *aTarget = mTempFile;
+
+ NS_IF_ADDREF(*aTarget);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) {
+ // Use the real target if it's been set
+ if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec);
+
+ // Otherwise, use the stored executable-ness of the temporary
+ *aExec = mTempFileIsExecutable;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) {
+ *aTime = mTimeDownloadStarted;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) {
+ *aContentLength = mContentLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId(
+ uint64_t* aBrowsingContextId) {
+ *aBrowsingContextId = mBrowsingContext ? mBrowsingContext->Id() : 0;
+ return NS_OK;
+}
+
+void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) {
+ // we are going to run the downloading of the helper app in our own little
+ // docloader / load group context. so go ahead and force the creation of a
+ // load group and doc loader for us to use...
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+ if (!aChannel) return;
+
+ bool isPrivate = NS_UsePrivateBrowsing(aChannel);
+
+ nsCOMPtr<nsILoadGroup> oldLoadGroup;
+ aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup));
+
+ if (oldLoadGroup) {
+ oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED);
+ }
+
+ aChannel->SetLoadGroup(nullptr);
+ aChannel->SetNotificationCallbacks(nullptr);
+
+ nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel);
+ if (pbChannel) {
+ pbChannel->SetPrivate(isPrivate);
+ }
+}
+
+nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
+ // First we need to try to get the destination directory for the temporary
+ // file.
+ nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // At this point, we do not have a filename for the temp file. For security
+ // purposes, this cannot be predictable, so we must use a cryptographic
+ // quality PRNG to generate one.
+ nsAutoCString tempLeafName;
+ rv = GenerateRandomName(tempLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now append our extension.
+ nsAutoCString ext;
+ mMimeInfo->GetPrimaryExtension(ext);
+ if (!ext.IsEmpty()) {
+ ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
+ if (ext.First() != '.') tempLeafName.Append('.');
+ tempLeafName.Append(ext);
+ }
+
+ // We need to temporarily create a dummy file with the correct
+ // file extension to determine the executable-ness, so do this before adding
+ // the extra .part extension.
+ nsCOMPtr<nsIFile> dummyFile;
+ rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set the file name without .part
+ rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Store executable-ness then delete
+ dummyFile->IsExecutable(&mTempFileIsExecutable);
+ dummyFile->Remove(false);
+
+ // Add an additional .part to prevent the OS from running this file in the
+ // default application.
+ tempLeafName.AppendLiteral(".part");
+
+ rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName));
+ // make this file unique!!!
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now save the temp leaf name, minus the ".part" bit, so we can use it later.
+ // This is a bit broken in the case when createUnique actually had to append
+ // some numbers, because then we now have a filename like foo.bar-1.part and
+ // we'll end up with foo.bar-1.bar as our final filename if we end up using
+ // this. But the other options are all bad too.... Ideally we'd have a way
+ // to tell createUnique to put its unique marker before the extension that
+ // comes before ".part" or something.
+ rv = mTempFile->GetLeafName(mTempLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns),
+ NS_ERROR_UNEXPECTED);
+
+ // Strip off the ".part" from mTempLeafName
+ mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
+
+ MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
+ mSaver =
+ do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mSaver->SetObserver(this);
+ if (NS_FAILED(rv)) {
+ mSaver = nullptr;
+ return rv;
+ }
+
+ rv = mSaver->EnableSha256();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mSaver->EnableSignatureInfo();
+ NS_ENSURE_SUCCESS(rv, rv);
+ LOG("Enabled hashing and signature verification");
+
+ rv = mSaver->SetTarget(mTempFile, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+void nsExternalAppHandler::MaybeApplyDecodingForExtension(
+ nsIRequest* aRequest) {
+ MOZ_ASSERT(aRequest);
+
+ nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aRequest);
+ if (!encChannel) {
+ return;
+ }
+
+ // Turn off content encoding conversions if needed
+ bool applyConversion = true;
+
+ // First, check to see if conversion is already disabled. If so, we
+ // have nothing to do here.
+ encChannel->GetApplyConversion(&applyConversion);
+ if (!applyConversion) {
+ return;
+ }
+
+ nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(mSourceUrl));
+ if (sourceURL) {
+ nsAutoCString extension;
+ sourceURL->GetFileExtension(extension);
+ if (!extension.IsEmpty()) {
+ nsCOMPtr<nsIUTF8StringEnumerator> encEnum;
+ encChannel->GetContentEncodings(getter_AddRefs(encEnum));
+ if (encEnum) {
+ bool hasMore;
+ nsresult rv = encEnum->HasMore(&hasMore);
+ if (NS_SUCCEEDED(rv) && hasMore) {
+ nsAutoCString encType;
+ rv = encEnum->GetNext(encType);
+ if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) {
+ MOZ_ASSERT(mExtProtSvc);
+ mExtProtSvc->ApplyDecodingForExtension(extension, encType,
+ &applyConversion);
+ }
+ }
+ }
+ }
+ }
+
+ encChannel->SetApplyConversion(applyConversion);
+}
+
+already_AddRefed<nsIInterfaceRequestor>
+nsExternalAppHandler::GetDialogParent() {
+ nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext;
+
+ if (!dialogParent && mBrowsingContext) {
+ dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow());
+ }
+ if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) {
+ RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement();
+ if (element) {
+ dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow());
+ }
+ }
+ return dialogParent.forget();
+}
+
+NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
+ MOZ_ASSERT(request, "OnStartRequest without request?");
+
+ // Set mTimeDownloadStarted here as the download has already started and
+ // we want to record the start time before showing the filepicker.
+ mTimeDownloadStarted = PR_Now();
+
+ mRequest = request;
+
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+
+ nsresult rv;
+ nsAutoCString MIMEType;
+ if (mMimeInfo) {
+ mMimeInfo->GetMIMEType(MIMEType);
+ }
+ // Now get the URI
+ if (aChannel) {
+ aChannel->GetURI(getter_AddRefs(mSourceUrl));
+ // HTTPS-Only/HTTPS-FirstMode tries to upgrade connections to https. Once
+ // the download is in progress we set that flag so that timeout counter
+ // measures do not kick in.
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ bool isPrivateWin = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
+ if (nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin) ||
+ nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) {
+ uint32_t httpsOnlyStatus = loadInfo->GetHttpsOnlyStatus();
+ httpsOnlyStatus |= nsILoadInfo::HTTPS_ONLY_DOWNLOAD_IN_PROGRESS;
+ loadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
+ }
+ }
+
+ if (!mForceSave && StaticPrefs::browser_download_enable_spam_prevention() &&
+ IsDownloadSpam(aChannel)) {
+ return NS_OK;
+ }
+
+ mDownloadClassification =
+ nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType);
+
+ if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) {
+ // If the download is rated as forbidden,
+ // cancel the request so no ui knows about this.
+ mCanceled = true;
+ request->Cancel(NS_ERROR_ABORT);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(request));
+ mIsFileChannel = fileChan != nullptr;
+ if (!mIsFileChannel) {
+ // It's possible that this request came from the child process and the
+ // file channel actually lives there. If this returns true, then our
+ // mSourceUrl will be an nsIFileURL anyway.
+ nsCOMPtr<dom::nsIExternalHelperAppParent> parent(
+ do_QueryInterface(request));
+ mIsFileChannel = parent && parent->WasFileChannel();
+ }
+
+ // Get content length
+ if (aChannel) {
+ aChannel->GetContentLength(&mContentLength);
+ }
+
+ if (mBrowsingContext) {
+ mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext);
+ mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow);
+ nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv));
+ // Determine whether a new window was opened specifically for this request
+ if (props) {
+ bool tmp = false;
+ if (NS_SUCCEEDED(
+ props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) {
+ mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp);
+ }
+ }
+ }
+
+ // retarget all load notifications to our docloader instead of the original
+ // window's docloader...
+ RetargetLoadNotifications(request);
+
+ // Close the underlying DOMWindow if it was opened specifically for the
+ // download. We don't run this in the content process, since we have
+ // an instance running in the parent as well, which will handle this
+ // if needed.
+ if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) {
+ mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow();
+ }
+
+ // In an IPC setting, we're allowing the child process, here, to make
+ // decisions about decoding the channel (e.g. decompression). It will
+ // still forward the decoded (uncompressed) data back to the parent.
+ // Con: Uncompressed data means more IPC overhead.
+ // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel.
+ // Parent process doesn't need to expect CPU time on decompression.
+ MaybeApplyDecodingForExtension(aChannel);
+
+ // At this point, the child process has done everything it can usefully do
+ // for OnStartRequest.
+ if (XRE_IsContentProcess()) {
+ return NS_OK;
+ }
+
+ rv = SetUpTempFile(aChannel);
+ if (NS_FAILED(rv)) {
+ nsresult transferError = rv;
+
+ rv = CreateFailedTransfer();
+ if (NS_FAILED(rv)) {
+ LOG("Failed to create transfer to report failure."
+ "Will fallback to prompter!");
+ }
+
+ mCanceled = true;
+ request->Cancel(transferError);
+
+ nsAutoString path;
+ if (mTempFile) mTempFile->GetPath(path);
+
+ SendStatusChange(kWriteError, transferError, request, path);
+
+ return NS_OK;
+ }
+
+ // Inform channel it is open on behalf of a download to throttle it during
+ // page loads and prevent its caching.
+ nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel);
+ if (httpInternal) {
+ rv = httpInternal->SetChannelIsForDownload(true);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ if (mSourceUrl->SchemeIs("data")) {
+ // In case we're downloading a data:// uri
+ // we don't want to apply AllowTopLevelNavigationToDataURI.
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ loadInfo->SetForceAllowDataURI(true);
+ }
+
+ // now that the temp file is set up, find out if we need to invoke a dialog
+ // asking the user what they want us to do with this content...
+
+ // We can get here for three reasons: "can't handle", "sniffed type", or
+ // "server sent content-disposition:attachment". In the first case we want
+ // to honor the user's "always ask" pref; in the other two cases we want to
+ // honor it only if the default action is "save". Opening attachments in
+ // helper apps by default breaks some websites (especially if the attachment
+ // is one part of a multipart document). Opening sniffed content in helper
+ // apps by default introduces security holes that we'd rather not have.
+
+ // So let's find out whether the user wants to be prompted. If he does not,
+ // check mReason and the preferred action to see what we should do.
+
+ bool alwaysAsk = true;
+ mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
+ if (alwaysAsk) {
+ // But we *don't* ask if this mimeInfo didn't come from
+ // our user configuration datastore and the user has said
+ // at some point in the distant past that they don't
+ // want to be asked. The latter fact would have been
+ // stored in pref strings back in the old days.
+
+ bool mimeTypeIsInDatastore = false;
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID);
+ if (handlerSvc) {
+ handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore);
+ }
+ if (!handlerSvc || !mimeTypeIsInDatastore) {
+ if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF,
+ MIMEType.get())) {
+ // Don't need to ask after all.
+ alwaysAsk = false;
+ // Make sure action matches pref (save to disk).
+ mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF,
+ MIMEType.get())) {
+ // Don't need to ask after all.
+ alwaysAsk = false;
+ }
+ }
+ } else if (MIMEType.EqualsLiteral("text/plain")) {
+ nsAutoCString ext;
+ mMimeInfo->GetPrimaryExtension(ext);
+ // If people are sending us ApplicationReputation-eligible files with
+ // text/plain mimetypes, enforce asking the user what to do.
+ if (!ext.IsEmpty()) {
+ nsAutoCString dummyFileName("f");
+ if (ext.First() != '.') {
+ dummyFileName.Append(".");
+ }
+ ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
+ nsCOMPtr<nsIApplicationReputationService> appRep =
+ components::ApplicationReputation::Service();
+ appRep->IsBinary(dummyFileName + ext, &alwaysAsk);
+ }
+ }
+
+ int32_t action = nsIMIMEInfo::saveToDisk;
+ mMimeInfo->GetPreferredAction(&action);
+
+ bool forcePrompt = mReason == nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
+
+ // OK, now check why we're here
+ if (!alwaysAsk && forcePrompt) {
+ // Force asking if we're not saving. See comment back when we fetched the
+ // alwaysAsk boolean for details.
+ alwaysAsk = (action != nsIMIMEInfo::saveToDisk);
+ }
+
+ bool shouldAutomaticallyHandleInternally =
+ action == nsIMIMEInfo::handleInternally;
+
+ if (aChannel) {
+ uint32_t disposition = -1;
+ aChannel->GetContentDisposition(&disposition);
+ mForceSaveInternallyHandled =
+ shouldAutomaticallyHandleInternally &&
+ disposition == nsIChannel::DISPOSITION_ATTACHMENT &&
+ mozilla::StaticPrefs::
+ browser_download_force_save_internally_handled_attachments();
+ }
+
+ // If we're not asking, check we actually know what to do:
+ if (!alwaysAsk) {
+ alwaysAsk = action != nsIMIMEInfo::saveToDisk &&
+ action != nsIMIMEInfo::useHelperApp &&
+ action != nsIMIMEInfo::useSystemDefault &&
+ !shouldAutomaticallyHandleInternally;
+ }
+
+ // If we're handling with the OS default and we are that default, force
+ // asking, so we don't end up in an infinite loop:
+ if (!alwaysAsk && action == nsIMIMEInfo::useSystemDefault) {
+ bool areOSDefault = false;
+ alwaysAsk = NS_SUCCEEDED(mMimeInfo->IsCurrentAppOSDefault(&areOSDefault)) &&
+ areOSDefault;
+ } else if (!alwaysAsk && action == nsIMIMEInfo::useHelperApp) {
+ nsCOMPtr<nsIHandlerApp> preferredApp;
+ mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(preferredApp));
+ nsCOMPtr<nsILocalHandlerApp> handlerApp = do_QueryInterface(preferredApp);
+ if (handlerApp) {
+ nsCOMPtr<nsIFile> executable;
+ handlerApp->GetExecutable(getter_AddRefs(executable));
+ nsCOMPtr<nsIFile> ourselves;
+ if (executable &&
+ // Despite the name, this really just fetches an nsIFile...
+ NS_SUCCEEDED(NS_GetSpecialDirectory(XRE_EXECUTABLE_FILE,
+ getter_AddRefs(ourselves)))) {
+ ourselves = nsMIMEInfoBase::GetCanonicalExecutable(ourselves);
+ executable = nsMIMEInfoBase::GetCanonicalExecutable(executable);
+ bool isSameApp = false;
+ alwaysAsk =
+ NS_FAILED(executable->Equals(ourselves, &isSameApp)) || isSameApp;
+ }
+ }
+ }
+
+ // if we were told that we _must_ save to disk without asking, all the stuff
+ // before this is irrelevant; override it
+ if (mForceSave || mForceSaveInternallyHandled) {
+ alwaysAsk = false;
+ action = nsIMIMEInfo::saveToDisk;
+ shouldAutomaticallyHandleInternally = false;
+ }
+ // Additionally, if we are asked by the OS to open a local file,
+ // automatically downloading it to create a second copy of that file doesn't
+ // really make sense. We should ask the user what they want to do.
+ if (mSourceUrl->SchemeIs("file") && !alwaysAsk &&
+ action == nsIMIMEInfo::saveToDisk) {
+ alwaysAsk = true;
+ }
+
+ // If adding new checks, make sure this is the last check before telemetry
+ // and going ahead with opening the file!
+#ifdef XP_WIN
+ /* We need to see whether the file we've got here could be
+ * executable. If it could, we had better not try to open it!
+ * We can skip this check, though, if we have a setting to open in a
+ * helper app.
+ */
+ if (!alwaysAsk && action != nsIMIMEInfo::saveToDisk &&
+ !shouldAutomaticallyHandleInternally) {
+ nsCOMPtr<nsIHandlerApp> prefApp;
+ mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp));
+ if (action != nsIMIMEInfo::useHelperApp || !prefApp) {
+ nsCOMPtr<nsIFile> fileToTest;
+ GetTargetFile(getter_AddRefs(fileToTest));
+ if (fileToTest) {
+ bool isExecutable;
+ rv = fileToTest->IsExecutable(&isExecutable);
+ if (NS_FAILED(rv) || mTempFileIsExecutable ||
+ isExecutable) { // checking NS_FAILED, because paranoia is good
+ alwaysAsk = true;
+ }
+ } else { // Paranoia is good here too, though this really should not
+ // happen
+ NS_WARNING(
+ "GetDownloadInfo returned a null file after the temp file has been "
+ "set up! ");
+ alwaysAsk = true;
+ }
+ }
+ }
+#endif
+
+ if (alwaysAsk) {
+ // Display the dialog
+ mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this will create a reference cycle (the dialog holds a reference to us as
+ // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer.
+ nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
+ // Don't pop up the downloads panel since we're already going to pop up the
+ // UCT dialog for basically the same effect.
+ mDialogShowing = true;
+ rv = mDialog->Show(this, dialogParent, mReason);
+
+ // what do we do if the dialog failed? I guess we should call Cancel and
+ // abort the load....
+ } else {
+ // We need to do the save/open immediately, then.
+ if (action == nsIMIMEInfo::useHelperApp ||
+ action == nsIMIMEInfo::useSystemDefault ||
+ shouldAutomaticallyHandleInternally) {
+ // Check if the file is local, in which case just launch it from where it
+ // is. Otherwise, set the file to launch once it's finished downloading.
+ rv = mIsFileChannel ? LaunchLocalFile()
+ : SetDownloadToLaunch(
+ shouldAutomaticallyHandleInternally, nullptr);
+ } else {
+ rv = PromptForSaveDestination();
+ }
+ }
+ return NS_OK;
+}
+
+bool nsExternalAppHandler::IsDownloadSpam(nsIChannel* aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsIPermissionManager> permissionManager =
+ mozilla::services::GetPermissionManager();
+ nsCOMPtr<nsIPrincipal> principal = loadInfo->TriggeringPrincipal();
+ bool exactHostMatch = false;
+ constexpr auto type = "automatic-download"_ns;
+ nsCOMPtr<nsIPermission> permission;
+
+ permissionManager->GetPermissionObject(principal, type, exactHostMatch,
+ getter_AddRefs(permission));
+
+ if (permission) {
+ uint32_t capability;
+ permission->GetCapability(&capability);
+ if (capability == nsIPermissionManager::DENY_ACTION) {
+ mCanceled = true;
+ aChannel->Cancel(NS_ERROR_ABORT);
+ return true;
+ }
+ if (capability == nsIPermissionManager::ALLOW_ACTION) {
+ return false;
+ }
+ // If no action is set (i.e: null), we set PROMPT_ACTION by default,
+ // which will notify the Downloads UI to open the panel on the next request.
+ if (capability == nsIPermissionManager::PROMPT_ACTION) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ RefPtr<BrowsingContext> browsingContext;
+ loadInfo->GetBrowsingContext(getter_AddRefs(browsingContext));
+
+ nsAutoCString cStringURI;
+ loadInfo->TriggeringPrincipal()->GetPrePath(cStringURI);
+ observerService->NotifyObservers(
+ browsingContext, "blocked-automatic-download",
+ NS_ConvertASCIItoUTF16(cStringURI.get()).get());
+ // FIXME: In order to escape memory leaks, currently we cancel blocked
+ // downloads. This is temporary solution, because download data should be
+ // kept in order to restart the blocked download.
+ mCanceled = true;
+ aChannel->Cancel(NS_ERROR_ABORT);
+ // End cancel
+ return true;
+ }
+ }
+ if (!loadInfo->GetHasValidUserGestureActivation()) {
+ permissionManager->AddFromPrincipal(
+ principal, type, nsIPermissionManager::PROMPT_ACTION,
+ nsIPermissionManager::EXPIRE_NEVER, 0 /* expire time */);
+ }
+
+ return false;
+}
+
+// Convert error info into proper message text and send OnStatusChange
+// notification to the dialog progress listener or nsITransfer implementation.
+void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv,
+ nsIRequest* aRequest,
+ const nsString& path) {
+ const char* msgId = nullptr;
+ switch (rv) {
+ case NS_ERROR_OUT_OF_MEMORY:
+ // No memory
+ msgId = "noMemory";
+ break;
+
+ case NS_ERROR_FILE_NO_DEVICE_SPACE:
+ // Out of space on target volume.
+ msgId = "diskFull";
+ break;
+
+ case NS_ERROR_FILE_READ_ONLY:
+ // Attempt to write to read/only file.
+ msgId = "readOnly";
+ break;
+
+ case NS_ERROR_FILE_ACCESS_DENIED:
+ if (type == kWriteError) {
+ // Attempt to write without sufficient permissions.
+#if defined(ANDROID)
+ // On Android this means the SD card is present but
+ // unavailable (read-only).
+ msgId = "SDAccessErrorCardReadOnly";
+#else
+ msgId = "accessError";
+#endif
+ } else {
+ msgId = "launchError";
+ }
+ break;
+
+ case NS_ERROR_FILE_NOT_FOUND:
+ case NS_ERROR_FILE_UNRECOGNIZED_PATH:
+ // Helper app not found, let's verify this happened on launch
+ if (type == kLaunchError) {
+ msgId = "helperAppNotFound";
+ break;
+ }
+#if defined(ANDROID)
+ else if (type == kWriteError) {
+ // On Android this means the SD card is missing (not in
+ // SD slot).
+ msgId = "SDAccessErrorCardMissing";
+ break;
+ }
+#endif
+ [[fallthrough]];
+
+ default:
+ // Generic read/write/launch error message.
+ switch (type) {
+ case kReadError:
+ msgId = "readError";
+ break;
+ case kWriteError:
+ msgId = "writeError";
+ break;
+ case kLaunchError:
+ msgId = "launchError";
+ break;
+ }
+ break;
+ }
+
+ MOZ_LOG(
+ nsExternalHelperAppService::sLog, LogLevel::Error,
+ ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n",
+ msgId, type, mDialogProgressListener.get(), mTransfer.get(),
+ static_cast<uint32_t>(rv)));
+
+ MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error,
+ (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get()));
+
+ // Get properties file bundle and extract status string.
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (stringService) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ if (NS_SUCCEEDED(stringService->CreateBundle(
+ "chrome://global/locale/nsWebBrowserPersist.properties",
+ getter_AddRefs(bundle)))) {
+ nsAutoString msgText;
+ AutoTArray<nsString, 1> strings = {path};
+ if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) {
+ if (mDialogProgressListener) {
+ // We have a listener, let it handle the error.
+ mDialogProgressListener->OnStatusChange(
+ nullptr, (type == kReadError) ? aRequest : nullptr, rv,
+ msgText.get());
+ } else if (mTransfer) {
+ mTransfer->OnStatusChange(nullptr,
+ (type == kReadError) ? aRequest : nullptr,
+ rv, msgText.get());
+ } else if (XRE_IsParentProcess()) {
+ // We don't have a listener. Simply show the alert ourselves.
+ nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
+ nsresult qiRv;
+ nsCOMPtr<nsIPrompt> prompter(do_GetInterface(dialogParent, &qiRv));
+ nsAutoString title;
+ bundle->FormatStringFromName("title", strings, title);
+
+ MOZ_LOG(
+ nsExternalHelperAppService::sLog, LogLevel::Debug,
+ ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32
+ ", title='%s', msg='%s'",
+ mBrowsingContext.get(), prompter.get(),
+ static_cast<uint32_t>(qiRv), NS_ConvertUTF16toUTF8(title).get(),
+ NS_ConvertUTF16toUTF8(msgText).get()));
+
+ // If we didn't have a prompter we will try and get a window
+ // instead, get it's docshell and use it to alert the user.
+ if (!prompter) {
+ nsCOMPtr<nsPIDOMWindowOuter> window(do_GetInterface(dialogParent));
+ if (!window || !window->GetDocShell()) {
+ return;
+ }
+
+ prompter = do_GetInterface(window->GetDocShell(), &qiRv);
+
+ MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Debug,
+ ("No prompter from mBrowsingContext, using DocShell, "
+ "window=0x%p, docShell=0x%p, "
+ "prompter=0x%p, qi rv=0x%08" PRIX32,
+ window.get(), window->GetDocShell(), prompter.get(),
+ static_cast<uint32_t>(qiRv)));
+
+ // If we still don't have a prompter, there's nothing else we
+ // can do so just return.
+ if (!prompter) {
+ MOZ_LOG(nsExternalHelperAppService::sLog, LogLevel::Error,
+ ("No prompter from DocShell, no way to alert user"));
+ return;
+ }
+ }
+
+ // We should always have a prompter at this point.
+ prompter->Alert(title.get(), msgText.get());
+ }
+ }
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsExternalAppHandler::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset, uint32_t count) {
+ nsresult rv = NS_OK;
+ // first, check to see if we've been canceled....
+ if (mCanceled || !mSaver) {
+ // then go cancel our underlying channel too
+ return request->Cancel(NS_BINDING_ABORTED);
+ }
+
+ // read the data out of the stream and write it to the temp file.
+ if (count > 0) {
+ mProgress += count;
+
+ nsCOMPtr<nsIStreamListener> saver = do_QueryInterface(mSaver);
+ rv = saver->OnDataAvailable(request, inStr, sourceOffset, count);
+ if (NS_SUCCEEDED(rv)) {
+ // Send progress notification.
+ if (mTransfer) {
+ mTransfer->OnProgressChange64(nullptr, request, mProgress,
+ mContentLength, mProgress,
+ mContentLength);
+ }
+ } else {
+ // An error occurred, notify listener.
+ nsAutoString tempFilePath;
+ if (mTempFile) {
+ mTempFile->GetPath(tempFilePath);
+ }
+ SendStatusChange(kReadError, rv, request, tempFilePath);
+
+ // Cancel the download.
+ Cancel(rv);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ LOG("nsExternalAppHandler::OnStopRequest\n"
+ " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n",
+ mCanceled, mTransfer.get(), static_cast<uint32_t>(aStatus));
+
+ mStopRequestIssued = true;
+
+ // Cancel if the request did not complete successfully.
+ if (!mCanceled && NS_FAILED(aStatus)) {
+ // Send error notification.
+ nsAutoString tempFilePath;
+ if (mTempFile) mTempFile->GetPath(tempFilePath);
+ SendStatusChange(kReadError, aStatus, request, tempFilePath);
+
+ Cancel(aStatus);
+ }
+
+ // first, check to see if we've been canceled....
+ if (mCanceled || !mSaver) {
+ return NS_OK;
+ }
+
+ return mSaver->Finish(NS_OK);
+}
+
+NS_IMETHODIMP
+nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver,
+ nsIFile* aTarget) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
+ nsresult aStatus) {
+ LOG("nsExternalAppHandler::OnSaveComplete\n"
+ " aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n",
+ aSaver, static_cast<uint32_t>(aStatus), mCanceled, mTransfer.get());
+
+ if (!mCanceled) {
+ // Save the hash and signature information
+ (void)mSaver->GetSha256Hash(mHash);
+ (void)mSaver->GetSignatureInfo(mSignatureInfo);
+
+ // Free the reference that the saver keeps on us, even if we couldn't get
+ // the hash.
+ mSaver = nullptr;
+
+ // Save the redirect information.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
+ if (channel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMutableArray> redirectChain =
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ LOG("nsExternalAppHandler: Got %zu redirects\n",
+ loadInfo->RedirectChain().Length());
+ for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) {
+ redirectChain->AppendElement(entry);
+ }
+ mRedirects = redirectChain;
+ }
+
+ if (NS_FAILED(aStatus)) {
+ nsAutoString path;
+ mTempFile->GetPath(path);
+
+ // It may happen when e10s is enabled that there will be no transfer
+ // object available to communicate status as expected by the system.
+ // Let's try and create a temporary transfer object to take care of this
+ // for us, we'll fall back to using the prompt service if we absolutely
+ // have to.
+ if (!mTransfer) {
+ // We don't care if this fails.
+ CreateFailedTransfer();
+ }
+
+ SendStatusChange(kWriteError, aStatus, nullptr, path);
+ if (!mCanceled) Cancel(aStatus);
+ return NS_OK;
+ }
+ }
+
+ // Notify the transfer object that we are done if the user has chosen an
+ // action. If the user hasn't chosen an action, the progress listener
+ // (nsITransfer) will be notified in CreateTransfer.
+ if (mTransfer) {
+ NotifyTransfer(aStatus);
+ }
+
+ return NS_OK;
+}
+
+void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread");
+ MOZ_ASSERT(mTransfer, "We must have an nsITransfer");
+
+ LOG("Notifying progress listener");
+
+ if (NS_SUCCEEDED(aStatus)) {
+ (void)mTransfer->SetSha256Hash(mHash);
+ (void)mTransfer->SetSignatureInfo(mSignatureInfo);
+ (void)mTransfer->SetRedirects(mRedirects);
+ (void)mTransfer->OnProgressChange64(
+ nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength);
+ }
+
+ (void)mTransfer->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_STOP |
+ nsIWebProgressListener::STATE_IS_REQUEST |
+ nsIWebProgressListener::STATE_IS_NETWORK,
+ aStatus);
+
+ // This nsITransfer object holds a reference to us (we are its observer), so
+ // we need to release the reference to break a reference cycle (and therefore
+ // to prevent leaking). We do this even if the previous calls failed.
+ mTransfer = nullptr;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) {
+ *aMIMEInfo = mMimeInfo;
+ NS_ADDREF(*aMIMEInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) {
+ NS_ENSURE_ARG(aSourceURI);
+ *aSourceURI = mSourceUrl;
+ NS_IF_ADDREF(*aSourceURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName(
+ nsAString& aSuggestedFileName) {
+ aSuggestedFileName = mSuggestedFileName;
+ return NS_OK;
+}
+
+nsresult nsExternalAppHandler::CreateTransfer() {
+ LOG("nsExternalAppHandler::CreateTransfer");
+
+ MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread");
+ // We are back from the helper app dialog (where the user chooses to save or
+ // open), but we aren't done processing the load. in this case, throw up a
+ // progress dialog so the user can see what's going on.
+ // Also, release our reference to mDialog. We don't need it anymore, and we
+ // need to break the reference cycle.
+ mDialog = nullptr;
+ if (!mDialogProgressListener) {
+ NS_WARNING("The dialog should nullify the dialog progress listener");
+ }
+ // In case of a non acceptable download, we need to cancel the request and
+ // pass a FailedTransfer for the Download UI.
+ if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) {
+ mCanceled = true;
+ mRequest->Cancel(NS_ERROR_ABORT);
+ if (mSaver) {
+ mSaver->Finish(NS_ERROR_ABORT);
+ mSaver = nullptr;
+ }
+ return CreateFailedTransfer();
+ }
+ nsresult rv;
+
+ // We must be able to create an nsITransfer object. If not, it doesn't matter
+ // much that we can't launch the helper application or save to disk. Work on
+ // a local copy rather than mTransfer until we know we succeeded, to make it
+ // clearer that this function is re-entrant.
+ nsCOMPtr<nsITransfer> transfer =
+ do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Initialize the download
+ nsCOMPtr<nsIURI> target;
+ rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
+ if (httpChannel) {
+ referrerInfo = httpChannel->GetReferrerInfo();
+ }
+
+ if (mBrowsingContext) {
+ rv = transfer->InitWithBrowsingContext(
+ mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile,
+ this, channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification, referrerInfo, !mDialogShowing,
+ mBrowsingContext, mHandleInternally, nullptr);
+ } else {
+ rv = transfer->Init(mSourceUrl, nullptr, target, u""_ns, mMimeInfo,
+ mTimeDownloadStarted, mTempFile, this,
+ channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification, referrerInfo, !mDialogShowing);
+ }
+ mDialogShowing = false;
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we were cancelled since creating the transfer, just return. It is
+ // always ok to return NS_OK if we are cancelled. Callers of this function
+ // must call Cancel if CreateTransfer fails, but there's no need to cancel
+ // twice.
+ if (mCanceled) {
+ return NS_OK;
+ }
+ rv = transfer->OnStateChange(nullptr, mRequest,
+ nsIWebProgressListener::STATE_START |
+ nsIWebProgressListener::STATE_IS_REQUEST |
+ nsIWebProgressListener::STATE_IS_NETWORK,
+ NS_OK);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mCanceled) {
+ return NS_OK;
+ }
+
+ mRequest = nullptr;
+ // Finally, save the transfer to mTransfer.
+ mTransfer = transfer;
+ transfer = nullptr;
+
+ // While we were bringing up the progress dialog, we actually finished
+ // processing the url. If that's the case then mStopRequestIssued will be
+ // true and OnSaveComplete has been called.
+ if (mStopRequestIssued && !mSaver && mTransfer) {
+ NotifyTransfer(NS_OK);
+ }
+
+ return rv;
+}
+
+nsresult nsExternalAppHandler::CreateFailedTransfer() {
+ nsresult rv;
+ nsCOMPtr<nsITransfer> transfer =
+ do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We won't pass the temp file to the transfer, so if we have one it needs to
+ // get deleted now.
+ if (mTempFile) {
+ if (mSaver) {
+ mSaver->Finish(NS_BINDING_ABORTED);
+ mSaver = nullptr;
+ }
+ mTempFile->Remove(false);
+ }
+
+ nsCOMPtr<nsIURI> pseudoTarget;
+ if (!mFinalFileDestination) {
+ // If we don't have a download directory we're kinda screwed but it's OK
+ // we'll still report the error via the prompter.
+ nsCOMPtr<nsIFile> pseudoFile;
+ rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Append the default suggested filename. If the user restarts the transfer
+ // we will re-trigger a filename check anyway to ensure that it is unique.
+ rv = pseudoFile->Append(mSuggestedFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // Initialize the target, if present
+ rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), mFinalFileDestination);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mRequest);
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = nullptr;
+ if (httpChannel) {
+ referrerInfo = httpChannel->GetReferrerInfo();
+ }
+
+ if (mBrowsingContext) {
+ rv = transfer->InitWithBrowsingContext(
+ mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted,
+ mTempFile, this, channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification, referrerInfo, true, mBrowsingContext,
+ mHandleInternally, httpChannel);
+ } else {
+ rv = transfer->Init(mSourceUrl, nullptr, pseudoTarget, u""_ns, mMimeInfo,
+ mTimeDownloadStarted, mTempFile, this,
+ channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification, referrerInfo, true);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Our failed transfer is ready.
+ mTransfer = std::move(transfer);
+
+ return NS_OK;
+}
+
+nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile,
+ bool aDialogWasShown) {
+ if (aFile) {
+ if (aDialogWasShown) {
+ mDialogShowing = true;
+ }
+ ContinueSave(aFile);
+ } else {
+ Cancel(NS_BINDING_ABORTED);
+ }
+
+ return NS_OK;
+}
+
+void nsExternalAppHandler::RequestSaveDestination(
+ const nsString& aDefaultFile, const nsString& aFileExtension) {
+ // Display the dialog
+ // XXX Convert to use file picker? No, then embeddors could not do any sort of
+ // "AutoDownload" w/o showing a prompt
+ nsresult rv = NS_OK;
+ if (!mDialog) {
+ // Get helper app launcher dialog.
+ mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv);
+ if (rv != NS_OK) {
+ Cancel(NS_BINDING_ABORTED);
+ return;
+ }
+ }
+
+ // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we
+ // can't unescape it because the dialog is implemented by a JS component which
+ // doesn't have a window so no unescape routine is defined...
+
+ // Now, be sure to keep |this| alive, and the dialog
+ // If we don't do this, users that close the helper app dialog while the file
+ // picker is up would cause Cancel() to be called, and the dialog would be
+ // released, which would release this object too, which would crash.
+ // See Bug 249143
+ RefPtr<nsExternalAppHandler> kungFuDeathGrip(this);
+ nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog);
+ nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent();
+
+ rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(),
+ aFileExtension.get(), mForceSave);
+ if (NS_FAILED(rv)) {
+ Cancel(NS_BINDING_ABORTED);
+ }
+}
+
+// PromptForSaveDestination should only be called by the helper app dialog which
+// allows the user to say launch with application or save to disk.
+NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() {
+ if (mCanceled) return NS_OK;
+
+ if (mForceSave || mForceSaveInternallyHandled) {
+ mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ }
+
+ if (mSuggestedFileName.IsEmpty()) {
+ RequestSaveDestination(mTempLeafName, mFileExtension);
+ } else {
+ nsAutoString fileExt;
+ int32_t pos = mSuggestedFileName.RFindChar('.');
+ if (pos >= 0) {
+ mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
+ }
+ if (fileExt.IsEmpty()) {
+ fileExt = mFileExtension;
+ }
+
+ RequestSaveDestination(mSuggestedFileName, fileExt);
+ }
+
+ return NS_OK;
+}
+nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) {
+ if (mCanceled) return NS_OK;
+
+ MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file");
+
+ int32_t action = nsIMIMEInfo::saveToDisk;
+ mMimeInfo->GetPreferredAction(&action);
+ mHandleInternally = action == nsIMIMEInfo::handleInternally;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIFile> fileToUse = aNewFileLocation;
+ mFinalFileDestination = fileToUse;
+
+ // Move what we have in the final directory, but append .part
+ // to it, to indicate that it's unfinished. Do not call SetTarget on the
+ // saver if we are done (Finish has been called) but OnSaverComplete has
+ // not been called.
+ if (mFinalFileDestination && mSaver && !mStopRequestIssued) {
+ nsCOMPtr<nsIFile> movedFile;
+ mFinalFileDestination->Clone(getter_AddRefs(movedFile));
+ if (movedFile) {
+ nsAutoCString randomChars;
+ rv = GenerateRandomName(randomChars);
+ if (NS_SUCCEEDED(rv)) {
+ // Get the leaf name, strip any extensions, then
+ // add random bytes, followed by the extensions and '.part'.
+ nsAutoString leafName;
+ mFinalFileDestination->GetLeafName(leafName);
+ auto nameWithoutExtensionLength = leafName.FindChar('.');
+ nsAutoString extensions(u"");
+ if (nameWithoutExtensionLength == kNotFound) {
+ nameWithoutExtensionLength = leafName.Length();
+ } else {
+ extensions = Substring(leafName, nameWithoutExtensionLength);
+ }
+ leafName.Truncate(nameWithoutExtensionLength);
+
+ nsAutoString suffix = u"."_ns + NS_ConvertASCIItoUTF16(randomChars) +
+ extensions + u".part"_ns;
+#ifdef XP_WIN
+ // Deal with MAX_PATH on Windows. Worth noting that the original
+ // path for mFinalFileDestination must be valid for us to get
+ // here: either SetDownloadToLaunch or the caller of
+ // SaveDestinationAvailable has called CreateUnique or similar
+ // to ensure both a unique name and one that isn't too long.
+ // The only issue is we're making it longer to get the part
+ // file path...
+ nsAutoString path;
+ mFinalFileDestination->GetPath(path);
+ CheckedInt<uint16_t> fullPathLength =
+ CheckedInt<uint16_t>(path.Length()) + 1 + randomChars.Length() +
+ ArrayLength(".part");
+ if (!fullPathLength.isValid()) {
+ leafName.Truncate();
+ } else if (fullPathLength.value() > MAX_PATH) {
+ int32_t leafNameRemaining =
+ (int32_t)leafName.Length() - (fullPathLength.value() - MAX_PATH);
+ leafName.Truncate(std::max(leafNameRemaining, 0));
+ }
+#endif
+ leafName.Append(suffix);
+ movedFile->SetLeafName(leafName);
+
+ rv = mSaver->SetTarget(movedFile, true);
+ if (NS_FAILED(rv)) {
+ nsAutoString path;
+ mTempFile->GetPath(path);
+ SendStatusChange(kWriteError, rv, nullptr, path);
+ Cancel(rv);
+ return NS_OK;
+ }
+
+ mTempFile = movedFile;
+ }
+ }
+ }
+
+ // The helper app dialog has told us what to do and we have a final file
+ // destination.
+ rv = CreateTransfer();
+ // If we fail to create the transfer, Cancel.
+ if (NS_FAILED(rv)) {
+ Cancel(rv);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// SetDownloadToLaunch should only be called by the helper app dialog which
+// allows the user to say launch with application or save to disk.
+NS_IMETHODIMP nsExternalAppHandler::SetDownloadToLaunch(
+ bool aHandleInternally, nsIFile* aNewFileLocation) {
+ if (mCanceled) return NS_OK;
+
+ mHandleInternally = aHandleInternally;
+
+ // Now that the user has elected to launch the downloaded file with a helper
+ // app, we're justified in removing the 'salted' name. We'll rename to what
+ // was specified in mSuggestedFileName after the download is done prior to
+ // launching the helper app. So that any existing file of that name won't be
+ // overwritten we call CreateUnique(). Also note that we use the same
+ // directory as originally downloaded so the download can be renamed in place
+ // later.
+ nsCOMPtr<nsIFile> fileToUse;
+ if (aNewFileLocation) {
+ fileToUse = aNewFileLocation;
+ } else {
+ (void)GetDownloadDirectory(getter_AddRefs(fileToUse));
+
+ if (mSuggestedFileName.IsEmpty()) {
+ // Keep using the leafname of the temp file, since we're just starting a
+ // helper
+ mSuggestedFileName = mTempLeafName;
+ }
+
+#ifdef XP_WIN
+ // Ensure we don't double-append the file extension if it matches:
+ if (StringEndsWith(mSuggestedFileName, mFileExtension,
+ nsCaseInsensitiveStringComparator)) {
+ fileToUse->Append(mSuggestedFileName);
+ } else {
+ fileToUse->Append(mSuggestedFileName + mFileExtension);
+ }
+#else
+ fileToUse->Append(mSuggestedFileName);
+#endif
+ }
+
+ nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ if (NS_SUCCEEDED(rv)) {
+ mFinalFileDestination = fileToUse;
+ // launch the progress window now that the user has picked the desired
+ // action.
+ rv = CreateTransfer();
+ if (NS_FAILED(rv)) {
+ Cancel(rv);
+ }
+ } else {
+ // Cancel the download and report an error. We do not want to end up in
+ // a state where it appears that we have a normal download that is
+ // pointing to a file that we did not actually create.
+ nsAutoString path;
+ mTempFile->GetPath(path);
+ SendStatusChange(kWriteError, rv, nullptr, path);
+ Cancel(rv);
+ }
+ return rv;
+}
+
+nsresult nsExternalAppHandler::LaunchLocalFile() {
+ nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl));
+ if (!fileUrl) {
+ return NS_OK;
+ }
+ Cancel(NS_BINDING_ABORTED);
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = fileUrl->GetFile(getter_AddRefs(file));
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = mMimeInfo->LaunchWithFile(file);
+ if (NS_SUCCEEDED(rv)) return NS_OK;
+ }
+ nsAutoString path;
+ if (file) file->GetPath(path);
+ // If we get here, an error happened
+ SendStatusChange(kLaunchError, rv, nullptr, path);
+ return rv;
+}
+
+NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
+ NS_ENSURE_ARG(NS_FAILED(aReason));
+
+ if (mCanceled) {
+ return NS_OK;
+ }
+ mCanceled = true;
+
+ if (mSaver) {
+ // We are still writing to the target file. Give the saver a chance to
+ // close the target file, then notify the transfer object if necessary in
+ // the OnSaveComplete callback.
+ mSaver->Finish(aReason);
+ mSaver = nullptr;
+ } else {
+ if (mStopRequestIssued && mTempFile) {
+ // This branch can only happen when the user cancels the helper app dialog
+ // when the request has completed. The temp file has to be removed here,
+ // because mSaver has been released at that time with the temp file left.
+ (void)mTempFile->Remove(false);
+ }
+
+ // Notify the transfer object that the download has been canceled, if the
+ // user has already chosen an action and we didn't notify already.
+ if (mTransfer) {
+ NotifyTransfer(aReason);
+ }
+ }
+
+ // Break our reference cycle with the helper app dialog (set up in
+ // OnStartRequest)
+ mDialog = nullptr;
+ mDialogShowing = false;
+
+ mRequest = nullptr;
+
+ // Release the listener, to break the reference cycle with it (we are the
+ // observer of the listener).
+ mDialogProgressListener = nullptr;
+
+ return NS_OK;
+}
+
+bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName,
+ const char* aContentType) {
+ // Search the obsolete pref strings.
+ nsAutoCString prefCString;
+ Preferences::GetCString(prefName, prefCString);
+ if (prefCString.IsEmpty()) {
+ // Default is true, if not found in the pref string.
+ return true;
+ }
+
+ NS_UnescapeURL(prefCString);
+ nsACString::const_iterator start, end;
+ prefCString.BeginReading(start);
+ prefCString.EndReading(end);
+ return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start,
+ end);
+}
+
+NS_IMETHODIMP
+nsExternalAppHandler::GetName(nsACString& aName) {
+ aName.AssignLiteral("nsExternalAppHandler");
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// The following section contains our nsIMIMEService implementation and related
+// methods.
+//
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// nsIMIMEService methods
+NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension(
+ const nsACString& aMIMEType, const nsACString& aFileExt,
+ nsIMIMEInfo** _retval) {
+ MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(),
+ "Give me something to work with");
+ MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound,
+ "The extension should never contain null characters");
+ LOG("Getting mimeinfo from type '%s' ext '%s'\n",
+ PromiseFlatCString(aMIMEType).get(), PromiseFlatCString(aFileExt).get());
+
+ *_retval = nullptr;
+
+ // OK... we need a type. Get one.
+ nsAutoCString typeToUse(aMIMEType);
+ if (typeToUse.IsEmpty()) {
+ nsresult rv = GetTypeFromExtension(aFileExt, typeToUse);
+ if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We promise to only send lower case mime types to the OS
+ ToLowerCase(typeToUse);
+
+ // First, ask the OS for a mime info
+ bool found;
+ nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ LOG("OS gave back 0x%p - found: %i\n", *_retval, found);
+ // If we got no mimeinfo, something went wrong. Probably lack of memory.
+ if (!*_retval) return NS_ERROR_OUT_OF_MEMORY;
+
+ // The handler service can make up for bad mime types by checking the file
+ // extension. If the mime type is known (in extras or in the handler
+ // service), we stop it doing so by flipping this bool to true.
+ bool trustMIMEType = false;
+
+ // Check extras - not everything we support will be known by the OS store,
+ // unfortunately, and it may even miss some extensions that we know should
+ // be accepted. We only do this for non-octet-stream mimetypes, because
+ // our information for octet-stream would lead to us trying to open all such
+ // files as Binary file with exe, com or bin extension regardless of the
+ // real extension.
+ if (!typeToUse.Equals(APPLICATION_OCTET_STREAM,
+ nsCaseInsensitiveCStringComparator)) {
+ rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval);
+ LOG("Searched extras (by type), rv 0x%08" PRIX32 "\n",
+ static_cast<uint32_t>(rv));
+ trustMIMEType = NS_SUCCEEDED(rv);
+ found = found || NS_SUCCEEDED(rv);
+ }
+
+ // Now, let's see if we can find something in our datastore.
+ // This will not overwrite the OS information that interests us
+ // (i.e. default application, default app. description)
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID);
+ if (handlerSvc) {
+ bool hasHandler = false;
+ (void)handlerSvc->Exists(*_retval, &hasHandler);
+ if (hasHandler) {
+ rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns);
+ LOG("Data source: Via type: retval 0x%08" PRIx32 "\n",
+ static_cast<uint32_t>(rv));
+ trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv);
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+
+ found = found || NS_SUCCEEDED(rv);
+ }
+
+ // If we still haven't found anything, try finding a match for
+ // an extension in extras first:
+ if (!found && !aFileExt.IsEmpty()) {
+ rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval);
+ LOG("Searched extras (by ext), rv 0x%08" PRIX32 "\n",
+ static_cast<uint32_t>(rv));
+ }
+
+ // Then check the handler service - but only do so if we really do not know
+ // the mimetype. This avoids overwriting good mimetype info with bad file
+ // extension info.
+ if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) {
+ nsAutoCString overrideType;
+ rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType);
+ if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) {
+ // We can't check handlerSvc->Exists() here, because we have a
+ // overideType. That's ok, it just results in some console noise.
+ // (If there's no handler for the override type, it throws)
+ rv = handlerSvc->FillHandlerInfo(*_retval, overrideType);
+ LOG("Data source: Via ext: retval 0x%08" PRIx32 "\n",
+ static_cast<uint32_t>(rv));
+ found = found || NS_SUCCEEDED(rv);
+ }
+ }
+
+ // If we still don't have a match, at least set the file description
+ // to `${aFileExt} File` if it's empty:
+ if (!found && !aFileExt.IsEmpty()) {
+ // XXXzpao This should probably be localized
+ nsAutoCString desc(aFileExt);
+ desc.AppendLiteral(" File");
+ (*_retval)->SetDescription(NS_ConvertUTF8toUTF16(desc));
+ LOG("Falling back to 'File' file description\n");
+ }
+
+ // Sometimes, OSes give us bad data. We have a set of forbidden extensions
+ // for some MIME types. If the primary extension is forbidden,
+ // overwrite it with a known-good one. See bug 1571247 for context.
+ nsAutoCString primaryExtension;
+ (*_retval)->GetPrimaryExtension(primaryExtension);
+ if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) {
+ if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) {
+ (*_retval)->GetPrimaryExtension(primaryExtension);
+ }
+ }
+
+ // Finally, check if we got a file extension and if yes, if it is an
+ // extension on the mimeinfo, in which case we want it to be the primary one
+ if (!aFileExt.IsEmpty()) {
+ bool matches = false;
+ (*_retval)->ExtensionExists(aFileExt, &matches);
+ LOG("Extension '%s' matches mime info: %i\n",
+ PromiseFlatCString(aFileExt).get(), matches);
+ if (matches) {
+ nsAutoCString fileExt;
+ ToLowerCase(aFileExt, fileExt);
+ (*_retval)->SetPrimaryExtension(fileExt);
+ primaryExtension = fileExt;
+ }
+ }
+
+ // Overwrite with a generic description if the primary extension for the
+ // type is in our list; these are file formats supported by Firefox and
+ // we don't want other brands positioning themselves as the sole viewer
+ // for a system.
+ if (!primaryExtension.IsEmpty()) {
+ for (const char* ext : descriptionOverwriteExtensions) {
+ if (primaryExtension.Equals(ext)) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIStringBundle> unknownContentTypeBundle;
+ rv = bundleService->CreateBundle(
+ "chrome://mozapps/locale/downloads/unknownContentType.properties",
+ getter_AddRefs(unknownContentTypeBundle));
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString stringName(ext);
+ stringName.AppendLiteral("ExtHandlerDescription");
+ nsAutoString handlerDescription;
+ rv = unknownContentTypeBundle->GetStringFromName(stringName.get(),
+ handlerDescription);
+ if (NS_SUCCEEDED(rv)) {
+ (*_retval)->SetDescription(handlerDescription);
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (LOG_ENABLED()) {
+ nsAutoCString type;
+ (*_retval)->GetMIMEType(type);
+
+ LOG("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(),
+ primaryExtension.get());
+ }
+
+ return NS_OK;
+}
+
+bool nsExternalHelperAppService::GetMIMETypeFromDefaultForExtension(
+ const nsACString& aExtension, nsACString& aMIMEType) {
+ // First of all, check our default entries
+ for (auto& entry : defaultMimeEntries) {
+ if (aExtension.LowerCaseEqualsASCII(entry.mFileExtension)) {
+ aMIMEType = entry.mMimeType;
+ return true;
+ }
+ }
+ return false;
+}
+
+NS_IMETHODIMP
+nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt,
+ nsACString& aContentType) {
+ // OK. We want to try the following sources of mimetype information, in this
+ // order:
+ // 1. defaultMimeEntries array
+ // 2. OS-provided information
+ // 3. our "extras" array
+ // 4. Information from plugins
+ // 5. The "ext-to-type-mapping" category
+ // Note that, we are intentionally not looking at the handler service, because
+ // that can be affected by websites, which leads to undesired behavior.
+
+ // Early return if called with an empty extension parameter
+ if (aFileExt.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // First of all, check our default entries
+ if (GetMIMETypeFromDefaultForExtension(aFileExt, aContentType)) {
+ return NS_OK;
+ }
+
+ // Ask OS.
+ if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) {
+ return NS_OK;
+ }
+
+ // Check extras array.
+ bool found = GetTypeFromExtras(aFileExt, aContentType);
+ if (found) {
+ return NS_OK;
+ }
+
+ // Let's see if an extension added something
+ nsCOMPtr<nsICategoryManager> catMan(
+ do_GetService("@mozilla.org/categorymanager;1"));
+ if (catMan) {
+ // The extension in the category entry is always stored as lowercase
+ nsAutoCString lowercaseFileExt(aFileExt);
+ ToLowerCase(lowercaseFileExt);
+ // Read the MIME type from the category entry, if available
+ nsCString type;
+ nsresult rv =
+ catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type);
+ if (NS_SUCCEEDED(rv)) {
+ aContentType = type;
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension(
+ const nsACString& aMIMEType, const nsACString& aFileExt,
+ nsACString& _retval) {
+ NS_ENSURE_ARG(!aMIMEType.IsEmpty());
+
+ nsCOMPtr<nsIMIMEInfo> mi;
+ nsresult rv =
+ GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi));
+ if (NS_FAILED(rv)) return rv;
+
+ return mi->GetPrimaryExtension(_retval);
+}
+
+NS_IMETHODIMP nsExternalHelperAppService::GetDefaultTypeFromURI(
+ nsIURI* aURI, nsACString& aContentType) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+ aContentType.Truncate();
+
+ // Now try to get an nsIURL so we don't have to do our own parsing
+ nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
+ if (!url) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsAutoCString ext;
+ rv = url->GetFileExtension(ext);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!ext.IsEmpty()) {
+ UnescapeFragment(ext, url, ext);
+
+ if (GetMIMETypeFromDefaultForExtension(ext, aContentType)) {
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+};
+
+NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI(
+ nsIURI* aURI, nsACString& aContentType) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+ aContentType.Truncate();
+
+ // First look for a file to use. If we have one, we just use that.
+ nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI);
+ if (fileUrl) {
+ nsCOMPtr<nsIFile> file;
+ rv = fileUrl->GetFile(getter_AddRefs(file));
+ if (NS_SUCCEEDED(rv)) {
+ rv = GetTypeFromFile(file, aContentType);
+ if (NS_SUCCEEDED(rv)) {
+ // we got something!
+ return rv;
+ }
+ }
+ }
+
+ // Now try to get an nsIURL so we don't have to do our own parsing
+ nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
+ if (url) {
+ nsAutoCString ext;
+ rv = url->GetFileExtension(ext);
+ if (NS_FAILED(rv)) return rv;
+ if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE;
+
+ UnescapeFragment(ext, url, ext);
+
+ return GetTypeFromExtension(ext, aContentType);
+ }
+
+ // no url, let's give the raw spec a shot
+ nsAutoCString specStr;
+ rv = aURI->GetSpec(specStr);
+ if (NS_FAILED(rv)) return rv;
+ UnescapeFragment(specStr, aURI, specStr);
+
+ // find the file extension (if any)
+ int32_t extLoc = specStr.RFindChar('.');
+ int32_t specLength = specStr.Length();
+ if (-1 != extLoc && extLoc != specLength - 1 &&
+ // nothing over 20 chars long can be sanely considered an
+ // extension.... Dat dere would be just data.
+ specLength - extLoc < 20) {
+ return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType);
+ }
+
+ // We found no information; say so.
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile(
+ nsIFile* aFile, nsACString& aContentType) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ nsresult rv;
+
+ // Get the Extension
+ nsAutoString fileName;
+ rv = aFile->GetLeafName(fileName);
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString fileExt;
+ if (!fileName.IsEmpty()) {
+ int32_t len = fileName.Length();
+ for (int32_t i = len; i >= 0; i--) {
+ if (fileName[i] == char16_t('.')) {
+ CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt);
+ break;
+ }
+ }
+ }
+
+ if (fileExt.IsEmpty()) return NS_ERROR_FAILURE;
+
+ return GetTypeFromExtension(fileExt, aContentType);
+}
+
+nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras(
+ const nsACString& aContentType, bool aOverwriteDescription,
+ nsIMIMEInfo* aMIMEInfo) {
+ NS_ENSURE_ARG(aMIMEInfo);
+
+ NS_ENSURE_ARG(!aContentType.IsEmpty());
+
+ // Look for default entry with matching mime type.
+ nsAutoCString MIMEType(aContentType);
+ ToLowerCase(MIMEType);
+ for (auto entry : extraMimeEntries) {
+ if (MIMEType.Equals(entry.mMimeType)) {
+ // This is the one. Set attributes appropriately.
+ nsDependentCString extensions(entry.mFileExtensions);
+ nsACString::const_iterator start, end;
+ extensions.BeginReading(start);
+ extensions.EndReading(end);
+ while (start != end) {
+ nsACString::const_iterator cursor = start;
+ mozilla::Unused << FindCharInReadable(',', cursor, end);
+ aMIMEInfo->AppendExtension(Substring(start, cursor));
+ // If a comma was found, skip it for the next search.
+ start = cursor != end ? ++cursor : cursor;
+ }
+
+ nsAutoString desc;
+ aMIMEInfo->GetDescription(desc);
+ if (aOverwriteDescription || desc.IsEmpty()) {
+ aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription));
+ }
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras(
+ const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) {
+ nsAutoCString type;
+ bool found = GetTypeFromExtras(aExtension, type);
+ if (!found) return NS_ERROR_NOT_AVAILABLE;
+ return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo);
+}
+
+bool nsExternalHelperAppService::MaybeReplacePrimaryExtension(
+ const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) {
+ for (const auto& entry : sForbiddenPrimaryExtensions) {
+ if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) {
+ nsDependentCString mime(entry.mMimeType);
+ for (const auto& extraEntry : extraMimeEntries) {
+ if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) {
+ nsDependentCString goodExts(extraEntry.mFileExtensions);
+ int32_t commaPos = goodExts.FindChar(',');
+ commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos;
+ auto goodExt = Substring(goodExts, 0, commaPos);
+ aMIMEInfo->SetPrimaryExtension(goodExt);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+}
+
+bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension,
+ nsACString& aMIMEType) {
+ NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!");
+
+ // Look for default entry with matching extension.
+ nsDependentCString::const_iterator start, end, iter;
+ int32_t numEntries = ArrayLength(extraMimeEntries);
+ for (int32_t index = 0; index < numEntries; index++) {
+ nsDependentCString extList(extraMimeEntries[index].mFileExtensions);
+ extList.BeginReading(start);
+ extList.EndReading(end);
+ iter = start;
+ while (start != end) {
+ FindCharInReadable(',', iter, end);
+ if (Substring(start, iter)
+ .Equals(aExtension, nsCaseInsensitiveCStringComparator)) {
+ aMIMEType = extraMimeEntries[index].mMimeType;
+ return true;
+ }
+ if (iter != end) {
+ ++iter;
+ }
+ start = iter;
+ }
+ }
+
+ return false;
+}
+
+bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension(
+ const nsACString& aExtension, nsACString& aMIMEType) {
+ bool found = false;
+ nsCOMPtr<nsIMIMEInfo> mimeInfo;
+ nsresult rv =
+ GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo));
+ return NS_SUCCEEDED(rv) && found && mimeInfo &&
+ NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType));
+}
+
+nsresult nsExternalHelperAppService::GetMIMEInfoFromOS(
+ const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) {
+ *aMIMEInfo = nullptr;
+ *aFound = false;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsExternalHelperAppService::UpdateDefaultAppInfo(
+ nsIMIMEInfo* aMIMEInfo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+bool nsExternalHelperAppService::GetFileNameFromChannel(nsIChannel* aChannel,
+ nsAString& aFileName,
+ nsIURI** aURI) {
+ if (!aChannel) {
+ return false;
+ }
+
+ aChannel->GetURI(aURI);
+ nsCOMPtr<nsIURL> url = do_QueryInterface(*aURI);
+
+ // Check if we have a POST request, in which case we don't want to use
+ // the url's extension
+ bool allowURLExt = !net::ChannelIsPost(aChannel);
+
+ // Check if we had a query string - we don't want to check the URL
+ // extension if a query is present in the URI
+ // If we already know we don't want to check the URL extension, don't
+ // bother checking the query
+ if (url && allowURLExt) {
+ nsAutoCString query;
+
+ // We only care about the query for HTTP and HTTPS URLs
+ if (url->SchemeIs("http") || url->SchemeIs("https")) {
+ url->GetQuery(query);
+ }
+
+ // Only get the extension if the query is empty; if it isn't, then the
+ // extension likely belongs to a cgi script and isn't helpful
+ allowURLExt = query.IsEmpty();
+ }
+
+ aChannel->GetContentDispositionFilename(aFileName);
+
+ return allowURLExt;
+}
+
+NS_IMETHODIMP
+nsExternalHelperAppService::GetValidFileName(nsIChannel* aChannel,
+ const nsACString& aType,
+ nsIURI* aOriginalURI,
+ uint32_t aFlags,
+ nsAString& aOutFileName) {
+ nsCOMPtr<nsIURI> uri;
+ bool allowURLExtension =
+ GetFileNameFromChannel(aChannel, aOutFileName, getter_AddRefs(uri));
+
+ nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
+ aOutFileName, aType, uri, aOriginalURI, aFlags, allowURLExtension);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsExternalHelperAppService::ValidateFileNameForSaving(
+ const nsAString& aFileName, const nsACString& aType, uint32_t aFlags,
+ nsAString& aOutFileName) {
+ nsAutoString fileName(aFileName);
+
+ // Just sanitize the filename only.
+ if (aFlags & VALIDATE_SANITIZE_ONLY) {
+ SanitizeFileName(fileName, aFlags);
+ } else {
+ nsCOMPtr<nsIMIMEInfo> mimeInfo = ValidateFileNameForSaving(
+ fileName, aType, nullptr, nullptr, aFlags, true);
+ }
+
+ aOutFileName = fileName;
+ return NS_OK;
+}
+
+already_AddRefed<nsIMIMEInfo>
+nsExternalHelperAppService::ValidateFileNameForSaving(
+ nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
+ nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension) {
+ nsAutoString fileName(aFileName);
+ nsAutoCString extension;
+ nsCOMPtr<nsIMIMEInfo> mimeInfo;
+
+ bool isBinaryType = aMimeType.EqualsLiteral(APPLICATION_OCTET_STREAM) ||
+ aMimeType.EqualsLiteral(BINARY_OCTET_STREAM) ||
+ aMimeType.EqualsLiteral("application/x-msdownload");
+
+ // We don't want to save hidden files starting with a dot, so remove any
+ // leading periods. This is done first, so that the remainder will be
+ // treated as the filename, and not an extension.
+ // Also, Windows ignores terminating dots. So we have to as well, so
+ // that our security checks do "the right thing"
+ fileName.Trim(".");
+
+ bool urlIsFile = !!aURI && aURI->SchemeIs("file");
+
+ // We get the mime service here even though we're the default implementation
+ // of it, so it's possible to override only the mime service and not need to
+ // reimplement the whole external helper app service itself.
+ nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
+ if (mimeService) {
+ if (fileName.IsEmpty()) {
+ nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
+ // Try to extract the file name from the url and use that as a first
+ // pass as the leaf name of our temp file...
+ if (url) {
+ nsAutoCString leafName;
+ url->GetFileName(leafName);
+ if (!leafName.IsEmpty()) {
+ if (NS_FAILED(UnescapeFragment(leafName, url, fileName))) {
+ CopyUTF8toUTF16(leafName, fileName); // use escaped name instead
+ fileName.Trim(".");
+ }
+ }
+
+ // Only get the extension from the URL if allowed, or if this
+ // is a binary type in which case the type might not be valid
+ // anyway.
+ if (aAllowURLExtension || isBinaryType || urlIsFile) {
+ url->GetFileExtension(extension);
+ }
+ }
+ } else {
+ // Determine the current extension for the filename.
+ int32_t dotidx = fileName.RFind(u".");
+ if (dotidx != -1) {
+ CopyUTF16toUTF8(Substring(fileName, dotidx + 1), extension);
+ }
+ }
+
+ if (aFlags & VALIDATE_GUESS_FROM_EXTENSION) {
+ nsAutoCString mimeType;
+ if (!extension.IsEmpty()) {
+ mimeService->GetFromTypeAndExtension(EmptyCString(), extension,
+ getter_AddRefs(mimeInfo));
+ if (mimeInfo) {
+ mimeInfo->GetMIMEType(mimeType);
+ }
+ }
+
+ if (mimeType.IsEmpty()) {
+ // Extension lookup gave us no useful match, so use octet-stream
+ // instead.
+ mimeService->GetFromTypeAndExtension(
+ nsLiteralCString(APPLICATION_OCTET_STREAM), extension,
+ getter_AddRefs(mimeInfo));
+ }
+ } else if (!aMimeType.IsEmpty()) {
+ // If this is a binary type, include the extension as a hint to get
+ // the mime info. For other types, the mime type itself should be
+ // sufficient.
+ // Unfortunately, on Windows, the mimetype is usually insufficient.
+ // Compensate at least on `file` URLs by trusting the extension -
+ // that's likely what we used to get the mimetype in the first place.
+ // The special case for application/ogg is because that type could
+ // actually be used for a video which can better be determined by the
+ // extension. This is tested by browser_save_video.js.
+ bool useExtension =
+ isBinaryType || urlIsFile || aMimeType.EqualsLiteral(APPLICATION_OGG);
+ mimeService->GetFromTypeAndExtension(
+ aMimeType, useExtension ? extension : EmptyCString(),
+ getter_AddRefs(mimeInfo));
+ if (mimeInfo) {
+ // But if no primary extension was returned, this mime type is probably
+ // an unknown type. Look it up again but this time supply the extension.
+ nsAutoCString primaryExtension;
+ mimeInfo->GetPrimaryExtension(primaryExtension);
+ if (primaryExtension.IsEmpty()) {
+ mimeService->GetFromTypeAndExtension(aMimeType, extension,
+ getter_AddRefs(mimeInfo));
+ }
+ }
+ }
+ }
+
+ // If an empty filename is allowed, then return early. It will be saved
+ // using the filename of the temporary file that was created for the download.
+ if (aFlags & VALIDATE_ALLOW_EMPTY && fileName.IsEmpty()) {
+ aFileName.Truncate();
+ return mimeInfo.forget();
+ }
+
+ // This section modifies the extension on the filename if it isn't valid for
+ // the given content type.
+ if (mimeInfo) {
+ bool isValidExtension;
+ if (extension.IsEmpty() ||
+ NS_FAILED(mimeInfo->ExtensionExists(extension, &isValidExtension)) ||
+ !isValidExtension) {
+ // Skip these checks for text and binary, so we don't append the unneeded
+ // .txt or other extension.
+ if (aMimeType.EqualsLiteral(TEXT_PLAIN) || isBinaryType) {
+ extension.Truncate();
+ } else {
+ nsAutoCString originalExtension(extension);
+ // If an original url was supplied, see if it has a valid extension.
+ bool useOldExtension = false;
+ if (aOriginalURI) {
+ nsCOMPtr<nsIURL> originalURL(do_QueryInterface(aOriginalURI));
+ if (originalURL) {
+ nsAutoCString uriExtension;
+ originalURL->GetFileExtension(uriExtension);
+ if (!uriExtension.IsEmpty()) {
+ mimeInfo->ExtensionExists(uriExtension, &useOldExtension);
+ if (useOldExtension) {
+ extension = uriExtension;
+ }
+ }
+ }
+ }
+
+ if (!useOldExtension) {
+ // If the filename doesn't have a valid extension, or we don't know
+ // the extension, try to use the primary extension for the type. If we
+ // don't know the primary extension for the type, just continue with
+ // the existing extension, or leave the filename with no extension.
+ nsAutoCString primaryExtension;
+ mimeInfo->GetPrimaryExtension(primaryExtension);
+ if (!primaryExtension.IsEmpty()) {
+ extension = primaryExtension;
+ }
+ }
+
+ // If an suitable extension was found, we will append to or replace the
+ // existing extension.
+ if (!extension.IsEmpty()) {
+ ModifyExtensionType modify = ShouldModifyExtension(
+ mimeInfo, aFlags & VALIDATE_FORCE_APPEND_EXTENSION,
+ originalExtension);
+ if (modify == ModifyExtension_Replace) {
+ int32_t dotidx = fileName.RFind(u".");
+ if (dotidx != -1) {
+ // Remove the existing extension and replace it.
+ fileName.Truncate(dotidx);
+ }
+ }
+
+ // Otherwise, just append the proper extension to the end of the
+ // filename, adding to the invalid extension that might already be
+ // there.
+ if (modify != ModifyExtension_Ignore) {
+ fileName.AppendLiteral(".");
+ fileName.Append(NS_ConvertUTF8toUTF16(extension));
+ }
+ }
+ }
+ }
+ }
+
+ CheckDefaultFileName(fileName, aFlags);
+
+ // Make the filename safe for the filesystem.
+ SanitizeFileName(fileName, aFlags);
+
+ aFileName = fileName;
+ return mimeInfo.forget();
+}
+
+void nsExternalHelperAppService::CheckDefaultFileName(nsAString& aFileName,
+ uint32_t aFlags) {
+ // If no filename is present, use a default filename.
+ if (!(aFlags & VALIDATE_NO_DEFAULT_FILENAME) &&
+ (aFileName.Length() == 0 || aFileName.RFind(u".") == 0)) {
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::components::StringBundle::Service();
+ if (stringService) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ if (NS_SUCCEEDED(stringService->CreateBundle(
+ "chrome://global/locale/contentAreaCommands.properties",
+ getter_AddRefs(bundle)))) {
+ nsAutoString defaultFileName;
+ bundle->GetStringFromName("UntitledSaveFileName", defaultFileName);
+ // Append any existing extension to the default filename.
+ aFileName = defaultFileName + aFileName;
+ }
+ }
+
+ // Use 'Untitled' as a last resort.
+ if (!aFileName.Length()) {
+ aFileName.AssignLiteral("Untitled");
+ }
+ }
+}
+
+void nsExternalHelperAppService::SanitizeFileName(nsAString& aFileName,
+ uint32_t aFlags) {
+ nsAutoString fileName(aFileName);
+
+ // Replace known invalid characters.
+ fileName.ReplaceChar(u"" KNOWN_PATH_SEPARATORS, u'_');
+ fileName.ReplaceChar(u"" FILE_ILLEGAL_CHARACTERS, u' ');
+ fileName.StripChar(char16_t(0));
+
+ const char16_t *startStr, *endStr;
+ fileName.BeginReading(startStr);
+ fileName.EndReading(endStr);
+
+ // True if multiple consecutive whitespace characters should
+ // be replaced by single space ' '.
+ bool collapseWhitespace = !(aFlags & VALIDATE_DONT_COLLAPSE_WHITESPACE);
+
+ // The maximum filename length differs based on the platform:
+ // Windows (FAT/NTFS) stores filenames as a maximum of 255 UTF-16 code units.
+ // Mac (APFS) stores filenames with a maximum 255 of UTF-8 code units.
+ // Linux (ext3/ext4...) stores filenames with a maximum 255 bytes.
+ // So here we just use the maximum of 255 bytes.
+ // 0 means don't truncate at a maximum size.
+ const uint32_t maxBytes =
+ (aFlags & VALIDATE_DONT_TRUNCATE) ? 0 : kDefaultMaxFileNameLength;
+
+ // True if the last character added was whitespace.
+ bool lastWasWhitespace = false;
+
+ // Length of the filename that fits into the maximum size excluding the
+ // extension and period.
+ int32_t longFileNameEnd = -1;
+
+ // Index of the last character added that was not a character that can be
+ // trimmed off of the end of the string. Trimmable characters are whitespace,
+ // periods and the vowel separator u'\u180e'. If all the characters after this
+ // point are trimmable characters, truncate the string to this point after
+ // iterating over the filename.
+ int32_t lastNonTrimmable = -1;
+
+ // The number of bytes that the string would occupy if encoded in UTF-8.
+ uint32_t bytesLength = 0;
+
+ // The length of the extension in bytes.
+ uint32_t extensionBytesLength = 0;
+
+ // This algorithm iterates over each character in the string and appends it
+ // or a replacement character if needed to outFileName.
+ nsAutoString outFileName;
+ while (startStr < endStr) {
+ bool err = false;
+ char32_t nextChar = UTF16CharEnumerator::NextChar(&startStr, endStr, &err);
+ if (err) {
+ break;
+ }
+
+ // nulls are already stripped out above.
+ MOZ_ASSERT(nextChar != char16_t(0));
+
+ auto unicodeCategory = unicode::GetGeneralCategory(nextChar);
+ if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_CONTROL ||
+ unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_LINE_SEPARATOR ||
+ unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_PARAGRAPH_SEPARATOR) {
+ // Skip over any control characters and separators.
+ continue;
+ }
+
+ if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_SPACE_SEPARATOR ||
+ nextChar == u'\ufeff') {
+ // Trim out any whitespace characters at the beginning of the filename,
+ // and only add whitespace in the middle of the filename if the last
+ // character was not whitespace or if we are not collapsing whitespace.
+ if (!outFileName.IsEmpty() &&
+ (!lastWasWhitespace || !collapseWhitespace)) {
+ // Allow the ideographic space if it is present, otherwise replace with
+ // ' '.
+ if (nextChar != u'\u3000') {
+ nextChar = ' ';
+ }
+ lastWasWhitespace = true;
+ } else {
+ lastWasWhitespace = true;
+ continue;
+ }
+ } else {
+ lastWasWhitespace = false;
+ if (nextChar == '.' || nextChar == u'\u180e') {
+ // Don't add any periods or vowel separators at the beginning of the
+ // string. Note also that lastNonTrimmable is not adjusted in this
+ // case, because periods and vowel separators are included in the
+ // set of characters to trim at the end of the filename.
+ if (outFileName.IsEmpty()) {
+ continue;
+ }
+ } else {
+ if (unicodeCategory == HB_UNICODE_GENERAL_CATEGORY_FORMAT) {
+ // Replace formatting characters with an underscore.
+ nextChar = '_';
+ }
+
+ // Don't truncate surrogate pairs in the middle.
+ lastNonTrimmable =
+ int32_t(outFileName.Length()) +
+ (NS_IS_HIGH_SURROGATE(H_SURROGATE(nextChar)) ? 2 : 1);
+ }
+ }
+
+ if (maxBytes) {
+ // UTF16CharEnumerator already converts surrogate pairs, so we can use
+ // a simple computation of byte length here.
+ uint32_t charBytesLength = nextChar < 0x80 ? 1
+ : nextChar < 0x800 ? 2
+ : nextChar < 0x10000 ? 3
+ : 4;
+ bytesLength += charBytesLength;
+ if (bytesLength > maxBytes) {
+ if (longFileNameEnd == -1) {
+ longFileNameEnd = int32_t(outFileName.Length());
+ }
+ }
+
+ // If we encounter a period, it could be the start of an extension, so
+ // start counting the number of bytes in the extension. If another period
+ // is found, start again since we want to use the last extension found.
+ if (nextChar == u'.') {
+ extensionBytesLength = 1; // 1 byte for the period.
+ } else if (extensionBytesLength) {
+ extensionBytesLength += charBytesLength;
+ }
+ }
+
+ AppendUCS4ToUTF16(nextChar, outFileName);
+ }
+
+ // If the filename is longer than the maximum allowed filename size,
+ // truncate it, but preserve the desired extension that is currently
+ // on the filename.
+ if (bytesLength > maxBytes && !outFileName.IsEmpty()) {
+ // Get the sanitized extension from the filename without the dot.
+ nsAutoString extension;
+ int32_t dotidx = outFileName.RFind(u".");
+ if (dotidx != -1) {
+ extension = Substring(outFileName, dotidx + 1);
+ }
+
+ // There are two ways in which the filename should be truncated:
+ // - If the filename was too long, truncate the name at the length
+ // of the filename.
+ // This position is indicated by longFileNameEnd.
+ // - lastNonTrimmable will indicate the last character that was not
+ // whitespace, a period, or a vowel separator at the end of the
+ // the string, so the string should be truncated there as well.
+ // If both apply, use the earliest position.
+ if (lastNonTrimmable >= 0) {
+ // Subtract off the amount for the extension and the period.
+ // Note that the extension length is in bytes but longFileNameEnd is in
+ // characters, but if they don't match, it just means we crop off
+ // more than is necessary. This is OK since it is better than cropping
+ // off too little.
+ longFileNameEnd -= extensionBytesLength;
+ if (longFileNameEnd <= 0) {
+ // This is extremely unlikely, but if the extension is larger than the
+ // maximum size, just get rid of it. In this case, the extension
+ // wouldn't have been an ordinary one we would want to preserve (such
+ // as .html or .png) so just truncate off the file wherever the first
+ // period appears.
+ int32_t dotidx = outFileName.Find(u".");
+ outFileName.Truncate(dotidx > 0 ? dotidx : 1);
+ } else {
+ outFileName.Truncate(std::min(longFileNameEnd, lastNonTrimmable));
+
+ // Now that the filename has been truncated, re-append the extension
+ // again.
+ if (!extension.IsEmpty()) {
+ if (outFileName.Last() != '.') {
+ outFileName.AppendLiteral(".");
+ }
+
+ outFileName.Append(extension);
+ }
+ }
+ }
+ } else if (lastNonTrimmable >= 0) {
+ // Otherwise, the filename wasn't too long, so just trim off the
+ // extra whitespace and periods at the end.
+ outFileName.Truncate(lastNonTrimmable);
+ }
+
+#ifdef XP_WIN
+ if (nsLocalFile::CheckForReservedFileName(outFileName)) {
+ outFileName.Truncate();
+ CheckDefaultFileName(outFileName, aFlags);
+ }
+
+#endif
+
+ if (!(aFlags & VALIDATE_ALLOW_INVALID_FILENAMES)) {
+ // If the extension is one these types, replace it with .download, as these
+ // types of files can have significance on Windows or Linux.
+ // This happens for any file, not just those with the shortcut mime type.
+ if (StringEndsWith(outFileName, u".lnk"_ns,
+ nsCaseInsensitiveStringComparator) ||
+ StringEndsWith(outFileName, u".local"_ns,
+ nsCaseInsensitiveStringComparator) ||
+ StringEndsWith(outFileName, u".url"_ns,
+ nsCaseInsensitiveStringComparator) ||
+ StringEndsWith(outFileName, u".scf"_ns,
+ nsCaseInsensitiveStringComparator) ||
+ StringEndsWith(outFileName, u".desktop"_ns,
+ nsCaseInsensitiveStringComparator)) {
+ outFileName.AppendLiteral(".download");
+ }
+ }
+
+ aFileName = outFileName;
+}
+
+nsExternalHelperAppService::ModifyExtensionType
+nsExternalHelperAppService::ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
+ bool aForceAppend,
+ const nsCString& aFileExt) {
+ nsAutoCString MIMEType;
+ if (!aMimeInfo || NS_FAILED(aMimeInfo->GetMIMEType(MIMEType))) {
+ return ModifyExtension_Append;
+ }
+
+ // Determine whether the extensions should be appended or replaced depending
+ // on the content type.
+ bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
+ StringBeginsWith(MIMEType, "audio/"_ns) ||
+ StringBeginsWith(MIMEType, "video/"_ns) || aFileExt.IsEmpty();
+
+ if (!canForce) {
+ for (const char* mime : forcedExtensionMimetypes) {
+ if (MIMEType.Equals(mime)) {
+ if (!StaticPrefs::browser_download_sanitize_non_media_extensions()) {
+ return ModifyExtension_Ignore;
+ }
+ canForce = true;
+ break;
+ }
+ }
+
+ if (!canForce) {
+ return aForceAppend ? ModifyExtension_Append : ModifyExtension_Ignore;
+ }
+ }
+
+ // If we get here, we know for sure the mimetype allows us to modify the
+ // existing extension, if it's wrong. Return whether we should replace it
+ // or append it.
+ bool knownExtension = false;
+ // Note that aFileExt is either empty or consists of an extension
+ // excluding the dot.
+ if (aFileExt.IsEmpty() ||
+ (NS_SUCCEEDED(aMimeInfo->ExtensionExists(aFileExt, &knownExtension)) &&
+ !knownExtension)) {
+ return ModifyExtension_Replace;
+ }
+
+ return ModifyExtension_Append;
+}
diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h
new file mode 100644
index 0000000000..205f73cfa1
--- /dev/null
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
@@ -0,0 +1,571 @@
+/* -*- 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 nsExternalHelperAppService_h__
+#define nsExternalHelperAppService_h__
+
+#include "mozilla/Logging.h"
+#include "prtime.h"
+
+#include "nsIExternalHelperAppService.h"
+#include "nsIExternalProtocolService.h"
+#include "nsIWebProgressListener2.h"
+#include "nsIHelperAppLauncherDialog.h"
+
+#include "nsILoadInfo.h"
+#include "nsIMIMEInfo.h"
+#include "nsIMIMEService.h"
+#include "nsINamed.h"
+#include "nsIStreamListener.h"
+#include "nsIFile.h"
+#include "nsIPermission.h"
+#include "nsString.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIChannel.h"
+#include "nsIBackgroundFileSaver.h"
+
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "nsCOMArray.h"
+#include "nsWeakReference.h"
+#include "mozilla/Attributes.h"
+
+class nsExternalAppHandler;
+class nsIMIMEInfo;
+class nsITransfer;
+class nsIPrincipal;
+class MaybeCloseWindowHelper;
+
+#define EXTERNAL_APP_HANDLER_IID \
+ { \
+ 0x50eb7479, 0x71ff, 0x4ef8, { \
+ 0xb3, 0x1e, 0x3b, 0x59, 0xc8, 0xab, 0xb9, 0x24 \
+ } \
+ }
+
+/**
+ * The helper app service. Responsible for handling content that Mozilla
+ * itself can not handle
+ * Note that this is an abstract class - we depend on appropriate subclassing
+ * on a per-OS basis to implement some methods.
+ */
+class nsExternalHelperAppService : public nsIExternalHelperAppService,
+ public nsPIExternalAppLauncher,
+ public nsIExternalProtocolService,
+ public nsIMIMEService,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIEXTERNALHELPERAPPSERVICE
+ NS_DECL_NSPIEXTERNALAPPLAUNCHER
+ NS_DECL_NSIMIMESERVICE
+ NS_DECL_NSIOBSERVER
+
+ nsExternalHelperAppService();
+
+ /**
+ * Initializes internal state. Will be called automatically when
+ * this service is first instantiated.
+ */
+ [[nodiscard]] nsresult Init();
+
+ /**
+ * nsIExternalProtocolService methods that we provide in this class. Other
+ * methods should be implemented by per-OS subclasses.
+ */
+ NS_IMETHOD ExternalProtocolHandlerExists(const char* aProtocolScheme,
+ bool* aHandlerExists) override;
+ NS_IMETHOD IsExposedProtocol(const char* aProtocolScheme,
+ bool* aResult) override;
+ NS_IMETHOD GetProtocolHandlerInfo(const nsACString& aScheme,
+ nsIHandlerInfo** aHandlerInfo) override;
+
+ NS_IMETHOD LoadURI(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal,
+ nsIPrincipal* aRedirectPrincipal,
+ mozilla::dom::BrowsingContext* aBrowsingContext,
+ bool aWasTriggeredExternally,
+ bool aHasValidUserGestureActivation) override;
+ NS_IMETHOD SetProtocolHandlerDefaults(nsIHandlerInfo* aHandlerInfo,
+ bool aOSHandlerExists) override;
+
+ /**
+ * Given a string identifying an application, create an nsIFile representing
+ * it. This function should look in $PATH for the application.
+ * The base class implementation will first try to interpret platformAppPath
+ * as an absolute path, and if that fails it will look for a file next to the
+ * mozilla executable. Subclasses can override this method if they want a
+ * different behaviour.
+ * @param platformAppPath A platform specific path to an application that we
+ * got out of the rdf data source. This can be a mac
+ * file spec, a unix path or a windows path depending
+ * on the platform
+ * @param aFile [out] An nsIFile representation of that platform
+ * application path.
+ */
+ virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath,
+ nsIFile** aFile);
+
+ NS_IMETHOD OSProtocolHandlerExists(const char* aScheme, bool* aExists) = 0;
+
+ /**
+ * Given an extension, get a MIME type string from the builtin list of
+ * mime types.
+ * @return true if we successfully found a mimetype.
+ */
+ virtual bool GetMIMETypeFromDefaultForExtension(const nsACString& aExtension,
+ nsACString& aMIMEType);
+
+ /**
+ * Given an extension, get a MIME type string. If not overridden by
+ * the OS-specific nsOSHelperAppService, will call into GetMIMEInfoFromOS
+ * with an empty mimetype.
+ * @return true if we successfully found a mimetype.
+ */
+ virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension,
+ nsACString& aMIMEType);
+
+ static already_AddRefed<nsExternalHelperAppService> GetSingleton();
+
+ // Internal method. Only called directly from tests.
+ static nsresult EscapeURI(nsIURI* aURI, nsIURI** aResult);
+
+ /**
+ * Logging Module. Usage: set MOZ_LOG=HelperAppService:level, where level
+ * should be 2 for errors, 3 for debug messages from the cross- platform
+ * nsExternalHelperAppService, and 4 for os-specific debug messages.
+ */
+ static mozilla::LazyLogModule sLog;
+
+ protected:
+ virtual ~nsExternalHelperAppService();
+
+ /**
+ * Searches the "extra" array of MIMEInfo objects for an object
+ * with a specific type. If found, it will modify the passed-in
+ * MIMEInfo. Otherwise, it will return an error and the MIMEInfo
+ * will be untouched.
+ * @param aContentType The type to search for.
+ * @param aOverwriteDescription Whether to overwrite the description
+ * @param aMIMEInfo [inout] The mime info, if found
+ */
+ nsresult FillMIMEInfoForMimeTypeFromExtras(const nsACString& aContentType,
+ bool aOverwriteDescription,
+ nsIMIMEInfo* aMIMEInfo);
+ /**
+ * Searches the "extra" array of MIMEInfo objects for an object
+ * with a specific extension.
+ *
+ * Does not change the MIME Type of the MIME Info.
+ *
+ * @see FillMIMEInfoForMimeTypeFromExtras
+ */
+ nsresult FillMIMEInfoForExtensionFromExtras(const nsACString& aExtension,
+ nsIMIMEInfo* aMIMEInfo);
+
+ /**
+ * Replace the primary extension of the mimeinfo object if it's in our
+ * list of forbidden extensions. This fixes up broken information
+ * provided to us by the OS.
+ */
+ bool MaybeReplacePrimaryExtension(const nsACString& aPrimaryExtension,
+ nsIMIMEInfo* aMIMEInfo);
+
+ /**
+ * Searches the "extra" array for a MIME type, and gets its extension.
+ * @param aExtension The extension to search for
+ * @param aMIMEType [out] The found MIME type.
+ * @return true if the extension was found, false otherwise.
+ */
+ bool GetTypeFromExtras(const nsACString& aExtension, nsACString& aMIMEType);
+
+ // friend, so that it can access the nspr log module.
+ friend class nsExternalAppHandler;
+
+ /**
+ * Helper function for ExpungeTemporaryFiles and ExpungeTemporaryPrivateFiles
+ */
+ static void ExpungeTemporaryFilesHelper(nsCOMArray<nsIFile>& fileList);
+ /**
+ * Helper function for DeleteTemporaryFileOnExit and
+ * DeleteTemporaryPrivateFileWhenPossible
+ */
+ static nsresult DeleteTemporaryFileHelper(nsIFile* aTemporaryFile,
+ nsCOMArray<nsIFile>& aFileList);
+ /**
+ * Functions related to the tempory file cleanup service provided by
+ * nsExternalHelperAppService
+ */
+ void ExpungeTemporaryFiles();
+ /**
+ * Functions related to the tempory file cleanup service provided by
+ * nsExternalHelperAppService (for the temporary files added during
+ * the private browsing mode)
+ */
+ void ExpungeTemporaryPrivateFiles();
+
+ bool GetFileNameFromChannel(nsIChannel* aChannel, nsAString& aFileName,
+ nsIURI** aURI);
+
+ // Internal version of the method from nsIMIMEService.
+ already_AddRefed<nsIMIMEInfo> ValidateFileNameForSaving(
+ nsAString& aFileName, const nsACString& aMimeType, nsIURI* aURI,
+ nsIURI* aOriginalURI, uint32_t aFlags, bool aAllowURLExtension);
+
+ // If aFileName is blank or just an extension, set aFileName to the
+ // default filename.
+ void CheckDefaultFileName(nsAString& aFileName, uint32_t aFlags);
+
+ // Ensure that the filename is safe for the file system. This will remove or
+ // replace any invalid characters and trim extra whitespace as needed. If the
+ // filename is too long, it will be truncated but the existing period and
+ // extension, if any, will be preserved.
+ void SanitizeFileName(nsAString& aFileName, uint32_t aFlags);
+
+ /**
+ * Helper routine that checks how we should modify an extension
+ * for this file.
+ */
+ enum ModifyExtensionType {
+ // Replace an invalid extension with the preferred one.
+ ModifyExtension_Replace = 0,
+ // Append the preferred extension after any existing one.
+ ModifyExtension_Append = 1,
+ // Don't modify the extension.
+ ModifyExtension_Ignore = 2
+ };
+ ModifyExtensionType ShouldModifyExtension(nsIMIMEInfo* aMimeInfo,
+ bool aForceAppend,
+ const nsCString& aFileExt);
+
+ /**
+ * Array for the files that should be deleted
+ */
+ nsCOMArray<nsIFile> mTemporaryFilesList;
+ /**
+ * Array for the files that should be deleted (for the temporary files
+ * added during the private browsing mode)
+ */
+ nsCOMArray<nsIFile> mTemporaryPrivateFilesList;
+
+ private:
+ nsresult DoContentContentProcessHelper(
+ const nsACString& aMimeContentType, nsIChannel* aChannel,
+ mozilla::dom::BrowsingContext* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener);
+};
+
+/**
+ * An external app handler is just a small little class that presents itself as
+ * a nsIStreamListener. It saves the incoming data into a temp file. The handler
+ * is bound to an application when it is created. When it receives an
+ * OnStopRequest it launches the application using the temp file it has
+ * stored the data into. We create a handler every time we have to process
+ * data using a helper app.
+ */
+class nsExternalAppHandler final : public nsIStreamListener,
+ public nsIHelperAppLauncher,
+ public nsIBackgroundFileSaverObserver,
+ public nsINamed {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSIHELPERAPPLAUNCHER
+ NS_DECL_NSICANCELABLE
+ NS_DECL_NSIBACKGROUNDFILESAVEROBSERVER
+ NS_DECL_NSINAMED
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(EXTERNAL_APP_HANDLER_IID)
+
+ /**
+ * @param aMIMEInfo MIMEInfo object, representing the type of the
+ * content that should be handled
+ * @param aFileExtension The extension we need to append to our temp file,
+ * INCLUDING the ".". e.g. .mp3
+ * @param aContentContext dom Window context, as passed to DoContent.
+ * @param aWindowContext Top level window context used in dialog parenting,
+ * as passed to DoContent. This parameter may be null,
+ * in which case dialogs will be parented to
+ * aContentContext.
+ * @param mExtProtSvc nsExternalHelperAppService on creation
+ * @param aSuggestedFileName The filename to use
+ * @param aReason A constant from nsIHelperAppLauncherDialog
+ * indicating why the request is handled by a helper app.
+ */
+ nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsAString& aFileExtension,
+ mozilla::dom::BrowsingContext* aBrowsingContext,
+ nsIInterfaceRequestor* aWindowContext,
+ nsExternalHelperAppService* aExtProtSvc,
+ const nsAString& aSuggestedFileName, uint32_t aReason,
+ bool aForceSave);
+
+ /**
+ * Clean up after the request was diverted to the parent process.
+ */
+ void DidDivertRequest(nsIRequest* request);
+
+ /**
+ * Apply content conversions if needed.
+ */
+ void MaybeApplyDecodingForExtension(nsIRequest* request);
+
+ void SetShouldCloseWindow() { mShouldCloseWindow = true; }
+
+ protected:
+ bool IsDownloadSpam(nsIChannel* aChannel);
+
+ ~nsExternalAppHandler();
+
+ nsCOMPtr<nsIFile> mTempFile;
+ nsCOMPtr<nsIURI> mSourceUrl;
+ nsString mFileExtension;
+ nsString mTempLeafName;
+
+ /**
+ * The MIME Info for this load. Will never be null.
+ */
+ nsCOMPtr<nsIMIMEInfo> mMimeInfo;
+
+ /**
+ * The BrowsingContext associated with this request to handle content.
+ */
+ RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext;
+
+ /**
+ * If set, the parent window helper app dialogs and file pickers
+ * should use in parenting. If null, we use mContentContext.
+ */
+ nsCOMPtr<nsIInterfaceRequestor> mWindowContext;
+
+ /**
+ * Used to close the window on a timer, to avoid any exceptions that are
+ * thrown if we try to close the window before it's fully loaded.
+ */
+ RefPtr<MaybeCloseWindowHelper> mMaybeCloseWindowHelper;
+
+ /**
+ * The following field is set if we were processing an http channel that had
+ * a content disposition header which specified the SUGGESTED file name we
+ * should present to the user in the save to disk dialog.
+ */
+ nsString mSuggestedFileName;
+
+ /**
+ * If set, this handler should forcibly save the file to disk regardless of
+ * MIME info settings or anything else, without ever popping up the
+ * unknown content type handling dialog.
+ */
+ bool mForceSave;
+
+ /**
+ * If set, any internally handled type that has a disposition of
+ nsIChannel::DISPOSITION_ATTACHMENT will be saved to disk.
+ */
+ bool mForceSaveInternallyHandled;
+
+ /**
+ * The canceled flag is set if the user canceled the launching of this
+ * application before we finished saving the data to a temp file.
+ */
+ bool mCanceled;
+
+ /**
+ * True if a stop request has been issued.
+ */
+ bool mStopRequestIssued;
+
+ bool mIsFileChannel;
+
+ /**
+ * True if the ExternalHelperAppChild told us that we should close the window
+ * if we handle the content as a download.
+ */
+ bool mShouldCloseWindow;
+
+ /**
+ * True if the file should be handled internally.
+ */
+ bool mHandleInternally;
+
+ /**
+ * True if any dialog (e.g. unknown content type or file picker) is shown —
+ * can stop downloads panel from opening, to avoid redundant interruptions.
+ */
+ bool mDialogShowing;
+
+ /**
+ * One of the REASON_ constants from nsIHelperAppLauncherDialog. Indicates the
+ * reason the dialog was shown (unknown content type, server requested it,
+ * etc).
+ */
+ uint32_t mReason;
+
+ /**
+ * Indicates if the nsContentSecurityUtils rate this download as
+ * acceptable, potentialy unwanted or illigal request.
+ *
+ */
+ int32_t mDownloadClassification;
+
+ /**
+ * Track the executable-ness of the temporary file.
+ */
+ bool mTempFileIsExecutable;
+
+ PRTime mTimeDownloadStarted;
+ int64_t mContentLength;
+ int64_t mProgress; /**< Number of bytes received (for sending progress
+ notifications). */
+
+ /**
+ * When we are told to save the temp file to disk (in a more permament
+ * location) before we are done writing the content to a temp file, then
+ * we need to remember the final destination until we are ready to use it.
+ */
+ nsCOMPtr<nsIFile> mFinalFileDestination;
+
+ uint32_t mBufferSize;
+
+ /**
+ * This object handles saving the data received from the network to a
+ * temporary location first, and then move the file to its final location,
+ * doing all the input/output on a background thread.
+ */
+ nsCOMPtr<nsIBackgroundFileSaver> mSaver;
+
+ /**
+ * Stores the SHA-256 hash associated with the file that we downloaded.
+ */
+ nsAutoCString mHash;
+ /**
+ * Stores the signature information of the downloaded file in an nsTArray of
+ * nsTArray of Array of bytes. If the file is unsigned this will be
+ * empty.
+ */
+ nsTArray<nsTArray<nsTArray<uint8_t>>> mSignatureInfo;
+ /**
+ * Stores the redirect information associated with the channel.
+ */
+ nsCOMPtr<nsIArray> mRedirects;
+ /**
+ * Get the dialog parent: the parent window that we can attach
+ * a dialog to when prompting the user for a download.
+ */
+ already_AddRefed<nsIInterfaceRequestor> GetDialogParent();
+ /**
+ * Creates the temporary file for the download and an output stream for it.
+ * Upon successful return, both mTempFile and mSaver will be valid.
+ */
+ nsresult SetUpTempFile(nsIChannel* aChannel);
+ /**
+ * When we download a helper app, we are going to retarget all load
+ * notifications into our own docloader and load group instead of
+ * using the window which initiated the load....RetargetLoadNotifications
+ * contains that information...
+ */
+ void RetargetLoadNotifications(nsIRequest* request);
+ /**
+ * Once the user tells us how they want to dispose of the content
+ * create an nsITransfer so they know what's going on. If this fails, the
+ * caller MUST call Cancel.
+ */
+ nsresult CreateTransfer();
+
+ /**
+ * If we fail to create the necessary temporary file to initiate a transfer
+ * we will report the failure by creating a failed nsITransfer.
+ */
+ nsresult CreateFailedTransfer();
+
+ /*
+ * The following two functions are part of the split of SaveToDisk
+ * to make it async, and works as following:
+ *
+ * SaveToDisk -------> RequestSaveDestination
+ * .
+ * .
+ * v
+ * ContinueSave <------- SaveDestinationAvailable
+ */
+
+ /**
+ * This is called by SaveToDisk to decide what's the final
+ * file destination chosen by the user or by auto-download settings.
+ */
+ void RequestSaveDestination(const nsString& aDefaultFile,
+ const nsString& aDefaultFileExt);
+
+ /**
+ * When SaveToDisk is called, it possibly delegates to RequestSaveDestination
+ * to decide the file destination. ContinueSave must then be called when
+ * the final destination is finally known.
+ * @param aFile The file that was chosen as the final destination.
+ * Must not be null.
+ */
+ nsresult ContinueSave(nsIFile* aFile);
+
+ /**
+ * Notify our nsITransfer object that we are done with the download. This is
+ * always called after the target file has been closed.
+ *
+ * @param aStatus
+ * NS_OK for success, or a failure code if the download failed.
+ * A partially downloaded file may still be available in this case.
+ */
+ void NotifyTransfer(nsresult aStatus);
+
+ /**
+ * Helper routine that searches a pref string for a given mime type
+ */
+ bool GetNeverAskFlagFromPref(const char* prefName, const char* aContentType);
+
+ /**
+ * Helper routine to ensure that mSuggestedFileName ends in the correct
+ * extension, in case the original extension contains invalid characters
+ * or if this download is for a mimetype where we enforce using a specific
+ * extension (image/, video/, and audio/ based mimetypes, and a few specific
+ * document types).
+ *
+ * It also ensure that mFileExtension only contains an extension
+ * when it is different from mSuggestedFileName's extension.
+ */
+ void EnsureCorrectExtension(const nsString& aFileExt);
+
+ typedef enum { kReadError, kWriteError, kLaunchError } ErrorType;
+ /**
+ * Utility function to send proper error notification to web progress listener
+ */
+ void SendStatusChange(ErrorType type, nsresult aStatus, nsIRequest* aRequest,
+ const nsString& path);
+
+ /**
+ * Set in HelperAppDlg.jsm. This is always null after the user has chosen an
+ * action.
+ */
+ nsCOMPtr<nsIWebProgressListener2> mDialogProgressListener;
+ /**
+ * Set once the user has chosen an action. This is null after the download
+ * has been canceled or completes.
+ */
+ nsCOMPtr<nsITransfer> mTransfer;
+
+ nsCOMPtr<nsIHelperAppLauncherDialog> mDialog;
+
+ /**
+
+ * The request that's being loaded. Initialized in OnStartRequest.
+ * Nulled out in OnStopRequest or once we know what we're doing
+ * with the data, whichever happens later.
+ */
+ nsCOMPtr<nsIRequest> mRequest;
+
+ RefPtr<nsExternalHelperAppService> mExtProtSvc;
+};
+NS_DEFINE_STATIC_IID_ACCESSOR(nsExternalAppHandler, EXTERNAL_APP_HANDLER_IID)
+
+#endif // nsExternalHelperAppService_h__
diff --git a/uriloader/exthandler/nsExternalProtocolHandler.cpp b/uriloader/exthandler/nsExternalProtocolHandler.cpp
new file mode 100644
index 0000000000..4e93977b8f
--- /dev/null
+++ b/uriloader/exthandler/nsExternalProtocolHandler.cpp
@@ -0,0 +1,550 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:set ts=2 sts=2 sw=2 et cin:
+ *
+ * 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/dom/ContentChild.h"
+#include "mozilla/BasePrincipal.h"
+#include "nsIURI.h"
+#include "nsExternalProtocolHandler.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsNetUtil.h"
+#include "nsContentSecurityManager.h"
+#include "nsExternalHelperAppService.h"
+
+// used to dispatch urls to default protocol handlers
+#include "nsCExternalHandlerService.h"
+#include "nsIExternalProtocolService.h"
+#include "nsIChildChannel.h"
+#include "nsIParentChannel.h"
+
+class nsILoadInfo;
+
+////////////////////////////////////////////////////////////////////////
+// a stub channel implemenation which will map calls to AsyncRead and
+// OpenInputStream to calls in the OS for loading the url.
+////////////////////////////////////////////////////////////////////////
+
+class nsExtProtocolChannel : public nsIChannel,
+ public nsIChildChannel,
+ public nsIParentChannel {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICHANNEL
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUEST
+ NS_DECL_NSICHILDCHANNEL
+ NS_DECL_NSIPARENTCHANNEL
+
+ nsExtProtocolChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo);
+
+ private:
+ virtual ~nsExtProtocolChannel();
+
+ nsresult OpenURL();
+ void Finish(nsresult aResult);
+
+ nsCOMPtr<nsIURI> mUrl;
+ nsCOMPtr<nsIURI> mOriginalURI;
+ nsresult mStatus;
+ nsLoadFlags mLoadFlags;
+ bool mWasOpened;
+ bool mCanceled;
+ // Set true (as a result of ConnectParent invoked from child process)
+ // when this channel is on the parent process and is being used as
+ // a redirect target channel. It turns AsyncOpen into a no-op since
+ // we do it on the child.
+ bool mConnectedParent;
+
+ nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
+ nsCOMPtr<nsILoadGroup> mLoadGroup;
+ nsCOMPtr<nsILoadInfo> mLoadInfo;
+ nsCOMPtr<nsIStreamListener> mListener;
+};
+
+NS_IMPL_ADDREF(nsExtProtocolChannel)
+NS_IMPL_RELEASE(nsExtProtocolChannel)
+
+NS_INTERFACE_MAP_BEGIN(nsExtProtocolChannel)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIChannel)
+ NS_INTERFACE_MAP_ENTRY(nsIChannel)
+ NS_INTERFACE_MAP_ENTRY(nsIRequest)
+ NS_INTERFACE_MAP_ENTRY(nsIChildChannel)
+ NS_INTERFACE_MAP_ENTRY(nsIParentChannel)
+ NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
+ NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
+NS_INTERFACE_MAP_END
+
+nsExtProtocolChannel::nsExtProtocolChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo)
+ : mUrl(aURI),
+ mOriginalURI(aURI),
+ mStatus(NS_OK),
+ mLoadFlags(nsIRequest::LOAD_NORMAL),
+ mWasOpened(false),
+ mCanceled(false),
+ mConnectedParent(false),
+ mLoadInfo(aLoadInfo) {}
+
+nsExtProtocolChannel::~nsExtProtocolChannel() {}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ NS_IF_ADDREF(*aLoadGroup = mLoadGroup);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ mLoadGroup = aLoadGroup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aCallbacks) {
+ NS_IF_ADDREF(*aCallbacks = mCallbacks);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aCallbacks) {
+ mCallbacks = aCallbacks;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsExtProtocolChannel::GetSecurityInfo(
+ nsITransportSecurityInfo** aSecurityInfo) {
+ *aSecurityInfo = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetOriginalURI(nsIURI** aURI) {
+ NS_ADDREF(*aURI = mOriginalURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetOriginalURI(nsIURI* aURI) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ mOriginalURI = aURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetURI(nsIURI** aURI) {
+ *aURI = mUrl;
+ NS_IF_ADDREF(*aURI);
+ return NS_OK;
+}
+
+nsresult nsExtProtocolChannel::OpenURL() {
+ nsresult rv = NS_ERROR_FAILURE;
+ nsCOMPtr<nsIExternalProtocolService> extProtService(
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID));
+
+ if (extProtService) {
+#ifdef DEBUG
+ nsAutoCString urlScheme;
+ mUrl->GetScheme(urlScheme);
+ bool haveHandler = false;
+ extProtService->ExternalProtocolHandlerExists(urlScheme.get(),
+ &haveHandler);
+ NS_ASSERTION(haveHandler,
+ "Why do we have a channel for this url if we don't support "
+ "the protocol?");
+#endif
+
+ RefPtr<mozilla::dom::BrowsingContext> ctx;
+ rv = mLoadInfo->GetTargetBrowsingContext(getter_AddRefs(ctx));
+ if (NS_FAILED(rv)) {
+ goto finish;
+ }
+
+ RefPtr<nsIPrincipal> triggeringPrincipal = mLoadInfo->TriggeringPrincipal();
+ RefPtr<nsIPrincipal> redirectPrincipal;
+ if (!mLoadInfo->RedirectChain().IsEmpty()) {
+ mLoadInfo->RedirectChain().LastElement()->GetPrincipal(
+ getter_AddRefs(redirectPrincipal));
+ }
+ rv = extProtService->LoadURI(mUrl, triggeringPrincipal, redirectPrincipal,
+ ctx, mLoadInfo->GetLoadTriggeredFromExternal(),
+ mLoadInfo->GetHasValidUserGestureActivation());
+
+ if (NS_SUCCEEDED(rv) && mListener) {
+ mStatus = NS_ERROR_NO_CONTENT;
+
+ RefPtr<nsExtProtocolChannel> self = this;
+ nsCOMPtr<nsIStreamListener> listener = mListener;
+ MessageLoop::current()->PostTask(NS_NewRunnableFunction(
+ "nsExtProtocolChannel::OpenURL", [self, listener]() {
+ listener->OnStartRequest(self);
+ listener->OnStopRequest(self, self->mStatus);
+ }));
+ }
+ }
+
+finish:
+ mCallbacks = nullptr;
+ mListener = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::Open(nsIInputStream** aStream) {
+ nsCOMPtr<nsIStreamListener> listener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return OpenURL();
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::AsyncOpen(nsIStreamListener* aListener) {
+ nsCOMPtr<nsIStreamListener> listener = aListener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ if (NS_FAILED(rv)) {
+ mCallbacks = nullptr;
+ return rv;
+ }
+
+ if (mConnectedParent) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(
+ mLoadInfo->GetSecurityMode() == 0 ||
+ mLoadInfo->GetInitialSecurityCheckDone() ||
+ (mLoadInfo->GetSecurityMode() ==
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL &&
+ mLoadInfo->GetLoadingPrincipal() &&
+ mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()),
+ "security flags in loadInfo but doContentSecurityCheck() not called");
+
+ NS_ENSURE_ARG_POINTER(listener);
+ NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED);
+
+ mWasOpened = true;
+ mListener = listener;
+
+ return OpenURL();
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = mLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ mLoadFlags = aLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetIsDocument(bool* aIsDocument) {
+ return NS_GetIsDocumentChannel(this, aIsDocument);
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetContentType(nsACString& aContentType) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetContentType(
+ const nsACString& aContentType) {
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetContentCharset(
+ nsACString& aContentCharset) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetContentCharset(
+ const nsACString& aContentCharset) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetContentDisposition(
+ uint32_t* aContentDisposition) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetContentDisposition(
+ uint32_t aContentDisposition) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetContentDispositionFilename(
+ nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetContentDispositionFilename(
+ const nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetContentDispositionHeader(
+ nsACString& aContentDispositionHeader) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetContentLength(int64_t* aContentLength) {
+ *aContentLength = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsExtProtocolChannel::SetContentLength(int64_t aContentLength) {
+ MOZ_ASSERT_UNREACHABLE("SetContentLength");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetOwner(nsISupports** aPrincipal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetOwner(nsISupports* aPrincipal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) {
+ NS_IF_ADDREF(*aLoadInfo = mLoadInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) {
+ MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null");
+ mLoadInfo = aLoadInfo;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// From nsIRequest
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsExtProtocolChannel::GetName(nsACString& result) {
+ return mUrl->GetSpec(result);
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::IsPending(bool* result) {
+ *result = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetStatus(nsresult* status) {
+ *status = mStatus;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetCanceledReason(
+ const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::CancelWithReason(
+ nsresult aStatus, const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::Cancel(nsresult status) {
+ if (NS_SUCCEEDED(mStatus)) {
+ mStatus = status;
+ }
+ mCanceled = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetCanceled(bool* aCanceled) {
+ *aCanceled = mCanceled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::Suspend() {
+ MOZ_ASSERT_UNREACHABLE("Suspend");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::Resume() {
+ MOZ_ASSERT_UNREACHABLE("Resume");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+///////////////////////////////////////////////////////////////////////
+// From nsIChildChannel
+//////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsExtProtocolChannel::ConnectParent(uint32_t registrarId) {
+ mozilla::dom::ContentChild::GetSingleton()
+ ->SendExtProtocolChannelConnectParent(registrarId);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::CompleteRedirectSetup(
+ nsIStreamListener* listener) {
+ // For redirects to external protocols we AsyncOpen on the child
+ // (not the parent) because child channel has the right docshell
+ // (which is needed for the select dialog).
+ return AsyncOpen(listener);
+}
+
+///////////////////////////////////////////////////////////////////////
+// From nsIParentChannel (derives from nsIStreamListener)
+//////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsExtProtocolChannel::SetParentListener(
+ mozilla::net::ParentChannelListener* aListener) {
+ // This is called as part of the connect parent operation from
+ // ContentParent::RecvExtProtocolChannelConnectParent. Setting
+ // this flag tells this channel to not proceed and makes AsyncOpen
+ // just no-op. Actual operation will happen from the child process
+ // via CompleteRedirectSetup call on the child channel.
+ mConnectedParent = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetClassifierMatchedInfo(
+ const nsACString& aList, const nsACString& aProvider,
+ const nsACString& aFullHash) {
+ // nothing to do
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::SetClassifierMatchedTrackingInfo(
+ const nsACString& aLists, const nsACString& aFullHashes) {
+ // nothing to do
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::NotifyClassificationFlags(
+ uint32_t aClassificationFlags, bool aIsThirdParty) {
+ // nothing to do
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::Delete() {
+ // nothing to do
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::GetRemoteType(nsACString& aRemoteType) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::OnStartRequest(nsIRequest* aRequest) {
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ MOZ_ASSERT(NS_FAILED(aStatusCode));
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP nsExtProtocolChannel::OnDataAvailable(
+ nsIRequest* aRequest, nsIInputStream* aInputStream, uint64_t aOffset,
+ uint32_t aCount) {
+ // no data is expected
+ MOZ_CRASH("No data expected from external protocol channel");
+ return NS_ERROR_UNEXPECTED;
+}
+
+///////////////////////////////////////////////////////////////////////
+// the default protocol handler implementation
+//////////////////////////////////////////////////////////////////////
+
+nsExternalProtocolHandler::nsExternalProtocolHandler() {
+ m_schemeName = "default";
+}
+
+nsExternalProtocolHandler::~nsExternalProtocolHandler() {}
+
+NS_IMPL_ADDREF(nsExternalProtocolHandler)
+NS_IMPL_RELEASE(nsExternalProtocolHandler)
+
+NS_INTERFACE_MAP_BEGIN(nsExternalProtocolHandler)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIProtocolHandler)
+ NS_INTERFACE_MAP_ENTRY(nsIProtocolHandler)
+ NS_INTERFACE_MAP_ENTRY(nsIExternalProtocolHandler)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END
+
+NS_IMETHODIMP nsExternalProtocolHandler::GetScheme(nsACString& aScheme) {
+ aScheme = m_schemeName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsExternalProtocolHandler::AllowPort(int32_t port, const char* scheme,
+ bool* _retval) {
+ // don't override anything.
+ *_retval = false;
+ return NS_OK;
+}
+// returns TRUE if the OS can handle this protocol scheme and false otherwise.
+bool nsExternalProtocolHandler::HaveExternalProtocolHandler(nsIURI* aURI) {
+ MOZ_ASSERT(aURI);
+ nsAutoCString scheme;
+ aURI->GetScheme(scheme);
+
+ nsCOMPtr<nsIExternalProtocolService> extProtSvc(
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID));
+ if (!extProtSvc) {
+ return false;
+ }
+
+ bool haveHandler = false;
+ extProtSvc->ExternalProtocolHandlerExists(scheme.get(), &haveHandler);
+ return haveHandler;
+}
+
+NS_IMETHODIMP
+nsExternalProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIChannel** aRetval) {
+ NS_ENSURE_TRUE(aURI, NS_ERROR_UNKNOWN_PROTOCOL);
+ NS_ENSURE_TRUE(aRetval, NS_ERROR_UNKNOWN_PROTOCOL);
+
+ // Only try to return a channel if we have a protocol handler for the url.
+ // nsOSHelperAppService::LoadUriInternal relies on this to check trustedness
+ // for some platforms at least. (win uses ::ShellExecute and unix uses
+ // gnome_url_show.)
+ if (!HaveExternalProtocolHandler(aURI)) {
+ return NS_ERROR_UNKNOWN_PROTOCOL;
+ }
+
+ nsCOMPtr<nsIChannel> channel = new nsExtProtocolChannel(aURI, aLoadInfo);
+ channel.forget(aRetval);
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////
+// External protocol handler interface implementation
+//////////////////////////////////////////////////////////////////////
+NS_IMETHODIMP nsExternalProtocolHandler::ExternalAppExistsForScheme(
+ const nsACString& aScheme, bool* _retval) {
+ nsCOMPtr<nsIExternalProtocolService> extProtSvc(
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID));
+ if (extProtSvc)
+ return extProtSvc->ExternalProtocolHandlerExists(
+ PromiseFlatCString(aScheme).get(), _retval);
+
+ // In case we don't have external protocol service.
+ *_retval = false;
+ return NS_OK;
+}
diff --git a/uriloader/exthandler/nsExternalProtocolHandler.h b/uriloader/exthandler/nsExternalProtocolHandler.h
new file mode 100644
index 0000000000..373223bc39
--- /dev/null
+++ b/uriloader/exthandler/nsExternalProtocolHandler.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 4; 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 nsExternalProtocolHandler_h___
+#define nsExternalProtocolHandler_h___
+
+#include "nsIExternalProtocolHandler.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "mozilla/Attributes.h"
+
+class nsIURI;
+
+// protocol handlers need to support weak references if we want the netlib
+// nsIOService to cache them.
+class nsExternalProtocolHandler final : public nsIExternalProtocolHandler,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPROTOCOLHANDLER
+ NS_DECL_NSIEXTERNALPROTOCOLHANDLER
+
+ nsExternalProtocolHandler();
+
+ protected:
+ ~nsExternalProtocolHandler();
+
+ // helper function
+ bool HaveExternalProtocolHandler(nsIURI* aURI);
+ nsCString m_schemeName;
+};
+
+#endif // nsExternalProtocolHandler_h___
diff --git a/uriloader/exthandler/nsIContentDispatchChooser.idl b/uriloader/exthandler/nsIContentDispatchChooser.idl
new file mode 100644
index 0000000000..00d61e575b
--- /dev/null
+++ b/uriloader/exthandler/nsIContentDispatchChooser.idl
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIHandlerInfo;
+interface nsIPrincipal;
+interface nsIURI;
+webidl BrowsingContext;
+
+/**
+ * This is used to ask a user what they would like to do with a given piece of
+ * content.
+ */
+[scriptable, uuid(456ca3b2-02be-4f97-89a2-08c08d3ad88f)]
+interface nsIContentDispatchChooser : nsISupports {
+ /**
+ * Opens the handler associated with the given resource.
+ * If the caller does not have permission or no handler is set, we ask the
+ * user to grant permission and pick a handler.
+ *
+ * @param aHander
+ * The interface describing the details of how this content should or
+ * can be handled.
+ * @param aURI
+ * The URI of the resource that we are asking about.
+ * @param aTriggeringPrincipal
+ * The principal making the request.
+ * @param aBrowsingContext
+ * The browsing context where the load should happen.
+ * @param aWasTriggeredExternally
+ * True if the load was tripped by an external app.
+ */
+ void handleURI(in nsIHandlerInfo aHandler,
+ in nsIURI aURI,
+ in nsIPrincipal aTriggeringPrincipal,
+ in BrowsingContext aBrowsingContext,
+ [optional] in bool aWasTriggeredExternally);
+};
diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl
new file mode 100644
index 0000000000..4a399acb72
--- /dev/null
+++ b/uriloader/exthandler/nsIExternalHelperAppService.idl
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 3; 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 "nsICancelable.idl"
+
+interface nsIURI;
+interface nsIChannel;
+interface nsIStreamListener;
+interface nsIFile;
+interface nsIMIMEInfo;
+interface nsIWebProgressListener2;
+interface nsIInterfaceRequestor;
+webidl BrowsingContext;
+
+/**
+ * The external helper app service is used for finding and launching
+ * platform specific external applications for a given mime content type.
+ */
+[scriptable, uuid(1E4F3AE1-B737-431F-A95D-31FA8DA70199)]
+interface nsIExternalHelperAppService : nsISupports
+{
+ /**
+ * Binds an external helper application to a stream listener. The caller
+ * should pump data into the returned stream listener. When the OnStopRequest
+ * is issued, the stream listener implementation will launch the helper app
+ * with this data.
+ * @param aMimeContentType The content type of the incoming data
+ * @param aChannel The channel corresponding to the incoming data
+ * @param aContentContext Used in processing content document refresh
+ * headers after target content is downloaded.
+ * @param aForceSave True to always save this content to disk, regardless of
+ * nsIMIMEInfo and other such influences.
+ * @param aWindowContext Used in parenting helper app dialogs, usually
+ * points to the parent browser window. This parameter may be null,
+ * in which case dialogs will be parented to aContentContext.
+ * @return A nsIStreamListener which the caller should pump the data into.
+ */
+ nsIStreamListener doContent (in ACString aMimeContentType,
+ in nsIChannel aChannel,
+ in nsIInterfaceRequestor aContentContext,
+ in boolean aForceSave,
+ [optional] in nsIInterfaceRequestor aWindowContext);
+
+ /**
+ * Binds an external helper application to a stream listener. The caller
+ * should pump data into the returned stream listener. When the OnStopRequest
+ * is issued, the stream listener implementation will launch the helper app
+ * with this data.
+ * Replaces doContent for native code, and uses BrowsingContext.
+ *
+ * @param aMimeContentType The content type of the incoming data
+ * @param aChannel The channel corresponding to the incoming data
+ * @param aContentContext The BrowsingContext that the channel was initiated
+ * by. Used for closing the window if we opened one specifically for this download.
+ * @param aForceSave True to always save this content to disk, regardless of
+ * nsIMIMEInfo and other such influences.
+ * @param aWindowContext Used in parenting helper app dialogs, usually
+ * points to the parent browser window. This parameter may be null,
+ * in which case dialogs will be parented to aContentContext.
+ * @return A nsIStreamListener which the caller should pump the data into.
+ */
+ nsIStreamListener createListener (in ACString aMimeContentType,
+ in nsIChannel aChannel,
+ in BrowsingContext aContentContext,
+ in boolean aForceSave,
+ [optional] in nsIInterfaceRequestor aWindowContext);
+
+ /**
+ * Returns true if data from a URL with this extension combination
+ * is to be decoded from aEncodingType prior to saving or passing
+ * off to helper apps, false otherwise.
+ */
+ boolean applyDecodingForExtension(in AUTF8String aExtension,
+ in ACString aEncodingType);
+
+};
+
+/**
+ * This is a private interface shared between external app handlers and the platform specific
+ * external helper app service
+ */
+[scriptable, uuid(6613e2e7-feab-4e3a-bb1f-b03200d544ec)]
+interface nsPIExternalAppLauncher : nsISupports
+{
+ /**
+ * mscott --> eventually I should move this into a new service so other
+ * consumers can add temporary files they want deleted on exit.
+ * @param aTemporaryFile A temporary file we should delete on exit.
+ */
+ void deleteTemporaryFileOnExit(in nsIFile aTemporaryFile);
+ /**
+ * Delete a temporary file created inside private browsing mode when
+ * the private browsing mode has ended.
+ */
+ void deleteTemporaryPrivateFileWhenPossible(in nsIFile aTemporaryFile);
+};
+
+/**
+ * A helper app launcher is a small object created to handle the launching
+ * of an external application.
+ *
+ * Note that cancelling the load via the nsICancelable interface will release
+ * the reference to the launcher dialog.
+ */
+[scriptable, uuid(acf2a516-7d7f-4771-8b22-6c4a559c088e)]
+interface nsIHelperAppLauncher : nsICancelable
+{
+ /**
+ * The mime info object associated with the content type this helper app
+ * launcher is currently attempting to load
+ */
+ readonly attribute nsIMIMEInfo MIMEInfo;
+
+ /**
+ * The source uri
+ */
+ readonly attribute nsIURI source;
+
+ /**
+ * The suggested name for this file
+ */
+ readonly attribute AString suggestedFileName;
+
+ /**
+ * Saves the final destination of the file.
+ * NOTE: This will release the reference to the nsIHelperAppLauncherDialog.
+ */
+ void promptForSaveDestination();
+
+ /**
+ * Tell the launcher that we will want to open the file.
+ * NOTE: This will release the reference to the nsIHelperAppLauncherDialog.
+ * @param aHandleInternally TRUE if we should handle opening this internally.
+ * @param aNewFileLocation a preferred location choosen through the File Picker.
+ * Null if going through the fast save without File Picker.
+ */
+ void setDownloadToLaunch(in boolean aHandleInternally, in nsIFile aFile);
+
+
+ /**
+ * Use the MIMEInfo associated with us to open a file that is already local.
+ * Will no-op if `source` is not a local file.
+ */
+ void launchLocalFile();
+
+ /**
+ * Callback invoked by nsIHelperAppLauncherDialog::promptForSaveToFileAsync
+ * after the user has chosen a file through the File Picker (or dismissed it).
+ * @param aFile The file that was chosen by the user (or null if dialog was dismissed).
+ * @param aDialogWasShown Optional boolean - false by default. Pass true if a
+ * dialog was opened in the process of reaching this file result. If true, we
+ * suppress the opening of the downloads panel to avoid redundancy.
+ */
+ void saveDestinationAvailable(in nsIFile aFile, [optional] in boolean aDialogWasShown);
+
+ /**
+ * The following methods are used by the progress dialog to get or set
+ * information on the current helper app launcher download.
+ * This reference will be released when the download is finished (after the
+ * listener receives the STATE_STOP notification).
+ */
+ void setWebProgressListener(in nsIWebProgressListener2 aWebProgressListener);
+
+ /**
+ * The file we are saving to
+ */
+ readonly attribute nsIFile targetFile;
+
+ /**
+ * The executable-ness of the target file
+ */
+ readonly attribute boolean targetFileIsExecutable;
+
+ /**
+ * Time when the download started
+ */
+ readonly attribute PRTime timeDownloadStarted;
+
+ /**
+ * The download content length, or -1 if the length is not available.
+ */
+ readonly attribute int64_t contentLength;
+
+ /**
+ * The browsingContext ID of the launcher's source
+ */
+ readonly attribute uint64_t browsingContextId;
+};
diff --git a/uriloader/exthandler/nsIExternalProtocolService.idl b/uriloader/exthandler/nsIExternalProtocolService.idl
new file mode 100644
index 0000000000..7d714035e6
--- /dev/null
+++ b/uriloader/exthandler/nsIExternalProtocolService.idl
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 3; 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"
+
+interface nsIURI;
+interface nsIFile;
+interface nsIPrincipal;
+interface nsIInterfaceRequestor;
+interface nsIHandlerInfo;
+
+webidl BrowsingContext;
+
+/**
+ * The external protocol service is used for finding and launching
+ * web handlers (a la registerProtocolHandler in the HTML5 draft) or
+ * platform-specific applications for handling particular protocols.
+ *
+ * You can ask the external protocol service if it has an external
+ * handler for a given protocol scheme. And you can ask it to load
+ * the url using the default handler.
+ */
+[scriptable, uuid(70f93b7a-3ec6-4bcb-b093-92d9984c9f83)]
+interface nsIExternalProtocolService : nsISupports
+{
+ /**
+ * Check whether a handler for a specific protocol exists. Specifically,
+ * this looks to see whether there are any known possible application handlers
+ * in either the nsIHandlerService datastore or registered with the OS.
+ *
+ * @param aProtocolScheme The scheme from a url: http, ftp, mailto, etc.
+ *
+ * @return true if we have a handler and false otherwise.
+ *
+ * XXX shouldn't aProtocolScheme be an ACString like nsIURI::scheme?
+ */
+ boolean externalProtocolHandlerExists(in string aProtocolScheme);
+
+ /**
+ * Check whether a handler for a specific protocol is "exposed" as a visible
+ * feature of the current application.
+ *
+ * An exposed protocol handler is one that can be used in all contexts. A
+ * non-exposed protocol handler is one that can only be used internally by the
+ * application. For example, a non-exposed protocol would not be loaded by the
+ * application in response to a link click or a X-remote openURL command.
+ * Instead, it would be deferred to the system's external protocol handler.
+ * XXX shouldn't aProtocolScheme be an ACString like nsIURI::scheme?
+ */
+ boolean isExposedProtocol(in string aProtocolScheme);
+
+ /**
+ * Retrieve the handler for the given protocol. If neither the application
+ * nor the OS knows about a handler for the protocol, the object this method
+ * returns will represent a default handler for unknown content.
+ *
+ * @param aProtocolScheme the scheme from a URL: http, ftp, mailto, etc.
+ *
+ * Note: aProtocolScheme should not include a trailing colon, which is part
+ * of the URI syntax, not part of the scheme itself (i.e. pass "mailto" not
+ * "mailto:").
+ *
+ * @return the handler, if any; otherwise a default handler
+ */
+ nsIHandlerInfo getProtocolHandlerInfo(in ACString aProtocolScheme);
+
+ /**
+ * Given a scheme, looks up the protocol info from the OS. This should be
+ * overridden by each OS's implementation.
+ *
+ * @param aScheme The protocol scheme we are looking for.
+ * @param aFound Was an OS default handler for this scheme found?
+ * @return An nsIHanderInfo for the protocol.
+ */
+ nsIHandlerInfo getProtocolHandlerInfoFromOS(in ACString aProtocolScheme,
+ out boolean aFound);
+
+ /**
+ * Set some sane defaults for a protocol handler object.
+ *
+ * @param aHandlerInfo nsIHandlerInfo object, as returned by
+ * getProtocolHandlerInfoFromOS
+ * @param aOSHandlerExists was the object above created for an extant
+ * OS default handler? This is generally the
+ * value of the aFound out param from
+ * getProtocolHandlerInfoFromOS.
+ */
+ void setProtocolHandlerDefaults(in nsIHandlerInfo aHandlerInfo,
+ in boolean aOSHandlerExists);
+
+ /**
+ * Used to load a URI via an external application. Might prompt the user for
+ * permission to load the external application.
+ *
+ * @param aURI
+ * The URI to load
+ *
+ * @param aTriggeringPrincipal
+ * The principal triggering this load.
+ *
+ * @param aRedirectPrincipal
+ * The last post-redirect principal triggering this load.
+ * Used for display and permission purposes. If null, we'll
+ * use the triggering principal.
+ *
+ * @param aBrowsingContext
+ * The context to parent the dialog against, and, if a web handler
+ * is chosen, it is loaded in this window as well. This parameter
+ * may be ultimately passed nsIURILoader.openURI in the case of a
+ * web handler, and aWindowContext is null or not present, web
+ * handlers will fail. We need to do better than that; bug 394483
+ * filed in order to track.
+ *
+ * @param aWasTriggeredExternally
+ * If true, indicates the load was initiated by an external app.
+ *
+ * @param aHasValidUserGestureActivation
+ * Whether the document that triggered the load had user activation.
+ * Used for sandbox checks.
+ *
+ * @note Embedders that do not expose the http protocol should not currently
+ * use web-based protocol handlers, as handoff won't work correctly
+ * (bug 394479).
+ */
+ void loadURI(in nsIURI aURI,
+ [optional] in nsIPrincipal aTriggeringPrincipal,
+ [optional] in nsIPrincipal aRedirectPrincipal,
+ [optional] in BrowsingContext aBrowsingContext,
+ [optional] in bool aWasTriggeredExternally,
+ [optional] in bool aHasValidUserGestureActivation);
+
+ /**
+ * Gets a human-readable description for the application responsible for
+ * handling a specific protocol.
+ *
+ * @param aScheme The scheme to look up. For example, "mms".
+ *
+ * @throw NS_ERROR_NOT_IMPLEMENTED
+ * If getting descriptions for protocol helpers is not supported
+ * @throw NS_ERROR_NOT_AVAILABLE
+ * If no protocol helper exists for this scheme, or if it is not
+ * possible to get a description for it.
+ */
+ AString getApplicationDescription(in AUTF8String aScheme);
+
+ /**
+ * Check if this app is registered as the OS default for a given scheme.
+ *
+ * @param aScheme The scheme to look up. For example, "mms".
+ */
+ bool isCurrentAppOSDefaultForProtocol(in AUTF8String aScheme);
+};
diff --git a/uriloader/exthandler/nsIHandlerService.idl b/uriloader/exthandler/nsIHandlerService.idl
new file mode 100644
index 0000000000..890c8ab59d
--- /dev/null
+++ b/uriloader/exthandler/nsIHandlerService.idl
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIHandlerInfo;
+interface nsISimpleEnumerator;
+interface nsIMIMEInfo;
+
+[scriptable, uuid(53f0ad17-ec62-46a1-adbc-efccc06babcd)]
+interface nsIHandlerService : nsISupports
+{
+ /**
+ * Asynchronously performs any IO that the nsIHandlerService needs to do
+ * before it can be of use.
+ */
+ void asyncInit();
+
+ /**
+ * Retrieve a list of all handlers in the datastore. This list is not
+ * guaranteed to be in any particular order, and callers should not assume
+ * it will remain in the same order in the future.
+ *
+ * @returns a list of all handlers in the datastore
+ */
+ nsISimpleEnumerator enumerate();
+
+ /**
+ * Fill a handler info object with information from the datastore.
+ *
+ * Note: because of the way the external helper app service currently mixes
+ * OS and user handler info in the same handler info object, this method
+ * takes an existing handler info object (probably retrieved from the OS)
+ * and "fills it in" with information from the datastore, overriding any
+ * existing properties on the object with properties from the datastore.
+ *
+ * Ultimately, however, we're going to separate OS and user handler info
+ * into separate objects, at which point this method should be renamed to
+ * something like "get" or "retrieve", take a class and type (or perhaps
+ * a type whose class can be determined by querying the type, for example
+ * an nsIContentType which is also an nsIMIMEType or an nsIProtocolScheme),
+ * and return a handler info object representing only the user info.
+ *
+ * Note: if you specify an override type, then the service will fill in
+ * the handler info object with information about that type instead of
+ * the type specified by the object's nsIHandlerInfo::type attribute.
+ *
+ * This is useful when you are trying to retrieve information about a MIME
+ * type that doesn't exist in the datastore, but you have a file extension
+ * for that type, and nsIHandlerService::getTypeFromExtension returns another
+ * MIME type that does exist in the datastore and can handle that extension.
+ *
+ * For example, the user clicks on a link, and the content has a MIME type
+ * that isn't in the datastore, but the link has a file extension, and that
+ * extension is associated with another MIME type in the datastore (perhaps
+ * an unofficial MIME type preceded an official one, like with image/x-png
+ * and image/png).
+ *
+ * In that situation, you can call this method to fill in the handler info
+ * object with information about that other type by passing the other type
+ * as the aOverrideType parameter.
+ *
+ * @param aHandlerInfo the handler info object
+ * @param aOverrideType a type to use instead of aHandlerInfo::type
+ *
+ * Note: if there is no information in the datastore about this type,
+ * this method throws NS_ERROR_NOT_AVAILABLE. Callers are encouraged to
+ * check exists() before calling fillHandlerInfo(), to prevent spamming the
+ * console with XPCOM exception errors.
+ */
+ void fillHandlerInfo(in nsIHandlerInfo aHandlerInfo,
+ in ACString aOverrideType);
+
+ /**
+ * Save the preferred action, preferred handler, possible handlers, and
+ * always ask properties of the given handler info object to the datastore.
+ * Updates an existing record or creates a new one if necessary.
+ *
+ * Note: if preferred action is undefined or invalid, then we assume
+ * the default value nsIHandlerInfo::useHelperApp.
+ *
+ * @param aHandlerInfo the handler info object
+ */
+ void store(in nsIHandlerInfo aHandlerInfo);
+
+ /**
+ * Whether or not a record for the given handler info object exists
+ * in the datastore. If the datastore is corrupt (or some other error
+ * is caught in the implementation), false will be returned.
+ *
+ * @param aHandlerInfo a handler info object
+ *
+ * @returns whether or not a record exists
+ */
+ boolean exists(in nsIHandlerInfo aHandlerInfo);
+
+ /**
+ * Remove the given handler info object from the datastore. Deletes all
+ * records associated with the object, including the preferred handler, info,
+ * and type records plus the entry in the list of types, if they exist.
+ * Otherwise, it does nothing and does not return an error.
+ *
+ * @param aHandlerInfo the handler info object
+ */
+ void remove(in nsIHandlerInfo aHandlerInfo);
+
+ /**
+ * Get the MIME type mapped to the given file extension in the datastore.
+ *
+ * XXX If we ever support extension -> protocol scheme mappings, then this
+ * method should work for those as well.
+ *
+ * Note: in general, you should use nsIMIMEService::getTypeFromExtension
+ * to get a MIME type from a file extension, as that method checks a variety
+ * of other sources besides just the datastore. Use this only when you want
+ * to specifically get only the mapping available in the datastore.
+ *
+ * @param aFileExtension the file extension
+ *
+ * @returns the MIME type, if any; otherwise returns an empty string ("").
+ */
+ ACString getTypeFromExtension(in ACString aFileExtension);
+
+ /**
+ * Whether or not there is a handler known to the OS for the
+ * specified protocol type.
+ *
+ * @param aProtocolScheme scheme to check for support
+ *
+ * @returns whether or not a handler exists
+ */
+ boolean existsForProtocolOS(in ACString aProtocolScheme);
+
+ /**
+ * Whether or not there is a handler in the datastore or OS for
+ * the specified protocol type. If there is no handler in the datastore,
+ * falls back to a check for an OS handler.
+ *
+ * @param aProtocolScheme scheme to check for support
+ *
+ * @returns whether or not a handler exists
+ */
+ boolean existsForProtocol(in ACString aProtocolScheme);
+
+ /*
+ * This method only exists for nsOSHelperAppServiceChild using
+ * the ContentHandlerService implementation.
+ *
+ * Returns an nsIMIMEInfo for the provided MIME type and extension
+ * obtained from an OS lookup. If no handler is found for the type and
+ * extension, returns a generic nsIMIMEInfo object. The MIME type and
+ * extension can be the empty string. When the type and extension don't
+ * map to the same handler, the semantics/resolution are platform
+ * specific. See the platform implementations for details.
+ *
+ * @param aType The MIME type to get handler information for.
+ * @param aFileExtension The filename extension to use either alone
+ * or with the MIME type to get handler information
+ * for. UTF-8 encoded.
+ * @param [out] aFound Out param indicating whether a MIMEInfo could
+ * be found for the provided type and/or extension.
+ * Set to false when neither extension nor the MIME
+ * type are mapped to a handler.
+ * @return A nsIMIMEInfo object. This function must return
+ * a MIMEInfo object if it can allocate one. The
+ * only justifiable reason for not returning one is
+ * an out-of-memory error.
+ */
+ [noscript] nsIMIMEInfo getMIMEInfoFromOS(in ACString aType,
+ in ACString aFileExtension,
+ out boolean aFound);
+
+ /*
+ * Get a description for the application responsible for handling
+ * the provided protocol.
+ */
+ AString getApplicationDescription(in ACString aProtocolScheme);
+};
diff --git a/uriloader/exthandler/nsIHelperAppLauncherDialog.idl b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl
new file mode 100644
index 0000000000..9a7f76b7da
--- /dev/null
+++ b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIHelperAppLauncher;
+interface nsIFile;
+interface nsIInterfaceRequestor;
+
+/**
+ * This interface is used to display a confirmation dialog before
+ * launching a "helper app" to handle content not handled by
+ * Mozilla.
+ *
+ * Usage: Clients (of which there is one: the nsIExternalHelperAppService
+ * implementation in mozilla/uriloader/exthandler) create an instance of
+ * this interface (using the contract ID) and then call the show() method.
+ *
+ * The dialog is shown non-modally. The implementation of the dialog
+ * will access methods of the nsIHelperAppLauncher passed in to show()
+ * in order to cause a "save to disk" or "open using" action.
+ */
+[scriptable, uuid(bfc739f3-8d75-4034-a6f8-1039a5996bad)]
+interface nsIHelperAppLauncherDialog : nsISupports {
+ /**
+ * This request is passed to the helper app dialog because Gecko can not
+ * handle content of this type.
+ */
+ const unsigned long REASON_CANTHANDLE = 0;
+
+ /**
+ * The server requested external handling.
+ */
+ const unsigned long REASON_SERVERREQUEST = 1;
+
+ /**
+ * Gecko detected that the type sent by the server (e.g. text/plain) does
+ * not match the actual type.
+ */
+ const unsigned long REASON_TYPESNIFFED = 2;
+
+ /**
+ * Show confirmation dialog for launching application (or "save to
+ * disk") for content specified by aLauncher.
+ *
+ * @param aLauncher
+ * A nsIHelperAppLauncher to be invoked when a file is selected.
+ * @param aWindowContext
+ * Window associated with action.
+ * @param aReason
+ * One of the constants from above. It indicates why the dialog is
+ * shown. Implementors should treat unknown reasons like
+ * REASON_CANTHANDLE.
+ */
+ void show(in nsIHelperAppLauncher aLauncher,
+ in nsIInterfaceRequestor aWindowContext,
+ in unsigned long aReason);
+
+ /**
+ * Async invoke a save-to-file dialog instead of the full fledged helper app
+ * dialog. When the file is chosen (or the dialog is closed), the callback
+ * in aLauncher (aLauncher.saveDestinationAvailable) is called with the
+ * selected file.
+ *
+ * @param aLauncher
+ * A nsIHelperAppLauncher to be invoked when a file is selected.
+ * @param aWindowContext
+ * Window associated with action.
+ * @param aDefaultFileName
+ * Default file name to provide (can be null)
+ * @param aSuggestedFileExtension
+ * Sugested file extension
+ * @param aForcePrompt
+ * Set to true to force prompting the user for thet file
+ * name/location, otherwise perferences may control if the user is
+ * prompted.
+ */
+ void promptForSaveToFileAsync(in nsIHelperAppLauncher aLauncher,
+ in nsIInterfaceRequestor aWindowContext,
+ in wstring aDefaultFileName,
+ in wstring aSuggestedFileExtension,
+ in boolean aForcePrompt);
+};
+
+
+%{C++
+#define NS_HELPERAPPLAUNCHERDLG_CONTRACTID "@mozilla.org/helperapplauncherdialog;1"
+%}
diff --git a/uriloader/exthandler/nsISharingHandlerApp.idl b/uriloader/exthandler/nsISharingHandlerApp.idl
new file mode 100644
index 0000000000..5f497b6531
--- /dev/null
+++ b/uriloader/exthandler/nsISharingHandlerApp.idl
@@ -0,0 +1,11 @@
+/* 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 "nsIMIMEInfo.idl"
+
+[scriptable, uuid(7111f769-53ec-41fd-b314-613661d5b6ba)]
+interface nsISharingHandlerApp : nsIHandlerApp
+{
+ void share(in AString data, [optional] in AString title);
+};
diff --git a/uriloader/exthandler/nsLocalHandlerApp.cpp b/uriloader/exthandler/nsLocalHandlerApp.cpp
new file mode 100644
index 0000000000..6212d2aeb4
--- /dev/null
+++ b/uriloader/exthandler/nsLocalHandlerApp.cpp
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 "nsLocalHandlerApp.h"
+#include "nsIURI.h"
+#include "nsIProcess.h"
+#include "nsComponentManagerUtils.h"
+
+// XXX why does nsMIMEInfoImpl have a threadsafe nsISupports? do we need one
+// here too?
+NS_IMPL_ISUPPORTS(nsLocalHandlerApp, nsILocalHandlerApp, nsIHandlerApp)
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIHandlerApp
+
+NS_IMETHODIMP nsLocalHandlerApp::GetName(nsAString& aName) {
+ if (mName.IsEmpty() && mExecutable) {
+ // Don't want to cache this, just in case someone resets the app
+ // without changing the description....
+ mExecutable->GetLeafName(aName);
+ } else {
+ aName.Assign(mName);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsLocalHandlerApp::SetName(const nsAString& aName) {
+ mName.Assign(aName);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::SetDetailedDescription(const nsAString& aDescription) {
+ mDetailedDescription.Assign(aDescription);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::GetDetailedDescription(nsAString& aDescription) {
+ aDescription.Assign(mDetailedDescription);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(aHandlerApp);
+
+ *_retval = false;
+
+ // If the handler app isn't a local handler app, then it's not the same app.
+ nsCOMPtr<nsILocalHandlerApp> localHandlerApp = do_QueryInterface(aHandlerApp);
+ if (!localHandlerApp) return NS_OK;
+
+ // If either handler app doesn't have an executable, then they aren't
+ // the same app.
+ nsCOMPtr<nsIFile> executable;
+ nsresult rv = localHandlerApp->GetExecutable(getter_AddRefs(executable));
+ if (NS_FAILED(rv)) return rv;
+
+ // Equality for two empty nsIHandlerApp
+ if (!executable && !mExecutable) {
+ *_retval = true;
+ return NS_OK;
+ }
+
+ // At least one is set so they are not equal
+ if (!mExecutable || !executable) return NS_OK;
+
+ // Check the command line parameter list lengths
+ uint32_t len;
+ localHandlerApp->GetParameterCount(&len);
+ if (mParameters.Length() != len) return NS_OK;
+
+ // Check the command line params lists
+ for (uint32_t idx = 0; idx < mParameters.Length(); idx++) {
+ nsAutoString param;
+ if (NS_FAILED(localHandlerApp->GetParameter(idx, param)) ||
+ !param.Equals(mParameters[idx]))
+ return NS_OK;
+ }
+
+ return executable->Equals(mExecutable, _retval);
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) {
+ // pass the entire URI to the handler.
+ nsAutoCString spec;
+ aURI->GetAsciiSpec(spec);
+ return LaunchWithIProcess(spec);
+}
+
+nsresult nsLocalHandlerApp::LaunchWithIProcess(const nsCString& aArg) {
+ nsresult rv;
+ nsCOMPtr<nsIProcess> process = do_CreateInstance(NS_PROCESS_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ if (NS_FAILED(rv = process->Init(mExecutable))) return rv;
+
+ const char* string = aArg.get();
+
+ return process->Run(false, &string, 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsILocalHandlerApp
+
+NS_IMETHODIMP
+nsLocalHandlerApp::GetExecutable(nsIFile** aExecutable) {
+ NS_IF_ADDREF(*aExecutable = mExecutable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::SetExecutable(nsIFile* aExecutable) {
+ mExecutable = aExecutable;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::GetParameterCount(uint32_t* aParameterCount) {
+ *aParameterCount = mParameters.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::ClearParameters() {
+ mParameters.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::AppendParameter(const nsAString& aParam) {
+ mParameters.AppendElement(aParam);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::GetParameter(uint32_t parameterIndex, nsAString& _retval) {
+ if (mParameters.Length() <= parameterIndex) return NS_ERROR_INVALID_ARG;
+
+ _retval.Assign(mParameters[parameterIndex]);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsLocalHandlerApp::ParameterExists(const nsAString& aParam, bool* _retval) {
+ *_retval = mParameters.Contains(aParam);
+ return NS_OK;
+}
diff --git a/uriloader/exthandler/nsLocalHandlerApp.h b/uriloader/exthandler/nsLocalHandlerApp.h
new file mode 100644
index 0000000000..3ea8e3e4fc
--- /dev/null
+++ b/uriloader/exthandler/nsLocalHandlerApp.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 __nsLocalHandlerAppImpl_h__
+#define __nsLocalHandlerAppImpl_h__
+
+#include "nsString.h"
+#include "nsIMIMEInfo.h"
+#include "nsIFile.h"
+#include "nsTArray.h"
+
+class nsLocalHandlerApp : public nsILocalHandlerApp {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHANDLERAPP
+ NS_DECL_NSILOCALHANDLERAPP
+
+ nsLocalHandlerApp() {}
+
+ nsLocalHandlerApp(const char16_t* aName, nsIFile* aExecutable)
+ : mName(aName), mExecutable(aExecutable) {}
+
+ nsLocalHandlerApp(const nsAString& aName, nsIFile* aExecutable)
+ : mName(aName), mExecutable(aExecutable) {}
+
+ protected:
+ virtual ~nsLocalHandlerApp() {}
+
+ nsString mName;
+ nsString mDetailedDescription;
+ nsTArray<nsString> mParameters;
+ nsCOMPtr<nsIFile> mExecutable;
+
+ /**
+ * Launches this application with a single argument (typically either
+ * a file path or a URI spec). This is meant as a helper method for
+ * implementations of (e.g.) LaunchWithURI.
+ *
+ * @param aApp The application to launch (may not be null)
+ * @param aArg The argument to pass on the command line
+ */
+ nsresult LaunchWithIProcess(const nsCString& aArg);
+};
+
+// any platforms that need a platform-specific class instead of just
+// using nsLocalHandlerApp need to add an include and a typedef here.
+#ifdef XP_MACOSX
+# ifndef NSLOCALHANDLERAPPMAC_H_
+# include "mac/nsLocalHandlerAppMac.h"
+typedef nsLocalHandlerAppMac PlatformLocalHandlerApp_t;
+# endif
+#else
+typedef nsLocalHandlerApp PlatformLocalHandlerApp_t;
+#endif
+
+#endif // __nsLocalHandlerAppImpl_h__
diff --git a/uriloader/exthandler/nsMIMEInfoChild.h b/uriloader/exthandler/nsMIMEInfoChild.h
new file mode 100644
index 0000000000..707f19aa67
--- /dev/null
+++ b/uriloader/exthandler/nsMIMEInfoChild.h
@@ -0,0 +1,54 @@
+/* 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 nsMIMEInfoChild_h
+#define nsMIMEInfoChild_h
+
+#include "nsMIMEInfoImpl.h"
+
+/*
+ * A platform-generic nsMIMEInfo implementation to be used in child process
+ * generic code that needs a MIMEInfo with limited functionality.
+ */
+class nsChildProcessMIMEInfo : public nsMIMEInfoImpl {
+ public:
+ explicit nsChildProcessMIMEInfo(const char* aMIMEType = "")
+ : nsMIMEInfoImpl(aMIMEType) {}
+
+ explicit nsChildProcessMIMEInfo(const nsACString& aMIMEType)
+ : nsMIMEInfoImpl(aMIMEType) {}
+
+ nsChildProcessMIMEInfo(const nsACString& aType, HandlerClass aClass)
+ : nsMIMEInfoImpl(aType, aClass) {}
+
+ NS_IMETHOD LaunchWithFile(nsIFile* aFile) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ };
+
+ NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ };
+
+ protected:
+ [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ };
+
+#ifdef DEBUG
+ [[nodiscard]] virtual nsresult LaunchDefaultWithFile(
+ nsIFile* aFile) override {
+ return NS_ERROR_UNEXPECTED;
+ }
+#endif
+ [[nodiscard]] static nsresult OpenApplicationWithURI(nsIFile* aApplication,
+ const nsCString& aURI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ };
+};
+
+#endif
diff --git a/uriloader/exthandler/nsMIMEInfoImpl.cpp b/uriloader/exthandler/nsMIMEInfoImpl.cpp
new file mode 100644
index 0000000000..3616ab7fcf
--- /dev/null
+++ b/uriloader/exthandler/nsMIMEInfoImpl.cpp
@@ -0,0 +1,526 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 "nsMIMEInfoImpl.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsStringEnumerator.h"
+#include "nsIFile.h"
+#include "nsIFileURL.h"
+#include "nsEscape.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCURILoader.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIExternalProtocolService.h"
+#include "nsIObserverService.h"
+#include "nsISupportsPrimitives.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "xpcpublic.h"
+
+static bool sInitializedOurData = false;
+mozilla::StaticRefPtr<nsIFile> sOurAppFile;
+
+/* static */
+already_AddRefed<nsIFile> nsMIMEInfoBase::GetCanonicalExecutable(
+ nsIFile* aFile) {
+ nsCOMPtr<nsIFile> binary = aFile;
+#ifdef XP_MACOSX
+ nsAutoString path;
+ if (binary) {
+ binary->GetPath(path);
+ }
+ if (!StringEndsWith(path, u".app"_ns) && path.RFind(u".app/"_ns) == -1) {
+ // This shouldn't ever happen with Firefox's own binary, tracked in
+ // sOurAppFile, but might happen when called with other files.
+ return binary.forget();
+ }
+ nsAutoString leafName;
+ if (binary) {
+ binary->GetLeafName(leafName);
+ }
+ while (binary && !StringEndsWith(leafName, u".app"_ns)) {
+ nsCOMPtr<nsIFile> parent;
+ binary->GetParent(getter_AddRefs(parent));
+ binary = std::move(parent);
+ if (binary) {
+ binary->GetLeafName(leafName);
+ }
+ }
+#endif
+ return binary.forget();
+}
+
+static void EnsureAppDetailsAvailable() {
+ if (sInitializedOurData) {
+ return;
+ }
+ sInitializedOurData = true;
+ nsCOMPtr<nsIFile> binary;
+ XRE_GetBinaryPath(getter_AddRefs(binary));
+ sOurAppFile = nsMIMEInfoBase::GetCanonicalExecutable(binary);
+ ClearOnShutdown(&sOurAppFile);
+}
+
+// nsISupports methods
+NS_IMPL_ADDREF(nsMIMEInfoBase)
+NS_IMPL_RELEASE(nsMIMEInfoBase)
+
+NS_INTERFACE_MAP_BEGIN(nsMIMEInfoBase)
+ NS_INTERFACE_MAP_ENTRY(nsIHandlerInfo)
+ // This is only an nsIMIMEInfo if it's a MIME handler.
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIMIMEInfo, mClass == eMIMEInfo)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIHandlerInfo)
+NS_INTERFACE_MAP_END
+
+// nsMIMEInfoImpl methods
+
+// Constructors for a MIME handler.
+nsMIMEInfoBase::nsMIMEInfoBase(const char* aMIMEType)
+ : mSchemeOrType(aMIMEType),
+ mClass(eMIMEInfo),
+ mAlwaysAskBeforeHandling(
+ mozilla::StaticPrefs::
+ browser_download_always_ask_before_handling_new_types()) {}
+
+nsMIMEInfoBase::nsMIMEInfoBase(const nsACString& aMIMEType)
+ : mSchemeOrType(aMIMEType),
+ mClass(eMIMEInfo),
+ mAlwaysAskBeforeHandling(
+ mozilla::StaticPrefs::
+ browser_download_always_ask_before_handling_new_types()) {}
+
+// Constructor for a handler that lets the caller specify whether this is a
+// MIME handler or a protocol handler. In the long run, these will be distinct
+// classes (f.e. nsMIMEInfo and nsProtocolInfo), but for now we reuse this class
+// for both and distinguish between the two kinds of handlers via the aClass
+// argument to this method, which can be either eMIMEInfo or eProtocolInfo.
+nsMIMEInfoBase::nsMIMEInfoBase(const nsACString& aType, HandlerClass aClass)
+ : mSchemeOrType(aType),
+ mClass(aClass),
+ mAlwaysAskBeforeHandling(
+ mozilla::StaticPrefs::
+ browser_download_always_ask_before_handling_new_types() ||
+ aClass != eMIMEInfo) {}
+
+nsMIMEInfoBase::~nsMIMEInfoBase() {}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetFileExtensions(nsIUTF8StringEnumerator** aResult) {
+ return NS_NewUTF8StringEnumerator(aResult, &mExtensions, this);
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::ExtensionExists(const nsACString& aExtension, bool* _retval) {
+ MOZ_ASSERT(!aExtension.IsEmpty(), "no extension");
+ *_retval = mExtensions.Contains(aExtension,
+ nsCaseInsensitiveCStringArrayComparator());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetPrimaryExtension(nsACString& _retval) {
+ if (!mExtensions.Length()) {
+ _retval.Truncate();
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ _retval = mExtensions[0];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::SetPrimaryExtension(const nsACString& aExtension) {
+ if (MOZ_UNLIKELY(aExtension.IsEmpty())) {
+ MOZ_ASSERT(false, "No extension");
+ return NS_ERROR_INVALID_ARG;
+ }
+ int32_t i = mExtensions.IndexOf(aExtension, 0,
+ nsCaseInsensitiveCStringArrayComparator());
+ if (i != -1) {
+ mExtensions.RemoveElementAt(i);
+ }
+ mExtensions.InsertElementAt(0, aExtension);
+ mIsDefaultAppInfoFresh = false;
+ return NS_OK;
+}
+
+void nsMIMEInfoBase::AddUniqueExtension(const nsACString& aExtension) {
+ if (mExtensions.IsEmpty()) {
+ mIsDefaultAppInfoFresh = false;
+ }
+ if (!aExtension.IsEmpty() &&
+ !mExtensions.Contains(aExtension,
+ nsCaseInsensitiveCStringArrayComparator())) {
+ mExtensions.AppendElement(aExtension);
+ }
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::AppendExtension(const nsACString& aExtension) {
+ MOZ_ASSERT(!aExtension.IsEmpty(), "No extension");
+ AddUniqueExtension(aExtension);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetType(nsACString& aType) {
+ if (mSchemeOrType.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;
+
+ aType = mSchemeOrType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetMIMEType(nsACString& aMIMEType) {
+ if (mSchemeOrType.IsEmpty()) return NS_ERROR_NOT_INITIALIZED;
+
+ aMIMEType = mSchemeOrType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetDescription(nsAString& aDescription) {
+ aDescription = mDescription;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::SetDescription(const nsAString& aDescription) {
+ mDescription = aDescription;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) {
+ if (!aMIMEInfo) return NS_ERROR_NULL_POINTER;
+
+ nsAutoCString type;
+ nsresult rv = aMIMEInfo->GetMIMEType(type);
+ if (NS_FAILED(rv)) return rv;
+
+ *_retval = mSchemeOrType.Equals(type);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::SetFileExtensions(const nsACString& aExtensions) {
+ mExtensions.Clear();
+ nsACString::const_iterator start, end;
+ aExtensions.BeginReading(start);
+ aExtensions.EndReading(end);
+ while (start != end) {
+ nsACString::const_iterator cursor = start;
+ mozilla::Unused << FindCharInReadable(',', cursor, end);
+ AddUniqueExtension(Substring(start, cursor));
+ // If a comma was found, skip it for the next search.
+ start = cursor != end ? ++cursor : cursor;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetDefaultDescription(nsAString& aDefaultDescription) {
+ aDefaultDescription = mDefaultAppDescription;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetPreferredApplicationHandler(
+ nsIHandlerApp** aPreferredAppHandler) {
+ *aPreferredAppHandler = mPreferredApplication;
+ NS_IF_ADDREF(*aPreferredAppHandler);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::SetPreferredApplicationHandler(
+ nsIHandlerApp* aPreferredAppHandler) {
+ mPreferredApplication = aPreferredAppHandler;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetPossibleApplicationHandlers(
+ nsIMutableArray** aPossibleAppHandlers) {
+ if (!mPossibleApplications)
+ mPossibleApplications = do_CreateInstance(NS_ARRAY_CONTRACTID);
+
+ if (!mPossibleApplications) return NS_ERROR_OUT_OF_MEMORY;
+
+ *aPossibleAppHandlers = mPossibleApplications;
+ NS_IF_ADDREF(*aPossibleAppHandlers);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetPreferredAction(nsHandlerInfoAction* aPreferredAction) {
+ *aPreferredAction = mPreferredAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::SetPreferredAction(nsHandlerInfoAction aPreferredAction) {
+ mPreferredAction = aPreferredAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetAlwaysAskBeforeHandling(bool* aAlwaysAsk) {
+ *aAlwaysAsk = mAlwaysAskBeforeHandling;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::SetAlwaysAskBeforeHandling(bool aAlwaysAsk) {
+ mAlwaysAskBeforeHandling = aAlwaysAsk;
+ return NS_OK;
+}
+
+/* static */
+nsresult nsMIMEInfoBase::GetLocalFileFromURI(nsIURI* aURI, nsIFile** aFile) {
+ nsresult rv;
+
+ nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI, &rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIFile> file;
+ rv = fileUrl->GetFile(getter_AddRefs(file));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ file.forget(aFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::LaunchWithFile(nsIFile* aFile) {
+ nsresult rv;
+
+ // it doesn't make any sense to call this on protocol handlers
+ NS_ASSERTION(mClass == eMIMEInfo,
+ "nsMIMEInfoBase should have mClass == eMIMEInfo");
+
+ if (AutomationOnlyCheckIfLaunchStubbed(aFile)) {
+ return NS_OK;
+ }
+
+ if (mPreferredAction == useSystemDefault) {
+ return LaunchDefaultWithFile(aFile);
+ }
+
+ if (mPreferredAction == useHelperApp) {
+ if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND;
+
+ // at the moment, we only know how to hand files off to local handlers
+ nsCOMPtr<nsILocalHandlerApp> localHandler =
+ do_QueryInterface(mPreferredApplication, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> executable;
+ rv = localHandler->GetExecutable(getter_AddRefs(executable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return LaunchWithIProcess(executable, aFile->NativePath());
+ }
+
+ return NS_ERROR_INVALID_ARG;
+}
+
+bool nsMIMEInfoBase::AutomationOnlyCheckIfLaunchStubbed(nsIFile* aFile) {
+ // This is pretty gross and hacky, but otherwise we can't automatically
+ // test this, and we keep breaking edgecases around this, so...
+ if (!xpc::IsInAutomation()) {
+ return false;
+ }
+ nsAutoString path;
+ aFile->GetPath(path);
+ nsCOMPtr<nsISupportsPRBool> canOpen =
+ do_CreateInstance("@mozilla.org/supports-PRBool;1");
+ canOpen->SetData(true);
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->NotifyObservers(canOpen, "test-only-opening-downloaded-file",
+ path.get());
+ bool data = true;
+ canOpen->GetData(&data);
+ return !data;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::LaunchWithURI(nsIURI* aURI,
+ mozilla::dom::BrowsingContext* aBrowsingContext) {
+ // This is only being called with protocol handlers
+ NS_ASSERTION(mClass == eProtocolInfo,
+ "nsMIMEInfoBase should be a protocol handler");
+
+ if (mPreferredAction == useSystemDefault) {
+ // First, ensure we're not accidentally going to call ourselves.
+ // That'd lead to an infinite loop (see bug 215554).
+ nsCOMPtr<nsIExternalProtocolService> extProtService =
+ do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID);
+ if (!extProtService) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoCString scheme;
+ aURI->GetScheme(scheme);
+ bool isDefault = false;
+ nsresult rv =
+ extProtService->IsCurrentAppOSDefaultForProtocol(scheme, &isDefault);
+ if (NS_SUCCEEDED(rv) && isDefault) {
+ // Lie. This will trip the handler service into showing a dialog asking
+ // what the user wants.
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ return LoadUriInternal(aURI);
+ }
+
+ if (mPreferredAction == useHelperApp) {
+ if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND;
+
+ EnsureAppDetailsAvailable();
+ nsCOMPtr<nsILocalHandlerApp> localPreferredHandler =
+ do_QueryInterface(mPreferredApplication);
+ if (localPreferredHandler) {
+ nsCOMPtr<nsIFile> executable;
+ localPreferredHandler->GetExecutable(getter_AddRefs(executable));
+ executable = GetCanonicalExecutable(executable);
+ bool isOurExecutable = false;
+ if (!executable ||
+ NS_FAILED(executable->Equals(sOurAppFile, &isOurExecutable)) ||
+ isOurExecutable) {
+ // Lie. This will trip the handler service into showing a dialog asking
+ // what the user wants.
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ }
+ return mPreferredApplication->LaunchWithURI(aURI, aBrowsingContext);
+ }
+
+ return NS_ERROR_INVALID_ARG;
+}
+
+void nsMIMEInfoBase::CopyBasicDataTo(nsMIMEInfoBase* aOther) {
+ aOther->mSchemeOrType = mSchemeOrType;
+ aOther->mDefaultAppDescription = mDefaultAppDescription;
+ aOther->mExtensions = mExtensions.Clone();
+}
+
+/* static */
+already_AddRefed<nsIProcess> nsMIMEInfoBase::InitProcess(nsIFile* aApp,
+ nsresult* aResult) {
+ NS_ASSERTION(aApp, "Unexpected null pointer, fix caller");
+
+ nsCOMPtr<nsIProcess> process =
+ do_CreateInstance(NS_PROCESS_CONTRACTID, aResult);
+ if (NS_FAILED(*aResult)) return nullptr;
+
+ *aResult = process->Init(aApp);
+ if (NS_FAILED(*aResult)) return nullptr;
+
+ return process.forget();
+}
+
+/* static */
+nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp,
+ const nsCString& aArg) {
+ nsresult rv;
+ nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ const char* string = aArg.get();
+
+ return process->Run(false, &string, 1);
+}
+
+/* static */
+nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp,
+ const nsString& aArg) {
+ nsresult rv;
+ nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ const char16_t* string = aArg.get();
+
+ return process->Runw(false, &string, 1);
+}
+
+/* static */
+nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp, const int aArgc,
+ const char16_t** aArgv) {
+ nsresult rv;
+ nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return process->Runw(false, aArgv, aArgc);
+}
+
+// nsMIMEInfoImpl implementation
+NS_IMETHODIMP
+nsMIMEInfoImpl::GetDefaultDescription(nsAString& aDefaultDescription) {
+ if (mDefaultAppDescription.IsEmpty()) {
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (defaultApp) {
+ // Don't want to cache this, just in case someone resets the app
+ // without changing the description....
+ defaultApp->GetLeafName(aDefaultDescription);
+ return NS_OK;
+ }
+ }
+ aDefaultDescription = mDefaultAppDescription;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoImpl::GetHasDefaultHandler(bool* _retval) {
+ *_retval = !mDefaultAppDescription.IsEmpty();
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (defaultApp) {
+ bool exists;
+ *_retval = NS_SUCCEEDED(defaultApp->Exists(&exists)) && exists;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoImpl::IsCurrentAppOSDefault(bool* _retval) {
+ *_retval = false;
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (defaultApp) {
+ // Determine if the default executable is our executable.
+ EnsureAppDetailsAvailable();
+ bool isSame = false;
+ nsresult rv = defaultApp->Equals(sOurAppFile, &isSame);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *_retval = isSame;
+ }
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoImpl::LaunchDefaultWithFile(nsIFile* aFile) {
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (!defaultApp) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ return LaunchWithIProcess(defaultApp, aFile->NativePath());
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::GetPossibleLocalHandlers(nsIArray** _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/uriloader/exthandler/nsMIMEInfoImpl.h b/uriloader/exthandler/nsMIMEInfoImpl.h
new file mode 100644
index 0000000000..9b080ac545
--- /dev/null
+++ b/uriloader/exthandler/nsMIMEInfoImpl.h
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=4 sw=2 sts=2 et: */
+/* 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 __nsmimeinfoimpl_h___
+#define __nsmimeinfoimpl_h___
+
+#include "nsIMIMEInfo.h"
+#include "nsAtom.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsIMutableArray.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "nsIURI.h"
+#include "nsIProcess.h"
+#include "mozilla/dom/BrowsingContext.h"
+
+/**
+ * UTF8 moz-icon URI string for the default handler application's icon, if
+ * available.
+ */
+#define PROPERTY_DEFAULT_APP_ICON_URL "defaultApplicationIconURL"
+/**
+ * UTF8 moz-icon URI string for the user's preferred handler application's
+ * icon, if available.
+ */
+#define PROPERTY_CUSTOM_APP_ICON_URL "customApplicationIconURL"
+
+/**
+ * Basic implementation of nsIMIMEInfo. Incomplete - it is meant to be
+ * subclassed, and GetHasDefaultHandler as well as LaunchDefaultWithFile need to
+ * be implemented.
+ */
+class nsMIMEInfoBase : public nsIMIMEInfo {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // I'd use NS_DECL_NSIMIMEINFO, but I don't want GetHasDefaultHandler
+ NS_IMETHOD GetFileExtensions(nsIUTF8StringEnumerator** _retval) override;
+ NS_IMETHOD SetFileExtensions(const nsACString& aExtensions) override;
+ NS_IMETHOD ExtensionExists(const nsACString& aExtension,
+ bool* _retval) override;
+ NS_IMETHOD AppendExtension(const nsACString& aExtension) override;
+ NS_IMETHOD GetPrimaryExtension(nsACString& aPrimaryExtension) override;
+ NS_IMETHOD SetPrimaryExtension(const nsACString& aPrimaryExtension) override;
+ NS_IMETHOD GetType(nsACString& aType) override;
+ NS_IMETHOD GetMIMEType(nsACString& aMIMEType) override;
+ NS_IMETHOD GetDescription(nsAString& aDescription) override;
+ NS_IMETHOD SetDescription(const nsAString& aDescription) override;
+ NS_IMETHOD Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) override;
+ NS_IMETHOD GetPreferredApplicationHandler(
+ nsIHandlerApp** aPreferredAppHandler) override;
+ NS_IMETHOD SetPreferredApplicationHandler(
+ nsIHandlerApp* aPreferredAppHandler) override;
+ NS_IMETHOD GetPossibleApplicationHandlers(
+ nsIMutableArray** aPossibleAppHandlers) override;
+ NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override;
+ NS_IMETHOD LaunchWithFile(nsIFile* aFile) override;
+ NS_IMETHOD LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override;
+ NS_IMETHOD GetPreferredAction(nsHandlerInfoAction* aPreferredAction) override;
+ NS_IMETHOD SetPreferredAction(nsHandlerInfoAction aPreferredAction) override;
+ NS_IMETHOD GetAlwaysAskBeforeHandling(
+ bool* aAlwaysAskBeforeHandling) override;
+ NS_IMETHOD SetAlwaysAskBeforeHandling(bool aAlwaysAskBeforeHandling) override;
+ NS_IMETHOD GetPossibleLocalHandlers(nsIArray** _retval) override;
+
+ enum HandlerClass { eMIMEInfo, eProtocolInfo };
+
+ // nsMIMEInfoBase methods
+ explicit nsMIMEInfoBase(const char* aMIMEType = "");
+ explicit nsMIMEInfoBase(const nsACString& aMIMEType);
+ nsMIMEInfoBase(const nsACString& aType, HandlerClass aClass);
+
+ void SetMIMEType(const nsACString& aMIMEType) { mSchemeOrType = aMIMEType; }
+
+ void SetDefaultDescription(const nsString& aDesc) {
+ mDefaultAppDescription = aDesc;
+ }
+
+ /**
+ * Copies basic data of this MIME Info Implementation to the given other
+ * MIME Info. The data consists of the MIME Type, the (default) description,
+ * the MacOS type and creator, and the extension list (this object's
+ * extension list will replace aOther's list, not append to it). This
+ * function also ensures that aOther's primary extension will be the same as
+ * the one of this object.
+ */
+ void CopyBasicDataTo(nsMIMEInfoBase* aOther);
+
+ /**
+ * Return whether this MIMEInfo has any extensions
+ */
+ bool HasExtensions() const { return mExtensions.Length() != 0; }
+
+ static already_AddRefed<nsIFile> GetCanonicalExecutable(nsIFile* aFile);
+
+ protected:
+ virtual ~nsMIMEInfoBase(); // must be virtual, as the the base class's
+ // Release should call the subclass's destructor
+
+ /**
+ * Launch the default application for the given file.
+ * For even more control over the launching, override launchWithFile.
+ * Also see the comment about nsIMIMEInfo in general, above.
+ *
+ * @param aFile The file that should be opened
+ */
+ virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) = 0;
+
+ /**
+ * Loads the URI with the OS default app.
+ *
+ * @param aURI The URI to pass off to the OS.
+ */
+ virtual nsresult LoadUriInternal(nsIURI* aURI) = 0;
+
+ bool AutomationOnlyCheckIfLaunchStubbed(nsIFile* aFile);
+
+ static already_AddRefed<nsIProcess> InitProcess(nsIFile* aApp,
+ nsresult* aResult);
+
+ /**
+ * This method can be used to launch the file or URI with a single
+ * argument (typically either a file path or a URI spec). This is
+ * meant as a helper method for implementations of
+ * LaunchWithURI/LaunchDefaultWithFile.
+ *
+ * @param aApp The application to launch (may not be null)
+ * @param aArg The argument to pass on the command line
+ */
+ static nsresult LaunchWithIProcess(nsIFile* aApp, const nsCString& aArg);
+ static nsresult LaunchWithIProcess(nsIFile* aApp, const nsString& aArg);
+ static nsresult LaunchWithIProcess(nsIFile* aApp, const int aArgc,
+ const char16_t** aArgv);
+
+ /**
+ * Given a file: nsIURI, return the associated nsIFile
+ *
+ * @param aURI the file: URI in question
+ * @param aFile the associated nsIFile (out param)
+ */
+ static nsresult GetLocalFileFromURI(nsIURI* aURI, nsIFile** aFile);
+
+ /**
+ * Internal helper to avoid adding duplicates.
+ */
+ void AddUniqueExtension(const nsACString& aExtension);
+
+ // member variables
+ nsTArray<nsCString>
+ mExtensions; ///< array of file extensions associated w/ this MIME obj
+ nsString mDescription; ///< human readable description
+ nsCString mSchemeOrType;
+ HandlerClass mClass;
+ nsCOMPtr<nsIHandlerApp> mPreferredApplication;
+ nsCOMPtr<nsIMutableArray> mPossibleApplications;
+ nsHandlerInfoAction mPreferredAction =
+ nsIMIMEInfo::saveToDisk; ///< preferred action to associate with this
+ ///< type
+ nsString mPreferredAppDescription;
+ nsString mDefaultAppDescription;
+ bool mAlwaysAskBeforeHandling;
+ bool mIsDefaultAppInfoFresh = false;
+};
+
+/**
+ * This is a complete implementation of nsIMIMEInfo, and contains all necessary
+ * methods. However, depending on your platform you may want to use a different
+ * way of launching applications. This class stores the default application in a
+ * member variable and provides a function for setting it. For local
+ * applications, launching is done using nsIProcess, native path of the file to
+ * open as first argument.
+ */
+class nsMIMEInfoImpl : public nsMIMEInfoBase {
+ public:
+ explicit nsMIMEInfoImpl(const char* aMIMEType = "")
+ : nsMIMEInfoBase(aMIMEType) {}
+ explicit nsMIMEInfoImpl(const nsACString& aMIMEType)
+ : nsMIMEInfoBase(aMIMEType) {}
+ nsMIMEInfoImpl(const nsACString& aType, HandlerClass aClass)
+ : nsMIMEInfoBase(aType, aClass) {}
+ virtual ~nsMIMEInfoImpl() {}
+
+ // nsIMIMEInfo methods
+ NS_IMETHOD GetHasDefaultHandler(bool* _retval) override;
+ NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override;
+ NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override;
+
+ // additional methods
+ /**
+ * Sets the default application. Supposed to be only called by the OS Helper
+ * App Services.
+ */
+ void SetDefaultApplication(nsIFile* aApp) {
+ mDefaultApplication = aApp;
+ mIsDefaultAppInfoFresh = true;
+ }
+
+ protected:
+ // nsMIMEInfoBase methods
+ /**
+ * The base class implementation is to use LaunchWithIProcess in combination
+ * with mDefaultApplication. Subclasses can override that behaviour.
+ */
+ virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override;
+
+ /**
+ * Loads the URI with the OS default app. This should be overridden by each
+ * OS's implementation.
+ */
+ virtual nsresult LoadUriInternal(nsIURI* aURI) override = 0;
+
+ // Accessor for default application for subclasses.
+ nsIFile* GetDefaultApplication() { return mDefaultApplication; }
+
+ private:
+ nsCOMPtr<nsIFile>
+ mDefaultApplication; ///< default application associated with this type.
+};
+
+#endif //__nsmimeinfoimpl_h___
diff --git a/uriloader/exthandler/nsOSHelperAppServiceChild.cpp b/uriloader/exthandler/nsOSHelperAppServiceChild.cpp
new file mode 100644
index 0000000000..8ba3a2edc9
--- /dev/null
+++ b/uriloader/exthandler/nsOSHelperAppServiceChild.cpp
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 3; 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 <sys/types.h>
+#include <sys/stat.h>
+#include "mozilla/Logging.h"
+#include "mozilla/net/NeckoCommon.h"
+#include "nsOSHelperAppServiceChild.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsIFile.h"
+#include "nsIHandlerService.h"
+#include "nsMimeTypes.h"
+#include "nsMIMEInfoImpl.h"
+#include "nsCExternalHandlerService.h"
+#include "nsCRT.h"
+#include "nsEmbedCID.h"
+
+#undef LOG
+#define LOG(...) \
+ MOZ_LOG(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info, \
+ (__VA_ARGS__))
+#undef LOG_ERR
+#define LOG_ERR(...) \
+ MOZ_LOG(nsExternalHelperAppService::sLog, mozilla::LogLevel::Error, \
+ (__VA_ARGS__))
+#undef LOG_ENABLED
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(nsExternalHelperAppService::sLog, mozilla::LogLevel::Info)
+
+nsresult nsOSHelperAppServiceChild::ExternalProtocolHandlerExists(
+ const char* aProtocolScheme, bool* aHandlerExists) {
+ nsresult rv;
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG_ERR("nsOSHelperAppServiceChild error: no handler service");
+ return rv;
+ }
+
+ nsAutoCString scheme(aProtocolScheme);
+ *aHandlerExists = false;
+ rv = handlerSvc->ExistsForProtocol(scheme, aHandlerExists);
+ LOG("nsOSHelperAppServiceChild::ExternalProtocolHandlerExists(): "
+ "protocol: %s, result: %" PRId32,
+ aProtocolScheme, static_cast<uint32_t>(rv));
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+ return rv;
+}
+
+nsresult nsOSHelperAppServiceChild::OSProtocolHandlerExists(const char* aScheme,
+ bool* aExists) {
+ // Use ExternalProtocolHandlerExists() which includes the
+ // OS-level check and remotes the call to the parent process.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppServiceChild::GetApplicationDescription(const nsACString& aScheme,
+ nsAString& aRetVal) {
+ nsresult rv;
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG_ERR("nsOSHelperAppServiceChild error: no handler service");
+ return rv;
+ }
+
+ rv = handlerSvc->GetApplicationDescription(aScheme, aRetVal);
+ LOG("nsOSHelperAppServiceChild::GetApplicationDescription(): "
+ "scheme: %s, result: %" PRId32 ", description: %s",
+ PromiseFlatCString(aScheme).get(), static_cast<uint32_t>(rv),
+ NS_ConvertUTF16toUTF8(aRetVal).get());
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+ return rv;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppServiceChild::GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt,
+ bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) {
+ nsresult rv;
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG_ERR("nsOSHelperAppServiceChild error: no handler service");
+ return rv;
+ }
+
+ rv = handlerSvc->GetMIMEInfoFromOS(aMIMEType, aFileExt, aFound, aMIMEInfo);
+ LOG("nsOSHelperAppServiceChild::GetMIMEInfoFromOS(): "
+ "MIME type: %s, extension: %s, result: %" PRId32,
+ PromiseFlatCString(aMIMEType).get(), PromiseFlatCString(aFileExt).get(),
+ static_cast<uint32_t>(rv));
+ mozilla::Unused << NS_WARN_IF(NS_FAILED(rv));
+ return rv;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppServiceChild::GetProtocolHandlerInfoFromOS(
+ const nsACString& aScheme, bool* aFound, nsIHandlerInfo** aRetVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppServiceChild::IsCurrentAppOSDefaultForProtocol(
+ const nsACString& aScheme, bool* aRetVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsOSHelperAppServiceChild::GetFileTokenForPath(
+ const char16_t* platformAppPath, nsIFile** aFile) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/uriloader/exthandler/nsOSHelperAppServiceChild.h b/uriloader/exthandler/nsOSHelperAppServiceChild.h
new file mode 100644
index 0000000000..05c2195d10
--- /dev/null
+++ b/uriloader/exthandler/nsOSHelperAppServiceChild.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 3; 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 nsOSHelperAppServiceChild_h__
+#define nsOSHelperAppServiceChild_h__
+
+#include "nsExternalHelperAppService.h"
+
+class nsIMIMEInfo;
+
+/*
+ * Provides a generic implementation of the nsExternalHelperAppService
+ * platform-specific methods by remoting calls to the parent process.
+ * Only provides implementations for the methods needed in unprivileged
+ * child processes.
+ */
+class nsOSHelperAppServiceChild : public nsExternalHelperAppService {
+ public:
+ nsOSHelperAppServiceChild(){};
+ virtual ~nsOSHelperAppServiceChild() = default;
+
+ NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) override;
+
+ nsresult GetFileTokenForPath(const char16_t* platformAppPath,
+ nsIFile** aFile) override;
+
+ NS_IMETHOD ExternalProtocolHandlerExists(const char* aProtocolScheme,
+ bool* aHandlerExists) override;
+
+ NS_IMETHOD OSProtocolHandlerExists(const char* aScheme,
+ bool* aExists) override;
+
+ NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
+ nsAString& aRetVal) override;
+ NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval) override;
+
+ NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) override;
+};
+
+#endif // nsOSHelperAppServiceChild_h__
diff --git a/uriloader/exthandler/tests/HandlerServiceTestUtils.sys.mjs b/uriloader/exthandler/tests/HandlerServiceTestUtils.sys.mjs
new file mode 100644
index 0000000000..0e52cc2568
--- /dev/null
+++ b/uriloader/exthandler/tests/HandlerServiceTestUtils.sys.mjs
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Shared functions for tests related to invoking external handler applications.
+ */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gExternalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gHandlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+
+export var HandlerServiceTestUtils = {
+ /**
+ * Retrieves the names of all the MIME types and protocols configured in the
+ * handler service instance currently under testing.
+ *
+ * @return Array of strings like "example/type" or "example-scheme", sorted
+ * alphabetically regardless of category.
+ */
+ getAllHandlerInfoTypes() {
+ return Array.from(
+ lazy.gHandlerService.enumerate(),
+ info => info.type
+ ).sort();
+ },
+
+ /**
+ * Retrieves all the configured handlers for MIME types and protocols.
+ *
+ * @note The nsIHandlerInfo instances returned by the "enumerate" method
+ * cannot be used for testing because they incorporate information from
+ * the operating system and also from the default nsIHandlerService
+ * instance, independently from what instance is under testing.
+ *
+ * @return Array of nsIHandlerInfo instances sorted by their "type" property.
+ */
+ getAllHandlerInfos() {
+ return this.getAllHandlerInfoTypes().map(type => this.getHandlerInfo(type));
+ },
+
+ /**
+ * Retrieves an nsIHandlerInfo for the given MIME type or protocol, which
+ * incorporates information from the operating system and also from the
+ * handler service instance currently under testing.
+ *
+ * @note If the handler service instance currently under testing is not the
+ * default one and the requested type is a MIME type, the returned
+ * nsIHandlerInfo will include information from the default
+ * nsIHandlerService instance. This cannot be avoided easily because the
+ * getMIMEInfoFromOS method is not exposed to JavaScript.
+ *
+ * @param type
+ * MIME type or scheme name of the nsIHandlerInfo to retrieve.
+ *
+ * @return The populated nsIHandlerInfo instance.
+ */
+ getHandlerInfo(type) {
+ if (type.includes("/")) {
+ // We have to use the getFromTypeAndExtension method because we don't have
+ // access to getMIMEInfoFromOS. This means that we have to reset the data
+ // that may have been imported from the default nsIHandlerService instance
+ // and is not overwritten by fillHandlerInfo later.
+ let handlerInfo = lazy.gMIMEService.getFromTypeAndExtension(type, null);
+ if (AppConstants.platform == "android") {
+ // On Android, the first handler application is always the internal one.
+ while (handlerInfo.possibleApplicationHandlers.length > 1) {
+ handlerInfo.possibleApplicationHandlers.removeElementAt(1);
+ }
+ } else {
+ handlerInfo.possibleApplicationHandlers.clear();
+ }
+ handlerInfo.setFileExtensions("");
+ // Populate the object from the handler service instance under testing.
+ if (lazy.gHandlerService.exists(handlerInfo)) {
+ lazy.gHandlerService.fillHandlerInfo(handlerInfo, "");
+ }
+ return handlerInfo;
+ }
+
+ // Populate the protocol information from the handler service instance under
+ // testing, like the nsIExternalProtocolService::GetProtocolHandlerInfo
+ // method does on the default nsIHandlerService instance.
+ let osDefaultHandlerFound = {};
+ let handlerInfo =
+ lazy.gExternalProtocolService.getProtocolHandlerInfoFromOS(
+ type,
+ osDefaultHandlerFound
+ );
+ if (lazy.gHandlerService.exists(handlerInfo)) {
+ lazy.gHandlerService.fillHandlerInfo(handlerInfo, "");
+ } else {
+ lazy.gExternalProtocolService.setProtocolHandlerDefaults(
+ handlerInfo,
+ osDefaultHandlerFound.value
+ );
+ }
+ return handlerInfo;
+ },
+
+ /**
+ * Creates an nsIHandlerInfo for the given MIME type or protocol, initialized
+ * to the default values for the current platform.
+ *
+ * @note For this method to work, the specified MIME type or protocol must not
+ * be configured in the default handler service instance or the one
+ * under testing, and must not be registered in the operating system.
+ *
+ * @param type
+ * MIME type or scheme name of the nsIHandlerInfo to create.
+ *
+ * @return The blank nsIHandlerInfo instance.
+ */
+ getBlankHandlerInfo(type) {
+ let handlerInfo = this.getHandlerInfo(type);
+
+ let preferredAction, preferredApplicationHandler;
+ let alwaysAskBeforeHandling = true;
+
+ if (AppConstants.platform == "android") {
+ // On Android, the default preferredAction for MIME types is useHelperApp.
+ // For protocols, we always behave as if an operating system provided
+ // handler existed, and as such we initialize them to useSystemDefault.
+ // This is because the AndroidBridge is not available in xpcshell tests.
+ preferredAction = type.includes("/")
+ ? Ci.nsIHandlerInfo.useHelperApp
+ : Ci.nsIHandlerInfo.useSystemDefault;
+ // On Android, the default handler application is always the internal one.
+ preferredApplicationHandler = {
+ name: "Android chooser",
+ };
+ } else {
+ // On Desktop, the default preferredAction for MIME types is saveToDisk,
+ // while for protocols it is alwaysAsk. Since Bug 1735843, for new MIME
+ // types we default to not asking before handling unless a pref is set.
+ alwaysAskBeforeHandling = Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types",
+ false
+ );
+
+ if (type.includes("/")) {
+ preferredAction = Ci.nsIHandlerInfo.saveToDisk;
+ } else {
+ preferredAction = Ci.nsIHandlerInfo.alwaysAsk;
+ // we'll always ask before handling protocols
+ alwaysAskBeforeHandling = true;
+ }
+ preferredApplicationHandler = null;
+ }
+
+ this.assertHandlerInfoMatches(handlerInfo, {
+ type,
+ preferredAction,
+ alwaysAskBeforeHandling,
+ preferredApplicationHandler,
+ });
+ return handlerInfo;
+ },
+
+ /**
+ * Checks whether an nsIHandlerInfo instance matches the provided object.
+ */
+ assertHandlerInfoMatches(handlerInfo, expected) {
+ let expectedInterface = expected.type.includes("/")
+ ? Ci.nsIMIMEInfo
+ : Ci.nsIHandlerInfo;
+ Assert.ok(handlerInfo instanceof expectedInterface);
+ Assert.equal(handlerInfo.type, expected.type);
+
+ if (!expected.preferredActionOSDependent) {
+ Assert.equal(handlerInfo.preferredAction, expected.preferredAction);
+ Assert.equal(
+ handlerInfo.alwaysAskBeforeHandling,
+ expected.alwaysAskBeforeHandling
+ );
+ }
+
+ if (expectedInterface == Ci.nsIMIMEInfo) {
+ let fileExtensionsEnumerator = handlerInfo.getFileExtensions();
+ for (let expectedFileExtension of expected.fileExtensions || []) {
+ Assert.equal(fileExtensionsEnumerator.getNext(), expectedFileExtension);
+ }
+ Assert.ok(!fileExtensionsEnumerator.hasMore());
+ }
+
+ if (expected.preferredApplicationHandler) {
+ this.assertHandlerAppMatches(
+ handlerInfo.preferredApplicationHandler,
+ expected.preferredApplicationHandler
+ );
+ } else {
+ Assert.equal(handlerInfo.preferredApplicationHandler, null);
+ }
+
+ let handlerAppsArrayEnumerator =
+ handlerInfo.possibleApplicationHandlers.enumerate();
+ if (AppConstants.platform == "android") {
+ // On Android, the first handler application is always the internal one.
+ this.assertHandlerAppMatches(handlerAppsArrayEnumerator.getNext(), {
+ name: "Android chooser",
+ });
+ }
+ for (let expectedHandlerApp of expected.possibleApplicationHandlers || []) {
+ this.assertHandlerAppMatches(
+ handlerAppsArrayEnumerator.getNext(),
+ expectedHandlerApp
+ );
+ }
+ Assert.ok(!handlerAppsArrayEnumerator.hasMoreElements());
+ },
+
+ /**
+ * Checks whether an nsIHandlerApp instance matches the provided object.
+ */
+ assertHandlerAppMatches(handlerApp, expected) {
+ Assert.ok(handlerApp instanceof Ci.nsIHandlerApp);
+ Assert.equal(handlerApp.name, expected.name);
+ if (expected.executable) {
+ Assert.ok(handlerApp instanceof Ci.nsILocalHandlerApp);
+ Assert.ok(expected.executable.equals(handlerApp.executable));
+ } else if (expected.uriTemplate) {
+ Assert.ok(handlerApp instanceof Ci.nsIWebHandlerApp);
+ Assert.equal(handlerApp.uriTemplate, expected.uriTemplate);
+ } else if (expected.service) {
+ Assert.ok(handlerApp instanceof Ci.nsIDBusHandlerApp);
+ Assert.equal(handlerApp.service, expected.service);
+ Assert.equal(handlerApp.method, expected.method);
+ Assert.equal(handlerApp.dBusInterface, expected.dBusInterface);
+ Assert.equal(handlerApp.objectPath, expected.objectPath);
+ }
+ },
+};
diff --git a/uriloader/exthandler/tests/WriteArgument.cpp b/uriloader/exthandler/tests/WriteArgument.cpp
new file mode 100644
index 0000000000..603965150e
--- /dev/null
+++ b/uriloader/exthandler/tests/WriteArgument.cpp
@@ -0,0 +1,20 @@
+#include <stdio.h>
+#include "prenv.h"
+
+int main(int argc, char* argv[]) {
+ if (argc != 2) return 1;
+
+ const char* value = PR_GetEnv("WRITE_ARGUMENT_FILE");
+
+ if (!value) return 2;
+
+ FILE* outfile = fopen(value, "w");
+ if (!outfile) return 3;
+
+ // We only need to write out the first argument (no newline).
+ fputs(argv[argc - 1], outfile);
+
+ fclose(outfile);
+
+ return 0;
+}
diff --git a/uriloader/exthandler/tests/gtest/ExternalHelperAppServiceTest.cpp b/uriloader/exthandler/tests/gtest/ExternalHelperAppServiceTest.cpp
new file mode 100644
index 0000000000..e63922182c
--- /dev/null
+++ b/uriloader/exthandler/tests/gtest/ExternalHelperAppServiceTest.cpp
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsExternalHelperAppService.h"
+#include "gtest/gtest.h"
+#include "nsNetUtil.h"
+#include "mozilla/StaticPrefs_network.h"
+
+using namespace mozilla;
+
+TEST(ExternalHelperAppService, EscapeURI)
+{
+ if (StaticPrefs::network_url_useDefaultURI()) {
+ return;
+ }
+
+ nsCString input("myproto://hello world");
+ nsCString expected("myproto://hello%20world");
+
+ nsCOMPtr<nsIIOService> ios(do_GetIOService());
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = ios->NewURI(input, nullptr, nullptr, getter_AddRefs(uri));
+ EXPECT_EQ(rv, NS_OK);
+
+ nsAutoCString spec;
+ rv = uri->GetSpec(spec);
+ EXPECT_EQ(rv, NS_OK);
+
+ // Constructing the URI does not escape the space character.
+ ASSERT_TRUE(spec.Equals(input));
+
+ // Created an escaped version of the URI.
+ nsCOMPtr<nsIURI> escapedURI;
+ rv = nsExternalHelperAppService::EscapeURI(uri, getter_AddRefs(escapedURI));
+ EXPECT_EQ(rv, NS_OK);
+
+ nsAutoCString escapedSpec;
+ rv = escapedURI->GetSpec(escapedSpec);
+ EXPECT_EQ(rv, NS_OK);
+
+ // Escaped URI should have an escaped spec.
+ ASSERT_TRUE(escapedSpec.Equals(expected));
+}
diff --git a/uriloader/exthandler/tests/gtest/moz.build b/uriloader/exthandler/tests/gtest/moz.build
new file mode 100644
index 0000000000..163afe927d
--- /dev/null
+++ b/uriloader/exthandler/tests/gtest/moz.build
@@ -0,0 +1,11 @@
+# -*- 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/.
+
+UNIFIED_SOURCES += [
+ "ExternalHelperAppServiceTest.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/uriloader/exthandler/tests/mochitest/FTPprotocolHandler.html b/uriloader/exthandler/tests/mochitest/FTPprotocolHandler.html
new file mode 100644
index 0000000000..d0e98abace
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/FTPprotocolHandler.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Protocol handler</title>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+ </head>
+ <body>
+ <script type="text/javascript">
+ navigator.registerProtocolHandler("ftp",
+ "https://example.com/browser/uriloader/exthandler/tests/mochitest/blank.html?uri=%s",
+ "Test Protocol");
+ </script>
+ <a id="link" href="ftp://user:password@domain.com/path">ftp link</a>
+ </body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js
new file mode 100644
index 0000000000..4f66c99dc6
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js
@@ -0,0 +1,104 @@
+/* eslint-env mozilla/chrome-script */
+
+const { ComponentUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ComponentUtils.sys.mjs"
+);
+
+const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+
+let gMIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1";
+const HELPERAPP_DIALOG_CID = Components.ID(
+ Cc[HELPERAPP_DIALOG_CONTRACT].number
+);
+
+let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+tmpDir.append("testsavedir" + Math.floor(Math.random() * 2 ** 32));
+// Create this dir if it doesn't exist (ignores existing dirs)
+try {
+ tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o777, true);
+} catch (ex) {
+ if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ throw ex;
+ }
+}
+Services.prefs.setIntPref("browser.download.folderList", 2);
+Services.prefs.setCharPref("browser.download.dir", tmpDir.path);
+
+const FAKE_CID = Services.uuid.generateUUID();
+function HelperAppLauncherDialog() {}
+HelperAppLauncherDialog.prototype = {
+ show(aLauncher, aWindowContext, aReason) {
+ if (
+ Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types"
+ )
+ ) {
+ let f = tmpDir.clone();
+ f.append(aLauncher.suggestedFileName);
+ aLauncher.saveDestinationAvailable(f);
+ sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName);
+ } else {
+ sendAsyncMessage("wrongAPICall", "show");
+ }
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ },
+ promptForSaveToFileAsync(
+ appLauncher,
+ parent,
+ filename,
+ extension,
+ forceSave
+ ) {
+ if (
+ !Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types"
+ )
+ ) {
+ let f = tmpDir.clone();
+ f.append(filename);
+ appLauncher.saveDestinationAvailable(f);
+ sendAsyncMessage("suggestedFileName", filename);
+ } else {
+ sendAsyncMessage("wrongAPICall", "promptForSaveToFileAsync");
+ }
+ appLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+};
+
+var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(
+ FAKE_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT,
+ ComponentUtils.generateSingletonFactory(HelperAppLauncherDialog)
+);
+
+addMessageListener("unregister", async function () {
+ registrar.registerFactory(
+ HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT,
+ null
+ );
+ let list = await Downloads.getList(Downloads.ALL);
+ let downloads = await list.getAll();
+ for (let dl of downloads) {
+ await dl.refresh();
+ if (dl.target.exists || dl.target.partFileExists) {
+ dump("Finalizing download.\n");
+ await dl.finalize(true).catch(console.error);
+ }
+ }
+ await list.removeFinished();
+ dump("Clearing " + tmpDir.path + "\n");
+ tmpDir.remove(true);
+ sendAsyncMessage("unregistered");
+});
diff --git a/uriloader/exthandler/tests/mochitest/blank.html b/uriloader/exthandler/tests/mochitest/blank.html
new file mode 100644
index 0000000000..5417098d72
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/blank.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title></title>
+</head>
+<body>
+
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/browser.toml b/uriloader/exthandler/tests/mochitest/browser.toml
new file mode 100644
index 0000000000..2eaf6632ac
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser.toml
@@ -0,0 +1,164 @@
+[DEFAULT]
+head = "head.js"
+support-files = ["protocolHandler.html"]
+
+
+["browser_auto_close_window.js"]
+support-files = [
+ "download_page.html",
+ "download.bin",
+ "download.sjs",
+]
+
+["browser_auto_close_window_nodialog.js"]
+support-files = [
+ "download_page.html",
+ "download.bin",
+ "download.sjs",
+]
+
+["browser_bad_download_dir.js"]
+run-if = ["os == 'linux'"]
+support-files = ["download.bin"]
+
+["browser_download_always_ask_preferred_app.js"]
+
+["browser_download_idn_blocklist.js"]
+support-files = ["download.bin"]
+
+["browser_download_open_with_internal_handler.js"]
+support-files = [
+ "file_image_svgxml.svg",
+ "file_image_svgxml.svg^headers^",
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_application_pdf.pdf^headers^",
+ "file_pdf_application_unknown.pdf",
+ "file_pdf_application_unknown.pdf^headers^",
+ "file_pdf_application_octet_stream.pdf",
+ "file_pdf_application_octet_stream.pdf^headers^",
+ "file_pdf_binary_octet_stream.pdf",
+ "file_pdf_binary_octet_stream.pdf^headers^",
+ "file_txt_attachment_test.txt",
+ "file_txt_attachment_test.txt^headers^",
+ "file_xml_attachment_binary_octet_stream.xml",
+ "file_xml_attachment_binary_octet_stream.xml^headers^",
+ "file_xml_attachment_test.xml",
+ "file_xml_attachment_test.xml^headers^",
+ "file_green.webp",
+ "file_green.webp^headers^",
+]
+
+["browser_download_preferred_action.js"]
+support-files = ["mime_type_download.sjs"]
+
+["browser_download_privatebrowsing.js"]
+
+["browser_download_skips_dialog.js"]
+support-files = [
+ "file_green.webp",
+ "file_green.webp^headers^",
+]
+
+["browser_download_spam_permissions.js"]
+support-files = ["test_spammy_page.html"]
+
+["browser_download_urlescape.js"]
+support-files = [
+ "file_with@@funny_name.png",
+ "file_with@@funny_name.png^headers^",
+ "file_with[funny_name.webm",
+ "file_with[funny_name.webm^headers^",
+]
+
+["browser_extension_correction.js"]
+support-files = [
+ "file_as.exe",
+ "file_as.exe^headers^",
+]
+
+["browser_filehandling_loop.js"]
+
+["browser_first_prompt_not_blocked_without_user_interaction.js"]
+support-files = ["file_external_protocol_iframe.html"]
+
+["browser_ftp_protocol_handlers.js"]
+support-files = [
+ "FTPprotocolHandler.html",
+ "blank.html",
+]
+
+["browser_launched_app_save_directory.js"]
+support-files = [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_application_pdf.pdf^headers^",
+ "file_green.webp",
+ "file_green.webp^headers^",
+]
+
+["browser_local_files_no_save_without_asking.js"]
+support-files = ["file_pdf_binary_octet_stream.pdf"]
+
+["browser_local_files_open_doesnt_duplicate.js"]
+support-files = ["file_pdf_binary_octet_stream.pdf"]
+
+["browser_open_internal_choice_persistence.js"]
+skip-if = ["apple_silicon"] # bug 1752482
+support-files = [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_application_pdf.pdf^headers^",
+]
+
+["browser_pdf_save_as.js"]
+
+["browser_protocol_ask_dialog.js"]
+support-files = ["file_nested_protocol_request.html"]
+
+["browser_protocol_ask_dialog_external.js"]
+support-files = ["redirect_helper.sjs"]
+
+["browser_protocol_ask_dialog_permission.js"]
+support-files = [
+ "redirect_helper.sjs",
+ "script_redirect.html",
+]
+
+["browser_protocol_custom_sandbox.js"]
+support-files = ["protocol_custom_sandbox_helper.sjs"]
+
+["browser_protocol_custom_sandbox_csp.js"]
+support-files = ["protocol_custom_sandbox_helper.sjs"]
+
+["browser_protocolhandler_loop.js"]
+
+["browser_remember_download_option.js"]
+
+["browser_save_filenames.js"]
+support-files = ["save_filenames.html"]
+
+["browser_shows_where_to_save_dialog.js"]
+support-files = [
+ "file_green.webp",
+ "file_green.webp^headers^",
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_application_pdf.pdf^headers^",
+ "file_txt_attachment_test.txt",
+ "file_txt_attachment_test.txt^headers^",
+]
+
+["browser_standalone_application_chooser_window_fallback.js"]
+support-files = [
+ "FTPprotocolHandler.html",
+ "blank.html",
+]
+
+["browser_txt_download_save_as.js"]
+support-files = [
+ "file_txt_attachment_test.txt",
+ "file_txt_attachment_test.txt^headers^",
+ "!/toolkit/content/tests/browser/common/mockTransfer.js",
+]
+
+["browser_web_handler_app_pinned_tab.js"]
+support-files = ["mailto.html"]
+
+["browser_web_protocol_handlers.js"]
diff --git a/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
new file mode 100644
index 0000000000..af60b59c4b
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
@@ -0,0 +1,342 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ComponentUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ComponentUtils.sys.mjs"
+);
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const PAGE_URL = ROOT + "download_page.html";
+const SJS_URL = ROOT + "download.sjs";
+
+const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1";
+const HELPERAPP_DIALOG_CID = Components.ID(
+ Cc[HELPERAPP_DIALOG_CONTRACT_ID].number
+);
+const MOCK_HELPERAPP_DIALOG_CID = Components.ID(
+ "{2f372b6f-56c9-46d5-af0d-9f09bb69860c}"
+);
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+let curDialogResolve = null;
+
+function HelperAppLauncherDialog() {}
+
+HelperAppLauncherDialog.prototype = {
+ show(aLauncher, aWindowContext, aReason) {
+ ok(true, "Showing the helper app dialog");
+ curDialogResolve(aWindowContext);
+ executeSoon(() => {
+ aLauncher.cancel(Cr.NS_ERROR_ABORT);
+ });
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+};
+
+function promiseHelperAppDialog() {
+ return new Promise(resolve => {
+ curDialogResolve = resolve;
+ });
+}
+
+let mockHelperAppService;
+
+add_setup(async function () {
+ // Replace the real helper app dialog with our own.
+ mockHelperAppService = ComponentUtils.generateSingletonFactory(
+ HelperAppLauncherDialog
+ );
+ registrar.registerFactory(
+ MOCK_HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ mockHelperAppService
+ );
+
+ // Ensure we always prompt for these downloads.
+ const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+ ].getService(Ci.nsIHandlerService);
+
+ const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const mimeInfo = MIMEService.getFromTypeAndExtension(
+ "application/octet-stream",
+ "bin"
+ );
+ mimeInfo.alwaysAskBeforeHandling = true;
+ HandlerService.store(mimeInfo);
+
+ // On Mac, .bin is application/macbinary
+ let mimeInfoMac;
+ if (AppConstants.platform == "macosx") {
+ mimeInfoMac = MIMEService.getFromTypeAndExtension(
+ "application/macbinary",
+ "bin"
+ );
+ mimeInfoMac.alwaysAskBeforeHandling = true;
+ HandlerService.store(mimeInfoMac);
+ }
+
+ registerCleanupFunction(() => {
+ HandlerService.remove(mimeInfo);
+ if (mimeInfoMac) {
+ HandlerService.remove(mimeInfoMac);
+ }
+ });
+});
+
+add_task(async function simple_navigation() {
+ // Tests that simple navigation gives us the right windowContext (that is,
+ // the window that we're using).
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#regular_load",
+ {},
+ browser
+ );
+ let windowContext = await dialogAppeared;
+
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ }
+ );
+});
+
+add_task(async function accel_navigation() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#regular_load",
+ { accelKey: true },
+ browser
+ );
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+ }
+ );
+});
+
+// Given a browser pointing to download_page.html, clicks on the link that
+// opens with target="_blank" (i.e. a new tab) and ensures that we
+// automatically open and close that tab.
+async function testNewTab(browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("#target_blank", {}, browser);
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+}
+
+add_task(async function target_blank() {
+ // Tests that a link with target=_blank opens a new tab and closes it,
+ // returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ await testNewTab(browser);
+ }
+ );
+});
+
+add_task(async function target_blank_no_opener() {
+ // Tests that a link with target=_blank and no opener opens a new tab
+ // and closes it, returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#target_blank_no_opener",
+ {},
+ browser
+ );
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+ }
+ );
+});
+
+add_task(async function open_in_new_tab_no_opener() {
+ // Tests that a link with target=_blank and no opener opens a new tab
+ // and closes it, returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open_in_new_tab_no_opener",
+ {},
+ browser
+ );
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+ }
+ );
+});
+
+add_task(async function new_window() {
+ // Tests that a link that forces us to open a new window (by specifying a
+ // width and a height in window.open) opens a new window for the load,
+ // realizes that we need to close that window and returns the *original*
+ // window as the window context.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#new_window",
+ {},
+ browser
+ );
+ let win = await windowOpened;
+ // Now allow request to complete:
+ fetch(SJS_URL + "?finish");
+
+ let windowContext = await dialogAppeared;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+
+ // The window should close on its own. If not, this test will time out.
+ await BrowserTestUtils.domWindowClosed(win);
+ ok(win.closed, "window was opened and closed");
+
+ is(
+ await fetch(SJS_URL + "?reset").then(r => r.text()),
+ "OK",
+ "Test reseted"
+ );
+ }
+ );
+});
+
+add_task(async function new_window_no_opener() {
+ // Tests that a link that forces us to open a new window (by specifying a
+ // width and a height in window.open) opens a new window for the load,
+ // realizes that we need to close that window and returns the *original*
+ // window as the window context.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let dialogAppeared = promiseHelperAppDialog();
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#new_window_no_opener",
+ {},
+ browser
+ );
+ let win = await windowOpened;
+ // Now allow request to complete:
+ fetch(SJS_URL + "?finish");
+
+ await dialogAppeared;
+
+ // The window should close on its own. If not, this test will time out.
+ await BrowserTestUtils.domWindowClosed(win);
+ ok(win.closed, "window was opened and closed");
+
+ is(
+ await fetch(SJS_URL + "?reset").then(r => r.text()),
+ "OK",
+ "Test reseted"
+ );
+ }
+ );
+});
+
+add_task(async function nested_window_opens() {
+ // Tests that the window auto-closing feature works if the download is
+ // initiated by a window that, itself, has an opener (see bug 1373109).
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (outerBrowser) {
+ let secondTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ `${PAGE_URL}?newwin`,
+ true
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open_in_new_tab",
+ {},
+ outerBrowser
+ );
+ let secondTab = await secondTabPromise;
+ let nestedBrowser = secondTab.linkedBrowser;
+
+ await SpecialPowers.spawn(nestedBrowser, [], function () {
+ ok(content.opener, "this window has an opener");
+ });
+
+ await testNewTab(nestedBrowser);
+
+ isnot(
+ secondTab.linkedBrowser,
+ null,
+ "the page that triggered the download is still open"
+ );
+ BrowserTestUtils.removeTab(secondTab);
+ }
+ );
+});
+
+add_task(async function cleanup() {
+ // Unregister our factory from XPCOM and restore the original CID.
+ registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID, mockHelperAppService);
+ registrar.registerFactory(
+ HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ null
+ );
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_auto_close_window_nodialog.js b/uriloader/exthandler/tests/mochitest/browser_auto_close_window_nodialog.js
new file mode 100644
index 0000000000..4f1e8ab18e
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window_nodialog.js
@@ -0,0 +1,308 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ComponentUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ComponentUtils.sys.mjs"
+);
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const PAGE_URL = ROOT + "download_page.html";
+const SJS_URL = ROOT + "download.sjs";
+
+const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1";
+const HELPERAPP_DIALOG_CID = Components.ID(
+ Cc[HELPERAPP_DIALOG_CONTRACT_ID].number
+);
+const MOCK_HELPERAPP_DIALOG_CID = Components.ID(
+ "{2f372b6f-56c9-46d5-af0d-9f09bb69860c}"
+);
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+let curSaveResolve = null;
+
+function HelperAppLauncherDialog() {}
+
+HelperAppLauncherDialog.prototype = {
+ show(aLauncher, aWindowContext, aReason) {
+ ok(false, "Shouldn't be showing the helper app dialog");
+ executeSoon(() => {
+ aLauncher.cancel(Cr.NS_ERROR_ABORT);
+ });
+ },
+ promptForSaveToFileAsync(aLauncher, aWindowContext, aReason) {
+ ok(true, "Shouldn't be showing the helper app dialog");
+ curSaveResolve(aWindowContext);
+ executeSoon(() => {
+ aLauncher.cancel(Cr.NS_ERROR_ABORT);
+ });
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+};
+
+function promiseSave() {
+ return new Promise(resolve => {
+ curSaveResolve = resolve;
+ });
+}
+
+let mockHelperAppService;
+
+add_setup(async function () {
+ // Replace the real helper app dialog with our own.
+ mockHelperAppService = ComponentUtils.generateSingletonFactory(
+ HelperAppLauncherDialog
+ );
+ registrar.registerFactory(
+ MOCK_HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ mockHelperAppService
+ );
+
+ // Ensure we always prompt for these downloads.
+ const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+ ].getService(Ci.nsIHandlerService);
+
+ const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const mimeInfo = MIMEService.getFromTypeAndExtension(
+ "application/octet-stream",
+ "bin"
+ );
+ mimeInfo.alwaysAskBeforeHandling = false;
+ mimeInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
+ HandlerService.store(mimeInfo);
+
+ registerCleanupFunction(() => {
+ HandlerService.remove(mimeInfo);
+ });
+});
+
+add_task(async function simple_navigation() {
+ // Tests that simple navigation gives us the right windowContext (that is,
+ // the window that we're using).
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let saveHappened = promiseSave();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#regular_load",
+ {},
+ browser
+ );
+ let windowContext = await saveHappened;
+
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ }
+ );
+});
+
+// Given a browser pointing to download_page.html, clicks on the link that
+// opens with target="_blank" (i.e. a new tab) and ensures that we
+// automatically open and close that tab.
+async function testNewTab(browser) {
+ let saveHappened = promiseSave();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter("#target_blank", {}, browser);
+
+ let windowContext = await saveHappened;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+}
+
+add_task(async function target_blank() {
+ // Tests that a link with target=_blank opens a new tab and closes it,
+ // returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ await testNewTab(browser);
+ }
+ );
+});
+
+add_task(async function target_blank_no_opener() {
+ // Tests that a link with target=_blank and no opener opens a new tab
+ // and closes it, returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let saveHappened = promiseSave();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#target_blank_no_opener",
+ {},
+ browser
+ );
+
+ let windowContext = await saveHappened;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+ }
+ );
+});
+
+add_task(async function open_in_new_tab_no_opener() {
+ // Tests that a link with target=_blank and no opener opens a new tab
+ // and closes it, returning the window that we're using for navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let saveHappened = promiseSave();
+ let tabOpened = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ ).then(event => {
+ return [event.target, BrowserTestUtils.waitForTabClosing(event.target)];
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open_in_new_tab_no_opener",
+ {},
+ browser
+ );
+
+ let windowContext = await saveHappened;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+ let [tab, closingPromise] = await tabOpened;
+ await closingPromise;
+ is(tab.linkedBrowser, null, "tab was opened and closed");
+ }
+ );
+});
+
+add_task(async function new_window() {
+ // Tests that a link that forces us to open a new window (by specifying a
+ // width and a height in window.open) opens a new window for the load,
+ // realizes that we need to close that window and returns the *original*
+ // window as the window context.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let saveHappened = promiseSave();
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#new_window",
+ {},
+ browser
+ );
+ let win = await windowOpened;
+ // Now allow request to complete:
+ fetch(SJS_URL + "?finish");
+
+ let windowContext = await saveHappened;
+ is(windowContext, browser.ownerGlobal, "got the right windowContext");
+
+ // The window should close on its own. If not, this test will time out.
+ await BrowserTestUtils.domWindowClosed(win);
+ ok(win.closed, "window was opened and closed");
+
+ is(
+ await fetch(SJS_URL + "?reset").then(r => r.text()),
+ "OK",
+ "Test reseted"
+ );
+ }
+ );
+});
+
+add_task(async function new_window_no_opener() {
+ // Tests that a link that forces us to open a new window (by specifying a
+ // width and a height in window.open) opens a new window for the load,
+ // realizes that we need to close that window and returns the *original*
+ // window as the window context.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (browser) {
+ let saveHappened = promiseSave();
+ let windowOpened = BrowserTestUtils.waitForNewWindow();
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#new_window_no_opener",
+ {},
+ browser
+ );
+ let win = await windowOpened;
+ // Now allow request to complete:
+ fetch(SJS_URL + "?finish");
+
+ await saveHappened;
+
+ // The window should close on its own. If not, this test will time out.
+ await BrowserTestUtils.domWindowClosed(win);
+ ok(win.closed, "window was opened and closed");
+
+ is(
+ await fetch(SJS_URL + "?reset").then(r => r.text()),
+ "OK",
+ "Test reseted"
+ );
+ }
+ );
+});
+
+add_task(async function nested_window_opens() {
+ // Tests that the window auto-closing feature works if the download is
+ // initiated by a window that, itself, has an opener (see bug 1373109).
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: PAGE_URL },
+ async function (outerBrowser) {
+ let secondTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ `${PAGE_URL}?newwin`,
+ true
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#open_in_new_tab",
+ {},
+ outerBrowser
+ );
+ let secondTab = await secondTabPromise;
+ let nestedBrowser = secondTab.linkedBrowser;
+
+ await SpecialPowers.spawn(nestedBrowser, [], function () {
+ ok(content.opener, "this window has an opener");
+ });
+
+ await testNewTab(nestedBrowser);
+
+ isnot(
+ secondTab.linkedBrowser,
+ null,
+ "the page that triggered the download is still open"
+ );
+ BrowserTestUtils.removeTab(secondTab);
+ }
+ );
+});
+
+add_task(async function cleanup() {
+ // Unregister our factory from XPCOM and restore the original CID.
+ registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID, mockHelperAppService);
+ registrar.registerFactory(
+ HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ null
+ );
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_bad_download_dir.js b/uriloader/exthandler/tests/mochitest/browser_bad_download_dir.js
new file mode 100644
index 0000000000..7fe22e0661
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_bad_download_dir.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+
+const ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const TEST_FILE = ROOT + "download.bin";
+
+add_task(async function test_check_download_dir() {
+ // Force XDG dir to somewhere that has no config files, causing lookups of the
+ // system download dir to fail:
+ let newXDGRoot = FileTestUtils.getTempFile("xdgstuff");
+ newXDGRoot.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let oldXDG = Services.env.exists("XDG_CONFIG_HOME")
+ ? Services.env.get("XDG_CONFIG_HOME")
+ : "";
+ registerCleanupFunction(() => Services.env.set("XDG_CONFIG_HOME", oldXDG));
+ Services.env.set("XDG_CONFIG_HOME", newXDGRoot.path + "/");
+
+ let propBundle = Services.strings.createBundle(
+ "chrome://mozapps/locale/downloads/downloads.properties"
+ );
+ let dlRoot = PathUtils.join(
+ Services.dirsvc.get("Home", Ci.nsIFile).path,
+ propBundle.GetStringFromName("downloadsFolder")
+ );
+
+ // Check lookups fail:
+ Assert.throws(
+ () => Services.dirsvc.get("DfltDwnld", Ci.nsIFile),
+ /NS_ERROR_FAILURE/,
+ "Should throw when asking for downloads dir."
+ );
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Avoid opening dialogs
+ ["browser.download.always_ask_before_handling_new_types", false],
+ // Switch back to default OS downloads dir (changed in head.js):
+ ["browser.download.folderList", 1],
+ ],
+ });
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinished = promiseDownloadFinished(publicList);
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_FILE);
+ let dl = await downloadFinished;
+ ok(dl.succeeded, "Download should succeed.");
+ Assert.stringContains(
+ dl.target.path,
+ dlRoot,
+ "Should store download under DL folder root."
+ );
+ let dlKids = await IOUtils.getChildren(dlRoot);
+ ok(
+ dlKids.includes(dl.target.path),
+ "Download should be a direct child of the DL folder."
+ );
+ await IOUtils.remove(dl.target.path);
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Download a second file to make sure we're not continuously adding filenames
+ // onto the download folder path.
+ downloadFinished = promiseDownloadFinished(publicList);
+ tab = BrowserTestUtils.addTab(gBrowser, TEST_FILE);
+ dl = await downloadFinished;
+ Assert.stringContains(
+ dl.target.path,
+ dlRoot,
+ "Second download should store download under new DL folder root."
+ );
+ dlKids = await IOUtils.getChildren(dlRoot);
+ ok(
+ dlKids.includes(dl.target.path),
+ "Second download should be a direct child of the new DL folder."
+ );
+ BrowserTestUtils.removeTab(tab);
+ await IOUtils.remove(dl.target.path);
+
+ await publicList.removeFinished();
+ await IOUtils.remove(newXDGRoot.path);
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js
new file mode 100644
index 0000000000..b4bfe5e51a
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js
@@ -0,0 +1,25 @@
+add_task(async function () {
+ // Create mocked objects for test
+ let launcher = createMockedObjects(false);
+ // Open helper app dialog with mocked launcher
+ let dlg = await openHelperAppDialog(launcher);
+ let doc = dlg.document;
+ let location = doc.getElementById("source");
+ let expectedValue = launcher.source.prePath;
+ if (location.value != expectedValue) {
+ info("Waiting for dialog to be populated.");
+ await BrowserTestUtils.waitForAttribute("value", location, expectedValue);
+ }
+ is(
+ doc.getElementById("mode").selectedItem.id,
+ "open",
+ "Should be opening the file."
+ );
+ ok(
+ !dlg.document.getElementById("openHandler").selectedItem.hidden,
+ "Should not have selected a hidden item."
+ );
+ let helperAppDialogHiddenPromise = BrowserTestUtils.windowClosed(dlg);
+ doc.getElementById("unknownContentType").cancelDialog();
+ await helperAppDialogHiddenPromise;
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_idn_blocklist.js b/uriloader/exthandler/tests/mochitest/browser_download_idn_blocklist.js
new file mode 100644
index 0000000000..0d49a898a0
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_idn_blocklist.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_HOST = "example.org";
+const TEST_FILE = "\u3002.bin";
+const TEST_URL = `http://${TEST_HOST}/${TEST_FILE}`;
+
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+XPCShellContentUtils.initMochitest(this);
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: [TEST_HOST],
+});
+let file = getChromeDir(getResolvedURI(gTestPath));
+file.append("download.bin");
+server.registerFile(`/${encodeURIComponent(TEST_FILE)}`, file);
+
+/**
+ * Check that IDN blocklisted characters are not escaped in
+ * download file names.
+ */
+add_task(async function test_idn_blocklisted_char_not_escaped() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+
+ info("Testing with " + TEST_URL);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let downloadFinished = promiseDownloadFinished(publicList);
+ var tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
+ let dl = await downloadFinished;
+ ok(dl.succeeded, "Download should succeed.");
+ Assert.equal(
+ PathUtils.filename(dl.target.path),
+ TEST_FILE,
+ "Should not escape a download file name."
+ );
+ await IOUtils.remove(dl.target.path);
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js
new file mode 100644
index 0000000000..c73538fe19
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js
@@ -0,0 +1,814 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+const { DownloadIntegration } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const MimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+const HandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+function waitForAcceptButtonToGetEnabled(doc) {
+ let dialog = doc.querySelector("#unknownContentType");
+ let button = dialog.getButton("accept");
+ return TestUtils.waitForCondition(
+ () => !button.disabled,
+ "Wait for Accept button to get enabled"
+ );
+}
+
+async function waitForPdfJS(browser, url) {
+ // Runs tests after all "load" event handlers have fired off
+ let loadPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "documentloaded",
+ false,
+ null,
+ true
+ );
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ return loadPromise;
+}
+
+/**
+ * This test covers which choices are presented for downloaded files and how
+ * those choices are handled. Unless a pref is enabled
+ * (browser.download.always_ask_before_handling_new_types) the unknown content
+ * dialog will be skipped altogether by default when downloading.
+ * To retain coverage for the non-default scenario, each task sets `alwaysAskBeforeHandling`
+ * to true for the relevant mime-type and extensions.
+ */
+function alwaysAskForHandlingTypes(typeExtensions, ask = true) {
+ let mimeInfos = [];
+ for (let [type, ext] of Object.entries(typeExtensions)) {
+ const mimeInfo = MimeSvc.getFromTypeAndExtension(type, ext);
+ mimeInfo.alwaysAskBeforeHandling = ask;
+ if (!ask) {
+ mimeInfo.preferredAction = mimeInfo.handleInternally;
+ }
+ HandlerSvc.store(mimeInfo);
+ mimeInfos.push(mimeInfo);
+ }
+ return mimeInfos;
+}
+
+add_setup(async function () {
+ // Remove the security delay for the dialog during the test.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.dialog_enable_delay", 0],
+ ["browser.helperApps.showOpenOptionForPdfJS", true],
+ ["browser.helperApps.showOpenOptionForViewableInternally", true],
+ ],
+ });
+
+ // Restore handlers after the whole test has run
+ const registerRestoreHandler = function (type, ext) {
+ const mimeInfo = MimeSvc.getFromTypeAndExtension(type, ext);
+ const existed = HandlerSvc.exists(mimeInfo);
+
+ registerCleanupFunction(() => {
+ if (existed) {
+ HandlerSvc.store(mimeInfo);
+ } else {
+ HandlerSvc.remove(mimeInfo);
+ }
+ });
+ };
+ registerRestoreHandler("application/pdf", "pdf");
+ registerRestoreHandler("binary/octet-stream", "pdf");
+ registerRestoreHandler("application/unknown", "pdf");
+ registerRestoreHandler("image/webp", "webp");
+});
+
+/**
+ * Check that loading a PDF file with content-disposition: attachment
+ * shows an option to open with the internal handler, and that the
+ * internal option handler is not present when the download button
+ * is clicked from pdf.js.
+ */
+add_task(async function test_check_open_with_internal_handler() {
+ const mimeInfosToRestore = alwaysAskForHandlingTypes({
+ "application/pdf": "pdf",
+ "binary/octet-stream": "pdf",
+ });
+
+ for (let file of [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_binary_octet_stream.pdf",
+ ]) {
+ info("Testing with " + file);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + file,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ // Add an extra tab after the loading tab so we can test that
+ // pdf.js is opened in the adjacent tab and not at the end of
+ // the tab strip.
+ let extraTab = await BrowserTestUtils.addTab(gBrowser, "about:blank");
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ ok(!internalHandlerRadio.hidden, "The option should be visible for PDF");
+ ok(internalHandlerRadio.selected, "The option should be selected");
+
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ let dialog = doc.querySelector("#unknownContentType");
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ dialog.acceptDialog();
+ info("waiting for new tab to open");
+ let newTab = await newTabPromise;
+
+ is(
+ newTab._tPos - 1,
+ loadingTab._tPos,
+ "pdf.js should be opened in an adjacent tab"
+ );
+
+ await ContentTask.spawn(newTab.linkedBrowser, null, async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "complete"
+ );
+ });
+
+ let publicDownloads = await publicList.getAll();
+ is(
+ publicDownloads.length,
+ 1,
+ "download should appear in publicDownloads list"
+ );
+
+ let download = await downloadFinishedPromise;
+
+ let subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ // Current tab has file: URI and TEST_PATH is http uri, so uri will be different
+ BrowserTestUtils.startLoadingURIString(
+ newTab.linkedBrowser,
+ TEST_PATH + file
+ );
+ let subDialogWindow = await subdialogPromise;
+ let subDoc = subDialogWindow.document;
+
+ // Prevent racing with initialization of the dialog and make sure that
+ // the final state of the dialog has the correct visibility of the internal-handler option.
+ await waitForAcceptButtonToGetEnabled(subDoc);
+ let subInternalHandlerRadio = subDoc.querySelector("#handleInternally");
+ ok(
+ !subInternalHandlerRadio.hidden,
+ "This option should be shown when the dialog is shown for another PDF"
+ );
+ // Cancel dialog
+ subDoc.querySelector("#unknownContentType").cancelDialog();
+
+ let filepickerPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ setTimeout(() => {
+ resolve(fp.defaultString);
+ }, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ });
+
+ subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ await SpecialPowers.spawn(newTab.linkedBrowser, [], async () => {
+ let downloadButton;
+ await ContentTaskUtils.waitForCondition(() => {
+ downloadButton = content.document.querySelector("#download");
+ return !!downloadButton;
+ });
+ ok(downloadButton, "Download button should be present in pdf.js");
+ downloadButton.click();
+ });
+ info(
+ "Waiting for unknown content type dialog to appear from pdf.js download button click"
+ );
+ let filename = await filepickerPromise;
+ is(filename, file, "filename was set in filepicker");
+
+ // Remove the first file (can't do this sooner or the second load fails):
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+
+ BrowserTestUtils.removeTab(loadingTab);
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(extraTab);
+
+ await publicList.removeFinished();
+ }
+ for (let mimeInfo of mimeInfosToRestore) {
+ HandlerSvc.remove(mimeInfo);
+ }
+});
+
+/**
+ * Test that choosing to open in an external application doesn't
+ * open the PDF into pdf.js
+ */
+add_task(async function test_check_open_with_external_application() {
+ const mimeInfosToRestore = alwaysAskForHandlingTypes({
+ "application/pdf": "pdf",
+ "binary/octet-stream": "pdf",
+ });
+
+ for (let file of [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_binary_octet_stream.pdf",
+ ]) {
+ info("Testing with " + file);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + file,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ let doc = dialogWindow.document;
+ await waitForAcceptButtonToGetEnabled(doc);
+ let dialog = doc.querySelector("#unknownContentType");
+ doc.querySelector("#open").click();
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ info("Accepting the dialog");
+ dialog.acceptDialog();
+ info("Waiting until DownloadIntegration.launchFile is called");
+ await waitForLaunchFileCalled;
+ DownloadIntegration.launchFile = oldLaunchFile;
+
+ let publicDownloads = await publicList.getAll();
+ is(
+ publicDownloads.length,
+ 1,
+ "download should appear in publicDownloads list"
+ );
+ let download = publicDownloads[0];
+ ok(
+ !download.launchWhenSucceeded,
+ "launchWhenSucceeded should be false after launchFile is called"
+ );
+
+ BrowserTestUtils.removeTab(loadingTab);
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ await publicList.removeFinished();
+ }
+ for (let mimeInfo of mimeInfosToRestore) {
+ HandlerSvc.remove(mimeInfo);
+ }
+});
+
+/**
+ * Test that choosing to open a PDF with an external application works and
+ * then downloading the same file again and choosing Open with Firefox opens
+ * the download in Firefox.
+ */
+add_task(async function test_check_open_with_external_then_internal() {
+ // This test only runs on Windows because appPicker.xhtml is only used on Windows.
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ // This test covers a bug that only occurs when the mimeInfo is set to Always Ask
+ const mimeInfo = MimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+ console.log(
+ "mimeInfo.preferredAction is currently:",
+ mimeInfo.preferredAction
+ );
+ mimeInfo.preferredAction = mimeInfo.alwaysAsk;
+ mimeInfo.alwaysAskBeforeHandling = true;
+ HandlerSvc.store(mimeInfo);
+
+ for (let [file, mimeType] of [
+ ["file_pdf_application_pdf.pdf", "application/pdf"],
+ ["file_pdf_binary_octet_stream.pdf", "binary/octet-stream"],
+ ["file_pdf_application_unknown.pdf", "application/unknown"],
+ ]) {
+ info("Testing with " + file);
+ let originalMimeInfo = MimeSvc.getFromTypeAndExtension(mimeType, "pdf");
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ // Open a new tab to the PDF file which will trigger the Unknown Content Type dialog
+ // and choose to open the PDF with an external application.
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + file,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ let doc = dialogWindow.document;
+ await waitForAcceptButtonToGetEnabled(doc);
+ let dialog = doc.querySelector("#unknownContentType");
+ let openHandlerMenulist = doc.querySelector("#openHandler");
+ let originalDefaultHandler = openHandlerMenulist.label;
+ doc.querySelector("#open").click();
+ doc.querySelector("#openHandlerPopup").click();
+ let oldOpenDialog = dialogWindow.openDialog;
+ dialogWindow.openDialog = (location, unused2, unused3, params) => {
+ is(location, "chrome://global/content/appPicker.xhtml", "app picker");
+ let handlerApp = params.mimeInfo.possibleLocalHandlers.queryElementAt(
+ 0,
+ Ci.nsILocalHandlerApp
+ );
+ ok(handlerApp.executable, "handlerApp should be executable");
+ ok(handlerApp.executable.isFile(), "handlerApp should be a file");
+ params.handlerApp = handlerApp;
+ };
+ doc.querySelector("#choose").click();
+ dialogWindow.openDialog = oldOpenDialog;
+ await TestUtils.waitForCondition(
+ () => originalDefaultHandler != openHandlerMenulist.label,
+ "waiting for openHandler to get updated"
+ );
+ let newDefaultHandler = openHandlerMenulist.label;
+ info(`was ${originalDefaultHandler}, now ${newDefaultHandler}`);
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ info("Accepting the dialog");
+ dialog.acceptDialog();
+ info("Waiting until DownloadIntegration.launchFile is called");
+ await waitForLaunchFileCalled;
+ BrowserTestUtils.removeTab(loadingTab);
+
+ // Now, open a new tab to the PDF file which will trigger the Unknown Content Type dialog
+ // and choose to open the PDF internally. The previously used external application should be shown as
+ // the external option.
+ dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + file,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ DownloadIntegration.launchFile = () => {
+ ok(false, "The file should not be launched with an external application");
+ };
+
+ doc = dialogWindow.document;
+ await waitForAcceptButtonToGetEnabled(doc);
+ openHandlerMenulist = doc.querySelector("#openHandler");
+ is(openHandlerMenulist.label, newDefaultHandler, "'new' handler");
+ dialog = doc.querySelector("#unknownContentType");
+ doc.querySelector("#handleInternally").click();
+ info("Accepting the dialog");
+ button = dialog.getButton("accept");
+ button.disabled = false;
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ dialog.acceptDialog();
+
+ info("waiting for new tab to open");
+ let newTab = await newTabPromise;
+
+ await ContentTask.spawn(newTab.linkedBrowser, null, async () => {
+ await ContentTaskUtils.waitForCondition(
+ () => content.document.readyState == "complete"
+ );
+ });
+
+ is(
+ newTab.linkedBrowser.contentPrincipal.origin,
+ "resource://pdf.js",
+ "PDF should be opened with pdf.js"
+ );
+
+ BrowserTestUtils.removeTab(loadingTab);
+ BrowserTestUtils.removeTab(newTab);
+
+ // Now trigger the dialog again and select the system
+ // default option to reset the state for the next iteration of the test.
+ // Reset the state for the next iteration of the test.
+ HandlerSvc.store(originalMimeInfo);
+ DownloadIntegration.launchFile = oldLaunchFile;
+ let [download] = await publicList.getAll();
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ await publicList.removeFinished();
+ }
+});
+
+/**
+ * Check that the "Open with internal handler" option is presented
+ * for other viewable internally types.
+ */
+add_task(
+ async function test_internal_handler_hidden_with_viewable_internally_type() {
+ const mimeInfosToRestore = alwaysAskForHandlingTypes({
+ "binary/octet-stream": "xml",
+ "image/webp": "webp",
+ });
+
+ for (let [file, checkDefault] of [
+ // The default for binary/octet-stream is changed by the PDF tests above,
+ // this may change given bug 1659008, so I'm just ignoring the default for now.
+ ["file_xml_attachment_binary_octet_stream.xml", false],
+ ["file_green.webp", true],
+ ]) {
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + file,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+
+ // Prevent racing with initialization of the dialog and make sure that
+ // the final state of the dialog has the correct visibility of the internal-handler option.
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ let fileDesc = file.substring(file.lastIndexOf(".") + 1);
+
+ ok(
+ !internalHandlerRadio.hidden,
+ `The option should be visible for ${fileDesc}`
+ );
+ if (checkDefault) {
+ ok(
+ internalHandlerRadio.selected,
+ `The option should be selected for ${fileDesc}`
+ );
+ }
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ }
+ for (let mimeInfo of mimeInfosToRestore) {
+ HandlerSvc.remove(mimeInfo);
+ }
+ }
+);
+
+/**
+ * Check that the "Open with internal handler" option is not presented
+ * for non-PDF, non-viewable-internally types.
+ */
+add_task(async function test_internal_handler_hidden_with_other_type() {
+ const mimeInfosToRestore = alwaysAskForHandlingTypes({
+ "text/plain": "txt",
+ });
+
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_txt_attachment_test.txt",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+
+ // Prevent racing with initialization of the dialog and make sure that
+ // the final state of the dialog has the correct visibility of the internal-handler option.
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+ ok(
+ internalHandlerRadio.hidden,
+ "The option should be hidden for unknown file type"
+ );
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ for (let mimeInfo of mimeInfosToRestore) {
+ HandlerSvc.remove(mimeInfo);
+ }
+});
+
+/**
+ * Check that the "Open with internal handler" option is not presented
+ * when the feature is disabled for PDFs.
+ */
+add_task(async function test_internal_handler_hidden_with_pdf_pref_disabled() {
+ const mimeInfosToRestore = alwaysAskForHandlingTypes({
+ "application/pdf": "pdf",
+ "binary/octet-stream": "pdf",
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.helperApps.showOpenOptionForPdfJS", false]],
+ });
+ for (let file of [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_binary_octet_stream.pdf",
+ ]) {
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + file,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+ ok(
+ internalHandlerRadio.hidden,
+ "The option should be hidden for PDF when the pref is false"
+ );
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ }
+ for (let mimeInfo of mimeInfosToRestore) {
+ HandlerSvc.remove(mimeInfo);
+ }
+});
+
+/**
+ * Check that the "Open with internal handler" option is not presented
+ * for other viewable internally types when disabled.
+ */
+add_task(
+ async function test_internal_handler_hidden_with_viewable_internally_pref_disabled() {
+ const mimeInfosToRestore = alwaysAskForHandlingTypes({
+ "text/xml": "xml",
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.helperApps.showOpenOptionForViewableInternally", false]],
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_xml_attachment_test.xml",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+ let doc = dialogWindow.document;
+
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+ ok(
+ internalHandlerRadio.hidden,
+ "The option should be hidden for XML when the pref is false"
+ );
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ for (let mimeInfo of mimeInfosToRestore) {
+ HandlerSvc.remove(mimeInfo);
+ }
+ }
+);
+
+/*
+ * This test sets the action to internal. The files should open directly without asking.
+ */
+add_task(async function test_check_open_with_internal_handler_noask() {
+ const mimeInfosToRestore = alwaysAskForHandlingTypes(
+ {
+ "application/pdf": "pdf",
+ "binary/octet-stream": "pdf",
+ "application/octet-stream": "pdf",
+ },
+ false
+ );
+
+ // Build the matrix of tests to perform.
+ let matrix = {
+ alwaysOpenPDFInline: [false, true],
+ file: [
+ "file_pdf_application_pdf.pdf",
+ "file_pdf_binary_octet_stream.pdf",
+ "file_pdf_application_octet_stream.pdf",
+ ],
+ where: ["top", "popup", "frame"],
+ };
+ let tests = [{}];
+ for (let [key, values] of Object.entries(matrix)) {
+ tests = tests.flatMap(test =>
+ values.map(value => ({ [key]: value, ...test }))
+ );
+ }
+
+ for (let test of tests) {
+ info(`test case: ${JSON.stringify(test)}`);
+ let { alwaysOpenPDFInline, file, where } = test;
+
+ // These are the cases that can be opened inline. binary/octet-stream
+ // isn't handled by pdfjs.
+ let canHandleInline =
+ file == "file_pdf_application_pdf.pdf" ||
+ (file == "file_pdf_application_octet_stream.pdf" && where != "frame");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.helperApps.showOpenOptionForPdfJS", true],
+ ["browser.helperApps.showOpenOptionForViewableInternally", true],
+ ["browser.download.open_pdf_attachments_inline", alwaysOpenPDFInline],
+ ],
+ });
+
+ async function doNavigate(browser) {
+ await SpecialPowers.spawn(
+ browser,
+ [TEST_PATH + file, where],
+ async (contentUrl, where_) => {
+ switch (where_) {
+ case "top":
+ content.location = contentUrl;
+ break;
+ case "popup":
+ content.open(contentUrl);
+ break;
+ case "frame":
+ let frame = content.document.createElement("iframe");
+ frame.setAttribute("src", contentUrl);
+ content.document.body.appendChild(frame);
+ break;
+ default:
+ ok(false, "Unknown where value");
+ break;
+ }
+ }
+ );
+ }
+
+ // If this is true, the pdf is opened directly without downloading it.
+ // Otherwise, it must first be downloaded and optionally displayed in
+ // a tab with a file url.
+ let openPDFDirectly = alwaysOpenPDFInline && canHandleInline;
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TEST_PATH + "blank.html" },
+ async browser => {
+ let readyPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ null,
+ false,
+ !openPDFDirectly
+ );
+
+ await doNavigate(browser);
+
+ await readyPromise;
+
+ is(
+ gBrowser.selectedBrowser.currentURI.scheme,
+ openPDFDirectly ? "https" : "file",
+ "Loaded PDF uri has the correct scheme"
+ );
+
+ // intentionally don't bother checking session history without ship to
+ // keep complexity down.
+ if (Services.appinfo.sessionHistoryInParent) {
+ let shistory = browser.browsingContext.sessionHistory;
+ is(shistory.count, 1, "should a single shentry");
+ is(shistory.index, 0, "should be on the first entry");
+ let shentry = shistory.getEntryAtIndex(shistory.index);
+ is(shentry.URI.spec, TEST_PATH + "blank.html");
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [TEST_PATH + "blank.html"],
+ async blankUrl => {
+ ok(
+ !docShell.isAttemptingToNavigate,
+ "should not still be attempting to navigate"
+ );
+ is(
+ content.location.href,
+ blankUrl,
+ "original browser hasn't navigated"
+ );
+ }
+ );
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ );
+ }
+
+ for (let mimeInfo of mimeInfosToRestore) {
+ HandlerSvc.remove(mimeInfo);
+ }
+});
+
+add_task(async () => {
+ MockFilePicker.cleanup();
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_preferred_action.js b/uriloader/exthandler/tests/mochitest/browser_download_preferred_action.js
new file mode 100644
index 0000000000..b66266ec9e
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_preferred_action.js
@@ -0,0 +1,299 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DownloadIntegration } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+const { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+const gHandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+const gMIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+const localHandlerAppFactory = Cc["@mozilla.org/uriloader/local-handler-app;1"];
+
+const ROOT_URL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const FILE_TYPES_MIME_SETTINGS = {};
+
+// File types to test
+const FILE_TYPES_TO_TEST = [
+ // application/ms-word files cannot render in the browser so
+ // handleInternally does not work for it
+ {
+ extension: "doc",
+ mimeType: "application/ms-word",
+ blockHandleInternally: true,
+ },
+ {
+ extension: "pdf",
+ mimeType: "application/pdf",
+ },
+ {
+ extension: "pdf",
+ mimeType: "application/unknown",
+ },
+ {
+ extension: "pdf",
+ mimeType: "binary/octet-stream",
+ },
+ // text/plain files automatically render in the browser unless
+ // the CD header explicitly tells the browser to download it
+ {
+ extension: "txt",
+ mimeType: "text/plain",
+ requireContentDispositionHeader: true,
+ },
+ {
+ extension: "xml",
+ mimeType: "binary/octet-stream",
+ },
+].map(file => {
+ return {
+ ...file,
+ url: `${ROOT_URL}mime_type_download.sjs?contentType=${file.mimeType}&extension=${file.extension}`,
+ };
+});
+
+// Preferred action types to apply to each downloaded file
+const PREFERRED_ACTIONS = [
+ "saveToDisk",
+ "alwaysAsk",
+ "useHelperApp",
+ "handleInternally",
+ "useSystemDefault",
+].map(property => {
+ let label = property.replace(/([A-Z])/g, " $1");
+ label = label.charAt(0).toUpperCase() + label.slice(1);
+ return {
+ id: Ci.nsIHandlerInfo[property],
+ label,
+ };
+});
+
+async function createDownloadTest(
+ downloadList,
+ localHandlerApp,
+ file,
+ action,
+ useContentDispositionHeader
+) {
+ // Skip handleInternally case for files that cannot be handled internally
+ if (
+ action.id === Ci.nsIHandlerInfo.handleInternally &&
+ file.blockHandleInternally
+ ) {
+ return;
+ }
+ let skipDownload =
+ action.id === Ci.nsIHandlerInfo.handleInternally &&
+ file.mimeType === "application/pdf";
+ // Types that require the CD header only display as handleInternally
+ // when the CD header is missing
+ if (file.requireContentDispositionHeader && !useContentDispositionHeader) {
+ if (action.id === Ci.nsIHandlerInfo.handleInternally) {
+ skipDownload = true;
+ } else {
+ return;
+ }
+ }
+ info(
+ `Testing download with mime-type ${file.mimeType} and extension ${
+ file.extension
+ }, preferred action "${action.label}," and ${
+ useContentDispositionHeader
+ ? "Content-Disposition: attachment"
+ : "no Content-Disposition"
+ } header.`
+ );
+ info("Preparing for download...");
+ // apply preferredAction settings
+ let mimeSettings = gMIMEService.getFromTypeAndExtension(
+ file.mimeType,
+ file.extension
+ );
+ mimeSettings.preferredAction = action.id;
+ mimeSettings.alwaysAskBeforeHandling =
+ action.id === Ci.nsIHandlerInfo.alwaysAsk;
+ if (action.id === Ci.nsIHandlerInfo.useHelperApp) {
+ mimeSettings.preferredApplicationHandler = localHandlerApp;
+ }
+ gHandlerService.store(mimeSettings);
+ // delayed check for files opened in a new tab, except for skipped downloads
+ let expectViewInBrowserTab =
+ action.id === Ci.nsIHandlerInfo.handleInternally && !skipDownload;
+ let viewInBrowserTabOpened = null;
+ if (expectViewInBrowserTab) {
+ viewInBrowserTabOpened = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ uri => uri.includes("file://"),
+ true
+ );
+ }
+ // delayed check for launched files
+ let expectLaunch =
+ action.id === Ci.nsIHandlerInfo.useSystemDefault ||
+ action.id === Ci.nsIHandlerInfo.useHelperApp;
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let fileLaunched = null;
+ if (expectLaunch) {
+ fileLaunched = Promise.withResolvers();
+ DownloadIntegration.launchFile = () => {
+ ok(
+ expectLaunch,
+ `The file ${file.mimeType} should be launched with an external application.`
+ );
+ fileLaunched.resolve();
+ };
+ }
+ info(`Start download of ${file.url}`);
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let downloadFinishedPromise = skipDownload
+ ? null
+ : promiseDownloadFinished(downloadList);
+ BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, file.url);
+ if (action.id === Ci.nsIHandlerInfo.alwaysAsk) {
+ info("Check Always Ask dialog.");
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should show unknownContentType dialog for Always Ask preferred actions."
+ );
+ let doc = dialogWindow.document;
+ let dialog = doc.querySelector("#unknownContentType");
+ let acceptButton = dialog.getButton("accept");
+ acceptButton.disabled = false;
+ let saveItem = doc.querySelector("#save");
+ saveItem.disabled = false;
+ saveItem.click();
+ dialog.acceptDialog();
+ }
+ let download = null;
+ let downloadPath = null;
+ if (!skipDownload) {
+ info("Wait for download to finish...");
+ download = await downloadFinishedPromise;
+ downloadPath = download.target.path;
+ }
+ // check delayed assertions
+ if (expectLaunch) {
+ info("Wait for file to be launched in external application...");
+ await fileLaunched.promise;
+ }
+ if (expectViewInBrowserTab) {
+ info("Wait for file to be opened in new tab...");
+ let viewInBrowserTab = await viewInBrowserTabOpened;
+ ok(
+ viewInBrowserTab,
+ `The file ${file.mimeType} should be opened in a new tab.`
+ );
+ BrowserTestUtils.removeTab(viewInBrowserTab);
+ }
+ info("Checking for saved file...");
+ let saveFound = downloadPath && (await IOUtils.exists(downloadPath));
+ info("Cleaning up...");
+ if (saveFound) {
+ try {
+ info(`Deleting file ${downloadPath}...`);
+ await IOUtils.remove(downloadPath);
+ } catch (ex) {
+ info(`Error: ${ex}`);
+ }
+ }
+ info("Removing download from list...");
+ await downloadList.removeFinished();
+ info("Clearing settings...");
+ DownloadIntegration.launchFile = oldLaunchFile;
+ info("Asserting results...");
+ if (download) {
+ ok(download.succeeded, "Download should complete successfully");
+ ok(
+ !download._launchedFromPanel,
+ "Download should never be launched from panel"
+ );
+ }
+ if (skipDownload) {
+ ok(!saveFound, "Download should not be saved to disk");
+ } else {
+ ok(saveFound, "Download should be saved to disk");
+ }
+}
+
+add_task(async function test_download_preferred_action() {
+ // Prepare tests
+ for (const index in FILE_TYPES_TO_TEST) {
+ let file = FILE_TYPES_TO_TEST[index];
+ let originalMimeSettings = gMIMEService.getFromTypeAndExtension(
+ file.mimeType,
+ file.extension
+ );
+ if (gHandlerService.exists(originalMimeSettings)) {
+ FILE_TYPES_MIME_SETTINGS[index] = originalMimeSettings;
+ }
+ }
+ let downloadList = await Downloads.getList(Downloads.PUBLIC);
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ registerCleanupFunction(async function () {
+ await removeAllDownloads();
+ DownloadIntegration.launchFile = oldLaunchFile;
+ Services.prefs.clearUserPref(
+ "browser.download.always_ask_before_handling_new_types"
+ );
+ BrowserTestUtils.startLoadingURIString(
+ gBrowser.selectedBrowser,
+ "about:home"
+ );
+ for (const index in FILE_TYPES_TO_TEST) {
+ let file = FILE_TYPES_TO_TEST[index];
+ let mimeSettings = gMIMEService.getFromTypeAndExtension(
+ file.mimeType,
+ file.extension
+ );
+ if (FILE_TYPES_MIME_SETTINGS[index]) {
+ gHandlerService.store(FILE_TYPES_MIME_SETTINGS[index]);
+ } else {
+ gHandlerService.remove(mimeSettings);
+ }
+ }
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+ let launcherPath = FileTestUtils.getTempFile("app-launcher").path;
+ let localHandlerApp = localHandlerAppFactory.createInstance(
+ Ci.nsILocalHandlerApp
+ );
+ localHandlerApp.executable = new FileUtils.File(launcherPath);
+ localHandlerApp.executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755);
+ // run tests
+ for (const file of FILE_TYPES_TO_TEST) {
+ // The CD header specifies the download file extension on download
+ let fileNoHeader = file;
+ let fileWithHeader = structuredClone(file);
+ fileWithHeader.url += "&withHeader";
+ for (const action of PREFERRED_ACTIONS) {
+ // Clone file objects to prevent side-effects between iterations
+ await createDownloadTest(
+ downloadList,
+ localHandlerApp,
+ structuredClone(fileWithHeader),
+ action,
+ true
+ );
+ await createDownloadTest(
+ downloadList,
+ localHandlerApp,
+ structuredClone(fileNoHeader),
+ action,
+ false
+ );
+ }
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js
new file mode 100644
index 0000000000..8ded5e6401
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that downloads started from a private window by clicking on a link end
+ * up in the global list of private downloads (see bug 1367581).
+ */
+
+"use strict";
+
+const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+const { DownloadPaths } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadPaths.sys.mjs"
+);
+const { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+add_task(async function test_setup() {
+ // Save downloads to disk without showing the dialog.
+ let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", {
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+ show(launcher) {
+ launcher.promptForSaveDestination();
+ },
+ promptForSaveToFileAsync(launcher) {
+ // The dialog should create the empty placeholder file.
+ let file = FileTestUtils.getTempFile();
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ launcher.saveDestinationAvailable(file);
+ },
+ });
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(cid);
+ });
+});
+
+add_task(async function test_download_privatebrowsing() {
+ let privateList = await Downloads.getList(Downloads.PRIVATE);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ try {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ `data:text/html,<a download href="data:text/plain,">download</a>`
+ );
+
+ let promiseNextPrivateDownload = new Promise(resolve => {
+ privateList.addView({
+ onDownloadAdded(download) {
+ privateList.removeView(this);
+ resolve(download);
+ },
+ });
+ });
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.document.querySelector("a").click();
+ });
+
+ // Wait for the download to finish so the file can be safely deleted later.
+ let download = await promiseNextPrivateDownload;
+ await download.whenSucceeded();
+
+ // Clean up after checking that there are no new public downloads either.
+ let publicDownloads = await publicList.getAll();
+ Assert.equal(publicDownloads.length, 0);
+ await privateList.removeFinished();
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_skips_dialog.js b/uriloader/exthandler/tests/mochitest/browser_download_skips_dialog.js
new file mode 100644
index 0000000000..0b8c816d2e
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_skips_dialog.js
@@ -0,0 +1,59 @@
+const { DownloadIntegration } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+// New file is being downloaded and no dialogs are shown in the way.
+add_task(async function skipDialogAndDownloadFile() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+
+ let initialTabsCount = gBrowser.tabs.length;
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_green.webp",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ // We just open the file to be downloaded... and wait for it to be downloaded!
+ // We see no dialogs to be accepted in the process.
+ let download = await downloadFinishedPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => gBrowser.tabs.length == initialTabsCount + 2
+ );
+
+ gBrowser.removeCurrentTab();
+ BrowserTestUtils.removeTab(loadingTab);
+
+ Assert.ok(
+ await IOUtils.exists(download.target.path),
+ "The file should have been downloaded."
+ );
+
+ try {
+ info("removing " + download.target.path);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ info("The file " + download.target.path + " is not removed, " + ex);
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_spam_permissions.js b/uriloader/exthandler/tests/mochitest/browser_download_spam_permissions.js
new file mode 100644
index 0000000000..0f80af5ac5
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_spam_permissions.js
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const TEST_URI = "https://example.com";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ TEST_URI
+);
+
+const AUTOMATIC_DOWNLOAD_TOPIC = "blocked-automatic-download";
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+registerCleanupFunction(() => MockFilePicker.cleanup());
+
+let gTempDownloadDir;
+
+add_setup(async function () {
+ // Create temp directory
+ let time = new Date().getTime();
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append(time);
+ gTempDownloadDir = tempDir;
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
+
+ PermissionTestUtils.add(
+ TEST_URI,
+ "automatic-download",
+ Services.perms.UNKNOWN_ACTION
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We disable browser.download.always_ask_before_handling_new_types here
+ // since the test expects the download to be saved directly to disk and
+ // not prompted by the UnknownContentType window.
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.enable_spam_prevention", true],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ await IOUtils.remove(tempDir.path, { recursive: true });
+ });
+});
+
+add_task(async function check_download_spam_permissions() {
+ const INITIAL_TABS_COUNT = gBrowser.tabs.length;
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinishedPromise = promiseDownloadFinished(
+ publicList,
+ true /* stop the download from openning */
+ );
+ let blockedDownloadsCount = 0;
+ let blockedDownloadsURI = "";
+ let automaticDownloadObserver = {
+ observe: function automatic_download_observe(aSubject, aTopic, aData) {
+ if (aTopic === AUTOMATIC_DOWNLOAD_TOPIC) {
+ blockedDownloadsCount++;
+ blockedDownloadsURI = aData;
+ }
+ },
+ };
+ Services.obs.addObserver(automaticDownloadObserver, AUTOMATIC_DOWNLOAD_TOPIC);
+
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "test_spammy_page.html"
+ );
+ registerCleanupFunction(async () => {
+ DownloadIntegration.downloadSpamProtection.removeDownloadSpamForWindow(
+ TEST_URI,
+ window
+ );
+ DownloadsPanel.hidePanel();
+ await publicList.removeFinished();
+ BrowserTestUtils.removeTab(newTab);
+ Services.obs.removeObserver(
+ automaticDownloadObserver,
+ AUTOMATIC_DOWNLOAD_TOPIC
+ );
+ });
+
+ let download = await downloadFinishedPromise;
+ await TestUtils.waitForCondition(
+ () => gBrowser.tabs.length == INITIAL_TABS_COUNT + 1
+ );
+ is(
+ PermissionTestUtils.testPermission(TEST_URI, "automatic-download"),
+ Services.perms.PROMPT_ACTION,
+ "The permission to prompt the user should be stored."
+ );
+
+ ok(
+ await IOUtils.exists(download.target.path),
+ "One file should be downloaded"
+ );
+
+ let aCopyFilePath = download.target.path.replace(".pdf", "(1).pdf");
+ is(
+ await IOUtils.exists(aCopyFilePath),
+ false,
+ "An other file should be blocked"
+ );
+
+ info("Will wait for blockedDownloadsCount to be >= 99");
+ await TestUtils.waitForCondition(() => blockedDownloadsCount >= 99);
+ is(blockedDownloadsCount, 99, "Browser should block 99 downloads");
+ is(
+ blockedDownloadsURI,
+ TEST_URI,
+ "The test URI should have blocked automatic downloads"
+ );
+
+ await savelink();
+});
+
+// Check to ensure that a link saved manually is not blocked.
+async function savelink() {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ "#image",
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShown;
+
+ await new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ resolve();
+ let file = gTempDownloadDir.clone();
+ file.append("file_with__funny_name.png");
+ MockFilePicker.setFiles([file]);
+ return Ci.nsIFilePicker.returnOK;
+ };
+ let menuitem = menu.querySelector("#context-savelink");
+ menu.activateItem(menuitem);
+ });
+
+ await promiseDownloadFinished(
+ publicList,
+ true // stop the download from openning
+ );
+}
diff --git a/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js
new file mode 100644
index 0000000000..ababcc095c
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+registerCleanupFunction(() => MockFilePicker.cleanup());
+
+/**
+ * Check downloading files URL-escapes content-disposition
+ * information when necessary.
+ */
+add_task(async function test_check_filename_urlescape() {
+ let pendingPromise;
+ let pendingTest = "";
+ let expectedFileName = "";
+ MockFilePicker.showCallback = function (fp) {
+ info(`${pendingTest} - Filepicker shown, checking filename`);
+ is(
+ fp.defaultString,
+ expectedFileName,
+ `${pendingTest} - Should have escaped filename`
+ );
+ ok(
+ pendingPromise,
+ `${pendingTest} - Should have expected this picker open.`
+ );
+ if (pendingPromise) {
+ pendingPromise.resolve();
+ }
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ function runTestFor(fileName, selector) {
+ return BrowserTestUtils.withNewTab(TEST_PATH + fileName, async browser => {
+ expectedFileName = fileName;
+ let tabLabel = gBrowser.getTabForBrowser(browser).getAttribute("label");
+ ok(
+ tabLabel.startsWith(fileName),
+ `"${tabLabel}" should have been escaped.`
+ );
+
+ pendingTest = "save browser";
+ pendingPromise = Promise.withResolvers();
+ // First try to save the browser
+ saveBrowser(browser);
+ await pendingPromise.promise;
+
+ // Next, try the context menu:
+ pendingTest = "save from context menu";
+ pendingPromise = Promise.withResolvers();
+ let menu = document.getElementById("contentAreaContextMenu");
+ let menuShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ selector,
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await menuShown;
+ gContextMenu.saveMedia();
+ menu.hidePopup();
+ await pendingPromise.promise;
+ pendingPromise = null;
+ });
+ }
+ await runTestFor("file_with@@funny_name.png", "img");
+ await runTestFor("file_with[funny_name.webm", "video");
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_extension_correction.js b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js
new file mode 100644
index 0000000000..9d67bf7213
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+let gPathsToRemove = [];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.useDownloadDir", true]],
+ });
+ registerCleanupFunction(async () => {
+ for (let path of gPathsToRemove) {
+ // IOUtils.remove ignores non-existing files out of the box.
+ await IOUtils.remove(path);
+ }
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ await publicList.removeFinished();
+ });
+});
+
+async function testLinkWithoutExtension(type, shouldHaveExtension) {
+ info("Checking " + type);
+
+ let task = function () {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [type], mimetype => {
+ let link = content.document.createElement("a");
+ link.textContent = "Click me";
+ link.href = "data:" + mimetype + ",hello";
+ link.download = "somefile";
+ content.document.body.appendChild(link);
+ link.click();
+ });
+ };
+
+ await checkDownloadWithExtensionState(task, {
+ type,
+ shouldHaveExtension,
+ alwaysViewPDFInline: false,
+ });
+
+ if (type == "application/pdf") {
+ // For PDF, try again with the always open inline preference set
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.open_pdf_attachments_inline", true]],
+ });
+
+ await checkDownloadWithExtensionState(task, {
+ type,
+ shouldHaveExtension,
+ alwaysViewPDFInline: true,
+ });
+
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+async function checkDownloadWithExtensionState(
+ task,
+ { type, shouldHaveExtension, expectedName = null, alwaysViewPDFInline }
+) {
+ const shouldExpectDialog = Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types",
+ false
+ );
+
+ let winPromise;
+ if (shouldExpectDialog) {
+ winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ }
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let shouldCheckFilename = shouldHaveExtension || !shouldExpectDialog;
+
+ let downloadFinishedPromise = shouldCheckFilename
+ ? promiseDownloadFinished(publicList)
+ : null;
+
+ // PDF should load using the internal viewer without downloading it.
+ let waitForLoad;
+ if (
+ (!shouldExpectDialog || alwaysViewPDFInline) &&
+ type == "application/pdf"
+ ) {
+ waitForLoad = BrowserTestUtils.waitForNewTab(gBrowser);
+ }
+
+ await task();
+ await waitForLoad;
+
+ let win;
+ if (shouldExpectDialog) {
+ info("Waiting for dialog.");
+ win = await winPromise;
+ }
+
+ expectedName ??= shouldHaveExtension
+ ? "somefile." + getMIMEInfoForType(type).primaryExtension
+ : "somefile";
+
+ let closedPromise = true;
+ if (shouldExpectDialog) {
+ let actualName = win.document.getElementById("location").value;
+ closedPromise = BrowserTestUtils.windowClosed(win);
+
+ if (shouldHaveExtension) {
+ is(actualName, expectedName, `${type} should get an extension`);
+ } else {
+ is(actualName, expectedName, `${type} should not get an extension`);
+ }
+ }
+
+ if (shouldExpectDialog && shouldHaveExtension) {
+ // Then pick "save" in the dialog, if we have a dialog.
+ let dialog = win.document.getElementById("unknownContentType");
+ win.document.getElementById("save").click();
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ dialog.acceptDialog();
+ }
+
+ if (!shouldExpectDialog && type == "application/pdf") {
+ if (alwaysViewPDFInline) {
+ is(
+ gURLBar.inputField.value,
+ "data:application/pdf,hello",
+ "url is correct for " + type
+ );
+ } else {
+ ok(
+ gURLBar.inputField.value.startsWith("file://") &&
+ gURLBar.inputField.value.endsWith("somefile.pdf"),
+ "url is correct for " + type
+ );
+ }
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ if (shouldExpectDialog || !alwaysViewPDFInline || type != "application/pdf") {
+ // Wait for the download if it exists (may produce null).
+ let download = await downloadFinishedPromise;
+ if (download) {
+ // Check the download's extension is correct.
+ is(
+ PathUtils.filename(download.target.path),
+ expectedName,
+ `Downloaded file should match ${expectedName}`
+ );
+ gPathsToRemove.push(download.target.path);
+ let pathToRemove = download.target.path;
+ // Avoid one file interfering with subsequent files.
+ await publicList.removeFinished();
+ await IOUtils.remove(pathToRemove);
+ } else if (win) {
+ // We just cancel out for files that would end up without a path, as we'd
+ // prompt for a filename.
+ win.close();
+ }
+ }
+
+ return closedPromise;
+}
+
+/**
+ * Check that for document types, images, videos and audio files,
+ * we enforce a useful extension.
+ */
+add_task(async function test_enforce_useful_extension() {
+ await BrowserTestUtils.withNewTab("data:text/html,", async browser => {
+ await testLinkWithoutExtension("image/png", true);
+ await testLinkWithoutExtension("audio/ogg", true);
+ await testLinkWithoutExtension("video/webm", true);
+ await testLinkWithoutExtension("application/pdf", true);
+
+ await testLinkWithoutExtension("application/x-nonsense", false);
+ await testLinkWithoutExtension("application/octet-stream", false);
+ await testLinkWithoutExtension("binary/octet-stream", false);
+ await testLinkWithoutExtension("application/x-msdownload", false);
+ });
+});
+
+/**
+ * Check that we still use URL extension info when we don't have anything else,
+ * despite bogus local info.
+ */
+add_task(async function test_broken_saved_handlerinfo_and_useless_mimetypes() {
+ let bogusType = getMIMEInfoForType("binary/octet-stream");
+ registerCleanupFunction(() => {
+ handlerSvc.remove(bogusType);
+ });
+ bogusType.setFileExtensions(["jpg"]);
+ let handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ handlerSvc.store(bogusType);
+ let tabToClean = null;
+ let task = function () {
+ return BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_as.exe?foo=bar",
+ waitForLoad: false,
+ waitForStateStop: true,
+ }).then(tab => {
+ return (tabToClean = tab);
+ });
+ };
+ await checkDownloadWithExtensionState(task, {
+ type: "binary/octet-stream",
+ shouldHaveExtension: true,
+ expectedName: "file_as.exe",
+ });
+ // Downloads should really close their tabs...
+ if (tabToClean?.isConnected) {
+ BrowserTestUtils.removeTab(tabToClean);
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_filehandling_loop.js b/uriloader/exthandler/tests/mochitest/browser_filehandling_loop.js
new file mode 100644
index 0000000000..0aabb222d9
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_filehandling_loop.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * If the user has set Firefox itself as a helper app,
+ * we should force prompting what to do, rather than ending up
+ * in an infinite loop.
+ * In an ideal world, we'd also test the case where we are the OS
+ * default handler app, but that would require test infrastructure
+ * to make ourselves the OS default (or at least fool ourselves into
+ * believing we are) which we don't have...
+ */
+add_task(async function test_helperapp() {
+ // Set up the test infrastructure:
+ const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ let handlerInfo = mimeSvc.getFromTypeAndExtension("application/x-foo", "foo");
+ registerCleanupFunction(() => {
+ handlerSvc.remove(handlerInfo);
+ });
+ // Say we want to use a specific app:
+ handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ handlerInfo.alwaysAskBeforeHandling = false;
+
+ // Say it's us:
+ let selfFile = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+ // Make sure it's the .app
+ if (AppConstants.platform == "macosx") {
+ while (
+ !selfFile.leafName.endsWith(".app") &&
+ !selfFile.leafName.endsWith(".app/")
+ ) {
+ selfFile = selfFile.parent;
+ }
+ }
+ let selfHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ selfHandlerApp.executable = selfFile;
+ handlerInfo.possibleApplicationHandlers.appendElement(selfHandlerApp);
+ handlerInfo.preferredApplicationHandler = selfHandlerApp;
+ handlerSvc.store(handlerInfo);
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ // Now, do some safety stubbing. If we do end up recursing we spawn
+ // infinite tabs. We definitely don't want that. Avoid it by stubbing
+ // our external URL handling bits:
+ let oldAddTab = gBrowser.addTab;
+ registerCleanupFunction(() => (gBrowser.addTab = oldAddTab));
+ let wrongThingHappenedPromise = new Promise(resolve => {
+ gBrowser.addTab = function (aURI) {
+ ok(false, "Tried to open unexpected URL in a tab: " + aURI);
+ resolve(null);
+ // Pass a dummy object to avoid upsetting BrowserContentHandler -
+ // if it thinks opening the tab failed, it tries to open a window instead,
+ // which we can't prevent as easily, and at which point we still end up
+ // with runaway tabs.
+ return {};
+ };
+ });
+
+ let askedUserPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+
+ info("Clicking a link that should open the unknown content type dialog");
+ await SpecialPowers.spawn(browser, [], () => {
+ let link = content.document.createElement("a");
+ link.download = "foo.foo";
+ link.textContent = "Foo file";
+ link.href = "data:application/x-foo,hello";
+ content.document.body.append(link);
+ link.click();
+ });
+ let dialog = await Promise.race([
+ wrongThingHappenedPromise,
+ askedUserPromise,
+ ]);
+ ok(dialog, "Should have gotten a dialog");
+ Assert.stringContains(
+ dialog.document.location.href,
+ "unknownContentType",
+ "Should have opened correct dialog."
+ );
+
+ let closePromise = BrowserTestUtils.windowClosed(dialog);
+ dialog.close();
+ await closePromise;
+ askedUserPromise = null;
+ });
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js
new file mode 100644
index 0000000000..df446f4c52
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_task(setupMailHandler);
+
+add_task(async function test_open_without_user_interaction() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.disable_open_during_load", true],
+ ["dom.block_external_protocol_in_iframes", true],
+ ["dom.delay.block_external_protocol_in_iframes.enabled", false],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ TEST_PATH + "file_external_protocol_iframe.html"
+ );
+
+ let dialog = await dialogWindowPromise;
+ ok(dialog, "Should show the dialog even without user interaction");
+
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+
+ // Adding another iframe without user interaction should be blocked.
+ let blockedWarning = new Promise(resolve => {
+ Services.console.registerListener(function onMessage(msg) {
+ let { message, logLevel } = msg;
+ if (logLevel != Ci.nsIConsoleMessage.warn) {
+ return;
+ }
+ if (!message.includes("Iframe with external protocol was blocked")) {
+ return;
+ }
+ Services.console.unregisterListener(onMessage);
+ resolve();
+ });
+ });
+
+ info("Adding another frame without user interaction");
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let frame = content.document.createElement("iframe");
+ frame.src = "mailto:foo@baz.com";
+ content.document.body.appendChild(frame);
+ });
+
+ await blockedWarning;
+
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_ftp_protocol_handlers.js b/uriloader/exthandler/tests/mochitest/browser_ftp_protocol_handlers.js
new file mode 100644
index 0000000000..6b7c70cc01
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_ftp_protocol_handlers.js
@@ -0,0 +1,111 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+let testURL =
+ "https://example.com/browser/" +
+ "uriloader/exthandler/tests/mochitest/FTPprotocolHandler.html";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", false]],
+ });
+
+ // Load a page registering a protocol handler.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, testURL);
+ await BrowserTestUtils.browserLoaded(browser, false, testURL);
+
+ // Register the protocol handler by clicking the notificationbar button.
+ let notificationValue = "Protocol Registration: ftp";
+ let getNotification = () =>
+ gBrowser.getNotificationBox().getNotificationWithValue(notificationValue);
+ await BrowserTestUtils.waitForCondition(getNotification);
+ let notification = getNotification();
+ let button = notification.buttonContainer.querySelector("button");
+ ok(button, "got registration button");
+ button.click();
+
+ // Set the new handler as default.
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("ftp");
+ ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set");
+ let handlers = protoInfo.possibleApplicationHandlers;
+ is(1, handlers.length, "only one handler registered for ftp");
+ let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp);
+ ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler");
+ is(
+ handler.uriTemplate,
+ "https://example.com/browser/uriloader/exthandler/tests/mochitest/blank.html?uri=%s",
+ "correct url template"
+ );
+ protoInfo.preferredAction = protoInfo.useHelperApp;
+ protoInfo.preferredApplicationHandler = handler;
+ protoInfo.alwaysAskBeforeHandling = false;
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ handlerSvc.store(protoInfo);
+
+ const expectedURL =
+ "https://example.com/browser/uriloader/exthandler/tests/mochitest/blank.html?uri=ftp%3A%2F%2Fdomain.com%2Fpath";
+
+ // Middle-click a testprotocol link and check the new tab is correct
+ let link = "#link";
+
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
+ await BrowserTestUtils.synthesizeMouseAtCenter(link, { button: 1 }, browser);
+ let tab = await promiseTabOpened;
+ gBrowser.selectedTab = tab;
+ is(
+ gURLBar.value,
+ UrlbarTestUtils.trimURL(expectedURL),
+ "the expected URL is displayed in the location bar"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ // Shift-click the testprotocol link and check the new window.
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: expectedURL,
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ link,
+ { shiftKey: true },
+ browser
+ );
+ let win = await newWindowPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == expectedURL
+ );
+ is(
+ win.gURLBar.value,
+ UrlbarTestUtils.trimURL(expectedURL),
+ "the expected URL is displayed in the location bar"
+ );
+ await BrowserTestUtils.closeWindow(win);
+
+ // Click the testprotocol link and check the url in the current tab.
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser);
+ await loadPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => gURLBar.value != UrlbarTestUtils.trimURL(testURL)
+ );
+ is(
+ gURLBar.value,
+ UrlbarTestUtils.trimURL(expectedURL),
+ "the expected URL is displayed in the location bar"
+ );
+
+ // Cleanup.
+ protoInfo.preferredApplicationHandler = null;
+ handlers.removeElementAt(0);
+ handlerSvc.store(protoInfo);
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_launched_app_save_directory.js b/uriloader/exthandler/tests/mochitest/browser_launched_app_save_directory.js
new file mode 100644
index 0000000000..d978410263
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_launched_app_save_directory.js
@@ -0,0 +1,111 @@
+const { DownloadIntegration } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+ const allowDirectoriesVal = DownloadIntegration.allowDirectories;
+ DownloadIntegration.allowDirectories = true;
+ registerCleanupFunction(() => {
+ DownloadIntegration.allowDirectories = allowDirectoriesVal;
+ Services.prefs.clearUserPref("browser.download.dir");
+ Services.prefs.clearUserPref("browser.download.folderList");
+ });
+});
+
+async function aDownloadLaunchedWithAppIsSavedInFolder(downloadDir) {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+ let initialTabsCount = gBrowser.tabs.length;
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_green.webp",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ let download = await downloadFinishedPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => gBrowser.tabs.length == initialTabsCount + 2
+ );
+
+ gBrowser.removeCurrentTab();
+ BrowserTestUtils.removeTab(loadingTab);
+
+ ok(
+ download.target.path.startsWith(downloadDir),
+ "Download should be placed in default download directory: " +
+ downloadDir +
+ ", and it's located in " +
+ download.target.path
+ );
+
+ Assert.ok(
+ await IOUtils.exists(download.target.path),
+ "The file should not have been deleted."
+ );
+
+ try {
+ info("removing " + download.target.path);
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ info("The file " + download.target.path + " is not removed, " + ex);
+ }
+}
+
+add_task(async function aDownloadLaunchedWithAppIsSavedInCustomDir() {
+ //Test the temp dir.
+ let time = new Date().getTime();
+ let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempDir.append(time);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
+ let downloadDir = await DownloadIntegration.getPreferredDownloadsDirectory();
+ Assert.notEqual(downloadDir, "");
+ Assert.equal(downloadDir, tempDir.path);
+ Assert.ok(await IOUtils.exists(downloadDir));
+ registerCleanupFunction(async () => {
+ await IOUtils.remove(tempDir.path, { recursive: true });
+ });
+ await aDownloadLaunchedWithAppIsSavedInFolder(downloadDir);
+});
+
+add_task(async function aDownloadLaunchedWithAppIsSavedInDownloadsDir() {
+ // Test the system downloads directory.
+ Services.prefs.setIntPref("browser.download.folderList", 1);
+ let systemDir = await DownloadIntegration.getSystemDownloadsDirectory();
+ let downloadDir = await DownloadIntegration.getPreferredDownloadsDirectory();
+ Assert.notEqual(downloadDir, "");
+ Assert.equal(downloadDir, systemDir);
+
+ await aDownloadLaunchedWithAppIsSavedInFolder(downloadDir);
+});
+
+add_task(async function aDownloadLaunchedWithAppIsSavedInDesktopDir() {
+ // Test the desktop directory.
+ Services.prefs.setIntPref("browser.download.folderList", 0);
+ let downloadDir = await DownloadIntegration.getPreferredDownloadsDirectory();
+ Assert.notEqual(downloadDir, "");
+ Assert.equal(downloadDir, Services.dirsvc.get("Desk", Ci.nsIFile).path);
+
+ await aDownloadLaunchedWithAppIsSavedInFolder(downloadDir);
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_local_files_no_save_without_asking.js b/uriloader/exthandler/tests/mochitest/browser_local_files_no_save_without_asking.js
new file mode 100644
index 0000000000..cfa4788a43
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_local_files_no_save_without_asking.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Check that loading a local PDF file
+ * prompts the user when pdfjs.disabled is set to true,
+ * and alwaysAsk is false;
+ */
+add_task(
+ async function test_check_browser_local_files_no_save_without_asking() {
+ // Get a ref to the pdf we want to open.
+ let file = getChromeDir(getResolvedURI(gTestPath));
+ file.append("file_pdf_binary_octet_stream.pdf");
+
+ await SpecialPowers.pushPrefEnv({ set: [["pdfjs.disabled", true]] });
+
+ const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const handlerSvc = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+ ].getService(Ci.nsIHandlerService);
+ const mimeInfo = mimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+ // This test covers a bug that only occurs when the mimeInfo is set to Always Ask = false
+ // Here we check if we ask the user what to do for local files, if the file is set to save to disk automatically;
+ // that is, we check that we prompt the user despite the user's preference.
+ mimeInfo.preferredAction = mimeInfo.saveToDisk;
+ mimeInfo.alwaysAskBeforeHandling = false;
+ handlerSvc.store(mimeInfo);
+
+ info("Testing with " + file.path);
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+
+ let publicDownloads = await publicList.getAll();
+ is(
+ publicDownloads.length,
+ 0,
+ "download should not appear in publicDownloads list"
+ );
+
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ var loadingTab = BrowserTestUtils.addTab(gBrowser, file.path);
+
+ let dialogWindow = await dialogWindowPromise;
+
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ let doc = dialogWindow.document;
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ }
+);
diff --git a/uriloader/exthandler/tests/mochitest/browser_local_files_open_doesnt_duplicate.js b/uriloader/exthandler/tests/mochitest/browser_local_files_open_doesnt_duplicate.js
new file mode 100644
index 0000000000..d8e7c87c10
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_local_files_open_doesnt_duplicate.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+let mimeInfo = mimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({ set: [["pdfjs.disabled", true]] });
+
+ let oldAsk = mimeInfo.alwaysAskBeforeHandling;
+ let oldPreferredAction = mimeInfo.preferredAction;
+ let oldPreferredApp = mimeInfo.preferredApplicationHandler;
+ registerCleanupFunction(() => {
+ mimeInfo.preferredApplicationHandler = oldPreferredApp;
+ mimeInfo.preferredAction = oldPreferredAction;
+ mimeInfo.alwaysAskBeforeHandling = oldAsk;
+ handlerSvc.store(mimeInfo);
+ });
+
+ if (!mimeInfo.preferredApplicationHandler) {
+ let handlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ handlerApp.executable = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ handlerApp.executable.append("foopydoo.exe");
+ mimeInfo.possibleApplicationHandlers.appendElement(handlerApp);
+ mimeInfo.preferredApplicationHandler = handlerApp;
+ }
+});
+
+add_task(async function open_from_dialog() {
+ // Force PDFs to prompt:
+ mimeInfo.preferredAction = mimeInfo.useHelperApp;
+ mimeInfo.alwaysAskBeforeHandling = true;
+ handlerSvc.store(mimeInfo);
+
+ let openingPromise = TestUtils.topicObserved(
+ "test-only-opening-downloaded-file",
+ (subject, data) => {
+ subject.QueryInterface(Ci.nsISupportsPRBool);
+ // Block opening the file:
+ subject.data = false;
+ return true;
+ }
+ );
+
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let openedFile = getChromeDir(getResolvedURI(gTestPath));
+ openedFile.append("file_pdf_binary_octet_stream.pdf");
+ let expectedPath = openedFile.isSymlink()
+ ? openedFile.target
+ : openedFile.path;
+ let loadingTab = BrowserTestUtils.addTab(gBrowser, expectedPath);
+
+ let dialogWindow = await dialogWindowPromise;
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Should have seen the unknown content dialogWindow."
+ );
+
+ let doc = dialogWindow.document;
+
+ // Select the 'open' entry.
+ doc.querySelector("#open").click();
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.getButton("accept").removeAttribute("disabled");
+ dialog.acceptDialog();
+ let [, openedPath] = await openingPromise;
+ is(
+ openedPath,
+ expectedPath,
+ "Should have opened file directly (not created a copy)."
+ );
+ if (openedPath != expectedPath) {
+ await IOUtils.setPermissions(openedPath, 0o666);
+ await IOUtils.remove(openedPath);
+ }
+ BrowserTestUtils.removeTab(loadingTab);
+});
+
+add_task(async function open_directly() {
+ // Force PDFs to open immediately:
+ mimeInfo.preferredAction = mimeInfo.useHelperApp;
+ mimeInfo.alwaysAskBeforeHandling = false;
+ handlerSvc.store(mimeInfo);
+
+ let openingPromise = TestUtils.topicObserved(
+ "test-only-opening-downloaded-file",
+ (subject, data) => {
+ subject.QueryInterface(Ci.nsISupportsPRBool);
+ // Block opening the file:
+ subject.data = false;
+ return true;
+ }
+ );
+
+ let openedFile = getChromeDir(getResolvedURI(gTestPath));
+ openedFile.append("file_pdf_binary_octet_stream.pdf");
+ let expectedPath = openedFile.isSymlink()
+ ? openedFile.target
+ : openedFile.path;
+ let loadingTab = BrowserTestUtils.addTab(gBrowser, expectedPath);
+
+ let [, openedPath] = await openingPromise;
+ is(
+ openedPath,
+ expectedPath,
+ "Should have opened file directly (not created a copy)."
+ );
+ if (openedPath != expectedPath) {
+ await IOUtils.setPermissions(openedPath, 0o666);
+ await IOUtils.remove(openedPath);
+ }
+ BrowserTestUtils.removeTab(loadingTab);
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js
new file mode 100644
index 0000000000..424fd424f9
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js
@@ -0,0 +1,310 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+const { DownloadIntegration } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const {
+ handleInternally,
+ saveToDisk,
+ useSystemDefault,
+ alwaysAsk,
+ useHelperApp,
+} = Ci.nsIHandlerInfo;
+
+function waitForAcceptButtonToGetEnabled(doc) {
+ let dialog = doc.querySelector("#unknownContentType");
+ let button = dialog.getButton("accept");
+ return TestUtils.waitForCondition(
+ () => !button.disabled,
+ "Wait for Accept button to get enabled"
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Remove the security delay for the dialog during the test.
+ ["security.dialog_enable_delay", 0],
+ ["browser.helperApps.showOpenOptionForViewableInternally", true],
+ // Make sure we don't open a file picker dialog somehow.
+ ["browser.download.useDownloadDir", true],
+ ],
+ });
+
+ // Restore handlers after the whole test has run
+ const registerRestoreHandler = function (type, ext) {
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension(type, ext);
+ const existed = gHandlerSvc.exists(mimeInfo);
+ registerCleanupFunction(() => {
+ if (existed) {
+ gHandlerSvc.store(mimeInfo);
+ } else {
+ gHandlerSvc.remove(mimeInfo);
+ }
+ });
+ };
+ registerRestoreHandler("image/svg+xml", "svg");
+});
+
+function ensureMIMEState({ preferredAction, alwaysAskBeforeHandling }) {
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension("image/svg+xml", "svg");
+ mimeInfo.preferredAction = preferredAction;
+ mimeInfo.alwaysAskBeforeHandling = alwaysAskBeforeHandling;
+ gHandlerSvc.store(mimeInfo);
+}
+
+function waitDelay(delay) {
+ return new Promise((resolve, reject) => {
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ window.setTimeout(resolve, delay);
+ });
+}
+
+function promisePanelOpened() {
+ if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+ return Promise.resolve();
+ }
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+const kTestCasesPrefEnabled = [
+ {
+ description:
+ "Pref enabled - internal handling as default should not change prefs",
+ preDialogState: {
+ preferredAction: handleInternally,
+ alwaysAskBeforeHandling: false,
+ },
+ expectTab: true,
+ expectLaunch: false,
+ expectedPreferredAction: handleInternally,
+ expectedAlwaysAskBeforeHandling: false,
+ expectUCT: false,
+ },
+ {
+ description:
+ "Pref enabled - external handling as default should not change prefs",
+ preDialogState: {
+ preferredAction: useSystemDefault,
+ alwaysAskBeforeHandling: false,
+ },
+ expectTab: false,
+ expectLaunch: true,
+ expectedPreferredAction: useSystemDefault,
+ expectedAlwaysAskBeforeHandling: false,
+ expectUCT: false,
+ },
+ {
+ description: "Pref enabled - saveToDisk as default should not change prefs",
+ preDialogState: {
+ preferredAction: saveToDisk,
+ alwaysAskBeforeHandling: false,
+ },
+ expectTab: false,
+ expectLaunch: false,
+ expectedPreferredAction: saveToDisk,
+ expectedAlwaysAskBeforeHandling: false,
+ expectUCT: false,
+ },
+ {
+ description:
+ "Pref enabled - choose internal + alwaysAsk default + checkbox should update persisted default",
+ preDialogState: {
+ preferredAction: alwaysAsk,
+ alwaysAskBeforeHandling: false,
+ },
+ dialogActions(doc) {
+ let handleItem = doc.querySelector("#handleInternally");
+ handleItem.click();
+ ok(handleItem.selected, "The 'open' option should now be selected");
+ let checkbox = doc.querySelector("#rememberChoice");
+ checkbox.checked = true;
+ checkbox.doCommand();
+ },
+ // new tab will not launch in test environment when alwaysAsk is preferredAction
+ expectTab: false,
+ expectLaunch: false,
+ expectedPreferredAction: handleInternally,
+ expectedAlwaysAskBeforeHandling: false,
+ expectUCT: true,
+ },
+ {
+ description:
+ "Pref enabled - saveToDisk with alwaysAsk default should update persisted default",
+ preDialogState: {
+ preferredAction: alwaysAsk,
+ alwaysAskBeforeHandling: false,
+ },
+ dialogActions(doc) {
+ let saveItem = doc.querySelector("#save");
+ saveItem.click();
+ ok(saveItem.selected, "The 'save' option should now be selected");
+ },
+ expectTab: false,
+ expectLaunch: false,
+ expectedPreferredAction: saveToDisk,
+ expectedAlwaysAskBeforeHandling: false,
+ expectUCT: true,
+ },
+];
+
+add_task(
+ async function test_check_saving_handler_choices_with_always_ask_before_handling_new_types_pref_enabled() {
+ SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", false]],
+ });
+
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let file = "file_image_svgxml.svg";
+
+ for (let testCase of kTestCasesPrefEnabled) {
+ info("Testing with " + file + "; " + testCase.description);
+ ensureMIMEState(testCase.preDialogState);
+ const { expectTab, expectLaunch, description, expectUCT } = testCase;
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let fileLaunched = Promise.withResolvers();
+ DownloadIntegration.launchFile = () => {
+ ok(
+ expectLaunch,
+ `The file should ${
+ expectLaunch ? "" : "not "
+ }be launched with an external application - ${description}`
+ );
+ fileLaunched.resolve();
+ };
+
+ info("Load window and tabs");
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + file,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ // See if UCT window appears in loaded tab.
+ let dialogWindow = await Promise.race([
+ waitDelay(1000),
+ dialogWindowPromise,
+ ]);
+
+ is(
+ !!dialogWindow,
+ expectUCT,
+ `UCT window should${expectUCT ? "" : " not"} have appeared`
+ );
+
+ let download;
+
+ if (dialogWindow) {
+ is(
+ dialogWindow.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Unknown content dialogWindow should be loaded correctly."
+ );
+ let doc = dialogWindow.document;
+ let internalHandlerRadio = doc.querySelector("#handleInternally");
+
+ info("Waiting for accept button to get enabled");
+ await waitForAcceptButtonToGetEnabled(doc);
+
+ ok(
+ !internalHandlerRadio.hidden,
+ "The option should be visible for SVG"
+ );
+
+ info("Running UCT dialog options before downloading file");
+ await testCase.dialogActions(doc);
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.acceptDialog();
+
+ info("Waiting for downloads to finish");
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+ download = await downloadFinishedPromise;
+ } else {
+ let downloadPanelPromise = promisePanelOpened();
+ await downloadPanelPromise;
+ is(
+ DownloadsPanel.isPanelShowing,
+ true,
+ "DownloadsPanel should be open"
+ );
+
+ info("Skipping UCT dialog options");
+ info("Waiting for downloads to finish");
+ // Unlike when the UCT window opens, the download immediately starts.
+ let downloadList = await publicList;
+ [download] = downloadList._downloads;
+ }
+
+ if (expectLaunch) {
+ info("Waiting for launch to finish");
+ await fileLaunched.promise;
+ }
+ DownloadIntegration.launchFile = oldLaunchFile;
+
+ is(
+ download.contentType,
+ "image/svg+xml",
+ "File contentType should be correct"
+ );
+ is(
+ download.source.url,
+ `${TEST_PATH + file}`,
+ "File name should be correct."
+ );
+ is(
+ (await publicList.getAll()).length,
+ 1,
+ "download should appear in public list"
+ );
+
+ // Check mime info:
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension("image/svg+xml", "svg");
+ gHandlerSvc.fillHandlerInfo(mimeInfo, "");
+ is(
+ mimeInfo.preferredAction,
+ testCase.expectedPreferredAction,
+ "preferredAction - " + description
+ );
+ is(
+ mimeInfo.alwaysAskBeforeHandling,
+ testCase.expectedAlwaysAskBeforeHandling,
+ "alwaysAskBeforeHandling - " + description
+ );
+
+ info("Cleaning up");
+ BrowserTestUtils.removeTab(loadingTab);
+ // By default, if internal is default with pref enabled, we view the svg file in
+ // in a new tab. Close this tab in order for the test case to pass.
+ if (expectTab && testCase.preferredAction !== alwaysAsk) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ await publicList.removeFinished();
+ if (download?.target.exists) {
+ try {
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ }
+ }
+);
diff --git a/uriloader/exthandler/tests/mochitest/browser_pdf_save_as.js b/uriloader/exthandler/tests/mochitest/browser_pdf_save_as.js
new file mode 100644
index 0000000000..a08fe342cc
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_pdf_save_as.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const { saveToDisk, alwaysAsk, handleInternally, useSystemDefault } =
+ Ci.nsIHandlerInfo;
+const MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+async function testPdfFilePicker(mimeInfo) {
+ await BrowserTestUtils.withNewTab(
+ `data:text/html,<a id="test-link" href="${TEST_PATH}/file_pdf_application_pdf.pdf">Test PDF Link</a>`,
+ async browser => {
+ let menu = document.getElementById("contentAreaContextMenu");
+ ok(menu, "Context menu exists on the page");
+
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a#test-link",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShown;
+ info("Context menu popup was successfully displayed");
+
+ let filePickerPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = fp => {
+ ok(true, "filepicker should be visible");
+ ok(
+ fp.defaultExtension === "pdf",
+ "Default extension in filepicker should be pdf"
+ );
+ ok(
+ fp.defaultString === "file_pdf_application_pdf.pdf",
+ "Default string name in filepicker should have the correct pdf file name"
+ );
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ });
+
+ let menuitem = menu.querySelector("#context-savelink");
+ menu.activateItem(menuitem);
+ await filePickerPromise;
+ }
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", false],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ await publicList.removeFinished();
+
+ if (DownloadsPanel.isVisible) {
+ info("Closing downloads panel");
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+ }
+
+ let mimeInfo = MIMEService.getFromTypeAndExtension(
+ "application/pdf",
+ "pdf"
+ );
+ let existed = HandlerService.exists(mimeInfo);
+ if (existed) {
+ HandlerService.store(mimeInfo);
+ } else {
+ HandlerService.remove(mimeInfo);
+ }
+
+ // We only want to run MockFilerPicker.cleanup after the entire test is run.
+ // Otherwise, we cannot use MockFilePicker for each preferredAction.
+ MockFilePicker.cleanup();
+ });
+});
+
+/**
+ * Tests that selecting the context menu item `Save Link As…` on a PDF link
+ * opens the file picker when always_ask_before_handling_new_types is disabled,
+ * regardless of preferredAction.
+ */
+add_task(async function test_pdf_save_as_link() {
+ let mimeInfo;
+
+ for (let preferredAction of [
+ saveToDisk,
+ alwaysAsk,
+ handleInternally,
+ useSystemDefault,
+ ]) {
+ mimeInfo = MIMEService.getFromTypeAndExtension("application/pdf", "pdf");
+ mimeInfo.alwaysAskBeforeHandling = preferredAction === alwaysAsk;
+ mimeInfo.preferredAction = preferredAction;
+ HandlerService.store(mimeInfo);
+
+ info(`Testing filepicker for preferredAction ${preferredAction}`);
+ await testPdfFilePicker(mimeInfo);
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js
new file mode 100644
index 0000000000..7244aec503
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js
@@ -0,0 +1,467 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const CONTENT_HANDLING_URL =
+ "chrome://mozapps/content/handling/appChooser.xhtml";
+
+add_task(setupMailHandler);
+
+/**
+ * Check that if we open the protocol handler dialog from a subframe, we close
+ * it when closing the tab.
+ */
+add_task(async function test_closed_by_tab_closure() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_nested_protocol_request.html"
+ );
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed.");
+});
+
+/**
+ * Check that if we open the protocol handler dialog from a subframe, we close
+ * it when navigating the tab to a non-same-origin URL.
+ */
+add_task(async function test_closed_by_tab_navigation() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_nested_protocol_request.html"
+ );
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ info(
+ "Set up unload handler to ensure we don't break when the window global gets cleared"
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.addEventListener("unload", function () {});
+ });
+
+ info("Navigating tab to a different but same origin page.");
+ BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, TEST_PATH);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, TEST_PATH);
+ ok(dialog._frame.contentWindow, "Dialog should stay open.");
+
+ // The use of weak references in various parts of the code means that we're
+ // susceptible to dropping crucial bits of our implementation on the floor,
+ // if they get GC'd, and then the test hangs.
+ // Do a bunch of GC/CC runs so that if we ever break, it's deterministic.
+ let numCycles = 3;
+ for (let i = 0; i < numCycles; i++) {
+ Cu.forceGC();
+ Cu.forceCC();
+ }
+
+ info("Now navigate to a cross-origin page.");
+ const CROSS_ORIGIN_TEST_PATH = TEST_PATH.replace(".com", ".org");
+ BrowserTestUtils.startLoadingURIString(
+ tab.linkedBrowser,
+ CROSS_ORIGIN_TEST_PATH
+ );
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false,
+ CROSS_ORIGIN_TEST_PATH
+ );
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed.");
+
+ // Avoid errors from aborted loads by waiting for it to finish.
+ await loadPromise;
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Check that we cannot open more than one of these dialogs.
+ */
+add_task(async function test_multiple_dialogs() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_nested_protocol_request.html"
+ );
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+
+ // Navigate the parent frame:
+ await ContentTask.spawn(tab.linkedBrowser, [], () =>
+ content.eval("location.href = 'mailto:help@example.com'")
+ );
+
+ // Wait for a few ticks:
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 100));
+ // Check we only have one dialog
+
+ let tabDialogBox = gBrowser.getTabDialogBox(tab.linkedBrowser);
+ let dialogs = tabDialogBox
+ .getTabDialogManager()
+ ._dialogs.filter(d => d._openedURL == CONTENT_HANDLING_URL);
+
+ is(dialogs.length, 1, "Should only have 1 dialog open");
+
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ dialog = await dialogClosedPromise;
+
+ ok(!dialog._openedURL, "The dialog should have been closed.");
+
+ // Then reopen the dialog again, to make sure we don't keep blocking:
+ dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Second dialog URL is as expected"
+ );
+
+ dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed again.");
+});
+
+/**
+ * Check that navigating invisible frames to external-proto URLs
+ * is handled correctly.
+ */
+add_task(async function invisible_iframes() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ // Ensure we notice the dialog opening:
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let frame = content.document.createElement("iframe");
+ frame.style.display = "none";
+ frame.src = "mailto:help@example.com";
+ content.document.body.append(frame);
+ });
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog opens as expected for invisible iframe"
+ );
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ await dialogClosedPromise;
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Check that nested iframes are handled correctly.
+ */
+add_task(async function nested_iframes() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ // Ensure we notice the dialog opening:
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ let innerLoaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true,
+ "https://example.org/"
+ );
+ info("Constructing top frame");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let frame = content.document.createElement("iframe");
+ frame.src = "https://example.org/"; // cross-origin frame.
+ content.document.body.prepend(frame);
+
+ content.eval(
+ `window.addEventListener("message", e => e.source.location = "mailto:help@example.com");`
+ );
+ });
+
+ await innerLoaded;
+ let parentBC = tab.linkedBrowser.browsingContext;
+
+ info("Creating innermost frame");
+ await SpecialPowers.spawn(parentBC.children[0], [], async function () {
+ let innerFrame = content.document.createElement("iframe");
+ let frameLoaded = ContentTaskUtils.waitForEvent(innerFrame, "load", true);
+ content.document.body.prepend(innerFrame);
+ await frameLoaded;
+ });
+
+ info("Posting event from innermost frame");
+ await SpecialPowers.spawn(
+ parentBC.children[0].children[0],
+ [],
+ async function () {
+ // Top browsing context needs reference to the innermost, which is cross origin.
+ content.eval("top.postMessage('hello', '*')");
+ }
+ );
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog opens as expected for deeply nested cross-origin iframe"
+ );
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ await dialogClosedPromise;
+ gBrowser.removeTab(tab);
+});
+
+add_task(async function test_oop_iframe() {
+ const URI = `data:text/html,<div id="root"><iframe src="http://example.com/document-builder.sjs?html=<a href='mailto:help@example.com'>Mail it</a>"></iframe></div>`;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI);
+
+ // Wait for the window and then click the link.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a:link",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog URL is as expected"
+ );
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+
+ info("Removing tab to close the dialog.");
+ gBrowser.removeTab(tab);
+ await dialogClosedPromise;
+ ok(!dialog._frame.contentWindow, "The dialog should have been closed.");
+});
+
+/**
+ * Check that a cross-origin iframe can navigate the top frame
+ * to an external protocol.
+ */
+add_task(async function xorigin_iframe_can_navigate_top() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ // Ensure we notice the dialog opening:
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ true
+ );
+ let innerLoaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true,
+ "https://example.org/"
+ );
+ info("Constructing frame");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let frame = content.document.createElement("iframe");
+ frame.src = "https://example.org/"; // cross-origin frame.
+ content.document.body.prepend(frame);
+ });
+ await innerLoaded;
+
+ info("Navigating top bc from frame");
+ let parentBC = tab.linkedBrowser.browsingContext;
+ await SpecialPowers.spawn(parentBC.children[0], [], async function () {
+ content.eval("window.top.location.href = 'mailto:example@example.com';");
+ });
+
+ let dialog = await dialogWindowPromise;
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Dialog opens as expected for navigating the top frame from an x-origin frame."
+ );
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ tab.linkedBrowser,
+ false
+ );
+ dialog.close();
+ await dialogClosedPromise;
+ gBrowser.removeTab(tab);
+});
+
+/**
+ * Check that when navigating to an external protocol from an iframe in a
+ * background tab, we show the dialog in the correct tab.
+ */
+add_task(async function iframe_background_tab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+
+ let innerLoaded = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ true,
+ "https://example.org/"
+ );
+ info("Constructing frame");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ let frame = content.document.createElement("iframe");
+ frame.src = "https://example.org/";
+ content.document.body.prepend(frame);
+ });
+ await innerLoaded;
+
+ info("Switching to new tab");
+ let newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.net/"
+ );
+
+ // Wait for the chooser dialog to open in the background tab. It should not
+ // open in the foreground tab which is unrelated to the external protocol
+ // navigation.
+ let dialogWindowPromise = waitForProtocolAppChooserDialog(gBrowser, true);
+
+ info("Navigating to external proto from frame in background tab");
+ let parentBC = tab.linkedBrowser.browsingContext;
+ await SpecialPowers.spawn(parentBC.children[0], [], async function () {
+ content.eval("location.href = 'mailto:example@example.com';");
+ });
+
+ // Wait for dialog to open in one of the tabs.
+ let dialog = await dialogWindowPromise;
+
+ is(
+ gBrowser.getTabDialogBox(tab.linkedBrowser)._tabDialogManager._topDialog,
+ dialog,
+ "Dialog opened in the background tab"
+ );
+
+ is(
+ dialog._frame.contentDocument.location.href,
+ CONTENT_HANDLING_URL,
+ "Opened dialog is appChooser dialog."
+ );
+
+ // Close the dialog:
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(gBrowser, false);
+ dialog.close();
+ await dialogClosedPromise;
+
+ gBrowser.removeTab(tab);
+ gBrowser.removeTab(newTab);
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js
new file mode 100644
index 0000000000..591f1afbc5
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_external.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+/**
+ * Creates dummy protocol handler
+ */
+function initTestHandlers() {
+ let handlerInfoThatAsks =
+ HandlerServiceTestUtils.getBlankHandlerInfo("local-app-test");
+
+ let appHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ // This is a dir and not executable, but that's enough for here.
+ appHandler.executable = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ handlerInfoThatAsks.possibleApplicationHandlers.appendElement(appHandler);
+ handlerInfoThatAsks.preferredApplicationHandler = appHandler;
+ handlerInfoThatAsks.preferredAction = handlerInfoThatAsks.useHelperApp;
+ handlerInfoThatAsks.alwaysAskBeforeHandling = false;
+ gHandlerService.store(handlerInfoThatAsks);
+
+ let webHandlerInfo =
+ HandlerServiceTestUtils.getBlankHandlerInfo("web+somesite");
+ let webHandler = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(Ci.nsIWebHandlerApp);
+ webHandler.name = "Somesite";
+ webHandler.uriTemplate = "https://example.com/handle_url?u=%s";
+ webHandlerInfo.possibleApplicationHandlers.appendElement(webHandler);
+ webHandlerInfo.preferredApplicationHandler = webHandler;
+ webHandlerInfo.preferredAction = webHandlerInfo.useHelperApp;
+ webHandlerInfo.alwaysAskBeforeHandling = false;
+ gHandlerService.store(webHandlerInfo);
+
+ registerCleanupFunction(() => {
+ gHandlerService.remove(webHandlerInfo);
+ gHandlerService.remove(handlerInfoThatAsks);
+ });
+}
+
+function makeCmdLineHelper(url) {
+ return Cu.createCommandLine(
+ ["-url", url],
+ null,
+ Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+ );
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.protocol-handler.prompt-from-external", true]],
+ });
+ initTestHandlers();
+});
+
+/**
+ * Check that if we get a direct request from another app / the OS to open a
+ * link, we always prompt, even if we think we know what the correct answer
+ * is. This is to avoid infinite loops in such situations where the OS and
+ * Firefox have conflicting ideas about the default handler, or where our
+ * checks with the OS don't work (Linux and/or Snap, at time of this comment).
+ */
+add_task(async function test_external_asks_anyway() {
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+ let chooserDialogOpenPromise = waitForProtocolAppChooserDialog(
+ gBrowser,
+ true
+ );
+ let fakeCmdLine = makeCmdLineHelper("local-app-test:dummy");
+ cmdLineHandler.handle(fakeCmdLine);
+ let dialog = await chooserDialogOpenPromise;
+ ok(dialog, "Should have prompted.");
+
+ let dialogClosedPromise = waitForProtocolAppChooserDialog(
+ gBrowser.selectedBrowser,
+ false
+ );
+ let dialogEl = dialog._frame.contentDocument.querySelector("dialog");
+ dialogEl.cancelDialog();
+ await dialogClosedPromise;
+ // We will have opened a tab; close it.
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Like the previous test, but avoid asking for web and extension handlers,
+ * as we can open those ourselves without looping.
+ */
+add_task(async function test_web_app_doesnt_ask() {
+ // Listen for a dialog open and fail the test if it does:
+ let dialogOpenListener = () => ok(false, "Shouldn't have opened a dialog!");
+ document.documentElement.addEventListener("dialogopen", dialogOpenListener);
+ registerCleanupFunction(() =>
+ document.documentElement.removeEventListener(
+ "dialogopen",
+ dialogOpenListener
+ )
+ );
+
+ // Set up a promise for a tab to open with the right URL:
+ const kURL = "web+somesite:dummy";
+ const kLoadedURL =
+ "https://example.com/handle_url?u=" + encodeURIComponent(kURL);
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, kLoadedURL);
+
+ // Load the URL:
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+ let fakeCmdLine = makeCmdLineHelper(kURL);
+ cmdLineHandler.handle(fakeCmdLine);
+
+ // Check that the tab loaded. If instead the dialog opened, the dialogopen handler
+ // will fail the test.
+ let tab = await tabPromise;
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ kLoadedURL,
+ "Should have opened the right URL."
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ // We do this both here and in cleanup so it's easy to add tasks to this test,
+ // and so we clean up correctly if the test aborts before we get here.
+ document.documentElement.removeEventListener(
+ "dialogopen",
+ dialogOpenListener
+ );
+});
+
+add_task(async function external_https_redirect_doesnt_ask() {
+ Services.perms.addFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ "https://example.com"
+ ),
+ "open-protocol-handler^local-app-test",
+ Services.perms.ALLOW_ACTION
+ );
+ // Listen for a dialog open and fail the test if it does:
+ let dialogOpenListener = () => ok(false, "Shouldn't have opened a dialog!");
+ document.documentElement.addEventListener("dialogopen", dialogOpenListener);
+ registerCleanupFunction(() => {
+ document.documentElement.removeEventListener(
+ "dialogopen",
+ dialogOpenListener
+ );
+ Services.perms.removeAll();
+ });
+
+ let initialTab = gBrowser.selectedTab;
+
+ gHandlerService.wrappedJSObject.mockProtocolHandler("local-app-test");
+ registerCleanupFunction(() =>
+ gHandlerService.wrappedJSObject.mockProtocolHandler()
+ );
+
+ // Set up a promise for an app to have launched with the right URI:
+ let loadPromise = TestUtils.topicObserved("mocked-protocol-handler");
+
+ // Load the URL:
+ const kURL = "local-app-test:redirect";
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+ let fakeCmdLine = makeCmdLineHelper(
+ TEST_PATH + "redirect_helper.sjs?uri=" + encodeURIComponent(kURL)
+ );
+ cmdLineHandler.handle(fakeCmdLine);
+
+ // Check that the mock app was launched. If the dialog showed instead,
+ // the test will fail.
+ let [uri] = await loadPromise;
+ is(uri.spec, "local-app-test:redirect", "Should have seen correct URI.");
+ // We might have opened a blank tab, see bug 1718104 and friends.
+ if (gBrowser.selectedTab != initialTab) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+
+ // We do this both here and in cleanup so it's easy to add tasks to this test,
+ // and so we clean up correctly if the test aborts before we get here.
+ document.documentElement.removeEventListener(
+ "dialogopen",
+ dialogOpenListener
+ );
+ gHandlerService.wrappedJSObject.mockProtocolHandler();
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
new file mode 100644
index 0000000000..c567722b6d
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
@@ -0,0 +1,1383 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+const ROOT_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ ""
+);
+
+// Testing multiple protocol / origin combinations takes long on debug.
+requestLongerTimeout(7);
+
+const DIALOG_URL_APP_CHOOSER =
+ "chrome://mozapps/content/handling/appChooser.xhtml";
+const DIALOG_URL_PERMISSION =
+ "chrome://mozapps/content/handling/permissionDialog.xhtml";
+
+const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler";
+const PERMISSION_KEY_DELIMITER = "^";
+
+const TEST_PROTOS = ["foo", "bar"];
+
+let testDir = getChromeDir(getResolvedURI(gTestPath));
+
+const ORIGIN1 = "https://example.com";
+const ORIGIN2 = "https://example.org";
+const ORIGIN3 = Services.io.newFileURI(testDir).spec;
+const PRINCIPAL1 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN1);
+const PRINCIPAL2 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN2);
+const PRINCIPAL3 =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(ORIGIN3);
+
+const NULL_PRINCIPAL_SCHEME = Services.scriptSecurityManager
+ .createNullPrincipal({})
+ .scheme.toLowerCase();
+
+/**
+ * Get the open protocol handler permission key for a given protocol scheme.
+ * @param {string} aProtocolScheme - Scheme of protocol to construct permission
+ * key with.
+ */
+function getSkipProtoDialogPermissionKey(aProtocolScheme) {
+ return (
+ PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + aProtocolScheme
+ );
+}
+
+function getSystemProtocol() {
+ // TODO add a scheme for Windows 10 or greater once support is added (see bug 1764599).
+ if (AppConstants.platform == "macosx") {
+ return "itunes";
+ }
+
+ info(
+ "Skipping this test since there isn't a suitable default protocol on this platform"
+ );
+ return null;
+}
+
+/**
+ * Creates dummy web protocol handlers used for testing.
+ */
+function initTestHandlers() {
+ TEST_PROTOS.forEach(scheme => {
+ let webHandler = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(Ci.nsIWebHandlerApp);
+ webHandler.name = scheme + "Handler";
+ webHandler.uriTemplate = ORIGIN1 + "/?url=%s";
+
+ let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(scheme);
+ handlerInfo.possibleApplicationHandlers.appendElement(webHandler);
+ handlerInfo.preferredApplicationHandler = webHandler;
+ gHandlerService.store(handlerInfo);
+ });
+}
+
+/**
+ * Update whether the protocol handler dialog is shown for our test protocol +
+ * handler.
+ * @param {string} scheme - Scheme of the protocol to change the ask state for.
+ * @param {boolean} ask - true => show dialog, false => skip dialog.
+ */
+function updateAlwaysAsk(scheme, ask) {
+ let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme);
+ handlerInfo.alwaysAskBeforeHandling = ask;
+ gHandlerService.store(handlerInfo);
+}
+
+/**
+ * Test whether the protocol handler dialog is set to show for our
+ * test protocol + handler.
+ * @param {string} scheme - Scheme of the protocol to test the ask state for.
+ * @param {boolean} ask - true => show dialog, false => skip dialog.
+ */
+function testAlwaysAsk(scheme, ask) {
+ is(
+ HandlerServiceTestUtils.getHandlerInfo(scheme).alwaysAskBeforeHandling,
+ ask,
+ "Should have correct alwaysAsk state."
+ );
+}
+
+/**
+ * Triggers the load via a server redirect.
+ * @param {string} serverRedirect - The redirect type.
+ */
+function useServerRedirect(serverRedirect) {
+ return async (browser, scheme) => {
+ let uri = `${scheme}://test`;
+
+ let innerParams = new URLSearchParams();
+ innerParams.set("uri", uri);
+ innerParams.set("redirectType", serverRedirect);
+ let params = new URLSearchParams();
+ params.set(
+ "uri",
+ "https://example.com/" +
+ ROOT_PATH +
+ "redirect_helper.sjs?" +
+ innerParams.toString()
+ );
+ uri =
+ "https://example.org/" +
+ ROOT_PATH +
+ "redirect_helper.sjs?" +
+ params.toString();
+ BrowserTestUtils.startLoadingURIString(browser, uri);
+ };
+}
+
+/**
+ * Triggers the load with a specific principal or the browser's current
+ * principal.
+ * @param {nsIPrincipal} [principal] - Principal to use to trigger the load.
+ */
+function useTriggeringPrincipal(principal = undefined) {
+ return async (browser, scheme) => {
+ let uri = `${scheme}://test`;
+ let triggeringPrincipal = principal ?? browser.contentPrincipal;
+
+ info("Loading uri: " + uri);
+ browser.loadURI(Services.io.newURI(uri), { triggeringPrincipal });
+ };
+}
+
+/**
+ * Navigates to a test URL with the given protocol scheme and waits for the
+ * result.
+ * @param {MozBrowser} browser - Browser to navigate.
+ * @param {string} scheme - Scheme of the test url. e.g. irc
+ * @param {Object} [options] - Test options.
+ * @param {Object} [options.permDialogOptions] - Test options for the permission
+ * dialog. If defined, we expect this dialog to be shown.
+ * @param {Object} [options.chooserDialogOptions] - Test options for the chooser
+ * dialog. If defined, we expect this dialog to be shown.
+ * @param {Function} [options.triggerLoad] - An async callback function to
+ * trigger the load. Will be passed the browser and scheme to use.
+ * @param {nsIPrincipal} [options.triggeringPrincipal] - Principal to trigger
+ * the load with. Defaults to the browsers content principal.
+ * @returns {Promise} - A promise which resolves once the test is complete.
+ */
+async function testOpenProto(
+ browser,
+ scheme,
+ {
+ permDialogOptions,
+ chooserDialogOptions,
+ triggerLoad = useTriggeringPrincipal(),
+ } = {}
+) {
+ let permDialogOpenPromise;
+ let chooserDialogOpenPromise;
+
+ if (permDialogOptions) {
+ info("Should see permission dialog");
+ permDialogOpenPromise = waitForProtocolPermissionDialog(browser, true);
+ }
+
+ if (chooserDialogOptions) {
+ info("Should see chooser dialog");
+ chooserDialogOpenPromise = waitForProtocolAppChooserDialog(browser, true);
+ }
+ await triggerLoad(browser, scheme);
+ let webHandlerLoadedPromise;
+
+ let webHandlerShouldOpen =
+ (!permDialogOptions && !chooserDialogOptions) ||
+ ((permDialogOptions?.actionConfirm || permDialogOptions?.actionChangeApp) &&
+ chooserDialogOptions?.actionConfirm);
+
+ // Register web handler load listener if we expect to trigger it.
+ if (webHandlerShouldOpen) {
+ webHandlerLoadedPromise = waitForHandlerURL(browser, scheme);
+ }
+
+ if (permDialogOpenPromise) {
+ let dialog = await permDialogOpenPromise;
+ let dialogEl = getDialogElementFromSubDialog(dialog);
+ let dialogType = getDialogType(dialog);
+
+ let {
+ hasCheckbox,
+ checkboxOrigin,
+ hasChangeApp,
+ chooserIsNext,
+ actionCheckbox,
+ actionConfirm,
+ actionChangeApp,
+ checkContents,
+ } = permDialogOptions;
+
+ if (actionChangeApp) {
+ actionConfirm = false;
+ }
+
+ let descriptionEl = dialogEl.querySelector("#description");
+ ok(
+ descriptionEl && BrowserTestUtils.isVisible(descriptionEl),
+ "Has a visible description element."
+ );
+
+ ok(
+ !descriptionEl.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME),
+ "Description does not include NullPrincipal scheme."
+ );
+
+ await testCheckbox(dialogEl, dialogType, {
+ hasCheckbox,
+ actionCheckbox,
+ checkboxOrigin,
+ });
+
+ // Check the button label depending on whether we would show the chooser
+ // dialog next or directly open the handler.
+ let acceptBtnLabel = dialogEl.getButton("accept")?.label;
+
+ if (chooserIsNext) {
+ is(
+ acceptBtnLabel,
+ "Choose Application",
+ "Accept button has choose app label"
+ );
+ } else {
+ is(acceptBtnLabel, "Open Link", "Accept button has open link label");
+ }
+
+ let changeAppLink = dialogEl.ownerDocument.getElementById("change-app");
+ if (typeof hasChangeApp == "boolean") {
+ ok(changeAppLink, "Permission dialog should have changeApp link label");
+ is(
+ !changeAppLink.hidden,
+ hasChangeApp,
+ "Permission dialog change app link label"
+ );
+ }
+
+ if (checkContents) {
+ checkContents(dialogEl);
+ }
+
+ if (actionChangeApp) {
+ let dialogClosedPromise = waitForProtocolPermissionDialog(browser, false);
+ changeAppLink.click();
+ await dialogClosedPromise;
+ } else {
+ await closeDialog(browser, dialog, actionConfirm, scheme);
+ }
+ }
+
+ if (chooserDialogOpenPromise) {
+ let dialog = await chooserDialogOpenPromise;
+ let dialogEl = getDialogElementFromSubDialog(dialog);
+ let dialogType = getDialogType(dialog);
+
+ let { hasCheckbox, actionCheckbox, actionConfirm } = chooserDialogOptions;
+
+ await testCheckbox(dialogEl, dialogType, {
+ hasCheckbox,
+ actionCheckbox,
+ });
+
+ await closeDialog(browser, dialog, actionConfirm, scheme);
+ }
+
+ if (webHandlerShouldOpen) {
+ info("Waiting for web handler to open");
+ await webHandlerLoadedPromise;
+ } else {
+ info("Web handler open canceled");
+ }
+}
+
+/**
+ * Inspects the checkbox state and interacts with it.
+ * @param {dialog} dialogEl
+ * @param {string} dialogType - String identifier of dialog type.
+ * Either "permission" or "chooser".
+ * @param {Object} options - Test Options.
+ * @param {boolean} [options.hasCheckbox] - Whether the dialog is expected to
+ * have a visible checkbox.
+ * @param {boolean} [options.hasCheckboxState] - The check state of the checkbox
+ * to test for. true = checked, false = unchecked.
+ * @param {boolean} [options.actionCheckbox] - The state to set on the checkbox.
+ * true = checked, false = unchecked.
+ */
+async function testCheckbox(
+ dialogEl,
+ dialogType,
+ { hasCheckbox, hasCheckboxState = false, actionCheckbox, checkboxOrigin }
+) {
+ let checkbox = dialogEl.ownerDocument.getElementById("remember");
+ if (typeof hasCheckbox == "boolean") {
+ is(
+ checkbox && BrowserTestUtils.isVisible(checkbox),
+ hasCheckbox,
+ "Dialog checkbox has correct visibility."
+ );
+
+ let checkboxLabel = dialogEl.ownerDocument.getElementById("remember-label");
+ is(
+ checkbox && BrowserTestUtils.isVisible(checkboxLabel),
+ hasCheckbox,
+ "Dialog checkbox label has correct visibility."
+ );
+ if (hasCheckbox) {
+ ok(
+ !checkboxLabel.innerHTML.toLowerCase().includes(NULL_PRINCIPAL_SCHEME),
+ "Dialog checkbox label does not include NullPrincipal scheme."
+ );
+ }
+ }
+
+ if (typeof hasCheckboxState == "boolean") {
+ is(checkbox.checked, hasCheckboxState, "Dialog checkbox has correct state");
+ }
+
+ if (checkboxOrigin) {
+ let doc = dialogEl.ownerDocument;
+ let hostFromLabel = doc.l10n.getAttributes(
+ doc.getElementById("remember-label")
+ ).args.host;
+ is(hostFromLabel, checkboxOrigin, "Checkbox should be for correct domain.");
+ }
+
+ if (typeof actionCheckbox == "boolean") {
+ checkbox.click();
+ }
+}
+
+/**
+ * Get the dialog element which is a child of the SubDialogs browser frame.
+ * @param {SubDialog} subDialog - Dialog to get the dialog element for.
+ */
+function getDialogElementFromSubDialog(subDialog) {
+ let dialogEl = subDialog._frame.contentDocument.querySelector("dialog");
+ ok(dialogEl, "SubDialog should have dialog element");
+ return dialogEl;
+}
+
+/**
+ * Wait for the test handler to be opened.
+ * @param {MozBrowser} browser - The browser the load should occur in.
+ * @param {string} scheme - Scheme which triggered the handler to open.
+ */
+function waitForHandlerURL(browser, scheme) {
+ return BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url => url == `${ORIGIN1}/?url=${scheme}%3A%2F%2Ftest`
+ );
+}
+
+/**
+ * Test for open-protocol-handler permission.
+ * @param {nsIPrincipal} principal - The principal to test the permission on.
+ * @param {string} scheme - Scheme to generate permission key.
+ * @param {boolean} hasPerm - Whether we expect the princial to set the
+ * permission (true), or not (false).
+ */
+function testPermission(principal, scheme, hasPerm) {
+ let permKey = getSkipProtoDialogPermissionKey(scheme);
+ let result = Services.perms.testPermissionFromPrincipal(principal, permKey);
+ let message = `${permKey} ${hasPerm ? "is" : "is not"} set for ${
+ principal.origin
+ }.`;
+ is(result == Services.perms.ALLOW_ACTION, hasPerm, message);
+}
+
+/**
+ * Get the checkbox element of the dialog used to remember the handler choice or
+ * store the permission.
+ * @param {SubDialog} dialog - Protocol handler dialog embedded in a SubDialog.
+ * @param {string} dialogType - Type of the dialog which holds the checkbox.
+ * @returns {HTMLInputElement} - Checkbox of the dialog.
+ */
+function getDialogCheckbox(dialog, dialogType) {
+ let id;
+ if (dialogType == "permission") {
+ id = "remember-permission";
+ } else {
+ id = "remember";
+ }
+ return dialog._frame.contentDocument.getElementById(id);
+}
+
+function getDialogType(dialog) {
+ let url = dialog._frame.currentURI.spec;
+
+ if (url === DIALOG_URL_PERMISSION) {
+ return "permission";
+ }
+ if (url === DIALOG_URL_APP_CHOOSER) {
+ return "chooser";
+ }
+ throw new Error("Dialog with unexpected url");
+}
+
+/**
+ * Exit a protocol handler SubDialog and wait for it to be fully closed.
+ * @param {MozBrowser} browser - Browser element of the tab where the dialog is
+ * shown.
+ * @param {SubDialog} dialog - SubDialog object which holds the protocol handler
+ * @param {boolean} confirm - Whether to confirm (true) or cancel (false) the
+ * dialog.
+ * @param {string} scheme - The scheme of the protocol the dialog is opened for.
+ * dialog.
+ */
+async function closeDialog(browser, dialog, confirm, scheme) {
+ let dialogClosedPromise = waitForSubDialog(browser, dialog._openedURL, false);
+ let dialogEl = getDialogElementFromSubDialog(dialog);
+
+ if (confirm) {
+ if (getDialogType(dialog) == "chooser") {
+ // Select our test protocol handler
+ let listItem = dialogEl.ownerDocument.querySelector(
+ `richlistitem[name="${scheme}Handler"]`
+ );
+ listItem.click();
+ }
+
+ dialogEl.setAttribute("buttondisabledaccept", false);
+ dialogEl.acceptDialog();
+ } else {
+ dialogEl.cancelDialog();
+ }
+
+ return dialogClosedPromise;
+}
+
+registerCleanupFunction(function () {
+ // Clean up test handlers
+ TEST_PROTOS.forEach(scheme => {
+ let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme);
+ gHandlerService.remove(handlerInfo);
+ });
+
+ // Clear permissions
+ Services.perms.removeAll();
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", true]],
+ });
+ initTestHandlers();
+});
+
+/**
+ * Tests that when "remember" is unchecked, we only allow the protocol to be
+ * opened once and don't store any permission.
+ */
+add_task(async function test_permission_allow_once() {
+ for (let scheme of TEST_PROTOS) {
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ // No permission should be set
+ testPermission(PRINCIPAL1, scheme, false);
+ testPermission(PRINCIPAL2, scheme, false);
+
+ // No preferred app should be set
+ testAlwaysAsk(scheme, true);
+
+ // If we open again we should see the permission dialog
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: false,
+ },
+ });
+ });
+ }
+});
+
+/**
+ * Tests that when checking the "remember" checkbox, the protocol permission
+ * is set correctly and allows the caller to skip the permission dialog in
+ * subsequent calls.
+ */
+add_task(async function test_permission_allow_persist() {
+ for (let [origin, principal] of [
+ [ORIGIN1, PRINCIPAL1],
+ [ORIGIN3, PRINCIPAL3],
+ ]) {
+ for (let scheme of TEST_PROTOS) {
+ info("Testing with origin " + origin);
+ info("testing with principal of origin " + principal.origin);
+ info("testing with protocol " + scheme);
+
+ // Set a permission for an unrelated protocol.
+ // We should still see the permission dialog.
+ Services.perms.addFromPrincipal(
+ principal,
+ getSkipProtoDialogPermissionKey("foobar"),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await BrowserTestUtils.withNewTab(origin, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionCheckbox: true,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ // Permission should be set
+ testPermission(principal, scheme, true);
+ testPermission(PRINCIPAL2, scheme, false);
+
+ // No preferred app should be set
+ testAlwaysAsk(scheme, true);
+
+ // If we open again with the origin where we granted permission, we should
+ // directly get the chooser dialog.
+ await BrowserTestUtils.withNewTab(origin, async browser => {
+ await testOpenProto(browser, scheme, {
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false,
+ },
+ });
+ });
+
+ // If we open with the other origin, we should see the permission dialog
+ await BrowserTestUtils.withNewTab(ORIGIN2, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionConfirm: false,
+ },
+ });
+ });
+
+ // Cleanup permissions
+ Services.perms.removeAll();
+ }
+ }
+});
+
+/**
+ * Tests that if a preferred protocol handler is set, the permission dialog
+ * shows the application name and a link which leads to the app chooser.
+ */
+add_task(async function test_permission_application_set() {
+ let scheme = TEST_PROTOS[0];
+ updateAlwaysAsk(scheme, false);
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: true,
+ chooserIsNext: false,
+ actionChangeApp: true,
+ },
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ // Cleanup
+ updateAlwaysAsk(scheme, true);
+});
+
+/**
+ * Tests that we correctly handle system principals. They should always
+ * show the permission dialog, but give the option to choose another
+ * app if there isn't a default handler.
+ */
+add_task(async function test_permission_system_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: false,
+ hasChangeApp: false,
+ chooserIsNext: true,
+ actionChangeApp: false,
+ },
+ triggerLoad: useTriggeringPrincipal(
+ Services.scriptSecurityManager.getSystemPrincipal()
+ ),
+ });
+ });
+});
+
+/**
+ * Tests that we correctly handle system principals and show
+ * a simplified permission dialog if there is a default handler.
+ */
+add_task(async function test_permission_system_principal() {
+ let scheme = getSystemProtocol();
+ if (!scheme) {
+ return;
+ }
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: false,
+ hasChangeApp: false,
+ chooserIsNext: false,
+ actionChangeApp: false,
+ },
+ triggerLoad: useTriggeringPrincipal(
+ Services.scriptSecurityManager.getSystemPrincipal()
+ ),
+ });
+ });
+});
+
+/**
+ * Tests that we don't show the permission dialog if the permission is disabled
+ * by pref.
+ */
+add_task(async function test_permission_disabled() {
+ let scheme = TEST_PROTOS[0];
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", false]],
+ });
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: true },
+ });
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Tests that we directly open the handler if permission and handler are set.
+ */
+add_task(async function test_app_and_permission_set() {
+ let scheme = TEST_PROTOS[1];
+
+ updateAlwaysAsk(scheme, false);
+ Services.perms.addFromPrincipal(
+ PRINCIPAL2,
+ getSkipProtoDialogPermissionKey(scheme),
+ Services.perms.ALLOW_ACTION
+ );
+
+ await BrowserTestUtils.withNewTab(ORIGIN2, async browser => {
+ await testOpenProto(browser, scheme);
+ });
+
+ // Cleanup
+ Services.perms.removeAll();
+ updateAlwaysAsk(scheme, true);
+});
+
+/**
+ * Tests that the alwaysAsk state is not updated if the user cancels the dialog
+ */
+add_task(async function test_change_app_checkbox_cancel() {
+ let scheme = TEST_PROTOS[0];
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionCheckbox: true, // Activate checkbox
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+
+ // Should not have applied value from checkbox
+ testAlwaysAsk(scheme, true);
+});
+
+/**
+ * Tests that the external protocol dialogs behave correctly when a null
+ * principal is passed.
+ */
+add_task(async function test_null_principal() {
+ let scheme = TEST_PROTOS[0];
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: () => {
+ let uri = `${scheme}://test`;
+ ContentTask.spawn(browser, { uri }, args => {
+ let frame = content.document.createElement("iframe");
+ frame.src = `data:text/html,<script>location.href="${args.uri}"</script>`;
+ content.document.body.appendChild(frame);
+ });
+ },
+ permDialogOptions: {
+ hasCheckbox: false,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Tests that the external protocol dialogs behave correctly when no principal
+ * is passed.
+ */
+add_task(async function test_no_principal() {
+ let scheme = TEST_PROTOS[1];
+
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: () => {
+ let uri = `${scheme}://test`;
+
+ let contentDispatchChooser = Cc[
+ "@mozilla.org/content-dispatch-chooser;1"
+ ].createInstance(Ci.nsIContentDispatchChooser);
+
+ let handler = HandlerServiceTestUtils.getHandlerInfo(scheme);
+
+ contentDispatchChooser.handleURI(
+ handler,
+ Services.io.newURI(uri),
+ null,
+ browser.browsingContext
+ );
+ },
+ permDialogOptions: {
+ hasCheckbox: false,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Tests that if a URI scheme has a non-standard protocol, an OS default exists,
+ * and the user hasn't selected an alternative only the permission dialog is shown.
+ */
+add_task(async function test_non_standard_protocol() {
+ let scheme = getSystemProtocol();
+ if (!scheme) {
+ return;
+ }
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ permDialogOptions: {
+ hasCheckbox: true,
+ hasChangeApp: true,
+ chooserIsNext: false,
+ actionChangeApp: false,
+ },
+ });
+ });
+});
+
+/**
+ * Tests that we show the permission dialog for extension content scripts.
+ */
+add_task(async function test_extension_content_script_permission() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ const EXTENSION_DATA = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: [browser.currentURI.spec],
+ js: ["navigate.js"],
+ },
+ ],
+ browser_specific_settings: {
+ gecko: { id: "allowed@mochi.test" },
+ },
+ },
+ files: {
+ "navigate.js": `window.location.href = "${uri}";`,
+ },
+ useAddonManager: "permanent",
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ },
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionCheckbox: true,
+ actionConfirm: true,
+ checkContents: dialogEl => {
+ let description = dialogEl.querySelector("#description");
+ let { id, args } =
+ description.ownerDocument.l10n.getAttributes(description);
+ is(
+ id,
+ "permission-dialog-description-extension",
+ "Should be using the correct string."
+ );
+ is(
+ args.extension,
+ "Generated extension",
+ "Should have the correct extension name."
+ );
+ },
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+ let extensionPrivatePrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ { privateBrowsingId: 1 }
+ );
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.ALLOW_ACTION,
+ "Should have permanently allowed the extension"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have changed the private principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have allowed the page"
+ );
+
+ await testExtension.unload();
+
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the extension's normal principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the private browsing principal"
+ );
+ });
+});
+
+/**
+ * Tests that we show the permission dialog for extension content scripts.
+ */
+add_task(async function test_extension_private_content_script_permission() {
+ let scheme = TEST_PROTOS[0];
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: ORIGIN1 },
+ async browser => {
+ let testExtension;
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ const EXTENSION_DATA = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: [browser.currentURI.spec],
+ js: ["navigate.js"],
+ },
+ ],
+ browser_specific_settings: {
+ gecko: { id: "allowed@mochi.test" },
+ },
+ },
+ files: {
+ "navigate.js": `window.location.href = "${uri}";`,
+ },
+ useAddonManager: "permanent",
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ let perms = {
+ permissions: ["internal:privateBrowsingAllowed"],
+ origins: [],
+ };
+ await ExtensionPermissions.add("allowed@mochi.test", perms);
+ let addon = await AddonManager.getAddonByID("allowed@mochi.test");
+ await addon.reload();
+ },
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionCheckbox: true,
+ actionConfirm: true,
+ checkContents: dialogEl => {
+ let description = dialogEl.querySelector("#description");
+ let { id, args } =
+ description.ownerDocument.l10n.getAttributes(description);
+ is(
+ id,
+ "permission-dialog-description-extension",
+ "Should be using the correct string."
+ );
+ is(
+ args.extension,
+ "Generated extension",
+ "Should have the correct extension name."
+ );
+ },
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+ let extensionPrivatePrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ { privateBrowsingId: 1 }
+ );
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have changed the extension's normal principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.ALLOW_ACTION,
+ "Should have allowed the private browsing principal"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have allowed the page"
+ );
+
+ await testExtension.unload();
+
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the extension's normal principal permission"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(
+ extensionPrivatePrincipal,
+ key
+ ),
+ Services.perms.UNKNOWN_ACTION,
+ "Should have cleared the private browsing principal"
+ );
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests that we do not show the permission dialog for extension content scripts
+ * when the page already has permission.
+ */
+add_task(async function test_extension_allowed_content() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ Services.perms.addFromPrincipal(
+ PRINCIPAL1,
+ key,
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ const EXTENSION_DATA = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: [browser.currentURI.spec],
+ js: ["navigate.js"],
+ },
+ ],
+ },
+ files: {
+ "navigate.js": `window.location.href = "${uri}";`,
+ },
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have permanently allowed the extension"
+ );
+
+ await testExtension.unload();
+ Services.perms.removeFromPrincipal(PRINCIPAL1, key);
+ });
+});
+
+/**
+ * Tests that we do not show the permission dialog for extension content scripts
+ * when the extension already has permission.
+ */
+add_task(async function test_extension_allowed_extension() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ const EXTENSION_DATA = {
+ manifest: {
+ permissions: [`${ORIGIN1}/*`],
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, uri) => {
+ switch (msg) {
+ case "engage":
+ browser.tabs.executeScript({
+ code: `window.location.href = "${uri}";`,
+ });
+ break;
+ default:
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ }
+ });
+ },
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+ Services.perms.addFromPrincipal(
+ extensionPrincipal,
+ key,
+ Services.perms.ALLOW_ACTION,
+ Services.perms.EXPIRE_NEVER
+ );
+
+ testExtension.sendMessage("engage", `${scheme}://test`);
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ await testExtension.unload();
+ Services.perms.removeFromPrincipal(PRINCIPAL1, key);
+ });
+});
+
+/**
+ * Tests that we show the permission dialog for extensions directly opening a
+ * protocol.
+ */
+add_task(async function test_extension_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ let testExtension;
+
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ const EXTENSION_DATA = {
+ background() {
+ browser.test.onMessage.addListener(async (msg, url) => {
+ switch (msg) {
+ case "engage":
+ browser.tabs.update({
+ url,
+ });
+ break;
+ default:
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ }
+ });
+ },
+ };
+
+ testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await testExtension.startup();
+ testExtension.sendMessage("engage", `${scheme}://test`);
+ },
+ permDialogOptions: {
+ hasCheckbox: true,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionCheckbox: true,
+ actionConfirm: true,
+ checkContents: dialogEl => {
+ let description = dialogEl.querySelector("#description");
+ let { id, args } =
+ description.ownerDocument.l10n.getAttributes(description);
+ is(
+ id,
+ "permission-dialog-description-extension",
+ "Should be using the correct string."
+ );
+ is(
+ args.extension,
+ "Generated extension",
+ "Should have the correct extension name."
+ );
+ },
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+
+ let extensionPrincipal =
+ Services.scriptSecurityManager.createContentPrincipal(
+ Services.io.newURI(`moz-extension://${testExtension.uuid}/`),
+ {}
+ );
+
+ let key = getSkipProtoDialogPermissionKey(scheme);
+ is(
+ Services.perms.testPermissionFromPrincipal(extensionPrincipal, key),
+ Services.perms.ALLOW_ACTION,
+ "Should have permanently allowed the extension"
+ );
+ is(
+ Services.perms.testPermissionFromPrincipal(PRINCIPAL1, key),
+ Services.perms.UNKNOWN_ACTION,
+ "Should not have allowed the page"
+ );
+
+ await testExtension.unload();
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog when applicable.
+ */
+add_task(async function test_redirect_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: useServerRedirect("location"),
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for refresh headers.
+ */
+add_task(async function test_redirect_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: useServerRedirect("refresh"),
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for meta refreshes.
+ */
+add_task(async function test_redirect_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: useServerRedirect("meta-refresh"),
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for JS redirects.
+ */
+add_task(async function test_redirect_principal_js() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: () => {
+ let uri = `${scheme}://test`;
+
+ let innerParams = new URLSearchParams();
+ innerParams.set("uri", uri);
+ let params = new URLSearchParams();
+ params.set(
+ "uri",
+ "https://example.com/" +
+ ROOT_PATH +
+ "script_redirect.html?" +
+ innerParams.toString()
+ );
+ uri =
+ "https://example.org/" +
+ ROOT_PATH +
+ "script_redirect.html?" +
+ params.toString();
+ BrowserTestUtils.startLoadingURIString(browser, uri);
+ },
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Test that we use the redirect principal for the dialog for link clicks.
+ */
+add_task(async function test_redirect_principal_links() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ await testOpenProto(browser, scheme, {
+ triggerLoad: async () => {
+ let uri = `${scheme}://test`;
+
+ let params = new URLSearchParams();
+ params.set("uri", uri);
+ uri =
+ "https://example.com/" +
+ ROOT_PATH +
+ "redirect_helper.sjs?" +
+ params.toString();
+ await ContentTask.spawn(browser, { uri }, args => {
+ let textLink = content.document.createElement("a");
+ textLink.href = args.uri;
+ textLink.textContent = "click me";
+ content.document.body.appendChild(textLink);
+ textLink.click();
+ });
+ },
+ permDialogOptions: {
+ checkboxOrigin: ORIGIN1,
+ chooserIsNext: true,
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js
new file mode 100644
index 0000000000..bc4b13730a
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests navigation to external protocol from sandboxed iframes.
+ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.block_external_protocol_navigation_from_sandbox", true]],
+ });
+
+ await setupMailHandler();
+});
+
+add_task(async function test_sandbox_disabled() {
+ await runExtProtocolSandboxTest({ blocked: false, sandbox: null });
+});
+
+add_task(async function test_sandbox_allowed() {
+ let flags = [
+ "allow-popups",
+ "allow-top-navigation",
+ "allow-top-navigation-by-user-activation",
+ "allow-top-navigation-to-custom-protocols",
+ ];
+
+ for (let flag of flags) {
+ await runExtProtocolSandboxTest({
+ blocked: false,
+ sandbox: `allow-scripts ${flag}`,
+ });
+ }
+});
+
+add_task(async function test_sandbox_blocked() {
+ let flags = [
+ "",
+ "allow-same-origin",
+ "allow-forms",
+ "allow-scripts",
+ "allow-pointer-lock",
+ "allow-orientation-lock",
+ "allow-modals",
+ "allow-popups-to-escape-sandbox",
+ "allow-presentation",
+ "allow-storage-access-by-user-activation",
+ "allow-downloads",
+ ];
+
+ for (let flag of flags) {
+ await runExtProtocolSandboxTest({
+ blocked: true,
+ sandbox: `allow-scripts ${flag}`,
+ });
+ }
+});
+
+add_task(async function test_sandbox_blocked_triggers() {
+ info(
+ "For sandboxed frames external protocol navigation is blocked, no matter how it is triggered."
+ );
+ for (let triggerMethod of [
+ "trustedClick",
+ "untrustedClick",
+ "trustedLocationAPI",
+ "untrustedLocationAPI",
+ "frameSrc",
+ ]) {
+ await runExtProtocolSandboxTest({
+ blocked: true,
+ sandbox: "allow-scripts",
+ triggerMethod,
+ });
+ }
+
+ info(
+ "When allow-top-navigation-by-user-activation navigation to external protocols with transient user activations is allowed."
+ );
+ await runExtProtocolSandboxTest({
+ blocked: false,
+ sandbox: "allow-scripts allow-top-navigation-by-user-activation",
+ triggerMethod: "trustedClick",
+ });
+
+ await runExtProtocolSandboxTest({
+ blocked: true,
+ sandbox: "allow-scripts allow-top-navigation-by-user-activation",
+ triggerMethod: "untrustedClick",
+ });
+
+ await runExtProtocolSandboxTest({
+ blocked: true,
+ sandbox: "allow-scripts allow-top-navigation-by-user-activation",
+ triggerMethod: "untrustedLocationAPI",
+ });
+
+ await runExtProtocolSandboxTest({
+ blocked: true,
+ sandbox: "allow-scripts allow-top-navigation-by-user-activation",
+ triggerMethod: "frameSrc",
+ });
+});
+
+add_task(async function test_sandbox_combination() {
+ await runExtProtocolSandboxTest({
+ blocked: false,
+ sandbox:
+ "allow-scripts allow-downloads allow-top-navigation-to-custom-protocols",
+ });
+
+ await runExtProtocolSandboxTest({
+ blocked: false,
+ sandbox:
+ "allow-scripts allow-top-navigation allow-top-navigation-to-custom-protocols",
+ });
+
+ await runExtProtocolSandboxTest({
+ blocked: true,
+ sandbox: "allow-scripts allow-modals",
+ });
+});
+
+add_task(async function test_sandbox_iframe_redirect() {
+ await runExtProtocolSandboxTest({
+ blocked: true,
+ sandbox: "allow-scripts",
+ triggerMethod: "frameSrcRedirect",
+ });
+
+ await runExtProtocolSandboxTest({
+ blocked: false,
+ sandbox: "allow-scripts allow-top-navigation-to-custom-protocols",
+ triggerMethod: "frameSrcRedirect",
+ });
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js
new file mode 100644
index 0000000000..03d2fe8cf5
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_custom_sandbox_csp.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests navigation to external protocol from csp-sandboxed iframes.
+ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.block_external_protocol_navigation_from_sandbox", true]],
+ });
+
+ await setupMailHandler();
+});
+
+add_task(async function test_sandbox_csp() {
+ for (let triggerMethod of [
+ "trustedClick",
+ "untrustedClick",
+ "trustedLocationAPI",
+ "untrustedLocationAPI",
+ ]) {
+ await runExtProtocolSandboxTest({
+ blocked: false,
+ sandbox: "allow-scripts",
+ useCSPSandbox: true,
+ triggerMethod,
+ });
+ }
+
+ await runExtProtocolSandboxTest({
+ blocked: false,
+ sandbox: "allow-scripts allow-top-navigation-to-custom-protocols",
+ useCSPSandbox: true,
+ });
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js
new file mode 100644
index 0000000000..d906d9c55b
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_helperapp() {
+ // Set up the test infrastructure:
+ const kProt = "foopydoopydoo";
+ const extProtocolSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ let handlerInfo = extProtocolSvc.getProtocolHandlerInfo(kProt);
+ if (handlerSvc.exists(handlerInfo)) {
+ handlerSvc.fillHandlerInfo(handlerInfo, "");
+ }
+ // Say we want to use a specific app:
+ handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ handlerInfo.alwaysAskBeforeHandling = false;
+
+ // Say it's us:
+ let selfFile = Services.dirsvc.get("XREExeF", Ci.nsIFile);
+ // Make sure it's the .app
+ if (AppConstants.platform == "macosx") {
+ while (
+ !selfFile.leafName.endsWith(".app") &&
+ !selfFile.leafName.endsWith(".app/")
+ ) {
+ selfFile = selfFile.parent;
+ }
+ }
+ let selfHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ selfHandlerApp.executable = selfFile;
+ handlerInfo.possibleApplicationHandlers.appendElement(selfHandlerApp);
+ handlerInfo.preferredApplicationHandler = selfHandlerApp;
+ handlerSvc.store(handlerInfo);
+
+ await BrowserTestUtils.withNewTab("about:blank", async browser => {
+ // Now, do some safety stubbing. If we do end up recursing we spawn
+ // infinite tabs. We definitely don't want that. Avoid it by stubbing
+ // our external URL handling bits:
+ let oldAddTab = gBrowser.addTab;
+ registerCleanupFunction(() => (gBrowser.addTab = oldAddTab));
+ let wrongThingHappenedPromise = new Promise(resolve => {
+ gBrowser.addTab = function (aURI) {
+ ok(false, "Tried to open unexpected URL in a tab: " + aURI);
+ resolve(null);
+ // Pass a dummy object to avoid upsetting BrowserContentHandler -
+ // if it thinks opening the tab failed, it tries to open a window instead,
+ // which we can't prevent as easily, and at which point we still end up
+ // with runaway tabs.
+ return {};
+ };
+ });
+
+ let askedUserPromise = waitForProtocolAppChooserDialog(browser, true);
+
+ gBrowser.fixupAndLoadURIString(kProt + ":test", {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL,
+ });
+ let dialog = await Promise.race([
+ wrongThingHappenedPromise,
+ askedUserPromise,
+ ]);
+ ok(dialog, "Should have gotten a dialog");
+
+ let closePromise = waitForProtocolAppChooserDialog(browser, false);
+ dialog.close();
+ await closePromise;
+ askedUserPromise = null;
+ });
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js
new file mode 100644
index 0000000000..f3fedab69c
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js
@@ -0,0 +1,61 @@
+add_task(async function () {
+ // create mocked objects
+ let launcher = createMockedObjects(true);
+
+ // open helper app dialog with mocked launcher
+ let dlg = await openHelperAppDialog(launcher);
+
+ let doc = dlg.document;
+ let dialogElement = doc.getElementById("unknownContentType");
+
+ // Set remember choice
+ ok(
+ !doc.getElementById("rememberChoice").checked,
+ "Remember choice checkbox should be not checked."
+ );
+ doc.getElementById("rememberChoice").checked = true;
+
+ // Make sure the mock handler information is not in nsIHandlerService
+ ok(
+ !gHandlerSvc.exists(launcher.MIMEInfo),
+ "Should not be in nsIHandlerService."
+ );
+
+ // close the dialog by pushing the ok button.
+ let dialogClosedPromise = BrowserTestUtils.windowClosed(dlg);
+ // Make sure the ok button is enabled, since the ok button might be disabled by
+ // EnableDelayHelper mechanism. Please refer the detailed
+ // https://searchfox.org/mozilla-central/source/toolkit/components/prompts/src/PromptUtils.sys.mjs#51
+ dialogElement.getButton("accept").disabled = false;
+ dialogElement.acceptDialog();
+ await dialogClosedPromise;
+
+ // check the mocked handler information is saved in nsIHandlerService
+ ok(gHandlerSvc.exists(launcher.MIMEInfo), "Should be in nsIHandlerService.");
+ // check the extension.
+ var mimeType = gHandlerSvc.getTypeFromExtension("abc");
+ is(mimeType, launcher.MIMEInfo.type, "Got correct mime type.");
+ for (let handlerInfo of gHandlerSvc.enumerate()) {
+ if (handlerInfo.type == launcher.MIMEInfo.type) {
+ // check the alwaysAskBeforeHandling
+ ok(
+ !handlerInfo.alwaysAskBeforeHandling,
+ "Should turn off the always ask."
+ );
+ // check the preferredApplicationHandler
+ ok(
+ handlerInfo.preferredApplicationHandler.equals(
+ launcher.MIMEInfo.preferredApplicationHandler
+ ),
+ "Should be equal to the mockedHandlerApp."
+ );
+ // check the perferredAction
+ is(
+ handlerInfo.preferredAction,
+ launcher.MIMEInfo.preferredAction,
+ "Should be equal to Ci.nsIHandlerInfo.useHelperApp."
+ );
+ break;
+ }
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_save_filenames.js b/uriloader/exthandler/tests/mochitest/browser_save_filenames.js
new file mode 100644
index 0000000000..46750d5c87
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_save_filenames.js
@@ -0,0 +1,860 @@
+// There are at least seven different ways in a which a file can be saved or downloaded. This
+// test ensures that the filename is determined correctly when saving in these ways. The seven
+// ways are:
+// - save the file individually from the File menu
+// - save as complete web page (this uses a different codepath than the previous one)
+// - dragging an image to the local file system
+// - copy an image and paste it as a file to the local file system (windows only)
+// - open a link with content-disposition set to attachment
+// - open a link with the download attribute
+// - save a link or image from the context menu
+
+requestLongerTimeout(8);
+
+let types = {
+ text: "text/plain",
+ html: "text/html",
+ png: "image/png",
+ jpeg: "image/jpeg",
+ webp: "image/webp",
+ otherimage: "image/unknown",
+ // Other js types (application/javascript and text/javascript) are handled by the system
+ // inconsistently, but application/x-javascript is handled by the external helper app service,
+ // so it is used here for this test.
+ js: "application/x-javascript",
+ binary: "application/octet-stream",
+ nonsense: "application/x-nonsense",
+ zip: "application/zip",
+ json: "application/json",
+ tar: "application/x-tar",
+};
+
+const PNG_DATA = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+);
+
+const JPEG_DATA = atob(
+ "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4z" +
+ "NDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEB" +
+ "AxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS" +
+ "0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKz" +
+ "tLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgEC" +
+ "BAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpj" +
+ "ZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6" +
+ "/9oADAMBAAIRAxEAPwD3+iiigD//2Q=="
+);
+
+const WEBP_DATA = atob(
+ "UklGRiIAAABXRUJQVlA4TBUAAAAvY8AYAAfQ/4j+B4CE8H+/ENH/VCIA"
+);
+
+const DEFAULT_FILENAME =
+ AppConstants.platform == "win" ? "Untitled.htm" : "Untitled.html";
+
+const PROMISE_FILENAME_TYPE = "application/x-moz-file-promise-dest-filename";
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+let expectedItems;
+let sendAsAttachment = false;
+let httpServer = null;
+
+function handleRequest(aRequest, aResponse) {
+ const queryString = new URLSearchParams(aRequest.queryString);
+ let type = queryString.get("type");
+ let filename = queryString.get("filename");
+ let dispname = queryString.get("dispname");
+
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ if (type) {
+ aResponse.setHeader("Content-Type", types[type]);
+ }
+
+ if (dispname) {
+ let dispositionType = sendAsAttachment ? "attachment" : "inline";
+ aResponse.setHeader(
+ "Content-Disposition",
+ dispositionType + ';name="' + dispname + '"'
+ );
+ } else if (filename) {
+ let dispositionType = sendAsAttachment ? "attachment" : "inline";
+ aResponse.setHeader(
+ "Content-Disposition",
+ dispositionType + ';filename="' + filename + '"'
+ );
+ } else if (sendAsAttachment) {
+ aResponse.setHeader("Content-Disposition", "attachment");
+ }
+
+ if (type == "png") {
+ aResponse.write(PNG_DATA);
+ } else if (type == "jpeg") {
+ aResponse.write(JPEG_DATA);
+ } else if (type == "webp") {
+ aResponse.write(WEBP_DATA);
+ } else if (type == "html") {
+ aResponse.write(
+ "<html><head><title>file.inv</title></head><body>File</body></html>"
+ );
+ } else {
+ aResponse.write("// Some Text");
+ }
+}
+
+function handleBasicImageRequest(aRequest, aResponse) {
+ aResponse.setHeader("Content-Type", "image/png");
+ aResponse.write(PNG_DATA);
+}
+
+function handleRedirect(aRequest, aResponse) {
+ const queryString = new URLSearchParams(aRequest.queryString);
+ let filename = queryString.get("filename");
+
+ aResponse.setStatusLine(aRequest.httpVersion, 302);
+ aResponse.setHeader("Location", "/bell" + filename[0] + "?" + queryString);
+}
+
+// nsIFile::CreateUnique crops long filenames if the path is too long, but
+// we don't know exactly how long depending on the full path length, so
+// for those save methods that use CreateUnique, instead just verify that
+// the filename starts with the right string and has the correct extension.
+function checkShortenedFilename(actual, expected) {
+ if (actual.length < expected.length) {
+ let actualDot = actual.lastIndexOf(".");
+ let actualExtension = actual.substring(actualDot);
+ let expectedExtension = expected.substring(expected.lastIndexOf("."));
+ if (
+ actualExtension == expectedExtension &&
+ expected.startsWith(actual.substring(0, actualDot))
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+add_setup(async function () {
+ const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+ );
+ httpServer = new HttpServer();
+ httpServer.start(8000);
+
+ // Need to load the page from localhost:8000 as the download attribute
+ // only applies to links from the same domain.
+ let saveFilenamesPage = await IOUtils.getFile(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ ..."browser/uriloader/exthandler/tests/mochitest/save_filenames.html".split(
+ "/"
+ )
+ );
+ httpServer.registerFile("/save_filenames.html", saveFilenamesPage);
+
+ // A variety of different scripts are set up to better ensure uniqueness.
+ httpServer.registerPathHandler("/save_filename.sjs", handleRequest);
+ httpServer.registerPathHandler("/save_thename.sjs", handleRequest);
+ httpServer.registerPathHandler("/getdata.png", handleRequest);
+ httpServer.registerPathHandler("/base", handleRequest);
+ httpServer.registerPathHandler("/basedata", handleRequest);
+ httpServer.registerPathHandler("/basetext", handleRequest);
+ httpServer.registerPathHandler("/text2.txt", handleRequest);
+ httpServer.registerPathHandler("/text3.gonk", handleRequest);
+ httpServer.registerPathHandler("/basic.png", handleBasicImageRequest);
+ httpServer.registerPathHandler("/aquamarine.jpeg", handleBasicImageRequest);
+ httpServer.registerPathHandler("/lazuli.exe", handleBasicImageRequest);
+ httpServer.registerPathHandler("/redir", handleRedirect);
+ httpServer.registerPathHandler("/bellr", handleRequest);
+ httpServer.registerPathHandler("/bellg", handleRequest);
+ httpServer.registerPathHandler("/bellb", handleRequest);
+ httpServer.registerPathHandler("/executable.exe", handleRequest);
+
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://localhost:8000/save_filenames.html"
+ );
+
+ expectedItems = await getItems("items");
+});
+
+function getItems(parentid) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [parentid, AppConstants.platform],
+ (id, platform) => {
+ let elements = [];
+ let elem = content.document.getElementById(id).firstElementChild;
+ while (elem) {
+ let filename =
+ elem.dataset["filenamePlatform" + platform] || elem.dataset.filename;
+ let url = elem.getAttribute("src") || elem.getAttribute("href");
+ let draggable =
+ elem.localName == "img" && elem.dataset.nodrag != "true";
+ let unknown = elem.dataset.unknown;
+ let noattach = elem.dataset.noattach;
+ let savepagename = elem.dataset.savepagename;
+ let pickedfilename = elem.dataset.pickedfilename;
+ elements.push({
+ draggable,
+ unknown,
+ filename,
+ url,
+ noattach,
+ savepagename,
+ pickedfilename,
+ });
+ elem = elem.nextElementSibling;
+ }
+ return elements;
+ }
+ );
+}
+
+function getDirectoryEntries(dir) {
+ let files = [];
+ let entries = dir.directoryEntries;
+ while (true) {
+ let file = entries.nextFile;
+ if (!file) {
+ break;
+ }
+ files.push(file.leafName);
+ }
+ entries.close();
+ return files;
+}
+
+// This test saves the document as a complete web page and verifies
+// that the resources are saved with the correct filename.
+add_task(async function save_document() {
+ let browser = gBrowser.selectedBrowser;
+
+ let tmp = SpecialPowers.Services.dirsvc.get("TmpD", Ci.nsIFile);
+ const baseFilename = "test_save_filenames_" + Date.now();
+
+ let tmpFile = tmp.clone();
+ tmpFile.append(baseFilename + "_document.html");
+ let tmpDir = tmp.clone();
+ tmpDir.append(baseFilename + "_document_files");
+
+ MockFilePicker.displayDirectory = tmpDir;
+ MockFilePicker.showCallback = function (fp) {
+ MockFilePicker.setFiles([tmpFile]);
+ MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
+ };
+
+ let downloadsList = await Downloads.getList(Downloads.PUBLIC);
+ let savePromise = promiseDownloadFinished(downloadsList);
+ saveBrowser(browser);
+ await savePromise;
+
+ let filesSaved = getDirectoryEntries(tmpDir);
+
+ for (let idx = 0; idx < expectedItems.length; idx++) {
+ let filename = expectedItems[idx].filename;
+ if (idx == 66 && AppConstants.platform == "win") {
+ // This is special-cased on Windows. The default filename will be used, since
+ // the filename is invalid, but since the previous test file has the same issue,
+ // this second file will be saved with a number suffix added to it.
+ filename = "Untitled_002";
+ }
+
+ let file = tmpDir.clone();
+ file.append(filename);
+
+ let fileIdx = -1;
+ // Use checkShortenedFilename to check long filenames.
+ if (filename.length > 240) {
+ for (let t = 0; t < filesSaved.length; t++) {
+ if (
+ filesSaved[t].length > 60 &&
+ checkShortenedFilename(filesSaved[t], filename)
+ ) {
+ fileIdx = t;
+ break;
+ }
+ }
+ } else {
+ fileIdx = filesSaved.indexOf(filename);
+ }
+
+ ok(
+ fileIdx >= 0,
+ "file i" +
+ idx +
+ " " +
+ filename +
+ " was saved with the correct name using saveDocument"
+ );
+ if (fileIdx >= 0) {
+ // If found, remove it from the list. At end of the test, the
+ // list should be empty.
+ filesSaved.splice(fileIdx, 1);
+ }
+ }
+
+ is(filesSaved.length, 0, "all files accounted for");
+ tmpDir.remove(true);
+ tmpFile.remove(false);
+ downloadsList.removeFinished();
+});
+
+// This test simulates dragging the images in the document and ensuring that
+// the correct filename is used for each one.
+// On Mac, the data is added in the parent process instead, so we cannot
+// test dragging directly.
+if (AppConstants.platform != "macosx") {
+ add_task(async function drag_files() {
+ let browser = gBrowser.selectedBrowser;
+
+ await SpecialPowers.spawn(browser, [PROMISE_FILENAME_TYPE], type => {
+ content.addEventListener("dragstart", event => {
+ content.draggedFile = event.dataTransfer.getData(type);
+ event.preventDefault();
+ });
+ });
+
+ for (let idx = 0; idx < expectedItems.length; idx++) {
+ if (!expectedItems[idx].draggable) {
+ // You can't drag non-images and invalid images.
+ continue;
+ }
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#i" + idx,
+ 1,
+ 1,
+ { type: "mousedown" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#i" + idx,
+ 11,
+ 11,
+ { type: "mousemove" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#i" + idx,
+ 20,
+ 20,
+ { type: "mousemove" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#i" + idx,
+ 20,
+ 20,
+ { type: "mouseup" },
+ browser
+ );
+
+ let draggedFile = await SpecialPowers.spawn(browser, [], () => {
+ let file = content.draggedFile;
+ content.draggedFile = null;
+ return file;
+ });
+
+ is(
+ draggedFile,
+ expectedItems[idx].filename,
+ "i" +
+ idx +
+ " " +
+ expectedItems[idx].filename +
+ " was saved with the correct name when dragging"
+ );
+ }
+ });
+}
+
+// This test checks that copying an image provides the right filename
+// for pasting to the local file system. This is only implemented on Windows.
+const imageAsFileEnabled = SpecialPowers.getBoolPref(
+ "clipboard.imageAsFile.enabled",
+ false
+);
+if (AppConstants.platform == "win" && imageAsFileEnabled) {
+ add_task(async function copy_image() {
+ for (let idx = 0; idx < expectedItems.length; idx++) {
+ if (!expectedItems[idx].draggable) {
+ // You can't context-click on non-images.
+ continue;
+ }
+
+ let data = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [idx, PROMISE_FILENAME_TYPE],
+ (imagenum, type) => {
+ // No need to wait for the data to be really on the clipboard, we only
+ // need the promise data added when the command is performed.
+ SpecialPowers.setCommandNode(
+ content,
+ content.document.getElementById("i" + imagenum)
+ );
+ SpecialPowers.doCommand(content, "cmd_copyImageContents");
+
+ return SpecialPowers.getClipboardData(type);
+ }
+ );
+
+ is(
+ data,
+ expectedItems[idx].filename,
+ "i" +
+ idx +
+ " " +
+ expectedItems[idx].filename +
+ " was saved with the correct name when copying"
+ );
+ }
+ });
+}
+
+// This test checks the default filename selected when selecting to save
+// a file from either the context menu or what would happen when save page
+// as was selected from the file menu. Note that this tests a filename assigned
+// when using content-disposition: inline.
+add_task(async function saveas_files() {
+ // Iterate over each item and try saving them from the context menu,
+ // and the Save Page As command on the File menu.
+ for (let testname of ["context menu", "save page as"]) {
+ for (let idx = 0; idx < expectedItems.length; idx++) {
+ let menu;
+ if (testname == "context menu") {
+ if (!expectedItems[idx].draggable) {
+ // You can't context-click on non-images.
+ continue;
+ }
+
+ menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ "#i" + idx,
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShown;
+ } else {
+ if (expectedItems[idx].unknown == "typeonly") {
+ // Items marked with unknown="typeonly" have unknown content types and
+ // will be downloaded instead of opened in a tab.
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinishedPromise = promiseDownloadFinished(list);
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: expectedItems[idx].url,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ let download = await downloadFinishedPromise;
+
+ let filename = PathUtils.filename(download.target.path);
+
+ let expectedFilename = expectedItems[idx].filename;
+ if (expectedFilename.length > 240) {
+ ok(
+ checkShortenedFilename(filename, expectedFilename),
+ "open link" +
+ idx +
+ " " +
+ expectedFilename +
+ " was downloaded with the correct name when opened as a url (with long name)"
+ );
+ } else {
+ is(
+ filename,
+ expectedFilename,
+ "open link" +
+ idx +
+ " " +
+ expectedFilename +
+ " was downloaded with the correct name when opened as a url"
+ );
+ }
+
+ try {
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {}
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ continue;
+ }
+
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: expectedItems[idx].url,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+ }
+
+ let filename = await new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ setTimeout(() => {
+ resolve(fp.defaultString);
+ }, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+
+ if (testname == "context menu") {
+ let menuitem = document.getElementById("context-saveimage");
+ menu.activateItem(menuitem);
+ } else if (testname == "save page as") {
+ document.getElementById("Browser:SavePage").doCommand();
+ }
+ });
+
+ // Trying to open an unknown or binary type will just open a blank
+ // page, so trying to save will just save the blank page with the
+ // filename Untitled.html.
+ let expectedFilename = expectedItems[idx].unknown
+ ? DEFAULT_FILENAME
+ : expectedItems[idx].savepagename || expectedItems[idx].filename;
+
+ // When saving via contentAreaUtils.js, the content disposition name
+ // field is used as an alternate.
+ if (expectedFilename == "save_thename.png") {
+ expectedFilename = "withname.png";
+ }
+
+ is(
+ filename,
+ expectedFilename,
+ "i" +
+ idx +
+ " " +
+ expectedFilename +
+ " was saved with the correct name " +
+ testname
+ );
+
+ if (testname == "save page as") {
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ }
+ }
+});
+
+// This test checks that the filename is saved correctly when it
+// has been modified within the file picker.
+add_task(async function saveas_files_modified_in_filepicker() {
+ let items = await getItems("modifieditems");
+ for (let idx = 0; idx < items.length; idx++) {
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: items[idx].url,
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ let savedFile = SpecialPowers.Services.dirsvc.get("TmpD", Ci.nsIFile);
+
+ let downloadsList = await Downloads.getList(Downloads.PUBLIC);
+ let savePromise = promiseDownloadFinished(downloadsList);
+
+ await new Promise(resolve => {
+ MockFilePicker.displayDirectory = savedFile;
+
+ MockFilePicker.showCallback = function (fp) {
+ MockFilePicker.filterIndex = 0; // kSaveAsType_Complete
+ savedFile.append(items[idx].pickedfilename);
+ MockFilePicker.setFiles([savedFile]);
+ setTimeout(() => {
+ resolve(items[idx].pickedfilename);
+ }, 0);
+
+ return Ci.nsIFilePicker.returnOK;
+ };
+
+ document.getElementById("Browser:SavePage").doCommand();
+ });
+
+ await savePromise;
+
+ savedFile.leafName = items[idx].filename;
+ ok(
+ savedFile.exists(),
+ "i" +
+ idx +
+ " '" +
+ savedFile.leafName +
+ "' was saved when modified with the correct name "
+ );
+ if (savedFile.exists()) {
+ savedFile.remove(false);
+ }
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+// This test checks that links that result in files with
+// content-disposition: attachment are saved with the right filenames.
+add_task(async function save_links() {
+ sendAsAttachment = true;
+
+ // Create some links based on each image and insert them into the document.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let doc = content.document;
+ let insertPos = doc.getElementById("attachment-links");
+
+ let idx = 0;
+ let elem = doc.getElementById("items").firstElementChild;
+ while (elem) {
+ let attachmentlink = doc.createElement("a");
+ attachmentlink.id = "attachmentlink" + idx;
+ attachmentlink.href = elem.localName == "object" ? elem.data : elem.src;
+ attachmentlink.textContent = elem.dataset.filename;
+ insertPos.appendChild(attachmentlink);
+ insertPos.appendChild(doc.createTextNode(" "));
+
+ elem = elem.nextElementSibling;
+ idx++;
+ }
+ });
+
+ let list = await Downloads.getList(Downloads.PUBLIC);
+
+ for (let idx = 0; idx < expectedItems.length; idx++) {
+ // Skip the items that won't have a content-disposition.
+ if (expectedItems[idx].noattach) {
+ continue;
+ }
+
+ let downloadFinishedPromise = promiseDownloadFinished(list);
+
+ BrowserTestUtils.synthesizeMouse(
+ "#attachmentlink" + idx,
+ 5,
+ 5,
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ let download = await downloadFinishedPromise;
+
+ let filename = PathUtils.filename(download.target.path);
+
+ let expectedFilename = expectedItems[idx].filename;
+ // Use checkShortenedFilename to check long filenames.
+ if (expectedItems[idx].filename.length > 240) {
+ ok(
+ checkShortenedFilename(filename, expectedFilename),
+ "attachmentlink" +
+ idx +
+ " " +
+ expectedFilename +
+ " was saved with the correct name when opened as attachment (with long name)"
+ );
+ } else {
+ is(
+ filename,
+ expectedFilename,
+ "attachmentlink" +
+ idx +
+ " " +
+ expectedFilename +
+ " was saved with the correct name when opened as attachment"
+ );
+ }
+
+ try {
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {}
+ }
+
+ sendAsAttachment = false;
+});
+
+// This test checks some cases where links to images are saved using Save Link As,
+// and when opening them in a new tab and then using Save Page As.
+add_task(async function saveas_image_links() {
+ let links = await getItems("links");
+
+ // Iterate over each link and try saving the links from the context menu,
+ // and then after opening a new tab for that link and then selecting
+ // the Save Page As command on the File menu.
+ for (let testname of ["save link as", "save link then save page as"]) {
+ for (let idx = 0; idx < links.length; idx++) {
+ let menu = document.getElementById("contentAreaContextMenu");
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouse(
+ "#link" + idx,
+ 5,
+ 5,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShown;
+
+ let promptPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ setTimeout(() => {
+ resolve(fp.defaultString);
+ }, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ });
+
+ if (testname == "save link as") {
+ let menuitem = document.getElementById("context-savelink");
+ menu.activateItem(menuitem);
+ } else {
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ let menuitem = document.getElementById("context-openlinkintab");
+ menu.activateItem(menuitem);
+
+ let tab = await newTabPromise;
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ document.getElementById("Browser:SavePage").doCommand();
+ }
+
+ let filename = await promptPromise;
+
+ let expectedFilename = links[idx].filename;
+ // Only codepaths that go through contentAreaUtils.js use the
+ // name from the content disposition.
+ if (testname == "save link as" && expectedFilename == "four.png") {
+ expectedFilename = "save_filename.png";
+ }
+
+ is(
+ filename,
+ expectedFilename,
+ "i" +
+ idx +
+ " " +
+ expectedFilename +
+ " link was saved with the correct name " +
+ testname
+ );
+
+ if (testname == "save link then save page as") {
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ }
+ }
+});
+
+// This test checks that links that with a download attribute
+// are saved with the right filenames.
+add_task(async function save_download_links() {
+ let downloads = await getItems("downloads");
+
+ let list = await Downloads.getList(Downloads.PUBLIC);
+ for (let idx = 0; idx < downloads.length; idx++) {
+ let downloadFinishedPromise = promiseDownloadFinished(list);
+
+ BrowserTestUtils.synthesizeMouse(
+ "#download" + idx,
+ 2,
+ 2,
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ let download = await downloadFinishedPromise;
+
+ let filename = PathUtils.filename(download.target.path);
+
+ if (downloads[idx].filename.length > 240) {
+ ok(
+ checkShortenedFilename(filename, downloads[idx].filename),
+ "download" +
+ idx +
+ " " +
+ downloads[idx].filename +
+ " was saved with the correct name when link has download attribute"
+ );
+ } else {
+ if (idx == 66 && filename == "Untitled(1)") {
+ // Sometimes, the previous test's file still exists or wasn't created in time
+ // and a non-duplicated name is created. Allow this rather than figuring out
+ // how to avoid it since it doesn't affect what is being tested here.
+ filename = "Untitled";
+ }
+
+ is(
+ filename,
+ downloads[idx].filename,
+ "download" +
+ idx +
+ " " +
+ downloads[idx].filename +
+ " was saved with the correct name when link has download attribute"
+ );
+ }
+
+ try {
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {}
+ }
+});
+
+// This test verifies that invalid extensions are not removed when they
+// have been entered in the file picker.
+add_task(async function save_page_with_invalid_after_filepicker() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://localhost:8000/save_filename.sjs?type=html&filename=invfile.lnk"
+ );
+
+ let filename = await new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ let expectedFilename =
+ AppConstants.platform == "win" ? "invfile.lnk.htm" : "invfile.lnk.html";
+ is(fp.defaultString, expectedFilename, "supplied filename is correct");
+ setTimeout(() => {
+ resolve("otherfile.local");
+ }, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+
+ document.getElementById("Browser:SavePage").doCommand();
+ });
+
+ is(filename, "otherfile.local", "lnk extension has been preserved");
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async function save_page_with_invalid_extension() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://localhost:8000/save_filename.sjs?type=html"
+ );
+
+ let filename = await new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ setTimeout(() => {
+ resolve(fp.defaultString);
+ }, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+
+ document.getElementById("Browser:SavePage").doCommand();
+ });
+
+ is(
+ filename,
+ AppConstants.platform == "win" ? "file.inv.htm" : "file.inv.html",
+ "html extension has been added"
+ );
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
+
+add_task(async () => {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ MockFilePicker.cleanup();
+ await new Promise(resolve => httpServer.stop(resolve));
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js b/uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js
new file mode 100644
index 0000000000..6bf375dfff
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_shows_where_to_save_dialog.js
@@ -0,0 +1,303 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DownloadIntegration } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const { handleInternally, useHelperApp, useSystemDefault, saveToDisk } =
+ Ci.nsIHandlerInfo;
+
+let MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", false],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+ MockFilePicker.cleanup();
+ });
+});
+
+// This test ensures that a "Save as..." filepicker dialog is shown for a file
+// if useDownloadDir ("Always ask where to save files") is set to false and
+// the filetype is set to save to disk.
+add_task(async function aDownloadSavedToDiskPromptsForFolder() {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ ensureMIMEState(
+ { preferredAction: saveToDisk },
+ { type: "text/plain", ext: "txt" }
+ );
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ let filePickerShownPromise = new Promise(resolve => {
+ MockFilePicker.showCallback = function (fp) {
+ setTimeout(resolve, 0);
+ return Ci.nsIFilePicker.returnCancel;
+ };
+ });
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_txt_attachment_test.txt",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ info("Waiting on filepicker.");
+ await filePickerShownPromise;
+ ok(true, "filepicker should have been shown");
+
+ BrowserTestUtils.removeTab(loadingTab);
+});
+
+// This test ensures that downloads configured to open internally create only
+// one file destination when saved via the filepicker, and don't prompt.
+add_task(async function testFilesHandledInternally() {
+ let dir = await setupFilePickerDirectory();
+
+ ensureMIMEState(
+ { preferredAction: handleInternally },
+ { type: "image/webp", ext: "webp" }
+ );
+
+ let filePickerShown = false;
+ MockFilePicker.showCallback = function (fp) {
+ filePickerShown = true;
+ return Ci.nsIFilePicker.returnCancel;
+ };
+
+ let thirdTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ url => {
+ info("Got load for " + url);
+ return url.endsWith("file_green.webp") && url.startsWith("file:");
+ },
+ true,
+ true
+ );
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_green.webp",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ let openedTab = await thirdTabPromise;
+ ok(!filePickerShown, "file picker should not have shown up.");
+
+ assertCorrectFile(dir, "file_green.webp");
+
+ // Cleanup
+ BrowserTestUtils.removeTab(loadingTab);
+ BrowserTestUtils.removeTab(openedTab);
+});
+
+// This test ensures that downloads configured to open with a system default
+// app create only one file destination and don't open the filepicker.
+add_task(async function testFilesHandledBySystemDefaultApp() {
+ let dir = await setupFilePickerDirectory();
+
+ ensureMIMEState({ preferredAction: useSystemDefault });
+
+ let filePickerShown = false;
+ MockFilePicker.showCallback = function (fp) {
+ filePickerShown = true;
+ return Ci.nsIFilePicker.returnCancel;
+ };
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let launchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = async (file, mimeInfo) => {
+ is(
+ useSystemDefault,
+ mimeInfo.preferredAction,
+ "The file should be launched with a system app handler."
+ );
+ resolve();
+ };
+ });
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_pdf_application_pdf.pdf",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ await launchFileCalled;
+ ok(!filePickerShown, "file picker should not have shown up.");
+
+ assertCorrectFile(dir, "file_pdf_application_pdf.pdf");
+
+ // Cleanup
+ BrowserTestUtils.removeTab(loadingTab);
+ DownloadIntegration.launchFile = oldLaunchFile;
+});
+
+// This test ensures that downloads configured to open with a helper app create
+// only one file destination when saved via the filepicker.
+add_task(async function testFilesHandledByHelperApp() {
+ let dir = await setupFilePickerDirectory();
+
+ // Create a custom helper app so we can check that a launcherPath is
+ // configured for the serialized download.
+ let appHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ appHandler.name = "Dummy Test Handler";
+ appHandler.executable = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ appHandler.executable.append("helper_handler_test.exe");
+
+ if (!appHandler.executable.exists()) {
+ appHandler.executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o777);
+ }
+
+ ensureMIMEState({
+ preferredAction: useHelperApp,
+ preferredHandlerApp: appHandler,
+ });
+
+ let filePickerShown = false;
+ MockFilePicker.showCallback = function (fp) {
+ filePickerShown = true;
+ return Ci.nsIFilePicker.returnCancel;
+ };
+
+ let publicDownloads = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinishedPromise = new Promise(resolve => {
+ publicDownloads.addView({
+ onDownloadChanged(download) {
+ if (download.succeeded || download.error) {
+ ok(
+ download.launcherPath.includes("helper_handler_test.exe"),
+ "Launcher path is available."
+ );
+ resolve();
+ }
+ },
+ });
+ });
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let launchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = async (file, mimeInfo) => {
+ is(
+ useHelperApp,
+ mimeInfo.preferredAction,
+ "The file should be launched with a helper app handler."
+ );
+ resolve();
+ };
+ });
+
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_PATH + "file_pdf_application_pdf.pdf",
+ waitForLoad: false,
+ waitForStateStop: true,
+ });
+
+ await downloadFinishedPromise;
+ await launchFileCalled;
+ ok(!filePickerShown, "file picker should not have shown up.");
+ assertCorrectFile(dir, "file_pdf_application_pdf.pdf");
+
+ // Cleanup
+ BrowserTestUtils.removeTab(loadingTab);
+ DownloadIntegration.launchFile = oldLaunchFile;
+});
+
+async function setupFilePickerDirectory() {
+ let saveDir = createSaveDir();
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, saveDir);
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+
+ MockFilePicker.displayDirectory = saveDir;
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ MockFilePicker.showCallback = function (fp) {
+ let file = saveDir.clone();
+ file.append(fp.defaultString);
+ MockFilePicker.setFiles([file]);
+ };
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.download.dir");
+ Services.prefs.clearUserPref("browser.download.folderList");
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let unfinishedDownloads = new Set(
+ (await publicList.getAll()).filter(dl => !dl.succeeded && !dl.error)
+ );
+ if (unfinishedDownloads.size) {
+ info(`Have ${unfinishedDownloads.size} unfinished downloads, waiting.`);
+ await new Promise(resolve => {
+ let view = {
+ onChanged(dl) {
+ if (unfinishedDownloads.has(dl) && (dl.succeeded || dl.error)) {
+ unfinishedDownloads.delete(dl);
+ info(`Removed another download.`);
+ if (!unfinishedDownloads.size) {
+ publicList.removeView(view);
+ resolve();
+ }
+ }
+ },
+ };
+ publicList.addView(view);
+ });
+ }
+ try {
+ await IOUtils.remove(saveDir.path, { recursive: true });
+ } catch (e) {
+ console.error(e);
+ }
+ });
+
+ return saveDir;
+}
+
+function assertCorrectFile(saveDir, filename) {
+ info("Make sure additional files haven't been created.");
+ let iter = saveDir.directoryEntries;
+ let file = iter.nextFile;
+ ok(file.path.includes(filename), "Download has correct filename");
+ ok(!iter.nextFile, "Only one file was created.");
+}
+
+function createSaveDir() {
+ info("Creating save directory.");
+ let time = new Date().getTime();
+ let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append(time);
+ return saveDir;
+}
+
+function ensureMIMEState(
+ { preferredAction, preferredHandlerApp = null },
+ { type = "application/pdf", ext = "pdf" } = {}
+) {
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension(type, ext);
+ mimeInfo.preferredAction = preferredAction;
+ mimeInfo.preferredApplicationHandler = preferredHandlerApp;
+ mimeInfo.alwaysAskBeforeHandling = false;
+ gHandlerSvc.store(mimeInfo);
+}
diff --git a/uriloader/exthandler/tests/mochitest/browser_standalone_application_chooser_window_fallback.js b/uriloader/exthandler/tests/mochitest/browser_standalone_application_chooser_window_fallback.js
new file mode 100644
index 0000000000..40496fb3b2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_standalone_application_chooser_window_fallback.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the fallback fixed in bug 1875460, if the modern tab dialog box is not
+// supported.
+
+const TEST_URL =
+ "https://example.com/browser/" +
+ "uriloader/exthandler/tests/mochitest/FTPprotocolHandler.html";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", false]],
+ });
+
+ // Load a page with an FTP link.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
+
+ // Make sure no handler is set, forcing the dialog to show.
+ let protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("ftp");
+ ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set");
+ let handlers = protoInfo.possibleApplicationHandlers;
+ is(0, handlers.length, "no handler registered for ftp");
+ protoInfo.alwaysAskBeforeHandling = true;
+ let handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ handlerSvc.store(protoInfo);
+
+ // Delete getTabDialogBox from gBrowser, to test the fallback to the standalone
+ // application chooser window.
+ let _getTabDialogBox = gBrowser.getTabDialogBox;
+ delete gBrowser.getTabDialogBox;
+
+ let appChooserDialogOpenPromise = BrowserTestUtils.domWindowOpened(
+ null,
+ async win => {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ Assert.ok(
+ win.document.documentURI ==
+ "chrome://mozapps/content/handling/appChooser.xhtml",
+ "application chooser dialog opened"
+ );
+ return true;
+ }
+ );
+ let link = "#link";
+ await BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser);
+ let appChooserDialog = await appChooserDialogOpenPromise;
+
+ let appChooserDialogClosePromise =
+ BrowserTestUtils.domWindowClosed(appChooserDialog);
+ let dialog = appChooserDialog.document.getElementsByTagName("dialog")[0];
+ let cancelButton = dialog.getButton("cancel");
+ cancelButton.click();
+ await appChooserDialogClosePromise;
+
+ // Restore the original getTabDialogBox(), to not affect other tests.
+ gBrowser.getTabDialogBox = _getTabDialogBox;
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js b/uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js
new file mode 100644
index 0000000000..7155c35fd9
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_txt_download_save_as.js
@@ -0,0 +1,168 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DownloadIntegration } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+const HandlerService = Cc[
+ "@mozilla.org/uriloader/handler-service;1"
+].getService(Ci.nsIHandlerService);
+const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+const {
+ saveToDisk,
+ alwaysAsk,
+ handleInternally,
+ useHelperApp,
+ useSystemDefault,
+} = Ci.nsIHandlerInfo;
+const testDir = createTemporarySaveDirectory();
+const MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js",
+ this
+);
+
+async function testSaveAsDownload() {
+ await BrowserTestUtils.withNewTab(
+ `data:text/html,<a id="test-link" href="${TEST_PATH}/file_txt_attachment_test.txt">Test TXT Link</a>`,
+ async browser => {
+ let menu = document.getElementById("contentAreaContextMenu");
+ ok(menu, "Context menu exists on the page");
+
+ let popupShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a#test-link",
+ { type: "contextmenu", button: 2 },
+ browser
+ );
+ await popupShown;
+ info("Context menu popup was successfully displayed");
+
+ let filePickerPromise = setupFilePicker();
+
+ info("Clicking Save As... context menu");
+ let menuitem = menu.querySelector("#context-savelink");
+ menu.activateItem(menuitem);
+ await filePickerPromise;
+ }
+ );
+}
+
+async function setupFilePicker() {
+ return new Promise(resolve => {
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ MockFilePicker.displayDirectory = testDir;
+ MockFilePicker.showCallback = fp => {
+ ok(true, "filepicker should be visible");
+ ok(
+ fp.defaultExtension === "txt",
+ "Default extension in filepicker should be txt"
+ );
+ ok(
+ fp.defaultString === "file_txt_attachment_test.txt",
+ "Default string name in filepicker should have the correct file name"
+ );
+ const destFile = testDir.clone();
+ destFile.append(fp.defaultString);
+ MockFilePicker.setFiles([destFile]);
+
+ mockTransferCallback = success => {
+ ok(success, "File should have been downloaded successfully");
+ ok(destFile.exists(), "File should exist in test directory");
+ resolve(destFile);
+ };
+ };
+ });
+}
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.always_ask_before_handling_new_types", false],
+ ["browser.download.useDownloadDir", false],
+ ],
+ });
+ mockTransferRegisterer.register();
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ DownloadIntegration.launchFile = () => {
+ ok(false, "Download should not have launched");
+ };
+
+ registerCleanupFunction(async () => {
+ DownloadIntegration.launchFile = oldLaunchFile;
+ mockTransferRegisterer.unregister();
+
+ // We only want to run MockFilerPicker.cleanup after the entire test is run.
+ // Otherwise, we cannot use MockFilePicker for each preferredAction.
+ MockFilePicker.cleanup();
+
+ testDir.remove(true);
+ ok(!testDir.exists(), "Test directory should be removed");
+ });
+});
+
+/**
+ * Tests that selecting the context menu item `Save Link As…` on a txt file link
+ * opens the file picker and only downloads the file without any launches when
+ * browser.download.always_ask_before_handling_new_types is disabled.
+ */
+add_task(async function test_txt_save_as_link() {
+ let mimeInfo;
+
+ for (let preferredAction of [
+ saveToDisk,
+ alwaysAsk,
+ handleInternally,
+ useHelperApp,
+ useSystemDefault,
+ ]) {
+ mimeInfo = MIMEService.getFromTypeAndExtension("text/plain", "txt");
+ mimeInfo.alwaysAskBeforeHandling = preferredAction === alwaysAsk;
+ mimeInfo.preferredAction = preferredAction;
+ HandlerService.store(mimeInfo);
+
+ info(
+ `Setting up filepicker with preferredAction ${preferredAction} and ask = ${mimeInfo.alwaysAskBeforeHandling}`
+ );
+ await testSaveAsDownload(mimeInfo);
+ }
+});
+
+/**
+ * Tests that selecting the context menu item `Save Link As…` on a txt file link
+ * opens the file picker and only downloads the file without any launches when
+ * browser.download.always_ask_before_handling_new_types is disabled. For this
+ * particular test, set alwaysAskBeforeHandling to true.
+ */
+add_task(async function test_txt_save_as_link_alwaysAskBeforeHandling() {
+ let mimeInfo;
+
+ for (let preferredAction of [
+ saveToDisk,
+ alwaysAsk,
+ handleInternally,
+ useHelperApp,
+ useSystemDefault,
+ ]) {
+ mimeInfo = MIMEService.getFromTypeAndExtension("text/plain", "txt");
+ mimeInfo.alwaysAskBeforeHandling = true;
+ mimeInfo.preferredAction = preferredAction;
+ HandlerService.store(mimeInfo);
+
+ info(
+ `Setting up filepicker with preferredAction ${preferredAction} and ask = ${mimeInfo.alwaysAskBeforeHandling}`
+ );
+ await testSaveAsDownload(mimeInfo);
+ }
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js b/uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js
new file mode 100644
index 0000000000..a9d4803fca
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_web_handler_app_pinned_tab.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+let testURL =
+ "http://mochi.test:8888/browser/" +
+ "uriloader/exthandler/tests/mochitest/mailto.html";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gExternalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gHandlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+
+let prevAlwaysAskBeforeHandling;
+let prevPreferredAction;
+let prevPreferredApplicationHandler;
+
+add_setup(async function () {
+ let handler = gExternalProtocolService.getProtocolHandlerInfo("mailto", {});
+
+ // Create a fake mail handler
+ const APP_NAME = "ExMail";
+ const HANDLER_URL = "https://example.com/?extsrc=mailto&url=%s";
+ let app = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ app.uriTemplate = HANDLER_URL;
+ app.name = APP_NAME;
+
+ // Store defaults
+ prevAlwaysAskBeforeHandling = handler.alwaysAskBeforeHandling;
+ prevPreferredAction = handler.preferredAction;
+ prevPreferredApplicationHandler = handler.preferredApplicationHandler;
+
+ // Set the fake app as default
+ handler.alwaysAskBeforeHandling = false;
+ handler.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ handler.preferredApplicationHandler = app;
+ gHandlerService.store(handler);
+});
+
+registerCleanupFunction(async function () {
+ let handler = gExternalProtocolService.getProtocolHandlerInfo("mailto", {});
+ handler.alwaysAskBeforeHandling = prevAlwaysAskBeforeHandling;
+ handler.preferredAction = prevPreferredAction;
+ handler.preferredApplicationHandler = prevPreferredApplicationHandler;
+ gHandlerService.store(handler);
+});
+
+add_task(async function () {
+ const expectedURL =
+ "https://example.com/?extsrc=mailto&url=mailto%3Amail%40example.com";
+
+ // Load a page with mailto handler.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, testURL);
+ await BrowserTestUtils.browserLoaded(browser, false, testURL);
+
+ // Pin as an app tab
+ gBrowser.pinTab(gBrowser.selectedTab);
+
+ // Click the link and check the new tab is correct
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
+ await BrowserTestUtils.synthesizeMouseAtCenter("#link", {}, browser);
+ let tab = await promiseTabOpened;
+ is(
+ gURLBar.value,
+ UrlbarTestUtils.trimURL(expectedURL),
+ "the mailto web handler is opened in a new tab"
+ );
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
new file mode 100644
index 0000000000..828d02d24b
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
@@ -0,0 +1,130 @@
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+let testURL =
+ "https://example.com/browser/" +
+ "uriloader/exthandler/tests/mochitest/protocolHandler.html";
+
+add_task(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.external_protocol_requires_permission", false]],
+ });
+
+ // Load a page registering a protocol handler.
+ let browser = gBrowser.selectedBrowser;
+ BrowserTestUtils.startLoadingURIString(browser, testURL);
+ await BrowserTestUtils.browserLoaded(browser, false, testURL);
+
+ // Register the protocol handler by clicking the notificationbar button.
+ let notificationValue = "Protocol Registration: web+testprotocol";
+ let getNotification = () =>
+ gBrowser.getNotificationBox().getNotificationWithValue(notificationValue);
+ await BrowserTestUtils.waitForCondition(getNotification);
+ let notification = getNotification();
+ let button = notification.buttonContainer.querySelector("button");
+ ok(button, "got registration button");
+ button.click();
+
+ // Set the new handler as default.
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+ let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol");
+ is(
+ protoInfo.preferredAction,
+ protoInfo.useHelperApp,
+ "using a helper application is the preferred action"
+ );
+ ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set");
+ let handlers = protoInfo.possibleApplicationHandlers;
+ is(1, handlers.length, "only one handler registered for web+testprotocol");
+ let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp);
+ ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler");
+ is(
+ handler.uriTemplate,
+ "https://example.com/foobar?uri=%s",
+ "correct url template"
+ );
+ protoInfo.preferredApplicationHandler = handler;
+ protoInfo.alwaysAskBeforeHandling = false;
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ handlerSvc.store(protoInfo);
+
+ const expectedURL =
+ "https://example.com/foobar?uri=web%2Btestprotocol%3Atest";
+
+ // Create a framed link:
+ await SpecialPowers.spawn(browser, [], async function () {
+ let iframe = content.document.createElement("iframe");
+ iframe.src = `data:text/html,<a href="web+testprotocol:test">Click me</a>`;
+ content.document.body.append(iframe);
+ // Can't return this promise because it resolves to the event object.
+ await ContentTaskUtils.waitForEvent(iframe, "load");
+ iframe.contentDocument.querySelector("a").click();
+ });
+ let kidContext = browser.browsingContext.children[0];
+ await TestUtils.waitForCondition(() => {
+ let spec = kidContext.currentWindowGlobal?.documentURI?.spec || "";
+ return spec == expectedURL;
+ });
+ is(
+ kidContext.currentWindowGlobal.documentURI.spec,
+ expectedURL,
+ "Should load in frame."
+ );
+
+ // Middle-click a testprotocol link and check the new tab is correct
+ let link = "#link";
+
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL);
+ await BrowserTestUtils.synthesizeMouseAtCenter(link, { button: 1 }, browser);
+ let tab = await promiseTabOpened;
+ gBrowser.selectedTab = tab;
+ is(
+ gURLBar.value,
+ UrlbarTestUtils.trimURL(expectedURL),
+ "the expected URL is displayed in the location bar"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ // Shift-click the testprotocol link and check the new window.
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow({
+ url: expectedURL,
+ });
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ link,
+ { shiftKey: true },
+ browser
+ );
+ let win = await newWindowPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == expectedURL
+ );
+ is(
+ win.gURLBar.value,
+ UrlbarTestUtils.trimURL(expectedURL),
+ "the expected URL is displayed in the location bar"
+ );
+ await BrowserTestUtils.closeWindow(win);
+
+ // Click the testprotocol link and check the url in the current tab.
+ let loadPromise = BrowserTestUtils.browserLoaded(browser);
+ await BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser);
+ await loadPromise;
+ await BrowserTestUtils.waitForCondition(
+ () => gURLBar.value != UrlbarTestUtils.trimURL(testURL)
+ );
+ is(
+ gURLBar.value,
+ UrlbarTestUtils.trimURL(expectedURL),
+ "the expected URL is displayed in the location bar"
+ );
+
+ // Cleanup.
+ protoInfo.preferredApplicationHandler = null;
+ handlers.removeElementAt(0);
+ handlerSvc.store(protoInfo);
+});
diff --git a/uriloader/exthandler/tests/mochitest/download.bin b/uriloader/exthandler/tests/mochitest/download.bin
new file mode 100644
index 0000000000..0e4b0c7bae
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/download.bin
@@ -0,0 +1 @@
+abc123
diff --git a/uriloader/exthandler/tests/mochitest/download.sjs b/uriloader/exthandler/tests/mochitest/download.sjs
new file mode 100644
index 0000000000..d02d2b7355
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/download.sjs
@@ -0,0 +1,42 @@
+"use strict";
+
+function actuallyHandleRequest(req, res) {
+ res.setHeader("Content-Type", "application/octet-stream", false);
+ res.write("abc123");
+ res.finish();
+}
+
+function handleRequest(req, res) {
+ if (req.queryString.includes("finish")) {
+ res.write("OK");
+ let downloadReq = null;
+ getObjectState("downloadReq", o => {
+ downloadReq = o;
+ });
+ // Two possibilities: either the download request has already reached us, or not.
+ if (downloadReq) {
+ downloadReq.wrappedJSObject.callback();
+ } else {
+ // Set a variable to allow the request to complete immediately:
+ setState("finishReq", "true");
+ }
+ } else if (req.queryString.includes("reset")) {
+ res.write("OK");
+ setObjectState("downloadReq", null);
+ setState("finishReq", "false");
+ } else {
+ res.processAsync();
+ if (getState("finishReq") === "true") {
+ actuallyHandleRequest(req, res);
+ } else {
+ let o = {
+ callback() {
+ actuallyHandleRequest(req, res);
+ },
+ };
+ o.wrappedJSObject = o;
+ o.QueryInterface = () => o;
+ setObjectState("downloadReq", o);
+ }
+ }
+}
diff --git a/uriloader/exthandler/tests/mochitest/download_page.html b/uriloader/exthandler/tests/mochitest/download_page.html
new file mode 100644
index 0000000000..5a264888fa
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/download_page.html
@@ -0,0 +1,22 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset=UTF-8>
+ <title>Test page for link clicking</title>
+ <script type="text/javascript">
+ function launch_download(extra) {
+ window.open("download.sjs", "_blank", "height=100,width=100" + extra);
+ }
+ </script>
+</head>
+<body>
+ <a href="download.bin" id="regular_load">regular load</a>
+ <a href="download.bin" id="target_blank" target="_blank" rel="opener">target blank</a>
+ <a href="#" onclick="launch_download(''); return false" id="new_window">new window</a>
+ <a href="#" onclick="window.open('download_page.html?newwin'); return false" id="open_in_new_tab">click to reopen</a>
+ <a href="download.bin" id="target_blank_no_opener" rel="noopener" target="_blank">target blank (noopener)</a>
+ <a href="#" onclick="window.open('download.bin', '_blank', 'noopener'); return false" id="open_in_new_tab_no_opener">click to reopen (noopener)</a>
+ <a href="#" onclick="launch_download(',noopener'); return false" id="new_window_no_opener">new window (noopener)</a>
+</body>
diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe b/uriloader/exthandler/tests/mochitest/file_as.exe
new file mode 100644
index 0000000000..f2f5ab47f3
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_as.exe
@@ -0,0 +1 @@
+Not actually an executable... but let's pretend!
diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^
new file mode 100644
index 0000000000..89f22e30be
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^
@@ -0,0 +1,2 @@
+Content-Type: binary/octet-stream
+Content-Disposition: attachment
diff --git a/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html
new file mode 100644
index 0000000000..eb2fb74441
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html
@@ -0,0 +1 @@
+<iframe src="mailto:foo@bar.com"></iframe>
diff --git a/uriloader/exthandler/tests/mochitest/file_green.webp b/uriloader/exthandler/tests/mochitest/file_green.webp
new file mode 100644
index 0000000000..04b7f003b4
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_green.webp
Binary files differ
diff --git a/uriloader/exthandler/tests/mochitest/file_green.webp^headers^ b/uriloader/exthandler/tests/mochitest/file_green.webp^headers^
new file mode 100644
index 0000000000..3f6afd6625
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_green.webp^headers^
@@ -0,0 +1,3 @@
+Content-Disposition: attachment; filename=file_green.webp
+Content-Type: image/webp
+
diff --git a/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg
new file mode 100644
index 0000000000..b730c4c492
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
+ <rect x="5" y="5" width="10" height="10" fill="white"/>
+</svg>
diff --git a/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^ b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^
new file mode 100644
index 0000000000..5279ae8636
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_image_svgxml.svg^headers^
@@ -0,0 +1,2 @@
+content-disposition: attachment; filename=file_image_svgxml_svg; filename*=UTF-8''file_image_svgxml.svg
+content-type: image/svg+xml
diff --git a/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html
new file mode 100644
index 0000000000..b1bb863f89
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html
@@ -0,0 +1 @@
+<iframe srcdoc="<a href='mailto:help@example.com'>Mail someone</a>"></iframe>
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^
new file mode 100644
index 0000000000..9e8cb41cba
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_octet_stream.pdf^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename="file_pdf_application_octet_stream.pdf"; filename*=UTF-8''file_pdf_application_octet_stream.pdf
+Content-Type: application/octet-stream
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^
new file mode 100644
index 0000000000..d1d59b9754
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^
@@ -0,0 +1,2 @@
+content-disposition: attachment; filename=file_pdf_application_pdf.pdf; filename*=UTF-8''file_pdf_application_pdf.pdf
+content-type: application/pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^
new file mode 100644
index 0000000000..157c0e0943
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^
@@ -0,0 +1,2 @@
+content-disposition: attachment; filename=file_pdf_application_unknown.pdf; filename*=UTF-8''file_pdf_application_unknown.pdf
+content-type: application/unknown
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf
diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^
new file mode 100644
index 0000000000..6358f54f48
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename="file_pdf_binary_octet_stream.pdf"; filename*=UTF-8''file_pdf_binary_octet_stream.pdf
+Content-Type: binary/octet-stream
diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt
diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^
new file mode 100644
index 0000000000..dcfed6af23
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename=file_txt_attachment_test.txt
+Content-Type: text/plain
diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png
new file mode 100644
index 0000000000..743292dc6f
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png
Binary files differ
diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^
new file mode 100644
index 0000000000..06e0cd957f
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: inline; filename=file_with%40%40funny_name.png
+Content-Type: image/png
diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm
new file mode 100644
index 0000000000..7bc738b8b4
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm
Binary files differ
diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^
new file mode 100644
index 0000000000..b77e9d3687
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: inline; filename=file_with%5Bfunny_name.webm
+Content-Type: video/webm
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml
new file mode 100644
index 0000000000..3a5792586a
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml
@@ -0,0 +1,4 @@
+<?xml version = "1.0" encoding = "utf-8"?>
+
+<something>
+</something>
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^
new file mode 100644
index 0000000000..5bdc4448e8
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment
+Content-Type: binary/octet-stream
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml
new file mode 100644
index 0000000000..3a5792586a
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml
@@ -0,0 +1,4 @@
+<?xml version = "1.0" encoding = "utf-8"?>
+
+<something>
+</something>
diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^
new file mode 100644
index 0000000000..ac0355d976
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename=file_xml_attachment_test.xml
+Content-Type: text/xml
diff --git a/uriloader/exthandler/tests/mochitest/head.js b/uriloader/exthandler/tests/mochitest/head.js
new file mode 100644
index 0000000000..4b973da06d
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/head.js
@@ -0,0 +1,537 @@
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { HandlerServiceTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/HandlerServiceTestUtils.sys.mjs"
+);
+
+var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+function createMockedHandlerApp() {
+ // Mock the executable
+ let mockedExecutable = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, "mockedExecutable")
+ );
+ if (!mockedExecutable.exists()) {
+ mockedExecutable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755);
+ }
+
+ // Mock the handler app
+ let mockedHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ mockedHandlerApp.executable = mockedExecutable;
+ mockedHandlerApp.detailedDescription = "Mocked handler app";
+
+ registerCleanupFunction(function () {
+ // remove the mocked executable from disk.
+ if (mockedExecutable.exists()) {
+ mockedExecutable.remove(true);
+ }
+ });
+
+ return mockedHandlerApp;
+}
+
+function createMockedObjects(createHandlerApp) {
+ // Mock the mime info
+ let internalMockedMIME = gMimeSvc.getFromTypeAndExtension(
+ "text/x-test-handler",
+ null
+ );
+ internalMockedMIME.alwaysAskBeforeHandling = true;
+ internalMockedMIME.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ internalMockedMIME.appendExtension("abc");
+ if (createHandlerApp) {
+ let mockedHandlerApp = createMockedHandlerApp();
+ internalMockedMIME.description = mockedHandlerApp.detailedDescription;
+ internalMockedMIME.possibleApplicationHandlers.appendElement(
+ mockedHandlerApp
+ );
+ internalMockedMIME.preferredApplicationHandler = mockedHandlerApp;
+ }
+
+ // Proxy for the mocked MIME info for faking the read-only attributes
+ let mockedMIME = new Proxy(internalMockedMIME, {
+ get(target, property) {
+ switch (property) {
+ case "hasDefaultHandler":
+ return true;
+ case "defaultDescription":
+ return "Default description";
+ default:
+ return target[property];
+ }
+ },
+ });
+
+ // Mock the launcher:
+ let mockedLauncher = {
+ MIMEInfo: mockedMIME,
+ source: Services.io.newURI("http://www.mozilla.org/"),
+ suggestedFileName: "test_download_dialog.abc",
+ targetFileIsExecutable: false,
+ saveToDisk() {},
+ cancel() {},
+ setDownloadToLaunch() {},
+ setWebProgressListener() {},
+ saveDestinationAvailable() {},
+ contentLength: 42,
+ targetFile: null, // never read
+ // PRTime is microseconds since epoch, Date.now() returns milliseconds:
+ timeDownloadStarted: Date.now() * 1000,
+ QueryInterface: ChromeUtils.generateQI([
+ "nsICancelable",
+ "nsIHelperAppLauncher",
+ ]),
+ };
+
+ registerCleanupFunction(function () {
+ // remove the mocked mime info from database.
+ let mockHandlerInfo = gMimeSvc.getFromTypeAndExtension(
+ "text/x-test-handler",
+ null
+ );
+ if (gHandlerSvc.exists(mockHandlerInfo)) {
+ gHandlerSvc.remove(mockHandlerInfo);
+ }
+ });
+
+ return mockedLauncher;
+}
+
+function createTemporarySaveDirectory() {
+ var saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ return saveDir;
+}
+
+async function openHelperAppDialog(launcher) {
+ let helperAppDialog = Cc[
+ "@mozilla.org/helperapplauncherdialog;1"
+ ].createInstance(Ci.nsIHelperAppLauncherDialog);
+
+ let helperAppDialogShownPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ try {
+ helperAppDialog.show(launcher, window, "foopy");
+ } catch (ex) {
+ ok(
+ false,
+ "Trying to show unknownContentType.xhtml failed with exception: " + ex
+ );
+ console.error(ex);
+ }
+ let dlg = await helperAppDialogShownPromise;
+
+ is(
+ dlg.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Got correct dialog"
+ );
+
+ return dlg;
+}
+
+/**
+ * Wait for a subdialog event indicating a dialog either opened
+ * or was closed.
+ *
+ * First argument is the browser in which to listen. If a tabbrowser,
+ * we listen to subdialogs for any tab of that browser.
+ */
+async function waitForSubDialog(browser, url, state) {
+ let eventStr = state ? "dialogopen" : "dialogclose";
+
+ let eventTarget;
+
+ // Tabbrowser?
+ if (browser.tabContainer) {
+ eventTarget = browser.tabContainer.ownerDocument.documentElement;
+ } else {
+ // Individual browser. Get its box:
+ let tabDialogBox = browser.ownerGlobal.gBrowser.getTabDialogBox(browser);
+ eventTarget = tabDialogBox.getTabDialogManager()._dialogStack;
+ }
+
+ let checkFn;
+
+ if (state) {
+ checkFn = dialogEvent => dialogEvent.detail.dialog?._openedURL == url;
+ }
+
+ let event = await BrowserTestUtils.waitForEvent(
+ eventTarget,
+ eventStr,
+ true,
+ checkFn
+ );
+
+ let { dialog } = event.detail;
+
+ // If the dialog is closing wait for it to be fully closed before resolving
+ if (!state) {
+ await dialog._closingPromise;
+ }
+
+ return event.detail.dialog;
+}
+
+/**
+ * Wait for protocol permission dialog open/close.
+ * @param {MozBrowser} browser - Browser element the dialog belongs to.
+ * @param {boolean} state - true: dialog open, false: dialog close
+ * @returns {Promise<SubDialog>} - Returns a promise which resolves with the
+ * SubDialog object of the dialog which closed or opened.
+ */
+async function waitForProtocolPermissionDialog(browser, state) {
+ return waitForSubDialog(
+ browser,
+ "chrome://mozapps/content/handling/permissionDialog.xhtml",
+ state
+ );
+}
+
+/**
+ * Wait for protocol app chooser dialog open/close.
+ * @param {MozBrowser} browser - Browser element the dialog belongs to.
+ * @param {boolean} state - true: dialog open, false: dialog close
+ * @returns {Promise<SubDialog>} - Returns a promise which resolves with the
+ * SubDialog object of the dialog which closed or opened.
+ */
+async function waitForProtocolAppChooserDialog(browser, state) {
+ return waitForSubDialog(
+ browser,
+ "chrome://mozapps/content/handling/appChooser.xhtml",
+ state
+ );
+}
+
+async function promiseDownloadFinished(list, stopFromOpening) {
+ return new Promise(resolve => {
+ list.addView({
+ onDownloadChanged(download) {
+ if (stopFromOpening) {
+ download.launchWhenSucceeded = false;
+ }
+ info("Download changed!");
+ if (download.succeeded || download.error) {
+ info("Download succeeded or errored");
+ list.removeView(this);
+ resolve(download);
+ }
+ },
+ });
+ });
+}
+
+function setupMailHandler() {
+ let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto");
+ let gOldMailHandlers = [];
+
+ // Remove extant web handlers because they have icons that
+ // we fetch from the web, which isn't allowed in tests.
+ let handlers = mailHandlerInfo.possibleApplicationHandlers;
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ gOldMailHandlers.push(handler);
+ // If we get here, this is a web handler app. Remove it:
+ handlers.removeElementAt(i);
+ } catch (ex) {}
+ }
+
+ let previousHandling = mailHandlerInfo.alwaysAskBeforeHandling;
+ mailHandlerInfo.alwaysAskBeforeHandling = true;
+
+ // Create a dummy web mail handler so we always know the mailto: protocol.
+ // Without this, the test fails on VMs without a default mailto: handler,
+ // because no dialog is ever shown, as we ignore subframe navigations to
+ // protocols that cannot be handled.
+ let dummy = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ dummy.name = "Handler 1";
+ dummy.uriTemplate = "https://example.com/first/%s";
+ mailHandlerInfo.possibleApplicationHandlers.appendElement(dummy);
+
+ gHandlerSvc.store(mailHandlerInfo);
+ registerCleanupFunction(() => {
+ // Re-add the original protocol handlers:
+ let mailHandlers = mailHandlerInfo.possibleApplicationHandlers;
+ for (let i = handlers.Count() - 1; i >= 0; i--) {
+ try {
+ // See if this is a web handler. If it is, it'll throw, otherwise,
+ // we will remove it.
+ mailHandlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ mailHandlers.removeElementAt(i);
+ } catch (ex) {}
+ }
+ for (let h of gOldMailHandlers) {
+ mailHandlers.appendElement(h);
+ }
+ mailHandlerInfo.alwaysAskBeforeHandling = previousHandling;
+ gHandlerSvc.store(mailHandlerInfo);
+ });
+}
+
+let gDownloadDir;
+
+async function setDownloadDir() {
+ let tmpDir = PathUtils.join(
+ PathUtils.tempDir,
+ "testsavedir" + Math.floor(Math.random() * 2 ** 32)
+ );
+ // Create this dir if it doesn't exist (ignores existing dirs)
+ await IOUtils.makeDirectory(tmpDir);
+ registerCleanupFunction(async function () {
+ Services.prefs.clearUserPref("browser.download.folderList");
+ Services.prefs.clearUserPref("browser.download.dir");
+ try {
+ await IOUtils.remove(tmpDir, { recursive: true });
+ } catch (e) {
+ console.error(e);
+ }
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+ return tmpDir;
+}
+
+add_setup(async function test_common_initialize() {
+ gDownloadDir = await setDownloadDir();
+ Services.prefs.setCharPref("browser.download.loglevel", "Debug");
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("browser.download.loglevel");
+ });
+});
+
+async function removeAllDownloads() {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloads = await publicList.getAll();
+ for (let download of downloads) {
+ await publicList.remove(download);
+ if (await IOUtils.exists(download.target.path)) {
+ await download.finalize(true);
+ }
+ }
+}
+
+// Helpers for external protocol sandbox tests.
+const EXT_PROTO_URI_MAILTO = "mailto:test@example.com";
+
+/**
+ * Creates and iframe and navigate to an external protocol from the iframe.
+ * @param {MozBrowser} browser - Browser to spawn iframe in.
+ * @param {string} sandboxAttr - Sandbox attribute value for the iframe.
+ * @param {'trustedClick'|'untrustedClick'|'trustedLocationAPI'|'untrustedLocationAPI'|'frameSrc'|'frameSrcRedirect'} triggerMethod
+ * - How to trigger the navigation to the external protocol.
+ */
+async function navigateExternalProtoFromIframe(
+ browser,
+ sandboxAttr,
+ useCSPSandbox = false,
+ triggerMethod = "trustedClick"
+) {
+ if (
+ ![
+ "trustedClick",
+ "untrustedClick",
+ "trustedLocationAPI",
+ "untrustedLocationAPI",
+ "frameSrc",
+ "frameSrcRedirect",
+ ].includes(triggerMethod)
+ ) {
+ throw new Error("Invalid trigger method " + triggerMethod);
+ }
+
+ // Construct the url to use as iframe src.
+ let testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ let frameSrc = testPath + "/protocol_custom_sandbox_helper.sjs";
+
+ // Load the external protocol directly via the frame src field.
+ if (triggerMethod == "frameSrc") {
+ frameSrc = EXT_PROTO_URI_MAILTO;
+ } else if (triggerMethod == "frameSrcRedirect") {
+ let url = new URL(frameSrc);
+ url.searchParams.set("redirectCustomProtocol", "true");
+ frameSrc = url.href;
+ }
+
+ // If enabled set the sandbox attributes via CSP header instead. To do
+ // this we need to pass the sandbox flags to the test server via query
+ // params.
+ if (useCSPSandbox) {
+ let url = new URL(frameSrc);
+ url.searchParams.set("cspSandbox", sandboxAttr);
+ frameSrc = url.href;
+
+ // If we use CSP sandbox attributes we shouldn't set any via iframe attribute.
+ sandboxAttr = null;
+ }
+
+ // Create a sandboxed iframe and navigate to the external protocol.
+ await SpecialPowers.spawn(
+ browser,
+ [sandboxAttr, frameSrc, EXT_PROTO_URI_MAILTO, triggerMethod],
+ async (sandbox, src, extProtoURI, trigger) => {
+ let frame = content.document.createElement("iframe");
+
+ if (sandbox != null) {
+ frame.sandbox = sandbox;
+ }
+
+ frame.src = src;
+
+ let useFrameSrc = trigger == "frameSrc" || trigger == "frameSrcRedirect";
+
+ // Create frame load promise.
+ let frameLoadPromise;
+ // We won't get a load event if we directly put the external protocol in
+ // the frame src.
+ if (!useFrameSrc) {
+ frameLoadPromise = ContentTaskUtils.waitForEvent(frame, "load", false);
+ }
+
+ content.document.body.appendChild(frame);
+ await frameLoadPromise;
+
+ if (!useFrameSrc) {
+ // Trigger the external protocol navigation in the iframe. We test
+ // navigation by clicking links and navigation via the history API.
+ await SpecialPowers.spawn(
+ frame,
+ [extProtoURI, trigger],
+ async (uri, trigger2) => {
+ let link = content.document.createElement("a");
+ link.innerText = "CLICK ME";
+ link.id = "extProtoLink";
+ content.document.body.appendChild(link);
+
+ if (trigger2 == "trustedClick" || trigger2 == "untrustedClick") {
+ link.href = uri;
+ } else if (
+ trigger2 == "trustedLocationAPI" ||
+ trigger2 == "untrustedLocationAPI"
+ ) {
+ link.setAttribute("onclick", `location.href = '${uri}'`);
+ }
+
+ if (
+ trigger2 == "untrustedClick" ||
+ trigger2 == "untrustedLocationAPI"
+ ) {
+ link.click();
+ } else if (
+ trigger2 == "trustedClick" ||
+ trigger2 == "trustedLocationAPI"
+ ) {
+ await ContentTaskUtils.waitForCondition(
+ () => link,
+ "wait for link to be present"
+ );
+ await EventUtils.synthesizeMouseAtCenter(link, {}, content);
+ }
+ }
+ );
+ }
+ }
+ );
+}
+
+/**
+ * Wait for the sandbox error message which is shown in the web console when an
+ * external protocol navigation from a sandboxed context is blocked.
+ * @returns {Promise} - Promise which resolves once message has been logged.
+ */
+function waitForExtProtocolSandboxError() {
+ return new Promise(resolve => {
+ Services.console.registerListener(function onMessage(msg) {
+ let { message, logLevel } = msg;
+ if (logLevel != Ci.nsIConsoleMessage.error) {
+ return;
+ }
+ if (
+ !message.includes(
+ `Blocked navigation to custom protocol “${EXT_PROTO_URI_MAILTO}” from a sandboxed context.`
+ )
+ ) {
+ return;
+ }
+ Services.console.unregisterListener(onMessage);
+ resolve();
+ });
+ });
+}
+
+/**
+ * Run the external protocol sandbox test using iframes.
+ * @param {Object} options
+ * @param {boolean} options.blocked - Whether the navigation should be blocked.
+ * @param {string} options.sandbox - See {@link navigateExternalProtoFromIframe}.
+ * @param {string} options.useCSPSandbox - See {@link navigateExternalProtoFromIframe}.
+ * @param {string} options.triggerMethod - See {@link navigateExternalProtoFromIframe}.
+ * @returns {Promise} - Promise which resolves once the test has finished.
+ */
+function runExtProtocolSandboxTest(options) {
+ let { blocked, sandbox, useCSPSandbox = false, triggerMethod } = options;
+
+ let testPath = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+
+ info("runSandboxTest options: " + JSON.stringify(options));
+ return BrowserTestUtils.withNewTab(
+ testPath + "/protocol_custom_sandbox_helper.sjs",
+ async browser => {
+ if (blocked) {
+ let errorPromise = waitForExtProtocolSandboxError();
+ await navigateExternalProtoFromIframe(
+ browser,
+ sandbox,
+ useCSPSandbox,
+ triggerMethod
+ );
+ await errorPromise;
+
+ ok(
+ errorPromise,
+ "Should not show the dialog for iframe with sandbox " + sandbox
+ );
+ } else {
+ let dialogWindowOpenPromise = waitForProtocolAppChooserDialog(
+ browser,
+ true
+ );
+ await navigateExternalProtoFromIframe(
+ browser,
+ sandbox,
+ useCSPSandbox,
+ triggerMethod
+ );
+ let dialog = await dialogWindowOpenPromise;
+
+ ok(dialog, "Should show the dialog for sandbox " + sandbox);
+
+ // Close dialog before closing the tab to avoid intermittent failures.
+ let dialogWindowClosePromise = waitForProtocolAppChooserDialog(
+ browser,
+ false
+ );
+
+ dialog.close();
+ await dialogWindowClosePromise;
+ }
+ }
+ );
+}
diff --git a/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs
new file mode 100644
index 0000000000..8afa04cfe0
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs
@@ -0,0 +1,18 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ if (!request.queryString.match(/^name=/)) {
+ return;
+ }
+ var name = decodeURIComponent(request.queryString.substring(5));
+
+ response.setHeader("Content-Type", 'image/png; name="' + name + '"');
+ response.setHeader(
+ "Content-Disposition",
+ 'attachment; filename="' + name + '"'
+ );
+}
diff --git a/uriloader/exthandler/tests/mochitest/mailto.html b/uriloader/exthandler/tests/mochitest/mailto.html
new file mode 100644
index 0000000000..d507697443
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/mailto.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Mailto handler</title>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+ </head>
+ <body>
+ <a id="link" href="mailto:mail@example.com">mailto link</a>
+ </body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/mime_type_download.sjs b/uriloader/exthandler/tests/mochitest/mime_type_download.sjs
new file mode 100644
index 0000000000..a12bfe9735
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/mime_type_download.sjs
@@ -0,0 +1,21 @@
+function handleRequest(request, response) {
+ "use strict";
+
+ let content = "";
+ let params = new URLSearchParams(request.queryString);
+ let extension = params.get("extension");
+ let contentType = params.get("contentType");
+ if (params.has("withHeader")) {
+ response.setHeader(
+ "Content-Disposition",
+ `attachment; filename="mime_type_download${
+ extension ? "." + extension : ""
+ }";`,
+ false
+ );
+ }
+ response.setHeader("Content-Type", contentType, false);
+ response.setHeader("Content-Length", "" + content.length, false);
+ response.setStatusLine(request.httpVersion, 200);
+ response.write(content);
+}
diff --git a/uriloader/exthandler/tests/mochitest/mochitest.toml b/uriloader/exthandler/tests/mochitest/mochitest.toml
new file mode 100644
index 0000000000..1e0afbcdde
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/mochitest.toml
@@ -0,0 +1,25 @@
+[DEFAULT]
+
+["test_invalidCharFileExtension.xhtml"]
+skip-if = ["os == 'android'"] # Bug 1525959
+support-files = [
+ "HelperAppLauncherDialog_chromeScript.js",
+ "invalidCharFileExtension.sjs",
+]
+
+["test_nullCharFile.xhtml"]
+skip-if = ["os == 'android'"] # Bug 1525959
+support-files = ["HelperAppLauncherDialog_chromeScript.js"]
+
+["test_unknown_ext_protocol_handlers.html"]
+
+["test_unsafeBidiChars.xhtml"]
+skip-if = [
+ "os == 'android'", # Bug 1525959
+ "http3",
+ "http2",
+]
+support-files = [
+ "HelperAppLauncherDialog_chromeScript.js",
+ "unsafeBidiFileName.sjs",
+]
diff --git a/uriloader/exthandler/tests/mochitest/protocolHandler.html b/uriloader/exthandler/tests/mochitest/protocolHandler.html
new file mode 100644
index 0000000000..eff8a53aab
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/protocolHandler.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Protocol handler</title>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+ </head>
+ <body>
+ <script type="text/javascript">
+ navigator.registerProtocolHandler("web+testprotocol",
+ "https://example.com/foobar?uri=%s",
+ "Test Protocol");
+ </script>
+ <a id="link" href="web+testprotocol:test">testprotocol link</a>
+ </body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs b/uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs
new file mode 100644
index 0000000000..d12a953b57
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/protocol_custom_sandbox_helper.sjs
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ let query = new URLSearchParams(request.queryString);
+
+ // Set CSP sandbox attributes if caller requests any.
+ let cspSandbox = query.get("cspSandbox");
+ if (cspSandbox) {
+ response.setHeader(
+ "Content-Security-Policy",
+ "sandbox " + cspSandbox,
+ false
+ );
+ }
+
+ // Redirect to custom protocol via HTTP 302.
+ if (query.get("redirectCustomProtocol")) {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+
+ response.setHeader("Location", "mailto:test@example.com", false);
+ response.write("Redirect!");
+ return;
+ }
+
+ response.setStatusLine(request.httpVersion, 200);
+ response.write("OK");
+}
diff --git a/uriloader/exthandler/tests/mochitest/redirect_helper.sjs b/uriloader/exthandler/tests/mochitest/redirect_helper.sjs
new file mode 100644
index 0000000000..cdf6415c54
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/redirect_helper.sjs
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function handleRequest(request, response) {
+ let params = new URLSearchParams(request.queryString);
+ let uri = params.get("uri");
+ let redirectType = params.get("redirectType") || "location";
+ switch (redirectType) {
+ case "refresh":
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Refresh", "0; url=" + uri);
+ break;
+
+ case "meta-refresh":
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html");
+ response.write(`<meta http-equiv="refresh" content="0; url=${uri}">`);
+ break;
+
+ case "location":
+ // fall through
+ default:
+ response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+ response.setHeader("Location", uri);
+ }
+}
diff --git a/uriloader/exthandler/tests/mochitest/save_filenames.html b/uriloader/exthandler/tests/mochitest/save_filenames.html
new file mode 100644
index 0000000000..8755186206
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/save_filenames.html
@@ -0,0 +1,377 @@
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+</head>
+<body>
+<style>
+ img { padding: 10px; border: 1px solid red; }
+ a { padding-left: 10px; }
+</style>
+
+<span id="items">
+
+<!-- simple filename -->
+<img id="i0" src="http://localhost:8000/basic.png"
+ data-noattach="true" data-filename="basic.png">
+
+<!-- simple filename with content disposition -->
+<img id="i1" src="http://localhost:8000/save_filename.sjs?type=png&filename=simple.png" data-filename="simple.png">
+
+<!-- invalid characters in the filename -->
+<img id="i2" src="http://localhost:8000/save_filename.sjs?type=png&filename=invalidfilename/a:b*c%63d.png" data-filename="invalidfilename_a b ccd.png">
+
+<!-- invalid extension for a png image -->
+<img id="i3" src="http://localhost:8000/save_filename.sjs?type=png&filename=invalidextension.pang" data-filename="invalidextension.png">
+
+<!-- jpeg extension for a png image -->
+<img id="i4" src="http://localhost:8000/save_filename.sjs?type=png&filename=reallyapng.jpeg" data-filename="reallyapng.png">
+
+<!-- txt extension for a png image -->
+<img id="i5" src="http://localhost:8000/save_filename.sjs?type=png&filename=nottext.txt" data-filename="nottext.png">
+
+<!-- no extension for a png image -->
+<img id="i6" src="http://localhost:8000/save_filename.sjs?type=png&filename=noext" data-filename="noext.png">
+
+<!-- empty extension for a png image -->
+<img id="i7" src="http://localhost:8000/save_filename.sjs?type=png&filename=noextdot." data-filename="noextdot.png">
+
+<!-- long filename -->
+<img id="i8" src="http://localhost:8000/save_filename.sjs?type=png&filename=averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.png"
+ data-filename="averylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongf.png">
+
+<!-- long filename with invalid extension -->
+<img id="i9" src="http://localhost:8000/save_filename.sjs?type=png&filename=bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe"
+ data-filename="bverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongf.png">
+
+<!-- long filename with invalid extension -->
+<img id="i10" src="http://localhost:8000/save_filename.sjs?type=png&filename=cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilename.exe.jpg"
+ data-filename="cverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongfilenameverylongf.png">
+
+<!-- jpeg with jpg extension -->
+<img id="i11" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=thejpg.jpg" data-filename="thejpg.jpg">
+
+<!-- jpeg with jpeg extension -->
+<img id="i12" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=thejpg.jpeg" data-filename="thejpg.jpeg">
+
+<!-- jpeg with invalid extension -->
+<img id="i13" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=morejpg.exe" data-filename="morejpg.jpg" data-filename-platformlinux="morejpg.jpeg">
+
+<!-- jpeg with multiple extensions -->
+<img id="i14" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=anotherjpg.jpg.exe" data-filename="anotherjpg.jpg.jpg" data-filename-platformlinux="anotherjpg.jpg.jpeg">
+
+<!-- jpeg with no filename portion -->
+<img id="i15" src="http://localhost:8000/save_filename.sjs?type=jpeg&filename=.jpg"
+ data-filename="jpg.jpg" data-filename-platformlinux="jpg.jpeg">
+
+<!-- png with no filename portion and invalid extension -->
+<img id="i16" src="http://localhost:8000/save_filename.sjs?type=png&filename=.exe" data-filename="exe.png">
+
+<!-- png with escaped characters -->
+<img id="i17" src="http://localhost:8000/save_filename.sjs?type=png&filename=first%20file.png" data-filename="first file.png">
+
+<!-- png with more escaped characters -->
+<img id="i18" src="http://localhost:8000/save_filename.sjs?type=png&filename=second%32file%2Eexe" data-filename="second2file.png">
+
+<!-- unknown type with png extension -->
+<img id="i19" src="http://localhost:8000/save_filename.sjs?type=nonsense&filename=nonsense1.png"
+ data-nodrag="true" data-unknown="typeonly" data-filename="nonsense1.png">
+
+<!-- unknown type with exe extension -->
+<img id="i20" src="http://localhost:8000/save_filename.sjs?type=nonsense&filename=nonsense2.exe"
+ data-nodrag="true" data-unknown="typeonly" data-filename="nonsense2.exe">
+
+<!-- unknown type with no extension -->
+<img id="i21" src="http://localhost:8000/save_filename.sjs?type=nonsense&filename=nonsense3"
+ data-nodrag="true" data-unknown="typeonly" data-filename="nonsense3">
+
+<!-- simple script -->
+<script id="i22" src="http://localhost:8000/save_filename.sjs?type=js&filename=script1.js" data-filename="script1.js"></script>
+
+<!-- script with invalid extension. -->
+<script id="i23" src="http://localhost:8000/save_filename.sjs?type=js&filename=script2.exe"
+ data-filename="script2.exe" data-savepagename="script2.exe.js"></script>
+
+<!-- script with escaped characters -->
+<script id="i24" src="http://localhost:8000/save_filename.sjs?type=js&filename=script%20%33.exe"
+ data-filename="script 3.exe" data-savepagename="script 3.exe.js"></script>
+
+<!-- script with long filename -->
+<script id="i25" src="http://localhost:8000/save_filename.sjs?type=js&filename=script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789.js"
+ data-filename="script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script123456789script12345.js"></script>
+
+<!-- binary with exe extension -->
+<object id="i26" data="http://localhost:8000/save_filename.sjs?type=binary&filename=download1.exe"
+ data-unknown="true" data-filename="download1.exe"></object>
+
+<!-- binary with invalid extension -->
+<object id="i27" data="http://localhost:8000/save_filename.sjs?type=binary&filename=download2.png"
+ data-unknown="true" data-filename="download2.png"></object>
+
+<!-- binary with no extension -->
+<object id="i28" data="http://localhost:8000/save_filename.sjs?type=binary&filename=downloadnoext"
+ data-unknown="true" data-filename="downloadnoext"></object>
+
+<!-- binary with no other invalid characters -->
+<object id="i29" data="http://localhost:8000/save_filename.sjs?type=binary&filename=binary^%31%20exe.exe"
+ data-unknown="true" data-filename="binary^1 exe.exe"></object>
+
+<!-- unknown image type with no extension, but ending in png -->
+<img id="i30" src="http://localhost:8000/save_filename.sjs?type=otherimage&filename=specialpng"
+ data-unknown="typeonly" data-nodrag="true" data-filename="specialpng">
+
+<!-- unknown image type with no extension, but ending in many dots -->
+<img id="i31" src="http://localhost:8000/save_filename.sjs?type=otherimage&filename=extrapng..."
+ data-unknown="typeonly" data-nodrag="true" data-filename="extrapng">
+
+<!-- image type with no content-disposition filename specified -->
+<img id="i32" src="http://localhost:8000/save_filename.sjs?type=png" data-filename="save_filename.png">
+
+<!-- binary with no content-disposition filename specified -->
+<object id="i33" data="http://localhost:8000/save_filename.sjs?type=binary"
+ data-unknown="true" data-filename="save_filename.sjs"></object>
+
+<!-- image where url has png extension -->
+<img id="i34" src="http://localhost:8000/getdata.png?type=png&filename=override.png" data-filename="override.png">
+
+<!-- image where url has png extension but content disposition has incorrect extension -->
+<img id="i35" src="http://localhost:8000/getdata.png?type=png&filename=flower.jpeg" data-filename="flower.png">
+
+<!-- image where url has png extension but content disposition does not -->
+<img id="i36" src="http://localhost:8000/getdata.png?type=png&filename=ruby" data-filename="ruby.png">
+
+<!-- image where url has png extension but content disposition has invalid characters -->
+<img id="i37" src="http://localhost:8000/getdata.png?type=png&filename=sapphire/data" data-filename="sapphire_data.png">
+
+<!-- image where neither content disposition or url have an extension -->
+<img id="i38" src="http://localhost:8000/base?type=png&filename=emerald" data-filename="emerald.png">
+
+<!-- image where filename is not specified -->
+<img id="i39" src="http://localhost:8000/base?type=png" data-filename="base.png">
+
+<!-- simple script where url filename has no extension -->
+<script id="i40" src="http://localhost:8000/base?type=js&filename=script4.js" data-filename="script4.js"></script>
+
+<!-- script where url filename has no extension and invalid extension in content disposition filename -->
+<script id="i41" src="http://localhost:8000/base?type=js&filename=script5.exe"
+ data-filename="script5.exe" data-savepagename="script5.exe.js"></script>
+
+<!-- script where url filename has no extension and escaped characters in content disposition filename-->
+<script id="i42" src="http://localhost:8000/base?type=js&filename=script%20%36.exe"
+ data-filename="script 6.exe" data-savepagename="script 6.exe.js"></script>
+
+<!-- text where filename is present -->
+<img id="i43" src="http://localhost:8000/getdata.png?type=text&filename=readme.txt"
+ data-nodrag="true" data-filename="readme.txt">
+
+<!-- text where filename is present with a different extension -->
+<img id="i44" src="http://localhost:8000/getdata.png?type=text&filename=main.cpp"
+ data-nodrag="true" data-filename="main.cpp">
+
+<!-- text where extension is not present -->
+<img id="i45" src="http://localhost:8000/getdata.png?type=text&filename=readme"
+ data-nodrag="true" data-filename="readme">
+
+<!-- text where extension is not present and url does not have extension -->
+<img id="i46" src="http://localhost:8000/base?type=text&filename=info"
+ data-nodrag="true" data-filename="info">
+
+<!-- text where filename is not present -->
+<img id="i47" src="http://localhost:8000/basetext?type=text"
+ data-nodrag="true" data-filename="basetext">
+
+<!-- text where url has extension -->
+<img id="i48" src="http://localhost:8000/text2.txt?type=text"
+ data-nodrag="true" data-filename="text2.txt">
+
+<!-- text where url has extension -->
+<img id="i49" src="http://localhost:8000/text3.gonk?type=text"
+ data-nodrag="true" data-filename="text3.gonk">
+
+<!-- text with long filename -->
+<img id="i50" src="http://localhost:8000/text3.gonk?type=text&filename=text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789text0123456789zztext0123456789zztext0123456789zztext01234567.exe.txt" data-nodrag="true" data-filename="text0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext0123456789zztext012345.txt">
+
+<!-- webp image -->
+<img id="i51" src="http://localhost:8000/save_filename.sjs?type=webp&filename=webpimage.webp"
+ data-filename="webpimage.webp">
+
+<!-- webp image with jpg extension -->
+<img id="i52" src="http://localhost:8000/save_filename.sjs?type=webp&filename=realwebpimage.jpg"
+ data-filename="realwebpimage.webp">
+
+<!-- no content type specified -->
+<img id="i53" src="http://localhost:8000/save_filename.sjs?&filename=notype.png"
+ data-nodrag="true" data-filename="notype.png">
+
+<!-- no content type specified. -->
+<img id="i54" src="http://localhost:8000/save_filename.sjs?&filename=notypebin.exe"
+ data-nodrag="true" data-filename="notypebin.exe">
+
+<!-- extension contains invalid characters -->
+<img id="i55" src="http://localhost:8000/save_filename.sjs?type=png&filename=extinvalid.a?*"
+ data-filename="extinvalid.png">
+
+<!-- filename with redirect and content disposition -->
+<img id="i56" src="http://localhost:8000/redir?type=png&filename=red.png" data-filename="red.png">
+
+<!-- filename with redirect and different type -->
+<img id="i57" src="http://localhost:8000/redir?type=jpeg&filename=green.png"
+ data-filename="green.jpg" data-filename-platformlinux="green.jpeg">
+
+<!-- filename with redirect and binary type -->
+<object id="i58" data="http://localhost:8000/redir?type=binary&filename=blue.png"
+ data-unknown="true" data-filename="blue.png"></object>
+
+<!-- filename in url with incorrect extension -->
+<img id="i59" src="http://localhost:8000/aquamarine.jpeg"
+ data-noattach="true" data-filename="aquamarine.png">
+
+<!-- filename in url with exe extension, but returns a png image -->
+<img id="i60" src="http://localhost:8000/lazuli.exe"
+ data-noattach="true" data-filename="lazuli.png">
+
+<!-- filename with leading, trailing and duplicate spaces -->
+<img id="i61" src="http://localhost:8000/save_filename.sjs?type=png&filename= with spaces.png "
+ data-filename="with spaces.png">
+
+<!-- filename with leading and trailing periods -->
+<img id="i62" src="http://localhost:8000/save_filename.sjs?type=png&filename=..with..dots..png.."
+ data-filename="with..dots..png">
+
+<!-- filename with non-ascii character -->
+<img id="i63" src="http://localhost:8000/base?type=png&filename=s%C3%B6meescapes.%C3%B7ng" data-filename="sömeescapes.png">
+
+<!-- filename with content disposition name assigned. The name is only used
+ when selecting to manually save, otherwise it is ignored. -->
+<img id="i64" src="http://localhost:8000/save_thename.sjs?type=png&dispname=withname"
+ data-filename="save_thename.png">
+
+<!-- reserved filename on Windows -->
+<img id="i65" src="http://localhost:8000/save_filename.sjs?type=text&filename=com1"
+ data-nodrag="true" data-filename="com1" data-filename-platformwin="Untitled">
+
+<!-- reserved filename with extension on Windows -->
+<img id="i66" src="http://localhost:8000/save_filename.sjs?type=text&filename=com2.any"
+ data-nodrag="true" data-filename="com2.any" data-filename-platformwin="Untitled">
+
+<!-- simple zip file -->
+<object id="i67" data="http://localhost:8000/save_filename.sjs?type=zip&filename=simple.zip" data-filename="simple.zip"
+ data-unknown="true"></object>
+
+<!-- simple zip file with differing extension -->
+<object id="i68" data="http://localhost:8000/save_filename.sjs?type=zip&filename=simple.jar" data-filename="simple.jar"
+ data-unknown="true"></object>
+
+<!-- simple zip file with no extension -->
+<object id="i69" data="http://localhost:8000/save_filename.sjs?type=zip&filename=simplepack" data-filename="simplepack.zip"
+ data-unknown="true"></object>
+
+<!-- simple json file -->
+<object id="i70" data="http://localhost:8000/save_filename.sjs?type=json&filename=simple.json" data-filename="simple.json"
+ data-unknown="true"></object>
+
+<!-- simple json file with differing extension -->
+<object id="i71" data="http://localhost:8000/save_filename.sjs?type=json&filename=simple.dat" data-filename="simple.dat"
+ data-unknown="true"></object>
+
+<!-- compressed file with .gz extension -->
+<img id="i72" src="http://localhost:8000/save_filename.sjs?type=png&filename=compressed.png.gz" data-filename="compressed.png.png">
+
+<!-- compressed file with .tar.gz extension -->
+<object id="i73" data="http://localhost:8000/save_filename.sjs?type=tar&filename=compressed2.tar.gz" data-filename="compressed2.tar.gz"
+ data-unknown="true"></object>
+
+<!-- compressed file with bittar.gz extension. There is no tar mime info on Windows so the filename is not changed. -->
+<object id="i74" data="http://localhost:8000/save_filename.sjs?type=tar&filename=compressed3.bittar.gz"
+ data-filename="compressed3.bittar.gz"
+ data-unknown="true"></object>
+
+<!-- compressed file with .tar.bz2 extension -->
+<object id="i75" data="http://localhost:8000/save_filename.sjs?type=tar&filename=buzz.tar.bz2" data-filename="buzz.tar.bz2"
+ data-unknown="true"></object>
+
+<!-- executable with no filename specified and an extension specified within the url -->
+<img id="i76" src="http://localhost:8000/executable.exe?type=nonsense"
+ data-nodrag="true" data-unknown="typeonly" data-filename="executable.exe">
+
+<!-- embedded child html -->
+<object id="i77" data="http://localhost:8000/save_filename.sjs?type=html&filename=child.par"
+ data-filename="child.par" data-unknown="true"></object>
+
+<!-- file starting with a dot with and unknown extension -->
+<img id="i78" src="http://localhost:8000/save_filename.sjs?type=png&filename=.extension" data-filename="extension.png">
+
+<!-- html file starting with a dot -->
+<object id="i79" data="http://localhost:8000/save_filename.sjs?type=html&filename=.alternate"
+ data-filename="alternate.html" data-filename-platformwin="alternate.htm" data-unknown="true"></object>
+
+<!-- unrecognized file type starting with a dot -->
+<object id="i80" data="http://localhost:8000/save_filename.sjs?type=otherimage&filename=.alternate" data-filename="alternate"
+ data-unknown="true"></object>
+
+<!-- filename with lnk extension -->
+<img id="i81" src="http://localhost:8000/save_filename.sjs?type=nonsense&filename=shortcut.lnk"
+ data-nodrag="true" data-unknown="typeonly"
+ data-filename="shortcut.lnk.download">
+
+<!-- long filename with lnk extension -->
+<img id="i82" src="http://localhost:8000/save_filename.sjs?type=nonsense&filename=longshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshort1234567890.lnk"
+ data-nodrag="true" data-unknown="typeonly"
+ data-filename="longshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshortcutnamelongshort1234567890.lnk.download">
+
+</span>
+
+<!-- This set is used to test filenames that are changed within the file picker. -->
+<span id="modifieditems">
+
+<!-- filename which is changed within the file picker -->
+<a id="mod0" href="http://localhost:8000/save_filename.sjs?type=png&filename=teal.png"
+ data-pickedfilename='cyan.png' data-filename='cyan.png'>
+
+<!-- filename which is changed to an invalid filename within the file picker -->
+<a id="mod1" href="http://localhost:8000/save_filename.sjs?type=png&filename=orange.png"
+ data-pickedfilename='"peach".png' data-filename='peach .png'>
+
+<!-- an invalid filename which is changed to another invalid filename within the file picker -->
+<a id="mod2" href="http://localhost:8000/save_filename.sjs?type=png&filename=%maroon%34.png"
+ data-pickedfilename='"violet".png' data-filename="violet .png">
+
+</span>
+
+<!-- This set is used to test the filename specified by the download attribute is validated correctly. -->
+<span id="downloads">
+ <a id="download0" href="http://localhost:8000/base" download="pearl.png" data-filename="pearl.png">Link</a>
+ <a id="download1" href="http://localhost:8000/save_filename.sjs?type=png" download="opal.jpeg" data-filename="opal.png">Link</a>
+ <a id="download2" href="http://localhost:8000/save_filename.sjs?type=jpeg"
+ download="amethyst.png" data-filename="amethyst.jpg"
+ data-filename-platformlinux="amethyst.jpeg">Link</a>
+ <a id="download3" href="http://localhost:8000/save_filename.sjs?type=text"
+ download="onyx.png" data-filename="onyx.png">Link</a>
+ <!-- The content-disposition overrides the download attribute. -->
+ <a id="download4" href="http://localhost:8000/save_filename.sjs?type=png&filename=fakename.jpeg" download="topaz.jpeg" data-filename="fakename.png">Link</a>
+ <a id="download5" href="http://localhost:8000/save_filename.sjs?type=png"
+ download="amber?.png" data-filename="amber .png">Link</a>
+ <a id="download6" href="http://localhost:8000/save_filename.sjs?type=jpeg"
+ download="jade.:*jpeg" data-filename="jade.jpg"
+ data-filename-platformlinux="jade.jpeg">Link</a>>
+ <a id="download7" href="http://localhost:8000/save_filename.sjs?type=png"
+ download="thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename.png"
+ data-filename="thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisaverylongfilename-thisisavery.png">Link</a>
+ <a id="download8" href="http://localhost:8000/base"
+ download="&Tab;&NewLine;&nbsp;&#11&#x180e;&#x180e;&#12 spa ced.png &Tab;&NewLine;&nbsp;&#x180e;&#x180e;&#11&#12 "
+ data-filename="spa ced.png">Link</a>
+</span>
+
+<span id="links">
+ <a id="link0" href="http://localhost:8000/save_filename.sjs?type=png&filename=one.png" data-filename="one.png">One</a>
+ <a id="link1" href="http://localhost:8000/save_filename.sjs?type=png&filename=two.jpeg" data-filename="two.png">Two</a>
+ <a id="link2" href="http://localhost:8000/save_filename.sjs?type=png&filename=three.con" data-filename="three.png">Three</a>
+ <a id="link3" href="http://localhost:8000/save_filename.sjs?type=png&dispname=four" data-filename="four.png">Four</a>
+ <a id="link4" href="http://localhost:8000/save_filename.sjs?type=png&filename=five.local" data-filename="five.png">Five</a>
+</span>
+
+<!-- The content-disposition attachment generates links from the images/objects/scripts above
+ and inserts them here. -->
+<p id="attachment-links">
+</p>
+
+</body></html>
diff --git a/uriloader/exthandler/tests/mochitest/script_redirect.html b/uriloader/exthandler/tests/mochitest/script_redirect.html
new file mode 100644
index 0000000000..31e0dc6a7e
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/script_redirect.html
@@ -0,0 +1,5 @@
+<script>
+ let params = new URL(document.location).searchParams;
+ let uri = params.get("uri");
+ document.location = uri;
+</script>
diff --git a/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml
new file mode 100644
index 0000000000..177af3757f
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml
@@ -0,0 +1,65 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handling of unsafe bidi chars</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe id="test"></iframe>
+<script type="text/javascript">
+var tests = [
+ ["test.png:large", "test.png"],
+ ["test.png/large", "test.png"],
+ [":test.png::large:", "test.png"],
+];
+
+add_task(async function() {
+ function promiseMessage(messageName) {
+ return new Promise(resolve => {
+ chromeScript.addMessageListener(messageName, function listener(data) {
+ chromeScript.removeMessageListener(messageName, listener);
+ resolve(data);
+ });
+ });
+ }
+
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ function wrongAPICallListener(msg) {
+ ok(
+ false,
+ `Called ${msg} when always ask pref was set to ${
+ Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types"
+ )
+ }, which shouldn't happen.`
+ );
+ }
+ chromeScript.addMessageListener("wrongAPICall", wrongAPICallListener);
+
+ for (let prefVal of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", prefVal]]
+ });
+ // Run all the tests.
+ for (let [name, expected] of tests) {
+ let promiseName = promiseMessage("suggestedFileName");
+ document.getElementById("test").src =
+ "invalidCharFileExtension.sjs?name=" + encodeURIComponent(name);
+ is((await promiseName), expected, "got the expected sanitized name");
+ }
+ }
+
+ // Clean up.
+ let promise = promiseMessage("unregistered");
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ chromeScript.removeMessageListener("wrongAPICall", wrongAPICallListener);
+ chromeScript.destroy();
+});
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml
new file mode 100644
index 0000000000..b153395e81
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml
@@ -0,0 +1,67 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handling of null char</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe id="test"></iframe>
+<script type="text/javascript">
+var tests = [
+ ["test.html\u0000.png", "test.html_.png"],
+ ["test.html.\u0000png", "test.html._png"],
+];
+
+add_task(async function() {
+ function promiseMessage(messageName) {
+ return new Promise(resolve => {
+ chromeScript.addMessageListener(messageName, function listener(data) {
+ chromeScript.removeMessageListener(messageName, listener);
+ resolve(data);
+ });
+ });
+ }
+
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ function wrongAPICallListener(msg) {
+ ok(
+ false,
+ `Called ${msg} when always ask pref was set to ${
+ Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types"
+ )
+ }, which shouldn't happen.`
+ );
+ }
+ chromeScript.addMessageListener("wrongAPICall", wrongAPICallListener);
+
+ for (let prefVal of [false, true]) {
+ info("Pushing pref");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", prefVal]]
+ });
+ for (let [name, expected] of tests) {
+ let promiseName = promiseMessage("suggestedFileName");
+ const a = document.createElement('a');
+ // Pass an unknown mimetype so we don't "correct" the extension:
+ a.href = "data:application/baconizer;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+ a.download = name;
+ a.dispatchEvent(new MouseEvent('click'));
+ is((await promiseName), expected, "got the expected sanitized name");
+ }
+ }
+
+ // Clean up.
+ let promise = promiseMessage("unregistered");
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ chromeScript.removeMessageListener("wrongAPICall", wrongAPICallListener);
+ chromeScript.destroy();
+});
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_spammy_page.html b/uriloader/exthandler/tests/mochitest/test_spammy_page.html
new file mode 100644
index 0000000000..b1e60a1e8e
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_spammy_page.html
@@ -0,0 +1,27 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>...</title>
+</head>
+<body>
+ <p> Hello, it's the spammy page! </p>
+<script type="text/javascript">
+ let count = 0;
+window.onload = window.onclick = function() {
+ if (count < 100) {
+ count++;
+ let l = document.createElement('a');
+ l.href = 'data:text/plain,some text';
+ l.download = 'sometext.pdf';
+
+ document.body.appendChild(l);
+ l.click();
+ }
+}
+</script>
+<a id="image" href="${TEST_ROOT}/file_with@@funny_name.png">Image</a>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html
new file mode 100644
index 0000000000..f8727db605
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for no error reporting for unknown external protocols</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<iframe id="testFrame"></iframe>
+<script type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+window.onload = () => {
+ let testFrame = document.getElementById("testFrame");
+
+ try {
+ testFrame.contentWindow.location.href = "unknownextproto:";
+ ok(true, "There is no error reporting for unknown external protocol navigation.");
+ } catch (e) {
+ ok(false, "There should be no error reporting for unknown external protocol navigation.");
+ }
+
+ SimpleTest.finish();
+};
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml
new file mode 100644
index 0000000000..34c6c956fd
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml
@@ -0,0 +1,89 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handling of unsafe bidi chars</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<iframe id="test"></iframe>
+<script type="text/javascript">
+var unsafeBidiChars = [
+ "\xe2\x80\xaa", // LRE
+ "\xe2\x80\xab", // RLE
+ "\xe2\x80\xac", // PDF
+ "\xe2\x80\xad", // LRO
+ "\xe2\x80\xae", // RLO
+];
+
+var tests = [
+ "{1}.test",
+ "{1}File.test",
+ "Fi{1}le.test",
+ "File{1}.test",
+ "File.{1}test",
+ "File.te{1}st",
+ "File.test{1}",
+ "File.{1}",
+];
+
+function replace(name, x) {
+ return name.replace(/\{1\}/, x);
+}
+
+function sanitize(name) {
+ return replace(name, "_");
+}
+
+add_task(async function() {
+ function promiseMessage(messageName) {
+ return new Promise(resolve => {
+ chromeScript.addMessageListener(messageName, function listener(data) {
+ chromeScript.removeMessageListener(messageName, listener);
+ resolve(data);
+ });
+ });
+ }
+
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ function wrongAPICallListener(msg) {
+ ok(
+ false,
+ `Called ${msg} when always ask pref was set to ${
+ Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types"
+ )
+ }, which shouldn't happen.`
+ );
+ }
+ chromeScript.addMessageListener("wrongAPICall", wrongAPICallListener);
+
+ for (let prefVal of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.download.always_ask_before_handling_new_types", prefVal]]
+ });
+ for (let test of tests) {
+ for (let char of unsafeBidiChars) {
+ let promiseName = promiseMessage("suggestedFileName");
+ let name = replace(test, char);
+ let expected = sanitize(test);
+ document.getElementById("test").src =
+ "unsafeBidiFileName.sjs?name=" + encodeURIComponent(name);
+ is((await promiseName), expected, "got the expected sanitized name");
+ }
+ }
+ }
+
+ // Clean up.
+ let promise = promiseMessage("unregistered");
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ chromeScript.removeMessageListener("wrongAPICall", wrongAPICallListener);
+ chromeScript.destroy();
+});
+</script>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs
new file mode 100644
index 0000000000..4f88ff6de2
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs
@@ -0,0 +1,18 @@
+/* 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/. */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ if (!request.queryString.match(/^name=/)) {
+ return;
+ }
+ var name = decodeURIComponent(request.queryString.substring(5));
+
+ response.setHeader(
+ "Content-Type",
+ 'application/octet-stream; name="' + name + '"'
+ );
+ response.setHeader("Content-Disposition", 'inline; filename="' + name + '"');
+}
diff --git a/uriloader/exthandler/tests/moz.build b/uriloader/exthandler/tests/moz.build
new file mode 100644
index 0000000000..95d7f5fe59
--- /dev/null
+++ b/uriloader/exthandler/tests/moz.build
@@ -0,0 +1,35 @@
+# -*- 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/.
+
+MOCHITEST_MANIFESTS += ["mochitest/mochitest.toml"]
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"]
+
+BROWSER_CHROME_MANIFESTS += ["mochitest/browser.toml"]
+
+TEST_DIRS += [
+ "gtest",
+]
+
+TESTING_JS_MODULES += [
+ "HandlerServiceTestUtils.sys.mjs",
+]
+
+GeckoSimplePrograms(
+ [
+ "WriteArgument",
+ ],
+ linkage=None,
+)
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ TEST_HARNESS_FILES.xpcshell.uriloader.exthandler.tests.unit += [
+ "!WriteArgument%s" % CONFIG["BIN_SUFFIX"]
+ ]
+
+USE_LIBS += [
+ "nspr",
+]
diff --git a/uriloader/exthandler/tests/unit/handlers.json b/uriloader/exthandler/tests/unit/handlers.json
new file mode 100644
index 0000000000..51ce581d83
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/handlers.json
@@ -0,0 +1,83 @@
+{
+ "defaultHandlersVersion": {
+ "en-US": 999
+ },
+ "mimeTypes": {
+ "example/type.handleinternally": {
+ "unknownProperty": "preserved",
+ "action": 3,
+ "extensions": ["example_one"]
+ },
+ "example/type.savetodisk": {
+ "action": 0,
+ "ask": true,
+ "handlers": [
+ {
+ "name": "Example Default Handler",
+ "uriTemplate": "https://www.example.com/?url=%s"
+ }
+ ],
+ "extensions": ["example_two", "example_three"]
+ },
+ "example/type.usehelperapp": {
+ "action": 2,
+ "ask": true,
+ "handlers": [
+ {
+ "name": "Example Default Handler",
+ "uriTemplate": "https://www.example.com/?url=%s"
+ },
+ {
+ "name": "Example Possible Handler One",
+ "uriTemplate": "http://www.example.com/?id=1&url=%s"
+ },
+ {
+ "name": "Example Possible Handler Two",
+ "uriTemplate": "http://www.example.com/?id=2&url=%s"
+ }
+ ],
+ "extensions": ["example_two", "example_three"]
+ },
+ "example/type.usesystemdefault": {
+ "action": 4,
+ "handlers": [
+ null,
+ {
+ "name": "Example Possible Handler",
+ "uriTemplate": "http://www.example.com/?url=%s"
+ }
+ ]
+ }
+ },
+ "schemes": {
+ "examplescheme.usehelperapp": {
+ "action": 2,
+ "ask": true,
+ "handlers": [
+ {
+ "name": "Example Default Handler",
+ "uriTemplate": "https://www.example.com/?url=%s"
+ },
+ {
+ "name": "Example Possible Handler One",
+ "uriTemplate": "http://www.example.com/?id=1&url=%s"
+ },
+ {
+ "name": "Example Possible Handler Two",
+ "uriTemplate": "http://www.example.com/?id=2&url=%s"
+ }
+ ]
+ },
+ "examplescheme.usesystemdefault": {
+ "action": 4,
+ "handlers": [
+ null,
+ {
+ "name": "Example Possible Handler",
+ "uriTemplate": "http://www.example.com/?url=%s"
+ }
+ ]
+ }
+ },
+ "isDownloadsImprovementsAlreadyMigrated": true
+}
diff --git a/uriloader/exthandler/tests/unit/head.js b/uriloader/exthandler/tests/unit/head.js
new file mode 100644
index 0000000000..72d1ac87c5
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/head.js
@@ -0,0 +1,84 @@
+/* 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/. */
+
+/*
+ * Initialization for tests related to invoking external handler applications.
+ */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+var { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const { HandlerServiceTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/HandlerServiceTestUtils.sys.mjs"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gHandlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+
+do_get_profile();
+
+let jsonPath = PathUtils.join(PathUtils.profileDir, "handlers.json");
+
+/**
+ * Unloads the nsIHandlerService data store, so the back-end file can be
+ * accessed or modified, and the new data will be loaded at the next access.
+ */
+let unloadHandlerStore = async function () {
+ // If this function is called before the nsIHandlerService instance has been
+ // initialized for the first time, the observer below will not be registered.
+ // We have to force initialization to prevent the function from stalling.
+ gHandlerService;
+
+ let promise = TestUtils.topicObserved("handlersvc-json-replace-complete");
+ Services.obs.notifyObservers(null, "handlersvc-json-replace");
+ await promise;
+};
+
+/**
+ * Unloads the data store and deletes it.
+ */
+let deleteHandlerStore = async function () {
+ await unloadHandlerStore();
+
+ await IOUtils.remove(jsonPath, { ignoreAbsent: true });
+
+ Services.prefs.clearUserPref("gecko.handlerService.defaultHandlersVersion");
+};
+
+/**
+ * Unloads the data store and replaces it with the test data file.
+ */
+let copyTestDataToHandlerStore = async function () {
+ await unloadHandlerStore();
+
+ await IOUtils.copy(do_get_file("handlers.json").path, jsonPath);
+
+ Services.prefs.setIntPref("gecko.handlerService.defaultHandlersVersion", 100);
+};
+
+/**
+ * Ensures the files are removed and the services unloaded when the tests end.
+ */
+registerCleanupFunction(async function test_terminate() {
+ await deleteHandlerStore();
+});
diff --git a/uriloader/exthandler/tests/unit/mailcap b/uriloader/exthandler/tests/unit/mailcap
new file mode 100644
index 0000000000..dc93ef8042
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/mailcap
@@ -0,0 +1,2 @@
+text/plain; cat '%s'; needsterminal
+text/plain; sed '%s'
diff --git a/uriloader/exthandler/tests/unit/test_badMIMEType.js b/uriloader/exthandler/tests/unit/test_badMIMEType.js
new file mode 100644
index 0000000000..49c5e8d848
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_badMIMEType.js
@@ -0,0 +1,29 @@
+/* -*- 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/. */
+
+function run_test() {
+ // "text/plain" has an 0xFF character appended to it. This means it's an
+ // invalid string, which is tricky to enter using a text editor (I used
+ // emacs' hexl-mode). It also means an ordinary text editor might drop it
+ // or convert it to something that *is* valid (in UTF8). So we measure
+ // its length to make sure this hasn't happened.
+ var badMimeType = "text/plainÿ";
+ Assert.equal(badMimeType.length, 11);
+ try {
+ Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getFromTypeAndExtension(badMimeType, "txt");
+ } catch (e) {
+ if (
+ !(e instanceof Ci.nsIException) ||
+ e.result != Cr.NS_ERROR_NOT_AVAILABLE
+ ) {
+ throw e;
+ }
+ // This is an expected exception, thrown if the type can't be determined
+ }
+ // Not crashing is good enough
+ Assert.equal(true, true);
+}
diff --git a/uriloader/exthandler/tests/unit/test_defaults_handlerService.js b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js
new file mode 100644
index 0000000000..72d3779fd8
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gExternalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ kHandlerList: "resource://gre/modules/handlers/HandlerList.sys.mjs",
+});
+
+add_task(async function test_check_defaults_get_added() {
+ let protocols = Object.keys(kHandlerList.default.schemes);
+ for (let protocol of protocols) {
+ let protocolHandlerCount =
+ kHandlerList.default.schemes[protocol].handlers.length;
+ Assert.ok(
+ gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry,
+ `Expect stub for ${protocol}`
+ );
+ let info = gExternalProtocolService.getProtocolHandlerInfo(protocol, {});
+ Assert.ok(
+ info,
+ `Should be able to get protocol handler info for ${protocol}`
+ );
+ let handlers = Array.from(
+ info.possibleApplicationHandlers.enumerate(Ci.nsIHandlerApp)
+ );
+ handlers = handlers.filter(h => h instanceof Ci.nsIWebHandlerApp);
+ Assert.equal(
+ handlers.length,
+ protocolHandlerCount,
+ `Default web handlers for ${protocol} should match`
+ );
+ let { alwaysAskBeforeHandling, preferredAction } = info;
+ // Actually store something, pretending there was a change:
+ let infoToWrite = gExternalProtocolService.getProtocolHandlerInfo(
+ protocol,
+ {}
+ );
+ gHandlerService.store(infoToWrite);
+ ok(
+ !gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry,
+ "Expect stub entry info to go away"
+ );
+
+ let newInfo = gExternalProtocolService.getProtocolHandlerInfo(protocol, {});
+ Assert.equal(
+ alwaysAskBeforeHandling,
+ newInfo.alwaysAskBeforeHandling,
+ protocol + " - always ask shouldn't change"
+ );
+ Assert.equal(
+ preferredAction,
+ newInfo.preferredAction,
+ protocol + " - preferred action shouldn't change"
+ );
+ await deleteHandlerStore();
+ }
+});
+
+add_task(async function test_check_default_modification() {
+ Assert.ok(
+ true,
+ JSON.stringify(gHandlerService.wrappedJSObject._store.data.schemes.mailto)
+ );
+ Assert.ok(
+ gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry,
+ "Expect stub for mailto"
+ );
+ let mailInfo = gExternalProtocolService.getProtocolHandlerInfo("mailto", {});
+ mailInfo.alwaysAskBeforeHandling = false;
+ mailInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault;
+ gHandlerService.store(mailInfo);
+ Assert.ok(
+ !gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry,
+ "Stub entry should be removed immediately."
+ );
+ let newMail = gExternalProtocolService.getProtocolHandlerInfo("mailto", {});
+ Assert.equal(newMail.preferredAction, Ci.nsIHandlerInfo.useSystemDefault);
+ Assert.equal(newMail.alwaysAskBeforeHandling, false);
+ await deleteHandlerStore();
+});
+
+add_task(async function test_migrations() {
+ const kTestData = [
+ ["A", "http://compose.mail.yahoo.co.jp/ym/Compose?To=%s"],
+ ["B", "http://www.inbox.lv/rfc2368/?value=%s"],
+ ["C", "http://poczta.interia.pl/mh/?mailto=%s"],
+ ["D", "http://win.mail.ru/cgi-bin/sentmsg?mailto=%s"],
+ ];
+ // Set up the test handlers. This doesn't use prefs like the previous test,
+ // because we now refuse to import insecure handler prefs. They can only
+ // exist if they were added into the handler store before this restriction
+ // was created (bug 1526890).
+ gHandlerService.wrappedJSObject._injectDefaultProtocolHandlers();
+ let handler = gExternalProtocolService.getProtocolHandlerInfo("mailto");
+ while (handler.possibleApplicationHandlers.length) {
+ handler.possibleApplicationHandlers.removeElementAt(0);
+ }
+ for (let [name, uriTemplate] of kTestData) {
+ let app = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance(
+ Ci.nsIWebHandlerApp
+ );
+ app.uriTemplate = uriTemplate;
+ app.name = name;
+ handler.possibleApplicationHandlers.appendElement(app);
+ }
+ gHandlerService.store(handler);
+
+ // Now migrate them:
+ Services.prefs.setCharPref("browser.handlers.migrations", "blah,secure-mail");
+ gHandlerService.wrappedJSObject._migrateProtocolHandlersIfNeeded();
+
+ // Now check the result:
+ handler = gExternalProtocolService.getProtocolHandlerInfo("mailto");
+
+ let expectedURIs = new Set([
+ "https://mail.yahoo.co.jp/compose/?To=%s",
+ "https://mail.inbox.lv/compose?to=%s",
+ "https://poczta.interia.pl/mh/?mailto=%s",
+ "https://e.mail.ru/cgi-bin/sentmsg?mailto=%s",
+ ]);
+
+ let possibleApplicationHandlers = Array.from(
+ handler.possibleApplicationHandlers.enumerate(Ci.nsIWebHandlerApp)
+ );
+ // Set iterators are stable to deletion, so this works:
+ for (let expected of expectedURIs) {
+ for (let app of possibleApplicationHandlers) {
+ if (app instanceof Ci.nsIWebHandlerApp && app.uriTemplate == expected) {
+ Assert.ok(true, "Found handler with URI " + expected);
+ // ... even when we remove items.
+ expectedURIs.delete(expected);
+ break;
+ }
+ }
+ }
+ Assert.equal(expectedURIs.size, 0, "Should have seen all the expected URIs.");
+
+ for (let app of possibleApplicationHandlers) {
+ if (app instanceof Ci.nsIWebHandlerApp) {
+ Assert.ok(
+ !kTestData.some(n => n[1] == app.uriTemplate),
+ "Should not be any of the original handlers"
+ );
+ }
+ }
+
+ Assert.ok(
+ !handler.preferredApplicationHandler,
+ "Shouldn't have preferred handler initially."
+ );
+ await deleteHandlerStore();
+});
+
+/**
+ * Check that non-https templates or ones without a '%s' are ignored.
+ */
+add_task(async function invalid_handlers_are_rejected() {
+ let schemes = kHandlerList.default.schemes;
+ schemes.myfancyinvalidstuff = {
+ handlers: [
+ {
+ name: "No template at all",
+ },
+ {
+ name: "Not secure",
+ uriTemplate: "http://example.com/%s",
+ },
+ {
+ name: "No replacement percent-s bit",
+ uriTemplate: "https://example.com/",
+ },
+ {
+ name: "Actually valid",
+ uriTemplate: "https://example.com/%s",
+ },
+ ],
+ };
+ gHandlerService.wrappedJSObject._injectDefaultProtocolHandlers();
+ // Now check the result:
+ let handler = gExternalProtocolService.getProtocolHandlerInfo(
+ "myfancyinvalidstuff"
+ );
+
+ let expectedURIs = ["https://example.com/%s"];
+
+ Assert.deepEqual(
+ Array.from(
+ handler.possibleApplicationHandlers.enumerate(Ci.nsIWebHandlerApp),
+ e => e.uriTemplate
+ ),
+ expectedURIs,
+ "Should have seen only 1 handler added."
+ );
+ await deleteHandlerStore();
+});
diff --git a/uriloader/exthandler/tests/unit/test_filename_sanitize.js b/uriloader/exthandler/tests/unit/test_filename_sanitize.js
new file mode 100644
index 0000000000..d8ab12c266
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_filename_sanitize.js
@@ -0,0 +1,398 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test verifies that
+// nsIMIMEService.validateFileNameForSaving sanitizes filenames
+// properly with different flags.
+
+"use strict";
+
+add_task(async function validate_filename_method() {
+ let mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+ function checkFilename(filename, flags, mime = "image/png") {
+ return mimeService.validateFileNameForSaving(filename, mime, flags);
+ }
+
+ Assert.equal(checkFilename("basicfile.png", 0), "basicfile.png");
+ Assert.equal(checkFilename(" whitespace.png ", 0), "whitespace.png");
+ Assert.equal(
+ checkFilename(" .whitespaceanddots.png...", 0),
+ "whitespaceanddots.png"
+ );
+ Assert.equal(
+ checkFilename(" \u00a0 \u00a0 extrawhitespace.png \u00a0 \u00a0 ", 0),
+ "extrawhitespace.png"
+ );
+ Assert.equal(
+ checkFilename(" filename with whitespace.png ", 0),
+ "filename with whitespace.png"
+ );
+ Assert.equal(checkFilename("\\path.png", 0), "_path.png");
+ Assert.equal(
+ checkFilename("\\path*and/$?~file.png", 0),
+ "_path and_$ ~file.png"
+ );
+ Assert.equal(
+ checkFilename(" \u180e whit\u180ee.png \u180e", 0),
+ "whit\u180ee.png"
+ );
+ Assert.equal(checkFilename("簡単簡単簡単", 0), "簡単簡単簡単.png");
+ Assert.equal(checkFilename(" happy\u061c\u2069.png", 0), "happy__.png");
+ Assert.equal(
+ checkFilename("12345678".repeat(31) + "abcdefgh.png", 0),
+ "12345678".repeat(31) + "ab.png"
+ );
+ Assert.equal(
+ checkFilename("簡単".repeat(41) + ".png", 0),
+ "簡単".repeat(41) + ".png"
+ );
+ Assert.equal(
+ checkFilename("a" + "簡単".repeat(42) + ".png", 0),
+ "a" + "簡単".repeat(40) + "簡.png"
+ );
+ Assert.equal(
+ checkFilename("a" + "簡単".repeat(56) + ".png", 0),
+ "a" + "簡単".repeat(40) + ".png"
+ );
+ Assert.equal(checkFilename("café.png", 0), "café.png");
+ Assert.equal(
+ checkFilename("café".repeat(50) + ".png", 0),
+ "café".repeat(50) + ".png"
+ );
+ Assert.equal(
+ checkFilename("café".repeat(51) + ".png", 0),
+ "café".repeat(49) + "caf.png"
+ );
+
+ Assert.equal(
+ checkFilename("\u{100001}\u{100002}.png", 0),
+ "\u{100001}\u{100002}.png"
+ );
+ Assert.equal(
+ checkFilename("\u{100001}\u{100002}".repeat(31) + ".png", 0),
+ "\u{100001}\u{100002}".repeat(31) + ".png"
+ );
+ Assert.equal(
+ checkFilename("\u{100001}\u{100002}".repeat(32) + ".png", 0),
+ "\u{100001}\u{100002}".repeat(30) + "\u{100001}.png"
+ );
+
+ Assert.equal(
+ checkFilename("noextensionfile".repeat(16), 0),
+ "noextensionfile".repeat(16) + ".png"
+ );
+ Assert.equal(
+ checkFilename("noextensionfile".repeat(17), 0),
+ "noextensionfile".repeat(16) + "noextensio.png"
+ );
+ Assert.equal(
+ checkFilename("noextensionfile".repeat(16) + "noextensionfil.", 0),
+ "noextensionfile".repeat(16) + "noextensio.png"
+ );
+
+ Assert.equal(checkFilename(" first .png ", 0), "first .png");
+ Assert.equal(
+ checkFilename(
+ " second .png ",
+ mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE
+ ),
+ "second .png"
+ );
+
+ // For whatever reason, the Android mime handler accepts the .jpeg
+ // extension for image/png, so skip this test there.
+ if (AppConstants.platform != "android") {
+ Assert.equal(checkFilename("thi/*rd.jpeg", 0), "thi_ rd.png");
+ }
+
+ Assert.equal(
+ checkFilename("f*\\ourth file.jpg", mimeService.VALIDATE_SANITIZE_ONLY),
+ "f _ourth file.jpg"
+ );
+ Assert.equal(
+ checkFilename(
+ "f*\\ift h.jpe*\\g",
+ mimeService.VALIDATE_SANITIZE_ONLY |
+ mimeService.VALIDATE_DONT_COLLAPSE_WHITESPACE
+ ),
+ "f _ift h.jpe _g"
+ );
+ Assert.equal(checkFilename("sixth.j pe/*g", 0), "sixth.png");
+
+ let repeatStr = "12345678".repeat(31);
+ Assert.equal(
+ checkFilename(
+ repeatStr + "seventh.png",
+ mimeService.VALIDATE_DONT_TRUNCATE
+ ),
+ repeatStr + "seventh.png"
+ );
+ Assert.equal(
+ checkFilename(repeatStr + "seventh.png", 0),
+ repeatStr + "se.png"
+ );
+
+ // no filename, so index is used by default.
+ Assert.equal(checkFilename(".png", 0), "png.png");
+
+ // sanitization only, so Untitled is not added, but initial period is stripped.
+ Assert.equal(
+ checkFilename(".png", mimeService.VALIDATE_SANITIZE_ONLY),
+ "png"
+ );
+
+ // correct .png extension is applied.
+ Assert.equal(checkFilename(".butterpecan.icecream", 0), "butterpecan.png");
+
+ // sanitization only, so extension is not modified, but initial period is stripped.
+ Assert.equal(
+ checkFilename(".butterpecan.icecream", mimeService.VALIDATE_SANITIZE_ONLY),
+ "butterpecan.icecream"
+ );
+
+ let ext = ".fairlyLongExtension";
+ Assert.equal(
+ checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY),
+ repeatStr.substring(0, 254 - ext.length) + ext
+ );
+
+ ext = "lo%?n/ginvalid? ch\\ars";
+ Assert.equal(
+ checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY),
+ repeatStr + "lo% n_"
+ );
+
+ ext = ".long/invalid%? ch\\ars";
+ Assert.equal(
+ checkFilename(repeatStr + ext, mimeService.VALIDATE_SANITIZE_ONLY),
+ repeatStr.substring(0, 233) + ".long_invalid% ch_ars"
+ );
+
+ Assert.equal(
+ checkFilename("test_テスト_T\x83E\\S\x83T.png", 0),
+ "test_テスト_T E_S T.png"
+ );
+ Assert.equal(
+ checkFilename("test_テスト_T\x83E\\S\x83T.pテ\x83ng", 0),
+ "test_テスト_T E_S T.png"
+ );
+
+ // Check we don't invalidate surrogate pairs when trimming.
+ Assert.equal(checkFilename("test😀", 0, ""), "test😀");
+ Assert.equal(checkFilename("test😀😀", 0, ""), "test😀😀");
+
+ // Now check some media types
+ Assert.equal(
+ mimeService.validateFileNameForSaving("video.ogg", "video/ogg", 0),
+ "video.ogg",
+ "video.ogg"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("video.ogv", "video/ogg", 0),
+ "video.ogv",
+ "video.ogv"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("video.ogt", "video/ogg", 0),
+ "video.ogv",
+ "video.ogt"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving("audio.mp3", "audio/mpeg", 0),
+ "audio.mp3",
+ "audio.mp3"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("audio.mpega", "audio/mpeg", 0),
+ "audio.mpega",
+ "audio.mpega"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("audio.mp2", "audio/mpeg", 0),
+ "audio.mp2",
+ "audio.mp2"
+ );
+
+ let expected = "audio.mp3";
+ if (AppConstants.platform == "linux") {
+ expected = "audio.mpga";
+ } else if (AppConstants.platform == "android") {
+ expected = "audio.mp4";
+ }
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving("audio.mp4", "audio/mpeg", 0),
+ expected,
+ "audio.mp4"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving("sound.m4a", "audio/mp4", 0),
+ "sound.m4a",
+ "sound.m4a"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("sound.m4b", "audio/mp4", 0),
+ AppConstants.platform == "android" ? "sound.m4a" : "sound.m4b",
+ "sound.m4b"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("sound.m4c", "audio/mp4", 0),
+ AppConstants.platform == "macosx" ? "sound.mp4" : "sound.m4a",
+ "sound.mpc"
+ );
+
+ // This has a long filename with a 13 character extension. The end of the filename should be
+ // cropped to fit into 255 bytes.
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데이 앵콜 🎁 1.등 유산균 컬처렐 특가!",
+ "text/unknown",
+ mimeService.VALIDATE_SANITIZE_ONLY
+ ),
+ "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 .등 유산균 컬처렐 특가!",
+ "very long filename with extension"
+ );
+
+ // This filename has a very long extension, almost the entire filename.
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "라이브9.9만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데이 앵콜 🎁 1등 유산균 컬처렐 특가!",
+ "text/unknown",
+ mimeService.VALIDATE_SANITIZE_ONLY
+ ),
+ "라이브9",
+ "another very long filename with long extension"
+ );
+
+ // This filename is cropped at 254 bytes.
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ ".라이브99만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데이 앵콜 🎁 1등 유산균 컬처렐 특가!",
+ "text/unknown",
+ mimeService.VALIDATE_SANITIZE_ONLY
+ ),
+ "라이브99만 시청컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장컬처렐 다이제스티브 3박스 - 3박스 더 (뚱랑이 굿즈 증정) - 선물용 쇼핑백 2장24%102 000원 브랜드데",
+ "very filename with extension only"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving("filename.LNK", "text/unknown", 0),
+ "filename.LNK.download",
+ "filename.LNK"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("filename.local", "text/unknown", 0),
+ "filename.local.download",
+ "filename.local"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("filename.url", "text/unknown", 0),
+ "filename.url.download",
+ "filename.url"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("filename.URl", "text/unknown", 0),
+ "filename.URl.download",
+ "filename.URl"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("filename.scf", "text/unknown", 0),
+ "filename.scf.download",
+ "filename.scf"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving("filename.sCF", "text/unknown", 0),
+ "filename.sCF.download",
+ "filename.sCF"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving("filename.lnk\n", "text/unknown", 0),
+ "filename.lnk.download",
+ "filename.lnk with newline"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.lnk\n ",
+ "text/unknown",
+ 0
+ ),
+ "filename.lnk.download",
+ "filename.lnk with newline"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.\n\t lnk",
+ "text/unknown",
+ 0
+ ),
+ "filename. lnk",
+ "filename.lnk with space and newline"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.local\u180e\u180e\u180e",
+ "text/unknown",
+ 0
+ ),
+ "filename.local.download",
+ "filename.lnk with vowel separators"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.LNK",
+ "text/unknown",
+ mimeService.VALIDATE_SANITIZE_ONLY
+ ),
+ "filename.LNK.download",
+ "filename.LNK sanitize only"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.LNK\n",
+ "text/unknown",
+ mimeService.VALIDATE_ALLOW_INVALID_FILENAMES
+ ),
+ "filename.LNK",
+ "filename.LNK allow invalid"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.URL\n",
+ "text/unknown",
+ mimeService.VALIDATE_SANITIZE_ONLY |
+ mimeService.VALIDATE_ALLOW_INVALID_FILENAMES
+ ),
+ "filename.URL",
+ "filename.URL allow invalid, sanitize only"
+ );
+
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.desktop",
+ "text/unknown",
+ mimeService.VALIDATE_SANITIZE_ONLY
+ ),
+ "filename.desktop.download",
+ "filename.desktop sanitize only"
+ );
+ Assert.equal(
+ mimeService.validateFileNameForSaving(
+ "filename.DESKTOP\n",
+ "text/unknown",
+ mimeService.VALIDATE_SANITIZE_ONLY |
+ mimeService.VALIDATE_ALLOW_INVALID_FILENAMES
+ ),
+ "filename.DESKTOP",
+ "filename.DESKTOP allow invalid, sanitize only"
+ );
+});
diff --git a/uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js b/uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js
new file mode 100644
index 0000000000..6f4fe52a49
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_getFromTypeAndExtension.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_utf8_extension() {
+ const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ let someMIME = mimeService.getFromTypeAndExtension(
+ "application/x-nonsense",
+ ".тест"
+ );
+ Assert.stringContains(someMIME.description, "тест");
+ // primary extension isn't set on macOS or android, see bug 1721181
+ if (AppConstants.platform != "macosx" && AppConstants.platform != "android") {
+ Assert.equal(someMIME.primaryExtension, ".тест");
+ }
+});
diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js
new file mode 100644
index 0000000000..03b0cea25e
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+
+// PDF files should always have a generic description instead
+// of relying on what is registered with the Operating System.
+add_task(async function test_check_unknown_mime_type() {
+ const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ let pdfType = mimeService.getTypeFromExtension("pdf");
+ Assert.equal(pdfType, "application/pdf");
+ let extension = mimeService.getPrimaryExtension("application/pdf", "");
+ Assert.equal(extension, "pdf", "Expect pdf extension when given mime");
+ let mimeInfo = gMIMEService.getFromTypeAndExtension("", "pdf");
+ let stringBundle = Services.strings.createBundle(
+ "chrome://mozapps/locale/downloads/unknownContentType.properties"
+ );
+ Assert.equal(
+ mimeInfo.description,
+ stringBundle.GetStringFromName("pdfExtHandlerDescription"),
+ "PDF has generic description"
+ );
+});
diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js
new file mode 100644
index 0000000000..9beef9d9c5
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Zip files can be opened by Windows explorer, so we should always be able to
+// determine a description and default handler for them. However, things can
+// get messy if they are sent to us with a mime type other than what Windows
+// considers the "right" mimetype (application/x-zip-compressed), like
+// application/zip, which is what most places (IANA, macOS, probably all linux
+// distros, Apache, etc.) think is the "right" mimetype.
+add_task(async function test_check_unknown_mime_type() {
+ const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ let zipType = mimeService.getTypeFromExtension("zip");
+ Assert.equal(zipType, "application/x-zip-compressed");
+ try {
+ let extension = mimeService.getPrimaryExtension("application/zip", "");
+ Assert.equal(
+ extension,
+ "zip",
+ "Expect our own info to provide an extension for zip files."
+ );
+ } catch (ex) {
+ Assert.ok(false, "We shouldn't throw when getting zip info.");
+ }
+ let found = {};
+ let mimeInfo = mimeService.getMIMEInfoFromOS("application/zip", "zip", found);
+ Assert.ok(
+ mimeInfo.hasDefaultHandler,
+ "Should have a default app for zip files"
+ );
+});
diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js
new file mode 100644
index 0000000000..7202db58de
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js
@@ -0,0 +1,65 @@
+/* -*- 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/. */
+
+/**
+ * Test for bug 508030 <https://bugzilla.mozilla.org/show_bug.cgi?id=508030>:
+ * nsIMIMEService.getTypeFromExtension fails to find a match in the
+ * "ext-to-type-mapping" category if the provided extension is not lowercase.
+ */
+function run_test() {
+ // --- Common services ---
+
+ const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+ const categoryManager = Services.catMan;
+
+ // --- Test procedure ---
+
+ const kTestExtension = "testextension";
+ const kTestExtensionMixedCase = "testExtensIon";
+ const kTestMimeType = "application/x-testextension";
+
+ // Ensure that the test extension is not initially recognized by the operating
+ // system or the "ext-to-type-mapping" category.
+ try {
+ // Try and get the MIME type associated with the extension.
+ mimeService.getTypeFromExtension(kTestExtension);
+ // The line above should have thrown an exception.
+ do_throw("nsIMIMEService.getTypeFromExtension succeeded unexpectedly");
+ } catch (e) {
+ if (
+ !(e instanceof Ci.nsIException) ||
+ e.result != Cr.NS_ERROR_NOT_AVAILABLE
+ ) {
+ throw e;
+ }
+ // This is an expected exception, thrown if the type can't be determined.
+ // Any other exception would cause the test to fail.
+ }
+
+ // Add a temporary category entry mapping the extension to the MIME type.
+ categoryManager.addCategoryEntry(
+ "ext-to-type-mapping",
+ kTestExtension,
+ kTestMimeType,
+ false,
+ true
+ );
+
+ // Check that the mapping is recognized in the simple case.
+ var type = mimeService.getTypeFromExtension(kTestExtension);
+ Assert.equal(type, kTestMimeType);
+
+ // Check that the mapping is recognized even if the extension has mixed case.
+ type = mimeService.getTypeFromExtension(kTestExtensionMixedCase);
+ Assert.equal(type, kTestMimeType);
+
+ // Clean up after ourselves.
+ categoryManager.deleteCategoryEntry(
+ "ext-to-type-mapping",
+ kTestExtension,
+ false
+ );
+}
diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js
new file mode 100644
index 0000000000..5076a57738
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js
@@ -0,0 +1,218 @@
+/* -*- 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/. */
+
+/**
+ * Test for bug 484579 <https://bugzilla.mozilla.org/show_bug.cgi?id=484579>:
+ * nsIMIMEService.getTypeFromExtension may fail unexpectedly on Windows when
+ * "Content Type" is empty in the registry.
+ */
+
+// We must use a file extension that isn't listed in nsExternalHelperAppService's
+// defaultMimeEntries, otherwise the code takes a shortcut skipping the registry.
+const FILE_EXTENSION = ".nfo";
+// This is used to ensure the test properly used the mock, so that if we change
+// the underlying code, it won't be skipped.
+let gTestUsedOurMock = false;
+
+function run_test() {
+ // Activate the override of the file association data in the registry.
+ registerMockWindowsRegKeyFactory();
+
+ // Check the mock has been properly activated.
+ let regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ regKey.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT,
+ FILE_EXTENSION,
+ Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE
+ );
+ Assert.equal(
+ regKey.readStringValue("content type"),
+ "",
+ "Check the mock replied as expected."
+ );
+ Assert.ok(gTestUsedOurMock, "The test properly used the mock registry");
+ // Reset gTestUsedOurMock, because we just used it.
+ gTestUsedOurMock = false;
+ // Try and get the MIME type associated with the extension. If this
+ // operation does not throw an unexpected exception, the test succeeds.
+ Assert.throws(
+ () => {
+ Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromExtension(FILE_EXTENSION);
+ },
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Should throw a NOT_AVAILABLE exception"
+ );
+
+ Assert.ok(gTestUsedOurMock, "The test properly used the mock registry");
+}
+
+/**
+ * Constructs a new mock registry key by wrapping the provided object.
+ *
+ * This mock implementation is tailored for this test, and forces consumers
+ * of the readStringValue method to believe that the "Content Type" value of
+ * the FILE_EXTENSION key under HKEY_CLASSES_ROOT is an empty string.
+ *
+ * The same value read from "HKEY_LOCAL_MACHINE\SOFTWARE\Classes" is not
+ * affected.
+ *
+ * @param aWrappedObject An actual nsIWindowsRegKey implementation.
+ */
+function MockWindowsRegKey(aWrappedObject) {
+ this._wrappedObject = aWrappedObject;
+
+ // This function creates a forwarding function for wrappedObject
+ function makeForwardingFunction(functionName) {
+ return function () {
+ return aWrappedObject[functionName].apply(aWrappedObject, arguments);
+ };
+ }
+
+ // Forward all the functions that are not explicitly overridden
+ for (var propertyName in aWrappedObject) {
+ if (!(propertyName in this)) {
+ if (typeof aWrappedObject[propertyName] == "function") {
+ this[propertyName] = makeForwardingFunction(propertyName);
+ } else {
+ this[propertyName] = aWrappedObject[propertyName];
+ }
+ }
+ }
+}
+
+MockWindowsRegKey.prototype = {
+ // --- Overridden nsISupports interface functions ---
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowsRegKey"]),
+
+ // --- Overridden nsIWindowsRegKey interface functions ---
+
+ open(aRootKey, aRelPath, aMode) {
+ // Remember the provided root key and path
+ this._rootKey = aRootKey;
+ this._relPath = aRelPath;
+
+ // Create the actual registry key
+ return this._wrappedObject.open(aRootKey, aRelPath, aMode);
+ },
+
+ openChild(aRelPath, aMode) {
+ // Open the child key and wrap it
+ var innerKey = this._wrappedObject.openChild(aRelPath, aMode);
+ var key = new MockWindowsRegKey(innerKey);
+
+ // Set the properties of the child key and return it
+ key._rootKey = this._rootKey;
+ key._relPath = this._relPath + aRelPath;
+ return key;
+ },
+
+ createChild(aRelPath, aMode) {
+ // Create the child key and wrap it
+ var innerKey = this._wrappedObject.createChild(aRelPath, aMode);
+ var key = new MockWindowsRegKey(innerKey);
+
+ // Set the properties of the child key and return it
+ key._rootKey = this._rootKey;
+ key._relPath = this._relPath + aRelPath;
+ return key;
+ },
+
+ get childCount() {
+ return this._wrappedObject.childCount;
+ },
+
+ get valueCount() {
+ return this._wrappedObject.valueCount;
+ },
+
+ readStringValue(aName) {
+ // If this is the key under test, return a fake value
+ if (
+ this._rootKey == Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT &&
+ this._relPath.toLowerCase() == FILE_EXTENSION &&
+ aName.toLowerCase() == "content type"
+ ) {
+ gTestUsedOurMock = true;
+ return "";
+ }
+ // Return the real value from the registry
+ return this._wrappedObject.readStringValue(aName);
+ },
+};
+
+function registerMockWindowsRegKeyFactory() {
+ const kMockCID = Components.ID("{9b23dfe9-296b-4740-ba1c-d39c9a16e55e}");
+ const kWindowsRegKeyContractID = "@mozilla.org/windows-registry-key;1";
+ // Preserve the original CID.
+ let originalWindowsRegKeyCID = Cc[kWindowsRegKeyContractID].number;
+
+ // See bug 1694345 - nsNotifyAddrListener::CheckAdaptersAddresses might
+ // attempt to use the registry off the main thread, so we disable that
+ // feature while the mock registry is active.
+ let oldSuffixListPref = Services.prefs.getBoolPref(
+ "network.notify.dnsSuffixList"
+ );
+ Services.prefs.setBoolPref("network.notify.dnsSuffixList", false);
+
+ let oldCheckForProxiesPref = Services.prefs.getBoolPref(
+ "network.notify.checkForProxies"
+ );
+ Services.prefs.setBoolPref("network.notify.checkForProxies", false);
+
+ let oldCheckForNRPTPref = Services.prefs.getBoolPref(
+ "network.notify.checkForNRPT"
+ );
+ Services.prefs.setBoolPref("network.notify.checkForNRPT", false);
+
+ info("Create a mock RegKey factory");
+ let originalRegKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ let mockWindowsRegKeyFactory = {
+ createInstance(iid) {
+ info("Create a mock wrapper around RegKey");
+ var key = new MockWindowsRegKey(originalRegKey);
+ return key.QueryInterface(iid);
+ },
+ };
+ info("Register the mock RegKey factory");
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(
+ kMockCID,
+ "Mock Windows Registry Key Implementation",
+ kWindowsRegKeyContractID,
+ mockWindowsRegKeyFactory
+ );
+
+ registerCleanupFunction(() => {
+ // Free references to the mock factory
+ registrar.unregisterFactory(kMockCID, mockWindowsRegKeyFactory);
+ // Restore the original factory
+ registrar.registerFactory(
+ Components.ID(originalWindowsRegKeyCID),
+ "",
+ kWindowsRegKeyContractID,
+ null
+ );
+
+ Services.prefs.setBoolPref(
+ "network.notify.dnsSuffixList",
+ oldSuffixListPref
+ );
+ Services.prefs.setBoolPref(
+ "network.notify.checkForProxies",
+ oldCheckForProxiesPref
+ );
+ Services.prefs.setBoolPref(
+ "network.notify.checkForNRPT",
+ oldCheckForNRPTPref
+ );
+ });
+}
diff --git a/uriloader/exthandler/tests/unit/test_handlerService.js b/uriloader/exthandler/tests/unit/test_handlerService.js
new file mode 100644
index 0000000000..46a4d9fdd9
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_handlerService.js
@@ -0,0 +1,467 @@
+/* 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/. */
+
+function run_test() {
+ //* *************************************************************************//
+ // Constants
+
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+
+ const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+
+ const protoSvc = Cc[
+ "@mozilla.org/uriloader/external-protocol-service;1"
+ ].getService(Ci.nsIExternalProtocolService);
+
+ const prefSvc = Services.prefs;
+
+ let noMailto = false;
+ if (mozinfo.os == "win") {
+ // Check mailto handler from registry.
+ // If registry entry is nothing, no mailto handler
+ let regSvc = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ try {
+ regSvc.open(regSvc.ROOT_KEY_CLASSES_ROOT, "mailto", regSvc.ACCESS_READ);
+ noMailto = false;
+ } catch (ex) {
+ noMailto = true;
+ }
+ regSvc.close();
+ }
+
+ if (mozinfo.os == "linux") {
+ // Check mailto handler from GIO
+ // If there isn't one, then we have no mailto handler
+ let gIOSvc = Cc["@mozilla.org/gio-service;1"].createInstance(
+ Ci.nsIGIOService
+ );
+ try {
+ gIOSvc.getAppForURIScheme("mailto");
+ noMailto = false;
+ } catch (ex) {
+ noMailto = true;
+ }
+ }
+
+ //* *************************************************************************//
+ // Sample Data
+
+ // It doesn't matter whether or not this nsIFile is actually executable,
+ // only that it has a path and exists. Since we don't know any executable
+ // that exists on all platforms (except possibly the application being
+ // tested, but there doesn't seem to be a way to get a reference to that
+ // from the directory service), we use the temporary directory itself.
+ var executable = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ // XXX We could, of course, create an actual executable in the directory:
+ // executable.append("localhandler");
+ // if (!executable.exists())
+ // executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755);
+
+ var localHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ localHandler.name = "Local Handler";
+ localHandler.executable = executable;
+
+ var webHandler = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(Ci.nsIWebHandlerApp);
+ webHandler.name = "Web Handler";
+ webHandler.uriTemplate = "http://www.example.com/?%s";
+
+ // FIXME: these tests create and manipulate enough variables that it would
+ // make sense to move each test into its own scope so we don't run the risk
+ // of one test stomping on another's data.
+
+ //* *************************************************************************//
+ // Test Default Properties
+
+ // Get a handler info for a MIME type that neither the application nor
+ // the OS knows about and make sure its properties are set to the proper
+ // default values.
+
+ var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null);
+
+ // Make sure it's also an nsIHandlerInfo.
+ Assert.ok(handlerInfo instanceof Ci.nsIHandlerInfo);
+
+ Assert.equal(handlerInfo.type, "nonexistent/type");
+
+ // Deprecated property, but we should still make sure it's set correctly.
+ Assert.equal(handlerInfo.MIMEType, "nonexistent/type");
+
+ // These properties are the ones the handler service knows how to store.
+ Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk);
+ Assert.equal(handlerInfo.preferredApplicationHandler, null);
+ Assert.equal(handlerInfo.possibleApplicationHandlers.length, 0);
+ Assert.equal(
+ handlerInfo.alwaysAskBeforeHandling,
+ prefSvc.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types",
+ false
+ )
+ );
+
+ // These properties are initialized to default values by the service,
+ // so we might as well make sure they're initialized to the right defaults.
+ Assert.equal(handlerInfo.description, "");
+ Assert.equal(handlerInfo.hasDefaultHandler, false);
+ Assert.equal(handlerInfo.defaultDescription, "");
+
+ const kExternalWarningDefault =
+ "network.protocol-handler.warn-external-default";
+ prefSvc.setBoolPref(kExternalWarningDefault, true);
+
+ // XXX add more thorough protocol info property checking
+
+ // no OS default handler exists
+ var protoInfo = protoSvc.getProtocolHandlerInfo("x-moz-rheet");
+ Assert.equal(protoInfo.preferredAction, protoInfo.alwaysAsk);
+ Assert.ok(protoInfo.alwaysAskBeforeHandling);
+
+ // OS default exists, injected default does not exist,
+ // explicit warning pref: false
+ const kExternalWarningPrefPrefix = "network.protocol-handler.warn-external.";
+ prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", false);
+ protoInfo = protoSvc.getProtocolHandlerInfo("http");
+ Assert.equal(0, protoInfo.possibleApplicationHandlers.length);
+ // NOTE: this assertion will fail if the system executing the test does not
+ // have a handler registered for the http protocol. This is very unlikely to
+ // actually happen except on certain configurations of Linux, but one of
+ // those configurations is the default WSL Ubuntu install. So, if you are
+ // running this test locally and seeing a failure here, it might not be
+ // anything to really worry about.
+ Assert.ok(!protoInfo.alwaysAskBeforeHandling);
+
+ // OS default exists, injected default does not exist,
+ // explicit warning pref: true
+ prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", true);
+ protoInfo = protoSvc.getProtocolHandlerInfo("http");
+ // OS handler isn't included in possibleApplicationHandlers, so length is 0
+ // Once they become instances of nsILocalHandlerApp, this number will need
+ // to change.
+ Assert.equal(0, protoInfo.possibleApplicationHandlers.length);
+ Assert.ok(protoInfo.alwaysAskBeforeHandling);
+
+ // OS default exists, injected default exists, explicit warning pref: false
+ prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false);
+ protoInfo = protoSvc.getProtocolHandlerInfo("mailto");
+ if (AppConstants.MOZ_APP_NAME == "thunderbird") {
+ Assert.equal(0, protoInfo.possibleApplicationHandlers.length);
+ } else {
+ Assert.equal(1, protoInfo.possibleApplicationHandlers.length);
+ }
+
+ // Win7+ or Linux's GIO might not have a default mailto: handler
+ if (noMailto) {
+ Assert.ok(protoInfo.alwaysAskBeforeHandling);
+ } else {
+ Assert.ok(!protoInfo.alwaysAskBeforeHandling);
+ }
+
+ // OS default exists, injected default exists, explicit warning pref: true
+ prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", true);
+ protoInfo = protoSvc.getProtocolHandlerInfo("mailto");
+ if (AppConstants.MOZ_APP_NAME == "thunderbird") {
+ Assert.equal(0, protoInfo.possibleApplicationHandlers.length);
+ } else {
+ Assert.equal(1, protoInfo.possibleApplicationHandlers.length);
+ // Win7+ or Linux's GIO may have no default mailto: handler, so we'd ask
+ // anyway. Otherwise, the default handlers will not have stored preferred
+ // actions etc., so re-requesting them after the warning pref has changed
+ // will use the updated pref value. So both when we have and do not have
+ // a default mailto: handler, we'll ask:
+ Assert.ok(protoInfo.alwaysAskBeforeHandling);
+ // As soon as anyone actually stores updated defaults into the profile
+ // database, that default will stop tracking the warning pref.
+ }
+ // Now set the value stored in RDF to true, and the pref to false, to make
+ // sure we still get the right value. (Basically, same thing as above but
+ // with the values reversed.)
+ prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false);
+ protoInfo.alwaysAskBeforeHandling = true;
+ handlerSvc.store(protoInfo);
+ protoInfo = protoSvc.getProtocolHandlerInfo("mailto");
+ if (AppConstants.MOZ_APP_NAME == "thunderbird") {
+ Assert.equal(0, protoInfo.possibleApplicationHandlers.length);
+ } else {
+ Assert.equal(1, protoInfo.possibleApplicationHandlers.length);
+ Assert.ok(protoInfo.alwaysAskBeforeHandling);
+ }
+
+ //* *************************************************************************//
+ // Test Round-Trip Data Integrity
+
+ // Test round-trip data integrity by setting the properties of the handler
+ // info object to different values, telling the handler service to store the
+ // object, and then retrieving a new info object for the same type and making
+ // sure its properties are identical.
+
+ handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ handlerInfo.preferredApplicationHandler = localHandler;
+ handlerInfo.alwaysAskBeforeHandling = false;
+
+ handlerSvc.store(handlerInfo);
+
+ handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null);
+
+ Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp);
+
+ Assert.notEqual(handlerInfo.preferredApplicationHandler, null);
+ var preferredHandler = handlerInfo.preferredApplicationHandler;
+ Assert.equal(typeof preferredHandler, "object");
+ Assert.equal(preferredHandler.name, "Local Handler");
+ Assert.ok(preferredHandler instanceof Ci.nsILocalHandlerApp);
+ preferredHandler.QueryInterface(Ci.nsILocalHandlerApp);
+ Assert.equal(preferredHandler.executable.path, localHandler.executable.path);
+
+ Assert.ok(!handlerInfo.alwaysAskBeforeHandling);
+
+ // Make sure the handler service's enumerate method lists all known handlers.
+ var handlerInfo2 = mimeSvc.getFromTypeAndExtension("nonexistent/type2", null);
+ handlerSvc.store(handlerInfo2);
+ var handlerTypes = ["nonexistent/type", "nonexistent/type2"];
+ handlerTypes.push("mailto");
+ for (let handler of handlerSvc.enumerate()) {
+ Assert.notEqual(handlerTypes.indexOf(handler.type), -1);
+ handlerTypes.splice(handlerTypes.indexOf(handler.type), 1);
+ }
+ Assert.equal(handlerTypes.length, 0);
+ // Make sure the handler service's remove method removes a handler record.
+ handlerSvc.remove(handlerInfo2);
+ let handlers = handlerSvc.enumerate();
+ while (handlers.hasMoreElements()) {
+ Assert.notEqual(
+ handlers.getNext().QueryInterface(Ci.nsIHandlerInfo).type,
+ handlerInfo2.type
+ );
+ }
+
+ // Make sure we can store and retrieve a handler info object with no preferred
+ // handler.
+ var noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/no-preferred-handler",
+ null
+ );
+ handlerSvc.store(noPreferredHandlerInfo);
+ noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/no-preferred-handler",
+ null
+ );
+ Assert.equal(noPreferredHandlerInfo.preferredApplicationHandler, null);
+
+ // Make sure that the handler service removes an existing handler record
+ // if we store a handler info object with no preferred handler.
+ var removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/rem-preferred-handler",
+ null
+ );
+ removePreferredHandlerInfo.preferredApplicationHandler = localHandler;
+ handlerSvc.store(removePreferredHandlerInfo);
+ removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/rem-preferred-handler",
+ null
+ );
+ removePreferredHandlerInfo.preferredApplicationHandler = null;
+ handlerSvc.store(removePreferredHandlerInfo);
+ removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/rem-preferred-handler",
+ null
+ );
+ Assert.equal(removePreferredHandlerInfo.preferredApplicationHandler, null);
+
+ // Make sure we can store and retrieve a handler info object with possible
+ // handlers. We test both adding and removing handlers.
+
+ // Get a handler info and make sure it has no possible handlers.
+ var possibleHandlersInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/possible-handlers",
+ null
+ );
+ Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0);
+
+ // Store and re-retrieve the handler and make sure it still has no possible
+ // handlers.
+ handlerSvc.store(possibleHandlersInfo);
+ possibleHandlersInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/possible-handlers",
+ null
+ );
+ Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0);
+
+ // Add two handlers, store the object, re-retrieve it, and make sure it has
+ // two handlers.
+ possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler);
+ possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler);
+ handlerSvc.store(possibleHandlersInfo);
+ possibleHandlersInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/possible-handlers",
+ null
+ );
+ Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 2);
+
+ // Figure out which is the local and which is the web handler and the index
+ // in the array of the local handler, which is the one we're going to remove
+ // to test removal of a handler.
+ var handler1 =
+ possibleHandlersInfo.possibleApplicationHandlers.queryElementAt(
+ 0,
+ Ci.nsIHandlerApp
+ );
+ var handler2 =
+ possibleHandlersInfo.possibleApplicationHandlers.queryElementAt(
+ 1,
+ Ci.nsIHandlerApp
+ );
+ var localPossibleHandler, webPossibleHandler, localIndex;
+ if (handler1 instanceof Ci.nsILocalHandlerApp) {
+ [localPossibleHandler, webPossibleHandler, localIndex] = [
+ handler1,
+ handler2,
+ 0,
+ ];
+ } else {
+ [localPossibleHandler, webPossibleHandler, localIndex] = [
+ handler2,
+ handler1,
+ 1,
+ ];
+ }
+ localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp);
+ webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp);
+
+ // Make sure the two handlers are the ones we stored.
+ Assert.equal(localPossibleHandler.name, localHandler.name);
+ Assert.ok(localPossibleHandler.equals(localHandler));
+ Assert.equal(webPossibleHandler.name, webHandler.name);
+ Assert.ok(webPossibleHandler.equals(webHandler));
+
+ // Remove a handler, store the object, re-retrieve it, and make sure
+ // it only has one handler.
+ possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex);
+ handlerSvc.store(possibleHandlersInfo);
+ possibleHandlersInfo = mimeSvc.getFromTypeAndExtension(
+ "nonexistent/possible-handlers",
+ null
+ );
+ Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 1);
+
+ // Make sure the handler is the one we didn't remove.
+ webPossibleHandler =
+ possibleHandlersInfo.possibleApplicationHandlers.queryElementAt(
+ 0,
+ Ci.nsIWebHandlerApp
+ );
+ Assert.equal(webPossibleHandler.name, webHandler.name);
+ Assert.ok(webPossibleHandler.equals(webHandler));
+
+ // ////////////////////////////////////////////////////
+ // handler info command line parameters and equality
+ var localApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ var handlerApp = localApp.QueryInterface(Ci.nsIHandlerApp);
+
+ Assert.ok(handlerApp.equals(localApp));
+
+ localApp.executable = executable;
+
+ Assert.equal(0, localApp.parameterCount);
+ localApp.appendParameter("-test1");
+ Assert.equal(1, localApp.parameterCount);
+ localApp.appendParameter("-test2");
+ Assert.equal(2, localApp.parameterCount);
+ Assert.ok(localApp.parameterExists("-test1"));
+ Assert.ok(localApp.parameterExists("-test2"));
+ Assert.ok(!localApp.parameterExists("-false"));
+ localApp.clearParameters();
+ Assert.equal(0, localApp.parameterCount);
+
+ var localApp2 = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+
+ localApp2.executable = executable;
+
+ localApp.clearParameters();
+ Assert.ok(localApp.equals(localApp2));
+
+ // equal:
+ // cut -d 1 -f 2
+ // cut -d 1 -f 2
+
+ localApp.appendParameter("-test1");
+ localApp.appendParameter("-test2");
+ localApp.appendParameter("-test3");
+ localApp2.appendParameter("-test1");
+ localApp2.appendParameter("-test2");
+ localApp2.appendParameter("-test3");
+ Assert.ok(localApp.equals(localApp2));
+
+ // not equal:
+ // cut -d 1 -f 2
+ // cut -f 1 -d 2
+
+ localApp.clearParameters();
+ localApp2.clearParameters();
+
+ localApp.appendParameter("-test1");
+ localApp.appendParameter("-test2");
+ localApp.appendParameter("-test3");
+ localApp2.appendParameter("-test2");
+ localApp2.appendParameter("-test1");
+ localApp2.appendParameter("-test3");
+ Assert.ok(!localApp2.equals(localApp));
+
+ var str;
+ str = localApp.getParameter(0);
+ Assert.equal(str, "-test1");
+ str = localApp.getParameter(1);
+ Assert.equal(str, "-test2");
+ str = localApp.getParameter(2);
+ Assert.equal(str, "-test3");
+
+ // FIXME: test round trip integrity for a protocol.
+ // FIXME: test round trip integrity for a handler info with a web handler.
+
+ //* *************************************************************************//
+ // getTypeFromExtension tests
+
+ // test nonexistent extension
+ var lolType = handlerSvc.getTypeFromExtension("lolcat");
+ Assert.equal(lolType, "");
+
+ // add a handler for the extension
+ var lolHandler = mimeSvc.getFromTypeAndExtension("application/lolcat", null);
+
+ Assert.ok(!lolHandler.extensionExists("lolcat"));
+ lolHandler.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ lolHandler.preferredApplicationHandler = localHandler;
+ lolHandler.alwaysAskBeforeHandling = false;
+ lolHandler.appendExtension("lolcat");
+
+ // store the handler
+ Assert.ok(!handlerSvc.exists(lolHandler));
+ handlerSvc.store(lolHandler);
+ Assert.ok(handlerSvc.exists(lolHandler));
+
+ // test now-existent extension
+ lolType = handlerSvc.getTypeFromExtension("lolcat");
+ Assert.equal(lolType, "application/lolcat");
+
+ // test mailcap entries with needsterminal are ignored on non-Windows non-Mac.
+ if (mozinfo.os != "win" && mozinfo.os != "mac") {
+ prefSvc.setStringPref(
+ "helpers.private_mailcap_file",
+ do_get_file("mailcap").path
+ );
+ handlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", null);
+ Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk);
+ Assert.equal(handlerInfo.defaultDescription, "sed");
+ }
+}
diff --git a/uriloader/exthandler/tests/unit/test_handlerService_store.js b/uriloader/exthandler/tests/unit/test_handlerService_store.js
new file mode 100644
index 0000000000..27c8dc6c9e
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_handlerService_store.js
@@ -0,0 +1,757 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the nsIHandlerService interface.
+ */
+
+// Set up an nsIWebHandlerApp instance that can be used in multiple tests.
+let webHandlerApp = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+].createInstance(Ci.nsIWebHandlerApp);
+webHandlerApp.name = "Web Handler";
+webHandlerApp.uriTemplate = "https://www.example.com/?url=%s";
+let expectedWebHandlerApp = {
+ name: webHandlerApp.name,
+ uriTemplate: webHandlerApp.uriTemplate,
+};
+
+// Set up an nsILocalHandlerApp instance that can be used in multiple tests. The
+// executable should exist, but it doesn't need to point to an actual file, so
+// we simply initialize it to the path of an existing directory.
+let localHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+].createInstance(Ci.nsILocalHandlerApp);
+localHandlerApp.name = "Local Handler";
+localHandlerApp.executable = new FileUtils.File(PathUtils.tempDir);
+let expectedLocalHandlerApp = {
+ name: localHandlerApp.name,
+ executable: localHandlerApp.executable,
+};
+
+/**
+ * Returns a new nsIHandlerInfo instance initialized to known values that don't
+ * depend on the platform and are easier to verify later.
+ *
+ * @param type
+ * Because the "preferredAction" is initialized to saveToDisk, this
+ * should represent a MIME type rather than a protocol.
+ */
+function getKnownHandlerInfo(type) {
+ let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(type);
+ handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
+ handlerInfo.alwaysAskBeforeHandling = false;
+ return handlerInfo;
+}
+
+/**
+ * Checks that the information stored in the handler service instance under
+ * testing matches the test data files.
+ */
+function assertAllHandlerInfosMatchTestData() {
+ let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos();
+
+ // It's important that the MIME types we check here do not exist at the
+ // operating system level, otherwise the list of handlers and file extensions
+ // will be merged. The current implementation avoids duplicate entries.
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "example/type.handleinternally",
+ preferredAction: Ci.nsIHandlerInfo.handleInternally,
+ alwaysAskBeforeHandling: false,
+ fileExtensions: ["example_one"],
+ });
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "example/type.savetodisk",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: true,
+ preferredApplicationHandler: {
+ name: "Example Default Handler",
+ uriTemplate: "https://www.example.com/?url=%s",
+ },
+ possibleApplicationHandlers: [
+ {
+ name: "Example Default Handler",
+ uriTemplate: "https://www.example.com/?url=%s",
+ },
+ ],
+ fileExtensions: ["example_two", "example_three"],
+ });
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "example/type.usehelperapp",
+ preferredAction: Ci.nsIHandlerInfo.useHelperApp,
+ alwaysAskBeforeHandling: true,
+ preferredApplicationHandler: {
+ name: "Example Default Handler",
+ uriTemplate: "https://www.example.com/?url=%s",
+ },
+ possibleApplicationHandlers: [
+ {
+ name: "Example Default Handler",
+ uriTemplate: "https://www.example.com/?url=%s",
+ },
+ {
+ name: "Example Possible Handler One",
+ uriTemplate: "http://www.example.com/?id=1&url=%s",
+ },
+ {
+ name: "Example Possible Handler Two",
+ uriTemplate: "http://www.example.com/?id=2&url=%s",
+ },
+ ],
+ fileExtensions: ["example_two", "example_three"],
+ });
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "example/type.usesystemdefault",
+ preferredAction: Ci.nsIHandlerInfo.useSystemDefault,
+ alwaysAskBeforeHandling: false,
+ possibleApplicationHandlers: [
+ {
+ name: "Example Possible Handler",
+ uriTemplate: "http://www.example.com/?url=%s",
+ },
+ ],
+ });
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "examplescheme.usehelperapp",
+ preferredAction: Ci.nsIHandlerInfo.useHelperApp,
+ alwaysAskBeforeHandling: true,
+ preferredApplicationHandler: {
+ name: "Example Default Handler",
+ uriTemplate: "https://www.example.com/?url=%s",
+ },
+ possibleApplicationHandlers: [
+ {
+ name: "Example Default Handler",
+ uriTemplate: "https://www.example.com/?url=%s",
+ },
+ {
+ name: "Example Possible Handler One",
+ uriTemplate: "http://www.example.com/?id=1&url=%s",
+ },
+ {
+ name: "Example Possible Handler Two",
+ uriTemplate: "http://www.example.com/?id=2&url=%s",
+ },
+ ],
+ });
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "examplescheme.usesystemdefault",
+ preferredAction: Ci.nsIHandlerInfo.useSystemDefault,
+ alwaysAskBeforeHandling: false,
+ possibleApplicationHandlers: [
+ {
+ name: "Example Possible Handler",
+ uriTemplate: "http://www.example.com/?url=%s",
+ },
+ ],
+ });
+
+ Assert.equal(handlerInfos.length, 0);
+}
+
+/**
+ * Loads data from a file in a predefined format, verifying that the format is
+ * recognized and all the known properties are loaded and saved.
+ */
+add_task(async function test_store_fillHandlerInfo_predefined() {
+ // Test that the file format used in previous versions can be loaded.
+ await copyTestDataToHandlerStore();
+ await assertAllHandlerInfosMatchTestData();
+
+ // Keep a copy of the nsIHandlerInfo instances, then delete the handler store
+ // and populate it with the known data. Since the handler store is empty, the
+ // default handlers for the current locale are also injected, so we have to
+ // delete them manually before adding the other nsIHandlerInfo instances.
+ let testHandlerInfos = HandlerServiceTestUtils.getAllHandlerInfos();
+ await deleteHandlerStore();
+ for (let handlerInfo of HandlerServiceTestUtils.getAllHandlerInfos()) {
+ gHandlerService.remove(handlerInfo);
+ }
+ for (let handlerInfo of testHandlerInfos) {
+ gHandlerService.store(handlerInfo);
+ }
+
+ // Test that the known data still matches after saving it and reloading.
+ await unloadHandlerStore();
+ await assertAllHandlerInfosMatchTestData();
+});
+
+/**
+ * Check that "store" is able to add new instances, that "remove" and "exists"
+ * work, and that "fillHandlerInfo" throws when the instance does not exist.
+ */
+add_task(async function test_store_remove_exists() {
+ // Test both MIME types and protocols.
+ for (let type of [
+ "example/type.usehelperapp",
+ "examplescheme.usehelperapp",
+ ]) {
+ // Create new nsIHandlerInfo instances before loading the test data.
+ await deleteHandlerStore();
+ let handlerInfoPresent = HandlerServiceTestUtils.getHandlerInfo(type);
+ let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2");
+
+ // Set up known properties that we can verify later.
+ handlerInfoAbsent.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
+ handlerInfoAbsent.alwaysAskBeforeHandling = false;
+
+ await copyTestDataToHandlerStore();
+
+ Assert.ok(gHandlerService.exists(handlerInfoPresent));
+ Assert.ok(!gHandlerService.exists(handlerInfoAbsent));
+
+ gHandlerService.store(handlerInfoAbsent);
+ gHandlerService.remove(handlerInfoPresent);
+
+ await unloadHandlerStore();
+
+ Assert.ok(!gHandlerService.exists(handlerInfoPresent));
+ Assert.ok(gHandlerService.exists(handlerInfoAbsent));
+
+ Assert.throws(
+ () => gHandlerService.fillHandlerInfo(handlerInfoPresent, ""),
+ ex => ex.result == Cr.NS_ERROR_NOT_AVAILABLE
+ );
+
+ let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo(type + "2");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: type + "2",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ });
+ }
+});
+
+/**
+ * Tests that it is possible to save an nsIHandlerInfo instance with a
+ * "preferredAction" that is either a valid or an unknown value, and the
+ * action always takes on an appropriate value when reloading.
+ */
+add_task(async function test_store_preferredAction() {
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ // Valid action values should all remain unchanged across a refresh, except
+ // for alwaysAsk which may be overridden with useHelperApp depending on prefs.
+ // Invalid action values should always convert to useHelperApp.
+ const actions = [
+ {
+ preferred: Ci.nsIHandlerInfo.alwaysAsk,
+ expected: Ci.nsIHandlerInfo.alwaysAsk,
+ },
+ {
+ preferred: Ci.nsIHandlerInfo.handleInternally,
+ expected: Ci.nsIHandlerInfo.handleInternally,
+ },
+ { preferred: 999, expected: Ci.nsIHandlerInfo.useHelperApp },
+ ];
+
+ for (let action of actions) {
+ handlerInfo.preferredAction = action.preferred;
+ gHandlerService.store(handlerInfo);
+ gHandlerService.fillHandlerInfo(handlerInfo, "");
+ Assert.equal(handlerInfo.preferredAction, action.expected);
+ }
+});
+
+/**
+ * Tests that it is possible to save an nsIHandlerInfo instance containing an
+ * nsILocalHandlerApp instance pointing to an executable that doesn't exist, but
+ * this entry is ignored when reloading.
+ */
+add_task(async function test_store_localHandlerApp_missing() {
+ if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) {
+ info("Skipping test because it does not apply to this platform.");
+ return;
+ }
+
+ let missingHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ missingHandlerApp.name = "Non-existing Handler";
+ missingHandlerApp.executable = new FileUtils.File(
+ PathUtils.join(PathUtils.tempDir, "nonexisting")
+ );
+
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.preferredApplicationHandler = missingHandlerApp;
+ handlerInfo.possibleApplicationHandlers.appendElement(missingHandlerApp);
+ handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp);
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+
+ let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ possibleApplicationHandlers: [expectedWebHandlerApp],
+ });
+});
+
+/**
+ * Test saving and reloading an instance of nsIDBusHandlerApp.
+ */
+add_task(async function test_store_dBusHandlerApp() {
+ if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) {
+ info("Skipping test because it does not apply to this platform.");
+ return;
+ }
+
+ // Set up an nsIDBusHandlerApp instance for testing.
+ let dBusHandlerApp = Cc[
+ "@mozilla.org/uriloader/dbus-handler-app;1"
+ ].createInstance(Ci.nsIDBusHandlerApp);
+ dBusHandlerApp.name = "DBus Handler";
+ dBusHandlerApp.service = "test.method.server";
+ dBusHandlerApp.method = "Method";
+ dBusHandlerApp.dBusInterface = "test.method.Type";
+ dBusHandlerApp.objectPath = "/test/method/Object";
+ let expectedDBusHandlerApp = {
+ name: dBusHandlerApp.name,
+ service: dBusHandlerApp.service,
+ method: dBusHandlerApp.method,
+ dBusInterface: dBusHandlerApp.dBusInterface,
+ objectPath: dBusHandlerApp.objectPath,
+ };
+
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.preferredApplicationHandler = dBusHandlerApp;
+ handlerInfo.possibleApplicationHandlers.appendElement(dBusHandlerApp);
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+
+ let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ preferredApplicationHandler: expectedDBusHandlerApp,
+ possibleApplicationHandlers: [expectedDBusHandlerApp],
+ });
+});
+
+/**
+ * Tests that it is possible to save an nsIHandlerInfo instance with a
+ * "preferredApplicationHandler" and no "possibleApplicationHandlers", but the
+ * former is always included in the latter list when reloading.
+ */
+add_task(
+ async function test_store_possibleApplicationHandlers_includes_preferred() {
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.preferredApplicationHandler = localHandlerApp;
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+
+ let actualHandlerInfo =
+ HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ preferredApplicationHandler: expectedLocalHandlerApp,
+ possibleApplicationHandlers: [expectedLocalHandlerApp],
+ });
+ }
+);
+
+/**
+ * Tests that it is possible to save an nsIHandlerInfo instance with a
+ * "preferredApplicationHandler" that is not the first element in
+ * "possibleApplicationHandlers", but the former is always included as the first
+ * element of the latter list when reloading.
+ */
+add_task(
+ async function test_store_possibleApplicationHandlers_preferred_first() {
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.preferredApplicationHandler = webHandlerApp;
+ // The preferred handler is appended after the other one.
+ handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp);
+ handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp);
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+
+ let actualHandlerInfo =
+ HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ preferredApplicationHandler: expectedWebHandlerApp,
+ possibleApplicationHandlers: [
+ expectedWebHandlerApp,
+ expectedLocalHandlerApp,
+ ],
+ });
+ }
+);
+
+/**
+ * Tests that it is possible to save an nsIHandlerInfo instance with an
+ * uppercase file extension, but it is converted to lowercase when reloading.
+ */
+add_task(async function test_store_fileExtensions_lowercase() {
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.appendExtension("extension_test1");
+ handlerInfo.appendExtension("EXTENSION_test2");
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+
+ let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ fileExtensions: ["extension_test1", "extension_test2"],
+ });
+});
+
+/**
+ * Tests that appendExtension doesn't add duplicates, and that anyway duplicates
+ * from possibleApplicationHandlers are removed when saving and reloading.
+ */
+add_task(async function test_store_no_duplicates() {
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.preferredApplicationHandler = webHandlerApp;
+ handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp);
+ handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp);
+ handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp);
+ handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp);
+ handlerInfo.appendExtension("extension_test1");
+ handlerInfo.appendExtension("extension_test2");
+ handlerInfo.appendExtension("extension_test1");
+ handlerInfo.appendExtension("EXTENSION_test1");
+ Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), [
+ "extension_test1",
+ "extension_test2",
+ ]);
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+
+ let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ preferredApplicationHandler: expectedWebHandlerApp,
+ possibleApplicationHandlers: [
+ expectedWebHandlerApp,
+ expectedLocalHandlerApp,
+ ],
+ fileExtensions: ["extension_test1", "extension_test2"],
+ });
+});
+
+/**
+ * Tests that setFileExtensions doesn't add duplicates.
+ */
+add_task(async function test_setFileExtensions_no_duplicates() {
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.setFileExtensions("a,b,A,b,c,a");
+ let expected = ["a", "b", "c"];
+ Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected);
+ // Test empty extensions, also at begin and end.
+ handlerInfo.setFileExtensions(",a,,b,A,c,");
+ Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected);
+});
+
+/**
+ * Tests that "store" deletes properties that have their default values from
+ * the data store.
+ *
+ * File extensions are never deleted once they have been associated.
+ */
+add_task(async function test_store_deletes_properties_except_extensions() {
+ await deleteHandlerStore();
+
+ // Prepare an nsIHandlerInfo instance with all the properties set to values
+ // that will result in deletions. The preferredAction is also set to a defined
+ // value so we can more easily verify it later.
+ let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(
+ "example/type.savetodisk"
+ );
+ handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk;
+ handlerInfo.alwaysAskBeforeHandling = false;
+
+ // All the properties for "example/type.savetodisk" are present in the test
+ // data, so we load the data before overwriting their values.
+ await copyTestDataToHandlerStore();
+ gHandlerService.store(handlerInfo);
+
+ // Now we can reload the data and verify that no extra values have been kept.
+ await unloadHandlerStore();
+ let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo(
+ "example/type.savetodisk"
+ );
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/type.savetodisk",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ fileExtensions: ["example_two", "example_three"],
+ });
+});
+
+/**
+ * Tests the "overrideType" argument of "fillHandlerInfo".
+ */
+add_task(async function test_fillHandlerInfo_overrideType() {
+ // Test both MIME types and protocols.
+ for (let type of [
+ "example/type.usesystemdefault",
+ "examplescheme.usesystemdefault",
+ ]) {
+ await deleteHandlerStore();
+
+ // Create new nsIHandlerInfo instances before loading the test data.
+ let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2");
+
+ // Fill the nsIHandlerInfo instance using the type that actually exists.
+ await copyTestDataToHandlerStore();
+ gHandlerService.fillHandlerInfo(handlerInfoAbsent, type);
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfoAbsent, {
+ // While the data is populated from another type, the type is unchanged.
+ type: type + "2",
+ preferredAction: Ci.nsIHandlerInfo.useSystemDefault,
+ alwaysAskBeforeHandling: false,
+ possibleApplicationHandlers: [
+ {
+ name: "Example Possible Handler",
+ uriTemplate: "http://www.example.com/?url=%s",
+ },
+ ],
+ });
+ }
+});
+
+/**
+ * Tests "getTypeFromExtension" including unknown extensions.
+ */
+add_task(async function test_getTypeFromExtension() {
+ await copyTestDataToHandlerStore();
+
+ Assert.equal(gHandlerService.getTypeFromExtension(""), "");
+ Assert.equal(gHandlerService.getTypeFromExtension("example_unknown"), "");
+ Assert.equal(
+ gHandlerService.getTypeFromExtension("example_one"),
+ "example/type.handleinternally"
+ );
+ Assert.equal(
+ gHandlerService.getTypeFromExtension("EXAMPLE_one"),
+ "example/type.handleinternally"
+ );
+});
+
+/**
+ * Checks that the information stored in the handler service instance under
+ * testing matches the default handlers for the English locale.
+ */
+function assertAllHandlerInfosMatchDefaultHandlers() {
+ let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos();
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "mailto",
+ preferredActionOSDependent: true,
+ possibleApplicationHandlers: [
+ {
+ name: "Gmail",
+ uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s",
+ },
+ ],
+ });
+
+ Assert.equal(handlerInfos.length, 0);
+}
+
+/**
+ * Tests the default protocol handlers imported from the locale-specific data.
+ */
+add_task(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ async function test_default_protocol_handlers() {
+ if (
+ !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion")
+ ) {
+ info("This platform or locale does not have default handlers.");
+ return;
+ }
+
+ // This will inject the default protocol handlers for the current locale.
+ await deleteHandlerStore();
+
+ await assertAllHandlerInfosMatchDefaultHandlers();
+ }
+);
+
+/**
+ * Tests that the default protocol handlers are not imported again from the
+ * locale-specific data if they already exist.
+ */
+add_task(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ async function test_default_protocol_handlers_no_duplicates() {
+ if (
+ !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion")
+ ) {
+ info("This platform or locale does not have default handlers.");
+ return;
+ }
+
+ // This will inject the default protocol handlers for the current locale.
+ await deleteHandlerStore();
+
+ // Clear the preference to force injecting again.
+ Services.prefs.clearUserPref("gecko.handlerService.defaultHandlersVersion");
+
+ await unloadHandlerStore();
+
+ // There should be no duplicate handlers in the protocols.
+ assertAllHandlerInfosMatchDefaultHandlers();
+ }
+);
+
+/**
+ * Ensures forward compatibility by checking that the "store" method preserves
+ * unknown properties in the test data.
+ */
+add_task(async function test_store_keeps_unknown_properties() {
+ // Create a new nsIHandlerInfo instance before loading the test data.
+ await deleteHandlerStore();
+ let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(
+ "example/type.handleinternally"
+ );
+
+ await copyTestDataToHandlerStore();
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+ let data = await IOUtils.readJSON(jsonPath);
+ Assert.equal(
+ data.mimeTypes["example/type.handleinternally"].unknownProperty,
+ "preserved"
+ );
+});
+
+/**
+ * Runs the asyncInit method, ensuring that it successfully inits the store
+ * and calls the handlersvc-store-initialized topic.
+ */
+add_task(async function test_async_init() {
+ await deleteHandlerStore();
+ await copyTestDataToHandlerStore();
+ gHandlerService.asyncInit();
+ await TestUtils.topicObserved("handlersvc-store-initialized");
+ await assertAllHandlerInfosMatchTestData();
+
+ await unloadHandlerStore();
+});
+
+/**
+ * Races the asyncInit method against the sync init (implicit in enumerate),
+ * to ensure that the store will be synchronously initialized without any
+ * ill effects.
+ */
+add_task(async function test_race_async_init() {
+ await deleteHandlerStore();
+ await copyTestDataToHandlerStore();
+ let storeInitialized = false;
+ // Pass a callback to synchronously observe the topic, as a promise would
+ // resolve asynchronously
+ TestUtils.topicObserved("handlersvc-store-initialized", () => {
+ storeInitialized = true;
+ return true;
+ });
+ gHandlerService.asyncInit();
+ Assert.ok(!storeInitialized);
+ gHandlerService.enumerate();
+ Assert.ok(storeInitialized);
+ await assertAllHandlerInfosMatchTestData();
+
+ await unloadHandlerStore();
+});
+
+/**
+ * Test saving and reloading an instance of nsIGIOMimeApp.
+ */
+add_task(async function test_store_gioHandlerApp() {
+ if (!("@mozilla.org/gio-service;1" in Cc)) {
+ info("Skipping test because it does not apply to this platform.");
+ return;
+ }
+
+ // Create dummy exec file that following won't fail because file not found error
+ let dummyHandlerFile = await IOUtils.getFile(
+ PathUtils.tempDir,
+ "dummyHandler"
+ );
+ dummyHandlerFile.createUnique(
+ Ci.nsIFile.NORMAL_FILE_TYPE,
+ parseInt("777", 8)
+ );
+
+ // Set up an nsIGIOMimeApp instance for testing.
+ let handlerApp = Cc["@mozilla.org/gio-service;1"]
+ .getService(Ci.nsIGIOService)
+ .createAppFromCommand(dummyHandlerFile.path, "Dummy GIO handler");
+ let expectedGIOMimeHandlerApp = {
+ name: handlerApp.name,
+ command: handlerApp.command,
+ };
+
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+ handlerInfo.preferredApplicationHandler = handlerApp;
+ handlerInfo.possibleApplicationHandlers.appendElement(handlerApp);
+ handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp);
+ gHandlerService.store(handlerInfo);
+
+ await unloadHandlerStore();
+
+ let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ preferredApplicationHandler: expectedGIOMimeHandlerApp,
+ possibleApplicationHandlers: [expectedGIOMimeHandlerApp, webHandlerApp],
+ });
+
+ await IOUtils.remove(dummyHandlerFile.path);
+
+ // After removing dummyHandlerFile, the handler should disappear from the
+ // list of possibleApplicationHandlers and preferredAppHandler should be null.
+ actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new");
+ HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, {
+ type: "example/new",
+ preferredAction: Ci.nsIHandlerInfo.saveToDisk,
+ alwaysAskBeforeHandling: false,
+ preferredApplicationHandler: null,
+ possibleApplicationHandlers: [webHandlerApp],
+ });
+});
diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js
new file mode 100644
index 0000000000..949c9914ec
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.js
@@ -0,0 +1,126 @@
+/* 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/. */
+
+// Encoded test URI to work on all platforms/independent of file encoding
+const kTestURI = "http://\u65e5\u672c\u8a93.jp/";
+const kExpectedURI = "http://xn--wgv71a309e.jp/";
+const kOutputFile = "result.txt";
+
+// Try several times in case the box we're running on is slow.
+const kMaxCheckExistAttempts = 30; // seconds
+var gCheckExistsAttempts = 0;
+
+const tempDir = do_get_tempdir();
+
+function checkFile() {
+ // This is where we expect the output
+ var tempFile = tempDir.clone();
+ tempFile.append(kOutputFile);
+
+ if (!tempFile.exists()) {
+ if (gCheckExistsAttempts >= kMaxCheckExistAttempts) {
+ do_throw(
+ "Expected File " +
+ tempFile.path +
+ " does not exist after " +
+ kMaxCheckExistAttempts +
+ " seconds"
+ );
+ } else {
+ ++gCheckExistsAttempts;
+ // Wait a bit longer then try again
+ do_timeout(1000, checkFile);
+ return;
+ }
+ }
+
+ // Now read it
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ fstream.init(tempFile, -1, 0, 0);
+ sstream.init(fstream);
+
+ // Read the first line only as that's the one we expect WriteArguments
+ // to be writing the argument to.
+ var data = sstream.read(4096);
+
+ sstream.close();
+ fstream.close();
+
+ // Now remove the old file
+ tempFile.remove(false);
+
+ // This currently fails on Mac with an argument like -psn_0_nnnnnn
+ // This seems to be to do with how the executable is called, but I couldn't
+ // find a way around it.
+ // Additionally the lack of OS detection in xpcshell tests sucks, so we'll
+ // have to check for the argument mac gives us.
+ if (data.substring(0, 7) != "-psn_0_") {
+ Assert.equal(data, kExpectedURI);
+ }
+
+ do_test_finished();
+}
+
+function run_test() {
+ if (mozinfo.os == "mac") {
+ dump("INFO | test_punycodeURIs.js | Skipping test on mac, bug 599475");
+ return;
+ }
+
+ // set up the uri to test with
+ var ioService = Services.io;
+
+ // set up the local handler object
+ var localHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ localHandler.name = "Test Local Handler App";
+
+ // WriteArgument will just dump its arguments to a file for us.
+ var processDir = do_get_cwd();
+ var exe = processDir.clone();
+ exe.append("WriteArgument");
+
+ if (!exe.exists()) {
+ // Maybe we are on windows
+ exe.leafName = "WriteArgument.exe";
+ if (!exe.exists()) {
+ do_throw("Could not locate the WriteArgument tests executable\n");
+ }
+ }
+
+ var outFile = tempDir.clone();
+ outFile.append(kOutputFile);
+
+ // Set an environment variable for WriteArgument to pick up
+ // The Write Argument file needs to know where its libraries are, so
+ // just force the path variable
+ // For mac
+ var greDir = Services.dirsvc.get("GreD", Ci.nsIFile);
+
+ Services.env.set("DYLD_LIBRARY_PATH", greDir.path);
+ // For Linux
+ Services.env.set("LD_LIBRARY_PATH", greDir.path);
+ // XXX: handle windows
+
+ // Now tell it where we want the file.
+ Services.env.set("WRITE_ARGUMENT_FILE", outFile.path);
+
+ var uri = ioService.newURI(kTestURI);
+
+ // Just check we've got these matching, if we haven't there's a problem
+ // with ascii spec or our test case.
+ Assert.equal(uri.asciiSpec, kExpectedURI);
+
+ localHandler.executable = exe;
+ localHandler.launchWithURI(uri);
+
+ do_test_pending();
+ do_timeout(1000, checkFile);
+}
diff --git a/uriloader/exthandler/tests/unit/xpcshell.toml b/uriloader/exthandler/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..216ad49555
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/xpcshell.toml
@@ -0,0 +1,43 @@
+[DEFAULT]
+head = "head.js"
+run-sequentially = "Bug 912235 - Intermittent failures"
+firefox-appdir = "browser"
+
+["test_badMIMEType.js"]
+run-if = ["buildapp == 'browser'"]
+
+["test_defaults_handlerService.js"]
+# No default stored handlers on android given lack of support.
+# No default stored handlers on Thunderbird.
+skip-if = [
+ "os == 'android'",
+ "appname == 'thunderbird'",
+]
+
+["test_filename_sanitize.js"]
+skip-if = ["os == 'mac'"] # Bug 1817727
+
+["test_getFromTypeAndExtension.js"]
+
+["test_getMIMEInfo_pdf.js"]
+
+["test_getMIMEInfo_unknown_mime_type.js"]
+run-if = ["os == 'win'"] # Windows only test
+
+["test_getTypeFromExtension_ext_to_type_mapping.js"]
+
+["test_getTypeFromExtension_with_empty_Content_Type.js"]
+run-if = ["os == 'win'"] # Windows only test
+
+["test_handlerService.js"]
+support-files = ["mailcap"]
+# Bug 676997: test consistently fails on Android
+fail-if = ["os == 'android'"]
+
+["test_handlerService_store.js"]
+# Disabled for 1563343 -- the app should determine possible handlers in GV.
+fail-if = ["os == 'android'"]
+support-files = ["handlers.json"]
+
+["test_punycodeURIs.js"]
+skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1809485
diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h
new file mode 100644
index 0000000000..06b31a723a
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 nslocalhandlerappuikit_h_
+#define nslocalhandlerappuikit_h_
+
+#include "nsLocalHandlerApp.h"
+
+class nsLocalHandlerAppUIKit final : public nsLocalHandlerApp {
+ public:
+ nsLocalHandlerAppUIKit() {}
+ ~nsLocalHandlerAppUIKit() {}
+
+ nsLocalHandlerAppUIKit(const char16_t* aName, nsIFile* aExecutable)
+ : nsLocalHandlerApp(aName, aExecutable) {}
+
+ nsLocalHandlerAppUIKit(const nsAString& aName, nsIFile* aExecutable)
+ : nsLocalHandlerApp(aName, aExecutable) {}
+
+ NS_IMETHOD LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override;
+};
+
+#endif /* nslocalhandlerappuikit_h_ */
diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm
new file mode 100644
index 0000000000..6f70e035bb
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 <CoreFoundation/CoreFoundation.h>
+
+#include "nsLocalHandlerAppUIKit.h"
+#include "nsIURI.h"
+
+NS_IMETHODIMP
+nsLocalHandlerAppUIKit::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h
new file mode 100644
index 0000000000..109eb16e3f
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 nsMIMEInfoUIKit_h_
+#define nsMIMEInfoUIKit_h_
+
+#include "nsMIMEInfoImpl.h"
+
+class nsMIMEInfoUIKit final : public nsMIMEInfoImpl {
+ public:
+ explicit nsMIMEInfoUIKit(const nsACString& aMIMEType)
+ : nsMIMEInfoImpl(aMIMEType) {}
+ nsMIMEInfoUIKit(const nsACString& aType, HandlerClass aClass)
+ : nsMIMEInfoImpl(aType, aClass) {}
+
+ NS_IMETHOD LaunchWithFile(nsIFile* aFile) override;
+
+ protected:
+ virtual nsresult LoadUriInternal(nsIURI* aURI) override;
+#ifdef DEBUG
+ virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override {
+ MOZ_ASSERT_UNREACHABLE("do not call this method, use LaunchWithFile");
+ return NS_ERROR_UNEXPECTED;
+ }
+#endif
+};
+
+#endif
diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm
new file mode 100644
index 0000000000..1226da220e
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 "nsMIMEInfoUIKit.h"
+
+NS_IMETHODIMP
+nsMIMEInfoUIKit::LaunchWithFile(nsIFile* aFile) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMIMEInfoUIKit::LoadUriInternal(nsIURI* aURI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.h b/uriloader/exthandler/uikit/nsOSHelperAppService.h
new file mode 100644
index 0000000000..426d8bb78a
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsOSHelperAppService.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 nsOSHelperAppService_h__
+#define nsOSHelperAppService_h__
+
+// The OS helper app service is a subclass of nsExternalHelperAppService and
+// is implemented on each platform. It contains platform specific code for
+// finding helper applications for a given mime type in addition to launching
+// those applications. This is the UIKit version.
+
+#include "nsExternalHelperAppService.h"
+#include "nsCExternalHandlerService.h"
+#include "nsCOMPtr.h"
+
+class nsOSHelperAppService final : public nsExternalHelperAppService {
+ public:
+ nsOSHelperAppService();
+ ~nsOSHelperAppService();
+
+ // override nsIExternalProtocolService methods
+ NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
+ nsAString& _retval) override;
+ NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval) override;
+
+ // method overrides --> used to hook the mime service into internet config....
+ NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType,
+ const nsACString& aFileExt,
+ nsIMIMEInfo** aMIMEInfo) override;
+ NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) override;
+ NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) override;
+
+ // GetFileTokenForPath must be implemented by each platform.
+ // platformAppPath --> a platform specific path to an application that we got
+ // out of the rdf data source. This can be a mac file
+ // spec, a unix path or a windows path depending on the
+ // platform
+ // aFile --> an nsIFile representation of that platform application path.
+ virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath,
+ nsIFile** aFile) override;
+
+ nsresult OSProtocolHandlerExists(const char* aScheme,
+ bool* aHandlerExists) override;
+};
+
+#endif // nsOSHelperAppService_h__
diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.mm b/uriloader/exthandler/uikit/nsOSHelperAppService.mm
new file mode 100644
index 0000000000..bfd83a583b
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:expandtab:shiftwidth=2:tabstop=2:cin:
+ * 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 "nsOSHelperAppService.h"
+
+nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() {}
+
+nsOSHelperAppService::~nsOSHelperAppService() {}
+
+nsresult nsOSHelperAppService::OSProtocolHandlerExists(
+ const char* aProtocolScheme, bool* aHandlerExists) {
+ *aHandlerExists = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme,
+ nsAString& _retval) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
+ const nsACString& aScheme, bool* _retval) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult nsOSHelperAppService::GetFileTokenForPath(
+ const char16_t* aPlatformAppPath, nsIFile** aFile) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetFromTypeAndExtension(const nsACString& aType,
+ const nsACString& aFileExt,
+ nsIMIMEInfo** aMIMEInfo) {
+ return nsExternalHelperAppService::GetFromTypeAndExtension(aType, aFileExt,
+ aMIMEInfo);
+}
+
+NS_IMETHODIMP nsOSHelperAppService::GetMIMEInfoFromOS(
+ const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) {
+ *aMIMEInfo = nullptr;
+ *aFound = false;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) {
+ *found = false;
+ return NS_OK;
+}
diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.cpp b/uriloader/exthandler/unix/nsGNOMERegistry.cpp
new file mode 100644
index 0000000000..dad4adbfd7
--- /dev/null
+++ b/uriloader/exthandler/unix/nsGNOMERegistry.cpp
@@ -0,0 +1,101 @@
+/* -*- 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 "nsGNOMERegistry.h"
+#include "nsString.h"
+#include "nsMIMEInfoUnix.h"
+#include "nsIGIOService.h"
+
+/* static */
+bool nsGNOMERegistry::HandlerExists(const char* aProtocolScheme) {
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (!giovfs) {
+ return false;
+ }
+
+ nsCOMPtr<nsIHandlerApp> app;
+ return NS_SUCCEEDED(giovfs->GetAppForURIScheme(
+ nsDependentCString(aProtocolScheme), getter_AddRefs(app)));
+}
+
+// XXX Check HandlerExists() before calling LoadURL.
+
+/* static */
+nsresult nsGNOMERegistry::LoadURL(nsIURI* aURL) {
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (!giovfs) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return giovfs->ShowURI(aURL);
+}
+
+/* static */
+void nsGNOMERegistry::GetAppDescForScheme(const nsACString& aScheme,
+ nsAString& aDesc) {
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (!giovfs) return;
+
+ nsCOMPtr<nsIHandlerApp> app;
+ if (NS_FAILED(giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(app))))
+ return;
+
+ app->GetName(aDesc);
+}
+
+/* static */
+already_AddRefed<nsMIMEInfoBase> nsGNOMERegistry::GetFromExtension(
+ const nsACString& aFileExt) {
+ nsAutoCString mimeType;
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (!giovfs) {
+ return nullptr;
+ }
+
+ // Get the MIME type from the extension, then call GetFromType to
+ // fill in the MIMEInfo.
+ if (NS_FAILED(giovfs->GetMimeTypeFromExtension(aFileExt, mimeType)) ||
+ mimeType.EqualsLiteral("application/octet-stream")) {
+ return nullptr;
+ }
+
+ RefPtr<nsMIMEInfoBase> mi = GetFromType(mimeType);
+ if (mi) {
+ mi->AppendExtension(aFileExt);
+ }
+
+ return mi.forget();
+}
+
+/* static */
+already_AddRefed<nsMIMEInfoBase> nsGNOMERegistry::GetFromType(
+ const nsACString& aMIMEType) {
+ RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(aMIMEType);
+ NS_ENSURE_TRUE(mimeInfo, nullptr);
+
+ nsAutoString name;
+ nsAutoCString description;
+
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (!giovfs) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIHandlerApp> handlerApp;
+ if (NS_FAILED(
+ giovfs->GetAppForMimeType(aMIMEType, getter_AddRefs(handlerApp))) ||
+ !handlerApp) {
+ return nullptr;
+ }
+ handlerApp->GetName(name);
+ giovfs->GetDescriptionForMimeType(aMIMEType, description);
+
+ mimeInfo->SetDefaultDescription(name);
+
+ mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ mimeInfo->SetDescription(NS_ConvertUTF8toUTF16(description));
+
+ return mimeInfo.forget();
+}
diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.h b/uriloader/exthandler/unix/nsGNOMERegistry.h
new file mode 100644
index 0000000000..ea626c5b50
--- /dev/null
+++ b/uriloader/exthandler/unix/nsGNOMERegistry.h
@@ -0,0 +1,28 @@
+/* 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 nsGNOMERegistry_h
+#define nsGNOMERegistry_h
+
+#include "nsIURI.h"
+#include "nsCOMPtr.h"
+
+class nsMIMEInfoBase;
+
+class nsGNOMERegistry {
+ public:
+ static bool HandlerExists(const char* aProtocolScheme);
+
+ static nsresult LoadURL(nsIURI* aURL);
+
+ static void GetAppDescForScheme(const nsACString& aScheme, nsAString& aDesc);
+
+ static already_AddRefed<nsMIMEInfoBase> GetFromExtension(
+ const nsACString& aFileExt);
+
+ static already_AddRefed<nsMIMEInfoBase> GetFromType(
+ const nsACString& aMIMEType);
+};
+
+#endif // nsGNOMERegistry_h
diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp
new file mode 100644
index 0000000000..330c441159
--- /dev/null
+++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 3; 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 "nsMIMEInfoUnix.h"
+#include "nsGNOMERegistry.h"
+#include "nsIGIOService.h"
+#include "nsNetCID.h"
+#include "nsIIOService.h"
+#ifdef MOZ_ENABLE_DBUS
+# include "nsDBusHandlerApp.h"
+#endif
+
+nsresult nsMIMEInfoUnix::LoadUriInternal(nsIURI* aURI) {
+ return nsGNOMERegistry::LoadURL(aURI);
+}
+
+NS_IMETHODIMP
+nsMIMEInfoUnix::GetHasDefaultHandler(bool* _retval) {
+ // if a default app is set, it means the application has been set from
+ // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to
+ // give the GNOME answer.
+ if (GetDefaultApplication()) {
+ return nsMIMEInfoImpl::GetHasDefaultHandler(_retval);
+ }
+
+ *_retval = false;
+
+ if (mClass == eProtocolInfo) {
+ *_retval = nsGNOMERegistry::HandlerExists(mSchemeOrType.get());
+ } else {
+ RefPtr<nsMIMEInfoBase> mimeInfo =
+ nsGNOMERegistry::GetFromType(mSchemeOrType);
+ if (!mimeInfo) {
+ nsAutoCString ext;
+ nsresult rv = GetPrimaryExtension(ext);
+ if (NS_SUCCEEDED(rv)) {
+ mimeInfo = nsGNOMERegistry::GetFromExtension(ext);
+ }
+ }
+ if (mimeInfo) *_retval = true;
+ }
+
+ if (*_retval) return NS_OK;
+
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoUnix::LaunchDefaultWithFile(nsIFile* aFile) {
+ // if a default app is set, it means the application has been set from
+ // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to
+ // give the GNOME answer.
+ if (GetDefaultApplication()) {
+ return nsMIMEInfoImpl::LaunchDefaultWithFile(aFile);
+ }
+
+ nsAutoCString nativePath;
+ aFile->GetNativePath(nativePath);
+
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (!giovfs) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // nsGIOMimeApp->Launch wants a URI string instead of local file
+ nsresult rv;
+ nsCOMPtr<nsIIOService> ioservice =
+ do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = ioservice->NewFileURI(aFile, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIHandlerApp> app;
+ if (NS_FAILED(
+ giovfs->GetAppForMimeType(mSchemeOrType, getter_AddRefs(app))) ||
+ !app) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ return app->LaunchWithURI(uri, nullptr);
+}
diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.h b/uriloader/exthandler/unix/nsMIMEInfoUnix.h
new file mode 100644
index 0000000000..2e32be4915
--- /dev/null
+++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 3; 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 nsMIMEInfoUnix_h_
+#define nsMIMEInfoUnix_h_
+
+#include "nsMIMEInfoImpl.h"
+
+class nsMIMEInfoUnix : public nsMIMEInfoImpl {
+ public:
+ explicit nsMIMEInfoUnix(const char* aMIMEType = "")
+ : nsMIMEInfoImpl(aMIMEType) {}
+ explicit nsMIMEInfoUnix(const nsACString& aMIMEType)
+ : nsMIMEInfoImpl(aMIMEType) {}
+ nsMIMEInfoUnix(const nsACString& aType, HandlerClass aClass)
+ : nsMIMEInfoImpl(aType, aClass) {}
+ static bool HandlerExists(const char* aProtocolScheme);
+
+ protected:
+ NS_IMETHOD GetHasDefaultHandler(bool* _retval) override;
+
+ virtual nsresult LoadUriInternal(nsIURI* aURI) override;
+
+ virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override;
+};
+
+#endif // nsMIMEInfoUnix_h_
diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.cpp b/uriloader/exthandler/unix/nsOSHelperAppService.cpp
new file mode 100644
index 0000000000..7f6eaa46f2
--- /dev/null
+++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp
@@ -0,0 +1,1498 @@
+/* -*- Mode: C++; tab-width: 3; 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 <sys/types.h>
+#include <sys/stat.h>
+
+#include "nsOSHelperAppService.h"
+#include "nsMIMEInfoUnix.h"
+#ifdef MOZ_WIDGET_GTK
+# include "nsGNOMERegistry.h"
+# ifdef MOZ_BUILD_APP_IS_BROWSER
+# include "nsIToolkitShellService.h"
+# include "nsIGNOMEShellService.h"
+# endif
+#endif
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIFileStreams.h"
+#include "nsILineInputStream.h"
+#include "nsIFile.h"
+#include "nsIProcess.h"
+#include "nsNetCID.h"
+#include "nsXPCOM.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCRT.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsXULAppAPI.h"
+#include "ContentHandlerService.h"
+#include "prenv.h" // for PR_GetEnv()
+#include "mozilla/EnumeratedRange.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "nsMimeTypes.h"
+#include <mutex>
+
+using namespace mozilla;
+
+#define LOG(...) \
+ MOZ_LOG(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug)
+
+static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter,
+ const nsAString::const_iterator& aEnd_iter);
+static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter,
+ nsAString::const_iterator& aMajorTypeStart,
+ nsAString::const_iterator& aMajorTypeEnd,
+ nsAString::const_iterator& aMinorTypeStart,
+ nsAString::const_iterator& aMinorTypeEnd,
+ const nsAString::const_iterator& aEnd_iter);
+
+inline bool IsNetscapeFormat(const nsACString& aBuffer);
+
+nsOSHelperAppService::~nsOSHelperAppService() {}
+
+/*
+ * Take a command with all the mailcap escapes in it and unescape it
+ * Ideally this needs the mime type, mime type options, and location of the
+ * temporary file, but this last can't be got from here
+ */
+// static
+nsresult nsOSHelperAppService::UnescapeCommand(const nsAString& aEscapedCommand,
+ const nsAString& aMajorType,
+ const nsAString& aMinorType,
+ nsACString& aUnEscapedCommand) {
+ LOG("-- UnescapeCommand");
+ LOG("Command to escape: '%s'\n",
+ NS_LossyConvertUTF16toASCII(aEscapedCommand).get());
+ // XXX This function will need to get the mime type and various stuff like
+ // that being passed in to work properly
+
+ LOG(
+ ("UnescapeCommand really needs some work -- it should actually do some "
+ "unescaping\n"));
+
+ CopyUTF16toUTF8(aEscapedCommand, aUnEscapedCommand);
+ LOG("Escaped command: '%s'\n", PromiseFlatCString(aUnEscapedCommand).get());
+ return NS_OK;
+}
+
+/* Put aSemicolon_iter at the first non-escaped semicolon after
+ * aStart_iter but before aEnd_iter
+ */
+
+static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter,
+ const nsAString::const_iterator& aEnd_iter) {
+ bool semicolonFound = false;
+ while (aSemicolon_iter != aEnd_iter && !semicolonFound) {
+ switch (*aSemicolon_iter) {
+ case '\\':
+ aSemicolon_iter.advance(2);
+ break;
+ case ';':
+ semicolonFound = true;
+ break;
+ default:
+ ++aSemicolon_iter;
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter,
+ nsAString::const_iterator& aMajorTypeStart,
+ nsAString::const_iterator& aMajorTypeEnd,
+ nsAString::const_iterator& aMinorTypeStart,
+ nsAString::const_iterator& aMinorTypeEnd,
+ const nsAString::const_iterator& aEnd_iter) {
+ nsAString::const_iterator iter(aStart_iter);
+
+ // skip leading whitespace
+ while (iter != aEnd_iter && nsCRT::IsAsciiSpace(*iter)) {
+ ++iter;
+ }
+
+ if (iter == aEnd_iter) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aMajorTypeStart = iter;
+
+ // find major/minor separator ('/')
+ while (iter != aEnd_iter && *iter != '/') {
+ ++iter;
+ }
+
+ if (iter == aEnd_iter) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aMajorTypeEnd = iter;
+
+ // skip '/'
+ ++iter;
+
+ if (iter == aEnd_iter) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aMinorTypeStart = iter;
+
+ // find end of minor type, delimited by whitespace or ';'
+ while (iter != aEnd_iter && !nsCRT::IsAsciiSpace(*iter) && *iter != ';') {
+ ++iter;
+ }
+
+ aMinorTypeEnd = iter;
+
+ return NS_OK;
+}
+
+// TODO: We should consider not only caching the file location but maybe the
+// file contents too?
+enum class FileKind {
+ PrivateMimeTypes = 0,
+ GlobalMimeTypes,
+ PrivateMailCap,
+ GlobalMailCap,
+
+ Count,
+};
+
+struct FileLocationCache {
+ struct Entry {
+ bool mIsCached = false;
+ nsresult mResult = NS_OK;
+ nsString mLocation;
+
+ void Clear() { *this = {}; }
+ };
+
+ EnumeratedArray<FileKind, FileKind::Count, Entry> mEntries;
+
+ static const char* PrefFor(FileKind aKind) {
+ switch (aKind) {
+ case FileKind::GlobalMimeTypes:
+ return "helpers.global_mime_types_file";
+ case FileKind::PrivateMimeTypes:
+ return "helpers.private_mime_types_file";
+ case FileKind::GlobalMailCap:
+ return "helpers.global_mailcap_file";
+ case FileKind::PrivateMailCap:
+ return "helpers.private_mailcap_file";
+ case FileKind::Count:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown file kind");
+ return "";
+ }
+
+ void Clear() {
+ for (auto& entry : mEntries) {
+ entry.Clear();
+ }
+ }
+
+ static void PrefChangeCallback(const char*, void*) { Get().Clear(); }
+
+ static FileLocationCache& Get() {
+ static FileLocationCache sCache;
+ static std::once_flag flag;
+ std::call_once(flag, [&] {
+ for (auto kind :
+ MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) {
+ Preferences::RegisterCallback(PrefChangeCallback,
+ nsDependentCString(PrefFor(kind)));
+ }
+ RunOnShutdown([] {
+ sCache.Clear();
+ for (auto kind :
+ MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) {
+ Preferences::UnregisterCallback(PrefChangeCallback,
+ nsDependentCString(PrefFor(kind)));
+ }
+ });
+ });
+ return sCache;
+ }
+
+ Entry& EntryFor(FileKind aKind) { return mEntries[aKind]; }
+};
+
+// The lookup order is:
+// 1) user pref
+// 2) env var (if any)
+// 3) pref
+static nsresult DoGetFileLocation(FileKind aKind, nsAString& aFileLocation) {
+ LOG("-- GetFileLocation(%d)\n", int(aKind));
+
+ aFileLocation.Truncate();
+
+ const char* envVar = [&]() -> const char* {
+ switch (aKind) {
+ case FileKind::PrivateMailCap:
+ return "PERSONAL_MAILCAP";
+ case FileKind::GlobalMailCap:
+ return "MAILCAP";
+ default:
+ return nullptr;
+ }
+ }();
+
+ const char* prefName = FileLocationCache::PrefFor(aKind);
+
+ if (envVar) {
+ // If we have an env var we should check whether the pref is a user pref. If
+ // we do not, we don't care.
+ if (Preferences::HasUserValue(prefName) &&
+ NS_SUCCEEDED(Preferences::GetString(prefName, aFileLocation))) {
+ return NS_OK;
+ }
+
+ char* envValue = PR_GetEnv(envVar);
+ if (envValue && *envValue) {
+ // the pref is in the system charset and it's a filepath... The
+ // natural way to do the charset conversion is by just initing
+ // an nsIFile with the native path and asking it for the Unicode
+ // version.
+ nsresult rv;
+ nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = file->InitWithNativePath(nsDependentCString(envValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return file->GetPath(aFileLocation);
+ }
+ }
+
+ return Preferences::GetString(prefName, aFileLocation);
+}
+
+static nsresult GetFileLocation(FileKind aKind, nsAString& aFileLocation) {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto& entry = FileLocationCache::Get().EntryFor(aKind);
+ if (!entry.mIsCached) {
+ entry.mIsCached = true;
+ entry.mResult = DoGetFileLocation(aKind, entry.mLocation);
+ }
+ aFileLocation = entry.mLocation;
+ return entry.mResult;
+}
+
+/* Get the mime.types file names from prefs and look up info in them
+ based on extension */
+// static
+nsresult nsOSHelperAppService::LookUpTypeAndDescription(
+ const nsAString& aFileExtension, nsAString& aMajorType,
+ nsAString& aMinorType, nsAString& aDescription, bool aUserData) {
+ LOG("-- LookUpTypeAndDescription for extension '%s'\n",
+ NS_LossyConvertUTF16toASCII(aFileExtension).get());
+ nsAutoString mimeFileName;
+
+ auto kind =
+ aUserData ? FileKind::PrivateMimeTypes : FileKind::GlobalMimeTypes;
+ nsresult rv = GetFileLocation(kind, mimeFileName);
+ if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) {
+ rv = GetTypeAndDescriptionFromMimetypesFile(
+ mimeFileName, aFileExtension, aMajorType, aMinorType, aDescription);
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return rv;
+}
+
+inline bool IsNetscapeFormat(const nsACString& aBuffer) {
+ return StringBeginsWith(
+ aBuffer,
+ nsLiteralCString(
+ "#--Netscape Communications Corporation MIME Information")) ||
+ StringBeginsWith(aBuffer, "#--MCOM MIME Information"_ns);
+}
+
+/*
+ * Create a file stream and line input stream for the filename.
+ * Leaves the first line of the file in aBuffer and sets the format to
+ * true for netscape files and false for normail ones
+ */
+// static
+nsresult nsOSHelperAppService::CreateInputStream(
+ const nsAString& aFilename, nsIFileInputStream** aFileInputStream,
+ nsILineInputStream** aLineInputStream, nsACString& aBuffer,
+ bool* aNetscapeFormat, bool* aMore) {
+ LOG("-- CreateInputStream");
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+ rv = file->InitWithPath(aFilename);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIFileInputStream> fileStream(
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+ rv = fileStream->Init(file, -1, -1, false);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsILineInputStream> lineStream(do_QueryInterface(fileStream, &rv));
+
+ if (NS_FAILED(rv)) {
+ LOG("Interface trouble in stream land!");
+ return rv;
+ }
+
+ rv = lineStream->ReadLine(aBuffer, aMore);
+ if (NS_FAILED(rv)) {
+ fileStream->Close();
+ return rv;
+ }
+
+ *aNetscapeFormat = IsNetscapeFormat(aBuffer);
+
+ *aFileInputStream = fileStream;
+ NS_ADDREF(*aFileInputStream);
+ *aLineInputStream = lineStream;
+ NS_ADDREF(*aLineInputStream);
+
+ return NS_OK;
+}
+
+/* Open the file, read the first line, decide what type of file it is,
+ then get info based on extension */
+// static
+nsresult nsOSHelperAppService::GetTypeAndDescriptionFromMimetypesFile(
+ const nsAString& aFilename, const nsAString& aFileExtension,
+ nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription) {
+ LOG("-- GetTypeAndDescriptionFromMimetypesFile\n");
+ LOG("Getting type and description from types file '%s'\n",
+ NS_LossyConvertUTF16toASCII(aFilename).get());
+ LOG("Using extension '%s'\n",
+ NS_LossyConvertUTF16toASCII(aFileExtension).get());
+ nsCOMPtr<nsIFileInputStream> mimeFile;
+ nsCOMPtr<nsILineInputStream> mimeTypes;
+ bool netscapeFormat;
+ nsAutoString buf;
+ nsAutoCString cBuf;
+ bool more = false;
+ nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile),
+ getter_AddRefs(mimeTypes), cBuf,
+ &netscapeFormat, &more);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsAutoString extensions;
+ nsAutoStringN<101> entry;
+ nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart,
+ minorTypeEnd, descriptionStart, descriptionEnd;
+
+ do {
+ CopyASCIItoUTF16(cBuf, buf);
+ // read through, building up an entry. If we finish an entry, check for
+ // a match and return out of the loop if we match
+
+ // skip comments and empty lines
+ if (!buf.IsEmpty() && buf.First() != '#') {
+ entry.Append(buf);
+ if (entry.Last() == '\\') {
+ entry.Truncate(entry.Length() - 1);
+ entry.Append(char16_t(
+ ' ')); // in case there is no trailing whitespace on this line
+ } else { // we have a full entry
+ LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get());
+ if (netscapeFormat) {
+ rv = ParseNetscapeMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
+ extensions, descriptionStart, descriptionEnd);
+ if (NS_FAILED(rv)) {
+ // We sometimes get things like RealPlayer appending
+ // "normal" entries to "Netscape" .mime.types files. Try
+ // to handle that. Bug 106381.
+ LOG("Bogus entry; trying 'normal' mode\n");
+ rv = ParseNormalMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart,
+ minorTypeEnd, extensions, descriptionStart, descriptionEnd);
+ }
+ } else {
+ rv = ParseNormalMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
+ extensions, descriptionStart, descriptionEnd);
+ if (NS_FAILED(rv)) {
+ // We sometimes get things like StarOffice prepending
+ // "normal" entries to "Netscape" .mime.types files. Try
+ // to handle that. Bug 136670.
+ LOG("Bogus entry; trying 'Netscape' mode\n");
+ rv = ParseNetscapeMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart,
+ minorTypeEnd, extensions, descriptionStart, descriptionEnd);
+ }
+ }
+
+ if (NS_SUCCEEDED(rv)) { // entry parses
+ nsAString::const_iterator start, end;
+ extensions.BeginReading(start);
+ extensions.EndReading(end);
+ nsAString::const_iterator iter(start);
+
+ while (start != end) {
+ FindCharInReadable(',', iter, end);
+ if (Substring(start, iter)
+ .Equals(aFileExtension,
+ nsCaseInsensitiveStringComparator)) {
+ // it's a match. Assign the type and description and run
+ aMajorType.Assign(Substring(majorTypeStart, majorTypeEnd));
+ aMinorType.Assign(Substring(minorTypeStart, minorTypeEnd));
+ aDescription.Assign(Substring(descriptionStart, descriptionEnd));
+ mimeFile->Close();
+ return NS_OK;
+ }
+ if (iter != end) {
+ ++iter;
+ }
+ start = iter;
+ }
+ } else {
+ LOG("Failed to parse entry: %s\n",
+ NS_LossyConvertUTF16toASCII(entry).get());
+ }
+ // truncate the entry for the next iteration
+ entry.Truncate();
+ }
+ }
+ if (!more) {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ break;
+ }
+ // read the next line
+ rv = mimeTypes->ReadLine(cBuf, &more);
+ } while (NS_SUCCEEDED(rv));
+
+ mimeFile->Close();
+ return rv;
+}
+
+/* Get the mime.types file names from prefs and look up info in them
+ based on mimetype */
+// static
+nsresult nsOSHelperAppService::LookUpExtensionsAndDescription(
+ const nsAString& aMajorType, const nsAString& aMinorType,
+ nsAString& aFileExtensions, nsAString& aDescription) {
+ LOG("-- LookUpExtensionsAndDescription for type '%s/%s'\n",
+ NS_LossyConvertUTF16toASCII(aMajorType).get(),
+ NS_LossyConvertUTF16toASCII(aMinorType).get());
+ nsAutoString mimeFileName;
+
+ nsresult rv = GetFileLocation(FileKind::PrivateMimeTypes, mimeFileName);
+ if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) {
+ rv = GetExtensionsAndDescriptionFromMimetypesFile(
+ mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription);
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+ if (NS_FAILED(rv) || aFileExtensions.IsEmpty()) {
+ rv = GetFileLocation(FileKind::GlobalMimeTypes, mimeFileName);
+ if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) {
+ rv = GetExtensionsAndDescriptionFromMimetypesFile(
+ mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription);
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+ return rv;
+}
+
+/* Open the file, read the first line, decide what type of file it is,
+ then get info based on extension */
+// static
+nsresult nsOSHelperAppService::GetExtensionsAndDescriptionFromMimetypesFile(
+ const nsAString& aFilename, const nsAString& aMajorType,
+ const nsAString& aMinorType, nsAString& aFileExtensions,
+ nsAString& aDescription) {
+ LOG("-- GetExtensionsAndDescriptionFromMimetypesFile\n");
+ LOG("Getting extensions and description from types file '%s'\n",
+ NS_LossyConvertUTF16toASCII(aFilename).get());
+ LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(),
+ NS_LossyConvertUTF16toASCII(aMinorType).get());
+ nsCOMPtr<nsIFileInputStream> mimeFile;
+ nsCOMPtr<nsILineInputStream> mimeTypes;
+ bool netscapeFormat;
+ nsAutoCString cBuf;
+ nsAutoString buf;
+ bool more = false;
+ nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile),
+ getter_AddRefs(mimeTypes), cBuf,
+ &netscapeFormat, &more);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsAutoString extensions;
+ nsAutoStringN<101> entry;
+ nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart,
+ minorTypeEnd, descriptionStart, descriptionEnd;
+
+ do {
+ CopyASCIItoUTF16(cBuf, buf);
+ // read through, building up an entry. If we finish an entry, check for
+ // a match and return out of the loop if we match
+
+ // skip comments and empty lines
+ if (!buf.IsEmpty() && buf.First() != '#') {
+ entry.Append(buf);
+ if (entry.Last() == '\\') {
+ entry.Truncate(entry.Length() - 1);
+ entry.Append(char16_t(
+ ' ')); // in case there is no trailing whitespace on this line
+ } else { // we have a full entry
+ LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get());
+ if (netscapeFormat) {
+ rv = ParseNetscapeMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
+ extensions, descriptionStart, descriptionEnd);
+
+ if (NS_FAILED(rv)) {
+ // We sometimes get things like RealPlayer appending
+ // "normal" entries to "Netscape" .mime.types files. Try
+ // to handle that. Bug 106381.
+ LOG("Bogus entry; trying 'normal' mode\n");
+ rv = ParseNormalMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart,
+ minorTypeEnd, extensions, descriptionStart, descriptionEnd);
+ }
+ } else {
+ rv = ParseNormalMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
+ extensions, descriptionStart, descriptionEnd);
+
+ if (NS_FAILED(rv)) {
+ // We sometimes get things like StarOffice prepending
+ // "normal" entries to "Netscape" .mime.types files. Try
+ // to handle that. Bug 136670.
+ LOG("Bogus entry; trying 'Netscape' mode\n");
+ rv = ParseNetscapeMIMETypesEntry(
+ entry, majorTypeStart, majorTypeEnd, minorTypeStart,
+ minorTypeEnd, extensions, descriptionStart, descriptionEnd);
+ }
+ }
+
+ if (NS_SUCCEEDED(rv) &&
+ Substring(majorTypeStart, majorTypeEnd)
+ .Equals(aMajorType, nsCaseInsensitiveStringComparator) &&
+ Substring(minorTypeStart, minorTypeEnd)
+ .Equals(aMinorType, nsCaseInsensitiveStringComparator)) {
+ // it's a match
+ aFileExtensions.Assign(extensions);
+ aDescription.Assign(Substring(descriptionStart, descriptionEnd));
+ mimeFile->Close();
+ return NS_OK;
+ }
+ if (NS_FAILED(rv)) {
+ LOG("Failed to parse entry: %s\n",
+ NS_LossyConvertUTF16toASCII(entry).get());
+ }
+
+ entry.Truncate();
+ }
+ }
+ if (!more) {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ break;
+ }
+ // read the next line
+ rv = mimeTypes->ReadLine(cBuf, &more);
+ } while (NS_SUCCEEDED(rv));
+
+ mimeFile->Close();
+ return rv;
+}
+
+/*
+ * This parses a Netscape format mime.types entry. There are two
+ * possible formats:
+ *
+ * type=foo/bar; options exts="baz" description="Some type"
+ *
+ * and
+ *
+ * type=foo/bar; options description="Some type" exts="baz"
+ */
+// static
+nsresult nsOSHelperAppService::ParseNetscapeMIMETypesEntry(
+ const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart,
+ nsAString::const_iterator& aMajorTypeEnd,
+ nsAString::const_iterator& aMinorTypeStart,
+ nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions,
+ nsAString::const_iterator& aDescriptionStart,
+ nsAString::const_iterator& aDescriptionEnd) {
+ LOG("-- ParseNetscapeMIMETypesEntry\n");
+ NS_ASSERTION(!aEntry.IsEmpty(),
+ "Empty Netscape MIME types entry being parsed.");
+
+ nsAString::const_iterator start_iter, end_iter, match_start, match_end;
+
+ aEntry.BeginReading(start_iter);
+ aEntry.EndReading(end_iter);
+
+ // skip trailing whitespace
+ do {
+ --end_iter;
+ } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter));
+ // if we're pointing to a quote, don't advance -- we don't want to
+ // include the quote....
+ if (*end_iter != '"') ++end_iter;
+ match_start = start_iter;
+ match_end = end_iter;
+
+ // Get the major and minor types
+ // First the major type
+ if (!FindInReadable(u"type="_ns, match_start, match_end)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ match_start = match_end;
+
+ while (match_end != end_iter && *match_end != '/') {
+ ++match_end;
+ }
+ if (match_end == end_iter) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aMajorTypeStart = match_start;
+ aMajorTypeEnd = match_end;
+
+ // now the minor type
+ if (++match_end == end_iter) {
+ return NS_ERROR_FAILURE;
+ }
+
+ match_start = match_end;
+
+ while (match_end != end_iter && !nsCRT::IsAsciiSpace(*match_end) &&
+ *match_end != ';') {
+ ++match_end;
+ }
+ if (match_end == end_iter) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aMinorTypeStart = match_start;
+ aMinorTypeEnd = match_end;
+
+ // ignore everything up to the end of the mime type from here on
+ start_iter = match_end;
+
+ // get the extensions
+ match_start = match_end;
+ match_end = end_iter;
+ if (FindInReadable(u"exts="_ns, match_start, match_end)) {
+ nsAString::const_iterator extStart, extEnd;
+
+ if (match_end == end_iter ||
+ (*match_end == '"' && ++match_end == end_iter)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ extStart = match_end;
+ match_start = extStart;
+ match_end = end_iter;
+ if (FindInReadable(u"desc=\""_ns, match_start, match_end)) {
+ // exts= before desc=, so we have to find the actual end of the extensions
+ extEnd = match_start;
+ if (extEnd == extStart) {
+ return NS_ERROR_FAILURE;
+ }
+
+ do {
+ --extEnd;
+ } while (extEnd != extStart && nsCRT::IsAsciiSpace(*extEnd));
+
+ if (extEnd != extStart && *extEnd == '"') {
+ --extEnd;
+ }
+ } else {
+ // desc= before exts=, so we can use end_iter as the end of the extensions
+ extEnd = end_iter;
+ }
+ aExtensions = Substring(extStart, extEnd);
+ } else {
+ // no extensions
+ aExtensions.Truncate();
+ }
+
+ // get the description
+ match_start = start_iter;
+ match_end = end_iter;
+ if (FindInReadable(u"desc=\""_ns, match_start, match_end)) {
+ aDescriptionStart = match_end;
+ match_start = aDescriptionStart;
+ match_end = end_iter;
+ if (FindInReadable(u"exts="_ns, match_start, match_end)) {
+ // exts= after desc=, so have to find actual end of description
+ aDescriptionEnd = match_start;
+ if (aDescriptionEnd == aDescriptionStart) {
+ return NS_ERROR_FAILURE;
+ }
+
+ do {
+ --aDescriptionEnd;
+ } while (aDescriptionEnd != aDescriptionStart &&
+ nsCRT::IsAsciiSpace(*aDescriptionEnd));
+ } else {
+ // desc= after exts=, so use end_iter for the description end
+ aDescriptionEnd = end_iter;
+ }
+ } else {
+ // no description
+ aDescriptionStart = start_iter;
+ aDescriptionEnd = start_iter;
+ }
+
+ return NS_OK;
+}
+
+/*
+ * This parses a normal format mime.types entry. The format is:
+ *
+ * major/minor ext1 ext2 ext3
+ */
+// static
+nsresult nsOSHelperAppService::ParseNormalMIMETypesEntry(
+ const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart,
+ nsAString::const_iterator& aMajorTypeEnd,
+ nsAString::const_iterator& aMinorTypeStart,
+ nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions,
+ nsAString::const_iterator& aDescriptionStart,
+ nsAString::const_iterator& aDescriptionEnd) {
+ LOG("-- ParseNormalMIMETypesEntry\n");
+ NS_ASSERTION(!aEntry.IsEmpty(),
+ "Empty Normal MIME types entry being parsed.");
+
+ nsAString::const_iterator start_iter, end_iter, iter;
+
+ aEntry.BeginReading(start_iter);
+ aEntry.EndReading(end_iter);
+
+ // no description
+ aDescriptionStart = start_iter;
+ aDescriptionEnd = start_iter;
+
+ // skip leading whitespace
+ while (start_iter != end_iter && nsCRT::IsAsciiSpace(*start_iter)) {
+ ++start_iter;
+ }
+ if (start_iter == end_iter) {
+ return NS_ERROR_FAILURE;
+ }
+ // skip trailing whitespace
+ do {
+ --end_iter;
+ } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter));
+
+ ++end_iter; // point to first whitespace char (or to end of string)
+ iter = start_iter;
+
+ // get the major type
+ if (!FindCharInReadable('/', iter, end_iter)) return NS_ERROR_FAILURE;
+
+ nsAString::const_iterator equals_sign_iter(start_iter);
+ if (FindCharInReadable('=', equals_sign_iter, iter))
+ return NS_ERROR_FAILURE; // see bug 136670
+
+ aMajorTypeStart = start_iter;
+ aMajorTypeEnd = iter;
+
+ // get the minor type
+ if (++iter == end_iter) {
+ return NS_ERROR_FAILURE;
+ }
+ start_iter = iter;
+
+ while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) {
+ ++iter;
+ }
+ aMinorTypeStart = start_iter;
+ aMinorTypeEnd = iter;
+
+ // get the extensions
+ aExtensions.Truncate();
+ while (iter != end_iter) {
+ while (iter != end_iter && nsCRT::IsAsciiSpace(*iter)) {
+ ++iter;
+ }
+
+ start_iter = iter;
+ while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) {
+ ++iter;
+ }
+ aExtensions.Append(Substring(start_iter, iter));
+ if (iter != end_iter) { // not the last extension
+ aExtensions.Append(char16_t(','));
+ }
+ }
+
+ return NS_OK;
+}
+
+// static
+nsresult nsOSHelperAppService::LookUpHandlerAndDescription(
+ const nsAString& aMajorType, const nsAString& aMinorType,
+ nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags) {
+ // The mailcap lookup is two-pass to handle the case of mailcap files
+ // that have something like:
+ //
+ // text/*; emacs %s
+ // text/rtf; soffice %s
+ //
+ // in that order. We want to pick up "soffice" for text/rtf in such cases
+ nsresult rv = DoLookUpHandlerAndDescription(
+ aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags, true);
+ if (NS_FAILED(rv)) {
+ rv = DoLookUpHandlerAndDescription(aMajorType, aMinorType, aHandler,
+ aDescription, aMozillaFlags, false);
+ }
+
+ // maybe we have an entry for "aMajorType/*"?
+ if (NS_FAILED(rv)) {
+ rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler,
+ aDescription, aMozillaFlags, true);
+ }
+
+ if (NS_FAILED(rv)) {
+ rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler,
+ aDescription, aMozillaFlags, false);
+ }
+
+ return rv;
+}
+
+// static
+nsresult nsOSHelperAppService::DoLookUpHandlerAndDescription(
+ const nsAString& aMajorType, const nsAString& aMinorType,
+ nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags,
+ bool aUserData) {
+ LOG("-- LookUpHandlerAndDescription for type '%s/%s'\n",
+ NS_LossyConvertUTF16toASCII(aMajorType).get(),
+ NS_LossyConvertUTF16toASCII(aMinorType).get());
+ nsAutoString mailcapFileName;
+
+ const auto kind =
+ aUserData ? FileKind::PrivateMailCap : FileKind::GlobalMailCap;
+ nsresult rv = GetFileLocation(kind, mailcapFileName);
+ if (NS_FAILED(rv) || mailcapFileName.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, aMajorType,
+ aMinorType, aHandler,
+ aDescription, aMozillaFlags);
+}
+
+// static
+nsresult nsOSHelperAppService::GetHandlerAndDescriptionFromMailcapFile(
+ const nsAString& aFilename, const nsAString& aMajorType,
+ const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription,
+ nsAString& aMozillaFlags) {
+ LOG("-- GetHandlerAndDescriptionFromMailcapFile\n");
+ LOG("Getting handler and description from mailcap file '%s'\n",
+ NS_LossyConvertUTF16toASCII(aFilename).get());
+ LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(),
+ NS_LossyConvertUTF16toASCII(aMinorType).get());
+
+ nsresult rv = NS_OK;
+ bool more = false;
+
+ nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+ rv = file->InitWithPath(aFilename);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIFileInputStream> mailcapFile(
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+ rv = mailcapFile->Init(file, -1, -1, false);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsILineInputStream> mailcap(do_QueryInterface(mailcapFile, &rv));
+
+ if (NS_FAILED(rv)) {
+ LOG("Interface trouble in stream land!");
+ return rv;
+ }
+
+ nsAutoStringN<129> entry;
+ nsAutoStringN<81> buffer;
+ nsAutoCStringN<81> cBuffer;
+ rv = mailcap->ReadLine(cBuffer, &more);
+ if (NS_FAILED(rv)) {
+ mailcapFile->Close();
+ return rv;
+ }
+
+ do { // return on end-of-file in the loop
+
+ CopyASCIItoUTF16(cBuffer, buffer);
+ if (!buffer.IsEmpty() && buffer.First() != '#') {
+ entry.Append(buffer);
+ if (entry.Last() == '\\') { // entry continues on next line
+ entry.Truncate(entry.Length() - 1);
+ entry.Append(char16_t(
+ ' ')); // in case there is no trailing whitespace on this line
+ } else { // we have a full entry in entry. Check it for the type
+ LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get());
+
+ nsAString::const_iterator semicolon_iter, start_iter, end_iter,
+ majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd;
+ entry.BeginReading(start_iter);
+ entry.EndReading(end_iter);
+ semicolon_iter = start_iter;
+ FindSemicolon(semicolon_iter, end_iter);
+ if (semicolon_iter !=
+ end_iter) { // we have something resembling a valid entry
+ rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd,
+ minorTypeStart, minorTypeEnd, semicolon_iter);
+ if (NS_SUCCEEDED(rv) &&
+ Substring(majorTypeStart, majorTypeEnd)
+ .Equals(aMajorType, nsCaseInsensitiveStringComparator) &&
+ Substring(minorTypeStart, minorTypeEnd)
+ .Equals(aMinorType, nsCaseInsensitiveStringComparator)) {
+ // we have a match
+ bool match = true;
+ ++semicolon_iter; // point at the first char past the semicolon
+ start_iter = semicolon_iter; // handler string starts here
+ FindSemicolon(semicolon_iter, end_iter);
+ while (start_iter != semicolon_iter &&
+ nsCRT::IsAsciiSpace(*start_iter)) {
+ ++start_iter;
+ }
+
+ LOG("The real handler is: '%s'\n",
+ NS_LossyConvertUTF16toASCII(
+ Substring(start_iter, semicolon_iter))
+ .get());
+
+ // XXX ugly hack. Just grab the executable name
+ nsAString::const_iterator end_handler_iter = semicolon_iter;
+ nsAString::const_iterator end_executable_iter = start_iter;
+ while (end_executable_iter != end_handler_iter &&
+ !nsCRT::IsAsciiSpace(*end_executable_iter)) {
+ ++end_executable_iter;
+ }
+ // XXX End ugly hack
+
+ aHandler = Substring(start_iter, end_executable_iter);
+
+ nsAString::const_iterator start_option_iter, end_optionname_iter,
+ equal_sign_iter;
+ bool equalSignFound;
+ while (match && semicolon_iter != end_iter &&
+ ++semicolon_iter !=
+ end_iter) { // there are options left and we still match
+ start_option_iter = semicolon_iter;
+ // skip over leading whitespace
+ while (start_option_iter != end_iter &&
+ nsCRT::IsAsciiSpace(*start_option_iter)) {
+ ++start_option_iter;
+ }
+ if (start_option_iter == end_iter) { // nothing actually here
+ break;
+ }
+ semicolon_iter = start_option_iter;
+ FindSemicolon(semicolon_iter, end_iter);
+ equal_sign_iter = start_option_iter;
+ equalSignFound = false;
+ while (equal_sign_iter != semicolon_iter && !equalSignFound) {
+ switch (*equal_sign_iter) {
+ case '\\':
+ equal_sign_iter.advance(2);
+ break;
+ case '=':
+ equalSignFound = true;
+ break;
+ default:
+ ++equal_sign_iter;
+ break;
+ }
+ }
+ end_optionname_iter = start_option_iter;
+ // find end of option name
+ while (end_optionname_iter != equal_sign_iter &&
+ !nsCRT::IsAsciiSpace(*end_optionname_iter)) {
+ ++end_optionname_iter;
+ }
+ nsDependentSubstring optionName(start_option_iter,
+ end_optionname_iter);
+ if (equalSignFound) {
+ // This is an option that has a name and value
+ if (optionName.EqualsLiteral("description")) {
+ aDescription = Substring(++equal_sign_iter, semicolon_iter);
+ } else if (optionName.EqualsLiteral("x-mozilla-flags")) {
+ aMozillaFlags = Substring(++equal_sign_iter, semicolon_iter);
+ } else if (optionName.EqualsLiteral("test")) {
+ nsAutoCString testCommand;
+ rv = UnescapeCommand(
+ Substring(++equal_sign_iter, semicolon_iter), aMajorType,
+ aMinorType, testCommand);
+ if (NS_FAILED(rv)) continue;
+ nsCOMPtr<nsIProcess> process =
+ do_CreateInstance(NS_PROCESS_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) continue;
+ nsCOMPtr<nsIFile> file(
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) continue;
+ rv = file->InitWithNativePath("/bin/sh"_ns);
+ if (NS_FAILED(rv)) continue;
+ rv = process->Init(file);
+ if (NS_FAILED(rv)) continue;
+ const char* args[] = {"-c", testCommand.get()};
+ LOG("Running Test: %s\n", testCommand.get());
+ rv = process->Run(true, args, 2);
+ if (NS_FAILED(rv)) continue;
+ int32_t exitValue;
+ rv = process->GetExitValue(&exitValue);
+ if (NS_FAILED(rv)) continue;
+ LOG("Exit code: %d\n", exitValue);
+ if (exitValue) {
+ match = false;
+ }
+ }
+ } else {
+ // This is an option that just has a name but no value (eg
+ // "copiousoutput")
+ if (optionName.EqualsLiteral("needsterminal")) {
+ match = false;
+ }
+ }
+ }
+
+ if (match) { // we did not fail any test clauses; all is good
+ // get out of here
+ mailcapFile->Close();
+ return NS_OK;
+ }
+ // pretend that this match never happened
+ aDescription.Truncate();
+ aMozillaFlags.Truncate();
+ aHandler.Truncate();
+ }
+ }
+ // zero out the entry for the next cycle
+ entry.Truncate();
+ }
+ }
+ if (!more) {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ break;
+ }
+ rv = mailcap->ReadLine(cBuffer, &more);
+ } while (NS_SUCCEEDED(rv));
+ mailcapFile->Close();
+ return rv;
+}
+
+nsresult nsOSHelperAppService::OSProtocolHandlerExists(
+ const char* aProtocolScheme, bool* aHandlerExists) {
+ nsresult rv = NS_OK;
+
+ if (!XRE_IsContentProcess()) {
+#ifdef MOZ_WIDGET_GTK
+ // Check the GNOME registry for a protocol handler
+ *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme);
+#else
+ *aHandlerExists = false;
+#endif
+ } else {
+ *aHandlerExists = false;
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv) && handlerSvc) {
+ rv = handlerSvc->ExistsForProtocolOS(nsCString(aProtocolScheme),
+ aHandlerExists);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(
+ const nsACString& aScheme, nsAString& _retval) {
+#ifdef MOZ_WIDGET_GTK
+ nsGNOMERegistry::GetAppDescForScheme(aScheme, _retval);
+ return _retval.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK;
+#else
+ return NS_ERROR_NOT_AVAILABLE;
+#endif
+}
+
+NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
+ const nsACString& aScheme, bool* _retval) {
+ *_retval = false;
+#if defined(MOZ_BUILD_APP_IS_BROWSER) && defined(MOZ_WIDGET_GTK)
+ if (nsCOMPtr<nsIGNOMEShellService> shell =
+ do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID)) {
+ return shell->IsDefaultForScheme(aScheme, _retval);
+ }
+#endif
+ return NS_OK;
+}
+
+nsresult nsOSHelperAppService::GetFileTokenForPath(
+ const char16_t* platformAppPath, nsIFile** aFile) {
+ LOG("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n",
+ NS_LossyConvertUTF16toASCII(platformAppPath).get());
+ if (!*platformAppPath) { // empty filename--return error
+ NS_WARNING("Empty filename passed in.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // first check if the base class implementation finds anything
+ nsresult rv =
+ nsExternalHelperAppService::GetFileTokenForPath(platformAppPath, aFile);
+ if (NS_SUCCEEDED(rv)) return rv;
+ // If the reason for failure was that the file doesn't exist, return too
+ // (because it means the path was absolute, and so that we shouldn't search in
+ // the path)
+ if (rv == NS_ERROR_FILE_NOT_FOUND) return rv;
+
+ // If we get here, we really should have a relative path.
+ NS_ASSERTION(*platformAppPath != char16_t('/'), "Unexpected absolute path");
+
+ nsCOMPtr<nsIFile> localFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID));
+
+ if (!localFile) return NS_ERROR_NOT_INITIALIZED;
+
+ bool exists = false;
+ // ugly hack. Walk the PATH variable...
+ char* unixpath = PR_GetEnv("PATH");
+ nsAutoCString path(unixpath);
+
+ const char* start_iter = path.BeginReading(start_iter);
+ const char* colon_iter = start_iter;
+ const char* end_iter = path.EndReading(end_iter);
+
+ while (start_iter != end_iter && !exists) {
+ while (colon_iter != end_iter && *colon_iter != ':') {
+ ++colon_iter;
+ }
+ localFile->InitWithNativePath(Substring(start_iter, colon_iter));
+ rv = localFile->AppendRelativePath(nsDependentString(platformAppPath));
+ // Failing AppendRelativePath is a bad thing - it should basically always
+ // succeed given a relative path. Show a warning if it does fail.
+ // To prevent infinite loops when it does fail, return at this point.
+ NS_ENSURE_SUCCESS(rv, rv);
+ localFile->Exists(&exists);
+ if (!exists) {
+ if (colon_iter == end_iter) {
+ break;
+ }
+ ++colon_iter;
+ start_iter = colon_iter;
+ }
+ }
+
+ if (exists) {
+ rv = NS_OK;
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+
+ *aFile = localFile;
+ NS_IF_ADDREF(*aFile);
+
+ return rv;
+}
+
+already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromExtension(
+ const nsCString& aFileExt) {
+ // if the extension is empty, return immediately
+ if (aFileExt.IsEmpty()) {
+ return nullptr;
+ }
+
+ LOG("Here we do an extension lookup for '%s'\n", aFileExt.get());
+
+ nsAutoString majorType, minorType, mime_types_description,
+ mailcap_description, handler, mozillaFlags;
+
+ nsresult rv =
+ LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType,
+ minorType, mime_types_description, true);
+
+ if (NS_FAILED(rv) || majorType.IsEmpty()) {
+#ifdef MOZ_WIDGET_GTK
+ LOG("Looking in GNOME registry\n");
+ RefPtr<nsMIMEInfoBase> gnomeInfo =
+ nsGNOMERegistry::GetFromExtension(aFileExt);
+ if (gnomeInfo) {
+ LOG("Got MIMEInfo from GNOME registry\n");
+ return gnomeInfo.forget();
+ }
+#endif
+
+ rv = LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType,
+ minorType, mime_types_description, false);
+ }
+
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ NS_LossyConvertUTF16toASCII asciiMajorType(majorType);
+ NS_LossyConvertUTF16toASCII asciiMinorType(minorType);
+
+ LOG("Type/Description results: majorType='%s', minorType='%s', "
+ "description='%s'\n",
+ asciiMajorType.get(), asciiMinorType.get(),
+ NS_LossyConvertUTF16toASCII(mime_types_description).get());
+
+ if (majorType.IsEmpty() && minorType.IsEmpty()) {
+ // we didn't get a type mapping, so we can't do anything useful
+ return nullptr;
+ }
+
+ nsAutoCString mimeType(asciiMajorType + "/"_ns + asciiMinorType);
+ RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(mimeType);
+
+ mimeInfo->AppendExtension(aFileExt);
+ rv = LookUpHandlerAndDescription(majorType, minorType, handler,
+ mailcap_description, mozillaFlags);
+ LOG("Handler/Description results: handler='%s', description='%s', "
+ "mozillaFlags='%s'\n",
+ NS_LossyConvertUTF16toASCII(handler).get(),
+ NS_LossyConvertUTF16toASCII(mailcap_description).get(),
+ NS_LossyConvertUTF16toASCII(mozillaFlags).get());
+ mailcap_description.Trim(" \t\"");
+ mozillaFlags.Trim(" \t");
+ if (!mime_types_description.IsEmpty()) {
+ mimeInfo->SetDescription(mime_types_description);
+ } else {
+ mimeInfo->SetDescription(mailcap_description);
+ }
+
+ if (NS_SUCCEEDED(rv) && handler.IsEmpty()) {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFile> handlerFile;
+ rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile));
+
+ if (NS_SUCCEEDED(rv)) {
+ mimeInfo->SetDefaultApplication(handlerFile);
+ mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ mimeInfo->SetDefaultDescription(handler);
+ }
+ }
+
+ if (NS_FAILED(rv)) {
+ mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ }
+
+ return mimeInfo.forget();
+}
+
+already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromType(
+ const nsCString& aMIMEType) {
+ // if the type is empty, return immediately
+ if (aMIMEType.IsEmpty()) {
+ return nullptr;
+ }
+
+ LOG("Here we do a mimetype lookup for '%s'\n", aMIMEType.get());
+
+ // extract the major and minor types
+ NS_ConvertASCIItoUTF16 mimeType(aMIMEType);
+ nsAString::const_iterator start_iter, end_iter, majorTypeStart, majorTypeEnd,
+ minorTypeStart, minorTypeEnd;
+
+ mimeType.BeginReading(start_iter);
+ mimeType.EndReading(end_iter);
+
+ // XXX FIXME: add typeOptions parsing in here
+ nsresult rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd,
+ minorTypeStart, minorTypeEnd, end_iter);
+
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ nsDependentSubstring majorType(majorTypeStart, majorTypeEnd);
+ nsDependentSubstring minorType(minorTypeStart, minorTypeEnd);
+
+ // First check the user's private mailcap file
+ nsAutoString mailcap_description, handler, mozillaFlags;
+ DoLookUpHandlerAndDescription(majorType, minorType, handler,
+ mailcap_description, mozillaFlags, true);
+
+ LOG("Private Handler/Description results: handler='%s', description='%s'\n",
+ NS_LossyConvertUTF16toASCII(handler).get(),
+ NS_LossyConvertUTF16toASCII(mailcap_description).get());
+
+ // Now look up our extensions
+ nsAutoString extensions, mime_types_description;
+ LookUpExtensionsAndDescription(majorType, minorType, extensions,
+ mime_types_description);
+
+#ifdef MOZ_WIDGET_GTK
+ if (handler.IsEmpty()) {
+ RefPtr<nsMIMEInfoBase> gnomeInfo = nsGNOMERegistry::GetFromType(aMIMEType);
+ if (gnomeInfo) {
+ LOG("Got MIMEInfo from GNOME registry without extensions; setting them "
+ "to %s\n",
+ NS_LossyConvertUTF16toASCII(extensions).get());
+
+ NS_ASSERTION(!gnomeInfo->HasExtensions(), "How'd that happen?");
+ gnomeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions));
+ return gnomeInfo.forget();
+ }
+ }
+#endif
+
+ if (handler.IsEmpty()) {
+ DoLookUpHandlerAndDescription(majorType, minorType, handler,
+ mailcap_description, mozillaFlags, false);
+ }
+
+ if (handler.IsEmpty()) {
+ DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler,
+ mailcap_description, mozillaFlags, true);
+ }
+
+ if (handler.IsEmpty()) {
+ DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler,
+ mailcap_description, mozillaFlags, false);
+ }
+
+ LOG("Handler/Description results: handler='%s', description='%s', "
+ "mozillaFlags='%s'\n",
+ NS_LossyConvertUTF16toASCII(handler).get(),
+ NS_LossyConvertUTF16toASCII(mailcap_description).get(),
+ NS_LossyConvertUTF16toASCII(mozillaFlags).get());
+
+ mailcap_description.Trim(" \t\"");
+ mozillaFlags.Trim(" \t");
+
+ if (handler.IsEmpty() && extensions.IsEmpty() &&
+ mailcap_description.IsEmpty() && mime_types_description.IsEmpty()) {
+ // No real useful info
+ return nullptr;
+ }
+
+ RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(aMIMEType);
+
+ mimeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions));
+ if (!mime_types_description.IsEmpty()) {
+ mimeInfo->SetDescription(mime_types_description);
+ } else {
+ mimeInfo->SetDescription(mailcap_description);
+ }
+
+ rv = NS_ERROR_NOT_AVAILABLE;
+ nsCOMPtr<nsIFile> handlerFile;
+ if (!handler.IsEmpty()) {
+ rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile));
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ mimeInfo->SetDefaultApplication(handlerFile);
+ mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ mimeInfo->SetDefaultDescription(handler);
+ } else {
+ mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+ }
+
+ return mimeInfo.forget();
+}
+
+nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType,
+ const nsACString& aFileExt,
+ bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) {
+ *aFound = true;
+ RefPtr<nsMIMEInfoBase> retval;
+ // Fallback to lookup by extension when generic 'application/octet-stream'
+ // content type is received.
+ if (!aType.EqualsLiteral(APPLICATION_OCTET_STREAM)) {
+ retval = GetFromType(PromiseFlatCString(aType));
+ }
+ bool hasDefault = false;
+ if (retval) retval->GetHasDefaultHandler(&hasDefault);
+ if (!retval || !hasDefault) {
+ RefPtr<nsMIMEInfoBase> miByExt =
+ GetFromExtension(PromiseFlatCString(aFileExt));
+ // If we had no extension match, but a type match, use that
+ if (!miByExt && retval) {
+ retval.forget(aMIMEInfo);
+ return NS_OK;
+ }
+ // If we had an extension match but no type match, set the mimetype and use
+ // it
+ if (!retval && miByExt) {
+ if (!aType.IsEmpty()) miByExt->SetMIMEType(aType);
+ miByExt.swap(retval);
+
+ retval.forget(aMIMEInfo);
+ return NS_OK;
+ }
+ // If we got nothing, make a new mimeinfo
+ if (!retval) {
+ *aFound = false;
+ retval = new nsMIMEInfoUnix(aType);
+ if (retval) {
+ if (!aFileExt.IsEmpty()) retval->AppendExtension(aFileExt);
+ }
+
+ retval.forget(aMIMEInfo);
+ return NS_OK;
+ }
+
+ // Copy the attributes of retval (mimeinfo from type) onto miByExt, to
+ // return it
+ // but reset to just collected mDefaultAppDescription (from ext)
+ nsAutoString byExtDefault;
+ miByExt->GetDefaultDescription(byExtDefault);
+ retval->SetDefaultDescription(byExtDefault);
+ retval->CopyBasicDataTo(miByExt);
+
+ miByExt.swap(retval);
+ }
+ retval.forget(aMIMEInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) {
+ NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!");
+
+ nsresult rv =
+ OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found);
+ if (NS_FAILED(rv)) return rv;
+
+ nsMIMEInfoUnix* handlerInfo =
+ new nsMIMEInfoUnix(aScheme, nsMIMEInfoBase::eProtocolInfo);
+ NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(*_retval = handlerInfo);
+
+ if (!*found) {
+ // Code that calls this requires an object regardless if the OS has
+ // something for us, so we return the empty object.
+ return NS_OK;
+ }
+
+ nsAutoString desc;
+ GetApplicationDescription(aScheme, desc);
+ handlerInfo->SetDefaultDescription(desc);
+
+ return NS_OK;
+}
diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.h b/uriloader/exthandler/unix/nsOSHelperAppService.h
new file mode 100644
index 0000000000..25feeac359
--- /dev/null
+++ b/uriloader/exthandler/unix/nsOSHelperAppService.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 3; 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 nsOSHelperAppService_h__
+#define nsOSHelperAppService_h__
+
+// The OS helper app service is a subclass of nsExternalHelperAppService and is
+// implemented on each platform. It contains platform specific code for finding
+// helper applications for a given mime type in addition to launching those
+// applications.
+
+#include "nsExternalHelperAppService.h"
+#include "nsCExternalHandlerService.h"
+#include "nsCOMPtr.h"
+
+class nsIFileInputStream;
+class nsILineInputStream;
+class nsMIMEInfoBase;
+
+class nsOSHelperAppService : public nsExternalHelperAppService {
+ public:
+ virtual ~nsOSHelperAppService();
+
+ // method overrides for mime.types and mime.info look up steps
+ NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMimeType,
+ const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) override;
+ NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) override;
+
+ // override nsIExternalProtocolService methods
+ nsresult OSProtocolHandlerExists(const char* aProtocolScheme,
+ bool* aHandlerExists) override;
+ NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
+ nsAString& _retval) override;
+ NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval) override;
+
+ // GetFileTokenForPath must be implemented by each platform.
+ // platformAppPath --> a platform specific path to an application that we got
+ // out of the rdf data source. This can be a mac file
+ // spec, a unix path or a windows path depending on the
+ // platform
+ // aFile --> an nsIFile representation of that platform application path.
+ virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath,
+ nsIFile** aFile) override;
+
+ protected:
+ already_AddRefed<nsMIMEInfoBase> GetFromType(const nsCString& aMimeType);
+ already_AddRefed<nsMIMEInfoBase> GetFromExtension(const nsCString& aFileExt);
+
+ private:
+ // Helper methods which have to access static members
+ static nsresult UnescapeCommand(const nsAString& aEscapedCommand,
+ const nsAString& aMajorType,
+ const nsAString& aMinorType,
+ nsACString& aUnEscapedCommand);
+ static nsresult LookUpTypeAndDescription(const nsAString& aFileExtension,
+ nsAString& aMajorType,
+ nsAString& aMinorType,
+ nsAString& aDescription,
+ bool aUserData);
+ static nsresult CreateInputStream(const nsAString& aFilename,
+ nsIFileInputStream** aFileInputStream,
+ nsILineInputStream** aLineInputStream,
+ nsACString& aBuffer, bool* aNetscapeFormat,
+ bool* aMore);
+
+ static nsresult GetTypeAndDescriptionFromMimetypesFile(
+ const nsAString& aFilename, const nsAString& aFileExtension,
+ nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription);
+
+ static nsresult LookUpExtensionsAndDescription(const nsAString& aMajorType,
+ const nsAString& aMinorType,
+ nsAString& aFileExtensions,
+ nsAString& aDescription);
+
+ static nsresult GetExtensionsAndDescriptionFromMimetypesFile(
+ const nsAString& aFilename, const nsAString& aMajorType,
+ const nsAString& aMinorType, nsAString& aFileExtensions,
+ nsAString& aDescription);
+
+ static nsresult ParseNetscapeMIMETypesEntry(
+ const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart,
+ nsAString::const_iterator& aMajorTypeEnd,
+ nsAString::const_iterator& aMinorTypeStart,
+ nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions,
+ nsAString::const_iterator& aDescriptionStart,
+ nsAString::const_iterator& aDescriptionEnd);
+
+ static nsresult ParseNormalMIMETypesEntry(
+ const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart,
+ nsAString::const_iterator& aMajorTypeEnd,
+ nsAString::const_iterator& aMinorTypeStart,
+ nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions,
+ nsAString::const_iterator& aDescriptionStart,
+ nsAString::const_iterator& aDescriptionEnd);
+
+ static nsresult LookUpHandlerAndDescription(const nsAString& aMajorType,
+ const nsAString& aMinorType,
+ nsAString& aHandler,
+ nsAString& aDescription,
+ nsAString& aMozillaFlags);
+
+ static nsresult DoLookUpHandlerAndDescription(const nsAString& aMajorType,
+ const nsAString& aMinorType,
+ nsAString& aHandler,
+ nsAString& aDescription,
+ nsAString& aMozillaFlags,
+ bool aUserData);
+
+ static nsresult GetHandlerAndDescriptionFromMailcapFile(
+ const nsAString& aFilename, const nsAString& aMajorType,
+ const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription,
+ nsAString& aMozillaFlags);
+};
+
+#endif // nsOSHelperAppService_h__
diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.cpp b/uriloader/exthandler/win/nsMIMEInfoWin.cpp
new file mode 100644
index 0000000000..758b2018a7
--- /dev/null
+++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp
@@ -0,0 +1,913 @@
+/* -*- Mode: C++; tab-width: 3; 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 "nsArrayEnumerator.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMArray.h"
+#include "nsLocalFile.h"
+#include "nsMIMEInfoWin.h"
+#include "nsIMIMEService.h"
+#include "nsNetUtil.h"
+#include <windows.h>
+#include <shellapi.h>
+#include "nsIMutableArray.h"
+#include "nsTArray.h"
+#include <shlobj.h>
+#include "nsIWindowsRegKey.h"
+#include "nsUnicharUtils.h"
+#include "nsTextToSubURI.h"
+#include "nsVariant.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/ShellHeaderOnlyUtils.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/UrlmonHeaderOnlyUtils.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+#define RUNDLL32_EXE L"\\rundll32.exe"
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMIMEInfoWin, nsMIMEInfoBase, nsIPropertyBag)
+
+nsMIMEInfoWin::~nsMIMEInfoWin() {}
+
+nsresult nsMIMEInfoWin::LaunchDefaultWithFile(nsIFile* aFile) {
+ // Launch the file, unless it is an executable.
+ bool executable = true;
+ aFile->IsExecutable(&executable);
+ if (executable) return NS_ERROR_FAILURE;
+
+ return aFile->Launch();
+}
+
+nsresult nsMIMEInfoWin::ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc,
+ const wchar_t** aArgv) {
+ nsresult rv;
+
+ NS_ASSERTION(aArgc >= 1, "aArgc must be at least 1");
+
+ nsAutoString execPath;
+ rv = aExecutable->GetTarget(execPath);
+ if (NS_FAILED(rv) || execPath.IsEmpty()) {
+ rv = aExecutable->GetPath(execPath);
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ auto assembledArgs = mozilla::MakeCommandLine(aArgc, aArgv);
+ if (!assembledArgs) {
+ return NS_ERROR_FILE_EXECUTION_FAILED;
+ }
+
+ _bstr_t execPathBStr(execPath.get());
+ // Pass VT_ERROR/DISP_E_PARAMNOTFOUND to omit an optional RPC parameter
+ // to execute a file with the default verb.
+ _variant_t verbDefault(DISP_E_PARAMNOTFOUND, VT_ERROR);
+ _variant_t workingDir;
+ _variant_t showCmd(SW_SHOWNORMAL);
+
+ // Ask Explorer to ShellExecute on our behalf, as some applications such as
+ // Skype for Business do not start correctly when inheriting our process's
+ // migitation policies.
+ // It does not work in a special environment such as Citrix. In such a case
+ // we fall back to launching an application as a child process. We need to
+ // find a way to handle the combination of these interop issues.
+ mozilla::LauncherVoidResult shellExecuteOk = mozilla::ShellExecuteByExplorer(
+ execPathBStr, assembledArgs.get(), verbDefault, workingDir, showCmd);
+ if (shellExecuteOk.isErr()) {
+ // No need to pass assembledArgs to LaunchWithIProcess. aArgv will be
+ // processed in nsProcess::RunProcess.
+ return LaunchWithIProcess(aExecutable, aArgc,
+ reinterpret_cast<const char16_t**>(aArgv));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoWin::LaunchWithFile(nsIFile* aFile) {
+ nsresult rv;
+
+ // it doesn't make any sense to call this on protocol handlers
+ NS_ASSERTION(mClass == eMIMEInfo,
+ "nsMIMEInfoBase should have mClass == eMIMEInfo");
+
+ if (AutomationOnlyCheckIfLaunchStubbed(aFile)) {
+ return NS_OK;
+ }
+
+ if (mPreferredAction == useSystemDefault) {
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (defaultApp &&
+ mozilla::StaticPrefs::browser_pdf_launchDefaultEdgeAsApp()) {
+ // Since Edgium is the default handler for PDF and other kinds of files,
+ // if we're using the OS default and it's Edgium prefer its app mode so it
+ // operates as a viewer (without browser toolbars). Bug 1632277.
+ nsAutoCString defaultAppExecutable;
+ rv = defaultApp->GetNativeLeafName(defaultAppExecutable);
+ if (NS_SUCCEEDED(rv) &&
+ defaultAppExecutable.LowerCaseEqualsLiteral("msedge.exe")) {
+ nsAutoString path;
+ rv = aFile->GetPath(path);
+ if (NS_SUCCEEDED(rv)) {
+ // If the --app flag doesn't work we'll want to fallback to a
+ // regular path. Send two args so we call `msedge.exe --app={path}
+ // {path}`.
+ nsAutoString appArg;
+ appArg.AppendLiteral("--app=");
+ appArg.Append(path);
+ const wchar_t* argv[] = {appArg.get(), path.get()};
+
+ return ShellExecuteWithIFile(defaultApp, mozilla::ArrayLength(argv),
+ argv);
+ }
+ }
+ }
+ return LaunchDefaultWithFile(aFile);
+ }
+
+ if (mPreferredAction == useHelperApp) {
+ if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND;
+
+ // at the moment, we only know how to hand files off to local handlers
+ nsCOMPtr<nsILocalHandlerApp> localHandler =
+ do_QueryInterface(mPreferredApplication, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> executable;
+ rv = localHandler->GetExecutable(getter_AddRefs(executable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Deal with local dll based handlers
+ nsCString filename;
+ executable->GetNativeLeafName(filename);
+ if (filename.Length() > 4) {
+ nsCString extension(Substring(filename, filename.Length() - 4, 4));
+
+ if (extension.LowerCaseEqualsLiteral(".dll")) {
+ nsAutoString args;
+
+ // executable is rundll32, everything else is a list of parameters,
+ // including the dll handler.
+ if (!GetDllLaunchInfo(executable, aFile, args, false))
+ return NS_ERROR_INVALID_ARG;
+
+ WCHAR rundll32Path[MAX_PATH + sizeof(RUNDLL32_EXE) / sizeof(WCHAR) +
+ 1] = {L'\0'};
+ if (!GetSystemDirectoryW(rundll32Path, MAX_PATH)) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ lstrcatW(rundll32Path, RUNDLL32_EXE);
+
+ SHELLEXECUTEINFOW seinfo;
+ memset(&seinfo, 0, sizeof(seinfo));
+ seinfo.cbSize = sizeof(SHELLEXECUTEINFOW);
+ seinfo.fMask = 0;
+ seinfo.hwnd = nullptr;
+ seinfo.lpVerb = nullptr;
+ seinfo.lpFile = rundll32Path;
+ seinfo.lpParameters = args.get();
+ seinfo.lpDirectory = nullptr;
+ seinfo.nShow = SW_SHOWNORMAL;
+ if (ShellExecuteExW(&seinfo)) return NS_OK;
+
+ switch ((LONG_PTR)seinfo.hInstApp) {
+ case 0:
+ case SE_ERR_OOM:
+ return NS_ERROR_OUT_OF_MEMORY;
+ case SE_ERR_ACCESSDENIED:
+ return NS_ERROR_FILE_ACCESS_DENIED;
+ case SE_ERR_ASSOCINCOMPLETE:
+ case SE_ERR_NOASSOC:
+ return NS_ERROR_UNEXPECTED;
+ case SE_ERR_DDEBUSY:
+ case SE_ERR_DDEFAIL:
+ case SE_ERR_DDETIMEOUT:
+ return NS_ERROR_NOT_AVAILABLE;
+ case SE_ERR_DLLNOTFOUND:
+ return NS_ERROR_FAILURE;
+ case SE_ERR_SHARE:
+ return NS_ERROR_FILE_IS_LOCKED;
+ default:
+ switch (GetLastError()) {
+ case ERROR_FILE_NOT_FOUND:
+ return NS_ERROR_FILE_NOT_FOUND;
+ case ERROR_PATH_NOT_FOUND:
+ return NS_ERROR_FILE_UNRECOGNIZED_PATH;
+ case ERROR_BAD_FORMAT:
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+ }
+ return NS_ERROR_FILE_EXECUTION_FAILED;
+ }
+ }
+ nsAutoString path;
+ aFile->GetPath(path);
+ const wchar_t* argv[] = {path.get()};
+ return ShellExecuteWithIFile(executable, mozilla::ArrayLength(argv), argv);
+ }
+
+ return NS_ERROR_INVALID_ARG;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoWin::GetHasDefaultHandler(bool* _retval) {
+ // We have a default application if we have a description
+ // We can ShellExecute anything; however, callers are probably interested if
+ // there is really an application associated with this type of file
+ *_retval = !mDefaultAppDescription.IsEmpty();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoWin::GetEnumerator(nsISimpleEnumerator** _retval) {
+ nsCOMArray<nsIVariant> properties;
+
+ nsCOMPtr<nsIVariant> variant;
+ GetProperty(u"defaultApplicationIconURL"_ns, getter_AddRefs(variant));
+ if (variant) properties.AppendObject(variant);
+
+ GetProperty(u"customApplicationIconURL"_ns, getter_AddRefs(variant));
+ if (variant) properties.AppendObject(variant);
+
+ return NS_NewArrayEnumerator(_retval, properties, NS_GET_IID(nsIVariant));
+}
+
+static nsresult GetIconURLVariant(nsIFile* aApplication, nsIVariant** _retval) {
+ nsAutoCString fileURLSpec;
+ NS_GetURLSpecFromFile(aApplication, fileURLSpec);
+ nsAutoCString iconURLSpec;
+ iconURLSpec.AssignLiteral("moz-icon://");
+ iconURLSpec += fileURLSpec;
+ RefPtr<nsVariant> writable(new nsVariant());
+ writable->SetAsAUTF8String(iconURLSpec);
+ writable.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoWin::GetProperty(const nsAString& aName, nsIVariant** _retval) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (defaultApp && aName.EqualsLiteral(PROPERTY_DEFAULT_APP_ICON_URL)) {
+ rv = GetIconURLVariant(defaultApp, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (mPreferredApplication &&
+ aName.EqualsLiteral(PROPERTY_CUSTOM_APP_ICON_URL)) {
+ nsCOMPtr<nsILocalHandlerApp> localHandler =
+ do_QueryInterface(mPreferredApplication, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> executable;
+ rv = localHandler->GetExecutable(getter_AddRefs(executable));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GetIconURLVariant(executable, _retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+// this implementation was pretty much copied verbatime from
+// Tony Robinson's code in nsExternalProtocolWin.cpp
+nsresult nsMIMEInfoWin::LoadUriInternal(nsIURI* aURL) {
+ nsresult rv = NS_OK;
+
+ // 1. Find the default app for this protocol
+ // 2. Set up the command line
+ // 3. Launch the app.
+
+ // For now, we'll just cheat essentially, check for the command line
+ // then just call ShellExecute()!
+
+ if (aURL) {
+ // extract the url spec from the url
+ nsAutoCString urlSpec;
+ aURL->GetAsciiSpec(urlSpec);
+
+ // Unescape non-ASCII characters in the URL
+ nsAutoString utf16Spec;
+ if (NS_FAILED(nsTextToSubURI::UnEscapeNonAsciiURI("UTF-8"_ns, urlSpec,
+ utf16Spec))) {
+ CopyASCIItoUTF16(urlSpec, utf16Spec);
+ }
+
+ // Ask the shell/urlmon to parse |utf16Spec| to avoid malformed URLs.
+ // Failure is indicative of a potential security issue so we should
+ // bail out if so.
+ mozilla::LauncherResult<_bstr_t> validatedUri =
+ mozilla::UrlmonValidateUri(utf16Spec.get());
+ if (validatedUri.isErr()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ _variant_t args;
+ _variant_t verb(L"open");
+ _variant_t workingDir;
+ _variant_t showCmd(SW_SHOWNORMAL);
+
+ // To open a uri, we first try ShellExecuteByExplorer, which starts a new
+ // process as a child process of explorer.exe, because applications may not
+ // support the mitigation policies inherited from our process. If it fails,
+ // we fall back to ShellExecuteExW.
+ //
+ // For Thunderbird, however, there is a known issue that
+ // ShellExecuteByExplorer succeeds but explorer.exe shows an error popup
+ // if a uri to open includes credentials. This does not happen in Firefox
+ // because Firefox does not have to launch a process to open a uri.
+ //
+ // Since Thunderbird does not use mitigation policies which could cause
+ // compatibility issues, we get no benefit from using
+ // ShellExecuteByExplorer. Thus we skip it and go straight to
+ // ShellExecuteExW for Thunderbird.
+#ifndef MOZ_THUNDERBIRD
+ mozilla::LauncherVoidResult shellExecuteOk =
+ mozilla::ShellExecuteByExplorer(validatedUri.inspect(), args, verb,
+ workingDir, showCmd);
+ if (shellExecuteOk.isOk()) {
+ return NS_OK;
+ }
+#endif // MOZ_THUNDERBIRD
+
+ SHELLEXECUTEINFOW sinfo = {sizeof(sinfo)};
+ sinfo.fMask = SEE_MASK_NOASYNC;
+ sinfo.lpVerb = V_BSTR(&verb);
+ sinfo.nShow = showCmd;
+ sinfo.lpFile = validatedUri.inspect();
+
+ BOOL result = ShellExecuteExW(&sinfo);
+ if (!result || reinterpret_cast<LONG_PTR>(sinfo.hInstApp) < 32) {
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+
+ return rv;
+}
+
+void nsMIMEInfoWin::UpdateDefaultInfoIfStale() {
+ if (!mIsDefaultAppInfoFresh) {
+ nsCOMPtr<nsIMIMEService> mime = do_GetService("@mozilla.org/mime;1");
+ if (mime) {
+ mime->UpdateDefaultAppInfo(static_cast<nsIMIMEInfo*>(this));
+ }
+ mIsDefaultAppInfoFresh = true;
+ }
+}
+
+// Given a path to a local file, return its nsILocalHandlerApp instance.
+bool nsMIMEInfoWin::GetLocalHandlerApp(const nsAString& aCommandHandler,
+ nsCOMPtr<nsILocalHandlerApp>& aApp) {
+ nsCOMPtr<nsIFile> locfile;
+ nsresult rv = NS_NewLocalFile(aCommandHandler, true, getter_AddRefs(locfile));
+ if (NS_FAILED(rv)) return false;
+
+ aApp = do_CreateInstance("@mozilla.org/uriloader/local-handler-app;1");
+ if (!aApp) return false;
+
+ aApp->SetExecutable(locfile);
+ return true;
+}
+
+// Return the cleaned up file path associated with a command verb
+// located in root/Applications.
+bool nsMIMEInfoWin::GetAppsVerbCommandHandler(const nsAString& appExeName,
+ nsAString& applicationPath,
+ bool edit) {
+ nsCOMPtr<nsIWindowsRegKey> appKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!appKey) return false;
+
+ // HKEY_CLASSES_ROOT\Applications\iexplore.exe
+ nsAutoString applicationsPath;
+ applicationsPath.AppendLiteral("Applications\\");
+ applicationsPath.Append(appExeName);
+
+ nsresult rv =
+ appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return false;
+
+ // Check for the NoOpenWith flag, if it exists
+ uint32_t value;
+ if (NS_SUCCEEDED(appKey->ReadIntValue(u"NoOpenWith"_ns, &value)) &&
+ value == 1)
+ return false;
+
+ nsAutoString dummy;
+ if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy)))
+ return false;
+
+ appKey->Close();
+
+ // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command
+ applicationsPath.AssignLiteral("Applications\\");
+ applicationsPath.Append(appExeName);
+ if (!edit)
+ applicationsPath.AppendLiteral("\\shell\\open\\command");
+ else
+ applicationsPath.AppendLiteral("\\shell\\edit\\command");
+
+ rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return false;
+
+ nsAutoString appFilesystemCommand;
+ if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) {
+ // Expand environment vars, clean up any misc.
+ if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false;
+
+ applicationPath = appFilesystemCommand;
+ return true;
+ }
+ return false;
+}
+
+// Return a fully populated command string based on
+// passing information. Used in launchWithFile to trace
+// back to the full handler path based on the dll.
+// (dll, targetfile, return args, open/edit)
+bool nsMIMEInfoWin::GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile,
+ nsAString& args, bool edit) {
+ if (!aDll || !aFile) return false;
+
+ nsString appExeName;
+ aDll->GetLeafName(appExeName);
+
+ nsCOMPtr<nsIWindowsRegKey> appKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!appKey) return false;
+
+ // HKEY_CLASSES_ROOT\Applications\iexplore.exe
+ nsAutoString applicationsPath;
+ applicationsPath.AppendLiteral("Applications\\");
+ applicationsPath.Append(appExeName);
+
+ nsresult rv =
+ appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return false;
+
+ // Check for the NoOpenWith flag, if it exists
+ uint32_t value;
+ rv = appKey->ReadIntValue(u"NoOpenWith"_ns, &value);
+ if (NS_SUCCEEDED(rv) && value == 1) return false;
+
+ nsAutoString dummy;
+ if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy)))
+ return false;
+
+ appKey->Close();
+
+ // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command
+ applicationsPath.AssignLiteral("Applications\\");
+ applicationsPath.Append(appExeName);
+ if (!edit)
+ applicationsPath.AppendLiteral("\\shell\\open\\command");
+ else
+ applicationsPath.AppendLiteral("\\shell\\edit\\command");
+
+ rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return false;
+
+ nsAutoString appFilesystemCommand;
+ if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) {
+ // Replace embedded environment variables.
+ uint32_t bufLength =
+ ::ExpandEnvironmentStringsW(appFilesystemCommand.get(), nullptr, 0);
+ if (bufLength == 0) // Error
+ return false;
+
+ auto destination = mozilla::MakeUniqueFallible<wchar_t[]>(bufLength);
+ if (!destination) return false;
+ if (!::ExpandEnvironmentStringsW(appFilesystemCommand.get(),
+ destination.get(), bufLength))
+ return false;
+
+ appFilesystemCommand.Assign(destination.get());
+
+ // C:\Windows\System32\rundll32.exe "C:\Program Files\Windows
+ // Photo Gallery\PhotoViewer.dll", ImageView_Fullscreen %1
+ nsAutoString params;
+ constexpr auto rundllSegment = u"rundll32.exe "_ns;
+ int32_t index = appFilesystemCommand.Find(rundllSegment);
+ if (index > kNotFound) {
+ params.Append(
+ Substring(appFilesystemCommand, index + rundllSegment.Length()));
+ } else {
+ params.Append(appFilesystemCommand);
+ }
+
+ // check to make sure we have a %1 and fill it
+ constexpr auto percentOneParam = u"%1"_ns;
+ index = params.Find(percentOneParam);
+ if (index == kNotFound) // no parameter
+ return false;
+
+ nsString target;
+ aFile->GetTarget(target);
+ params.Replace(index, 2, target);
+
+ args = params;
+
+ return true;
+ }
+ return false;
+}
+
+// Return the cleaned up file path associated with a progid command
+// verb located in root.
+bool nsMIMEInfoWin::GetProgIDVerbCommandHandler(const nsAString& appProgIDName,
+ nsAString& applicationPath,
+ bool edit) {
+ nsCOMPtr<nsIWindowsRegKey> appKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!appKey) return false;
+
+ nsAutoString appProgId(appProgIDName);
+
+ // HKEY_CLASSES_ROOT\Windows.XPSReachViewer\shell\open\command
+ if (!edit)
+ appProgId.AppendLiteral("\\shell\\open\\command");
+ else
+ appProgId.AppendLiteral("\\shell\\edit\\command");
+
+ nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, appProgId,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return false;
+
+ nsAutoString appFilesystemCommand;
+ if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) {
+ // Expand environment vars, clean up any misc.
+ if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false;
+
+ applicationPath = appFilesystemCommand;
+ return true;
+ }
+ return false;
+}
+
+// Helper routine used in tracking app lists. Converts path
+// entries to lower case and stores them in the trackList array.
+void nsMIMEInfoWin::ProcessPath(nsCOMPtr<nsIMutableArray>& appList,
+ nsTArray<nsString>& trackList,
+ const nsAString& appFilesystemCommand) {
+ nsAutoString lower(appFilesystemCommand);
+ ToLowerCase(lower);
+
+ // Don't include firefox.exe in the list
+ WCHAR exe[MAX_PATH + 1];
+ uint32_t len = GetModuleFileNameW(nullptr, exe, MAX_PATH);
+ if (len < MAX_PATH && len != 0) {
+ int32_t index = lower.Find(reinterpret_cast<const char16_t*>(exe));
+ if (index != -1) return;
+ }
+
+ nsCOMPtr<nsILocalHandlerApp> aApp;
+ if (!GetLocalHandlerApp(appFilesystemCommand, aApp)) return;
+
+ // Save in our main tracking arrays
+ appList->AppendElement(aApp);
+ trackList.AppendElement(lower);
+}
+
+// Helper routine that handles a compare between a path
+// and an array of paths.
+static bool IsPathInList(nsAString& appPath, nsTArray<nsString>& trackList) {
+ // trackList data is always lowercase, see ProcessPath
+ // above.
+ nsAutoString tmp(appPath);
+ ToLowerCase(tmp);
+
+ for (uint32_t i = 0; i < trackList.Length(); i++) {
+ if (tmp.Equals(trackList[i])) return true;
+ }
+ return false;
+}
+
+/**
+ * Returns a list of nsILocalHandlerApp objects containing local
+ * handlers associated with this mimeinfo. Implemented per
+ * platform using information in this object to generate the
+ * best list. Typically used for an "open with" style user
+ * option.
+ *
+ * @return nsIArray of nsILocalHandlerApp
+ */
+NS_IMETHODIMP
+nsMIMEInfoWin::GetPossibleLocalHandlers(nsIArray** _retval) {
+ nsresult rv;
+
+ *_retval = nullptr;
+
+ nsCOMPtr<nsIMutableArray> appList = do_CreateInstance("@mozilla.org/array;1");
+
+ if (!appList) return NS_ERROR_FAILURE;
+
+ nsTArray<nsString> trackList;
+
+ nsAutoCString fileExt;
+ GetPrimaryExtension(fileExt);
+
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) return NS_ERROR_FAILURE;
+ nsCOMPtr<nsIWindowsRegKey> appKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!appKey) return NS_ERROR_FAILURE;
+
+ nsAutoString workingRegistryPath;
+
+ bool extKnown = false;
+ if (fileExt.IsEmpty()) {
+ extKnown = true;
+ // Mime type discovery is possible in some cases, through
+ // HKEY_CLASSES_ROOT\MIME\Database\Content Type, however, a number
+ // of file extensions related to mime type are simply not defined,
+ // (application/rss+xml & application/atom+xml are good examples)
+ // in which case we can only provide a generic list.
+ nsAutoCString mimeType;
+ GetMIMEType(mimeType);
+ if (!mimeType.IsEmpty()) {
+ workingRegistryPath.AppendLiteral("MIME\\Database\\Content Type\\");
+ workingRegistryPath.Append(NS_ConvertASCIItoUTF16(mimeType));
+
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ workingRegistryPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString mimeFileExt;
+ if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, mimeFileExt))) {
+ CopyUTF16toUTF8(mimeFileExt, fileExt);
+ extKnown = false;
+ }
+ }
+ }
+ }
+
+ nsAutoString fileExtToUse;
+ if (!fileExt.IsEmpty() && fileExt.First() != '.') {
+ fileExtToUse = char16_t('.');
+ }
+ fileExtToUse.Append(NS_ConvertUTF8toUTF16(fileExt));
+
+ // Note, the order in which these occur has an effect on the
+ // validity of the resulting display list.
+
+ if (!extKnown) {
+ // 1) Get the default handler if it exists
+ workingRegistryPath = fileExtToUse;
+
+ rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString appProgId;
+ if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, appProgId))) {
+ // Bug 358297 - ignore the embedded internet explorer handler
+ if (appProgId != u"XPSViewer.Document"_ns) {
+ nsAutoString appFilesystemCommand;
+ if (GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand,
+ false) &&
+ !IsPathInList(appFilesystemCommand, trackList)) {
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ }
+ regKey->Close();
+ }
+
+ // 2) list HKEY_CLASSES_ROOT\.ext\OpenWithList
+
+ workingRegistryPath = fileExtToUse;
+ workingRegistryPath.AppendLiteral("\\OpenWithList");
+
+ rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t count = 0;
+ if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) {
+ for (uint32_t index = 0; index < count; index++) {
+ nsAutoString appName;
+ if (NS_FAILED(regKey->GetValueName(index, appName))) continue;
+
+ // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params"
+ nsAutoString appFilesystemCommand;
+ if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand,
+ false) ||
+ IsPathInList(appFilesystemCommand, trackList))
+ continue;
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ regKey->Close();
+ }
+
+ // 3) List HKEY_CLASSES_ROOT\.ext\OpenWithProgids, with the
+ // different step of resolving the progids for the command handler.
+
+ workingRegistryPath = fileExtToUse;
+ workingRegistryPath.AppendLiteral("\\OpenWithProgids");
+
+ rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t count = 0;
+ if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) {
+ for (uint32_t index = 0; index < count; index++) {
+ // HKEY_CLASSES_ROOT\.ext\OpenWithProgids\Windows.XPSReachViewer
+ nsAutoString appProgId;
+ if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue;
+
+ nsAutoString appFilesystemCommand;
+ if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand,
+ false) ||
+ IsPathInList(appFilesystemCommand, trackList))
+ continue;
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ regKey->Close();
+ }
+
+ // 4) Add any non configured applications located in the MRU list
+
+ // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion
+ // \Explorer\FileExts\.ext\OpenWithList
+ workingRegistryPath = nsLiteralString(
+ u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\");
+ workingRegistryPath += fileExtToUse;
+ workingRegistryPath.AppendLiteral("\\OpenWithList");
+
+ rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t count = 0;
+ if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) {
+ for (uint32_t index = 0; index < count; index++) {
+ nsAutoString appName, appValue;
+ if (NS_FAILED(regKey->GetValueName(index, appName))) continue;
+ if (appName.EqualsLiteral("MRUList")) continue;
+ if (NS_FAILED(regKey->ReadStringValue(appName, appValue))) continue;
+
+ // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params"
+ nsAutoString appFilesystemCommand;
+ if (!GetAppsVerbCommandHandler(appValue, appFilesystemCommand,
+ false) ||
+ IsPathInList(appFilesystemCommand, trackList))
+ continue;
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ }
+
+ // 5) Add any non configured progids in the MRU list, with the
+ // different step of resolving the progids for the command handler.
+
+ // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion
+ // \Explorer\FileExts\.ext\OpenWithProgids
+ workingRegistryPath = nsLiteralString(
+ u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\");
+ workingRegistryPath += fileExtToUse;
+ workingRegistryPath.AppendLiteral("\\OpenWithProgids");
+
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, workingRegistryPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t count = 0;
+ if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) {
+ for (uint32_t index = 0; index < count; index++) {
+ nsAutoString appIndex, appProgId;
+ if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue;
+
+ nsAutoString appFilesystemCommand;
+ if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand,
+ false) ||
+ IsPathInList(appFilesystemCommand, trackList))
+ continue;
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ regKey->Close();
+ }
+
+ // 6) Check the perceived type value, and use this to lookup the
+ // perceivedtype open with list.
+ // http://msdn2.microsoft.com/en-us/library/aa969373.aspx
+
+ workingRegistryPath = fileExtToUse;
+
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString perceivedType;
+ rv = regKey->ReadStringValue(u"PerceivedType"_ns, perceivedType);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoString openWithListPath(u"SystemFileAssociations\\"_ns);
+ openWithListPath.Append(perceivedType); // no period
+ openWithListPath.AppendLiteral("\\OpenWithList");
+
+ nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ openWithListPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t count = 0;
+ if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) {
+ for (uint32_t index = 0; index < count; index++) {
+ nsAutoString appName;
+ if (NS_FAILED(regKey->GetValueName(index, appName))) continue;
+
+ // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params"
+ nsAutoString appFilesystemCommand;
+ if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand,
+ false) ||
+ IsPathInList(appFilesystemCommand, trackList))
+ continue;
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ }
+ }
+ }
+ } // extKnown == false
+
+ // 7) list global HKEY_CLASSES_ROOT\*\OpenWithList
+ // Listing general purpose handlers, not specific to a mime type or file
+ // extension
+
+ workingRegistryPath = u"*\\OpenWithList"_ns;
+
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t count = 0;
+ if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) {
+ for (uint32_t index = 0; index < count; index++) {
+ nsAutoString appName;
+ if (NS_FAILED(regKey->GetValueName(index, appName))) continue;
+
+ // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params"
+ nsAutoString appFilesystemCommand;
+ if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) ||
+ IsPathInList(appFilesystemCommand, trackList))
+ continue;
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ regKey->Close();
+ }
+
+ // 8) General application's list - not file extension specific on windows
+ workingRegistryPath = u"Applications"_ns;
+
+ rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath,
+ nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS |
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t count = 0;
+ if (NS_SUCCEEDED(regKey->GetChildCount(&count)) && count > 0) {
+ for (uint32_t index = 0; index < count; index++) {
+ nsAutoString appName;
+ if (NS_FAILED(regKey->GetChildName(index, appName))) continue;
+
+ // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params"
+ nsAutoString appFilesystemCommand;
+ if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) ||
+ IsPathInList(appFilesystemCommand, trackList))
+ continue;
+ ProcessPath(appList, trackList, appFilesystemCommand);
+ }
+ }
+ }
+
+ // Return to the caller
+ *_retval = appList;
+ NS_ADDREF(*_retval);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoWin::IsCurrentAppOSDefault(bool* _retval) {
+ *_retval = false;
+ nsCOMPtr<nsIFile> defaultApp = GetDefaultApplication();
+ if (defaultApp) {
+ // Determine if the default executable is our executable.
+ nsCOMPtr<nsIFile> ourBinary;
+ XRE_GetBinaryPath(getter_AddRefs(ourBinary));
+ bool isSame = false;
+ nsresult rv = defaultApp->Equals(ourBinary, &isSame);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *_retval = isSame;
+ }
+ return NS_OK;
+}
diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.h b/uriloader/exthandler/win/nsMIMEInfoWin.h
new file mode 100644
index 0000000000..fa972b23a9
--- /dev/null
+++ b/uriloader/exthandler/win/nsMIMEInfoWin.h
@@ -0,0 +1,79 @@
+/* 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 nsMIMEInfoWin_h_
+#define nsMIMEInfoWin_h_
+
+#include "nsMIMEInfoImpl.h"
+#include "nsIPropertyBag.h"
+#include "nsIMutableArray.h"
+#include "nsTArray.h"
+
+class nsMIMEInfoWin : public nsMIMEInfoBase, public nsIPropertyBag {
+ virtual ~nsMIMEInfoWin();
+
+ public:
+ explicit nsMIMEInfoWin(const char* aType = "") : nsMIMEInfoBase(aType) {}
+ explicit nsMIMEInfoWin(const nsACString& aMIMEType)
+ : nsMIMEInfoBase(aMIMEType) {}
+ nsMIMEInfoWin(const nsACString& aType, HandlerClass aClass)
+ : nsMIMEInfoBase(aType, aClass) {}
+
+ NS_IMETHOD LaunchWithFile(nsIFile* aFile) override;
+ NS_IMETHOD GetHasDefaultHandler(bool* _retval) override;
+ NS_IMETHOD GetPossibleLocalHandlers(nsIArray** _retval) override;
+ NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override;
+
+ void UpdateDefaultInfoIfStale();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIPROPERTYBAG
+
+ void SetDefaultApplicationHandler(nsIFile* aDefaultApplication) {
+ mDefaultApplication = aDefaultApplication;
+ }
+
+ protected:
+ nsIFile* GetDefaultApplication() {
+ UpdateDefaultInfoIfStale();
+ return mDefaultApplication;
+ }
+
+ virtual nsresult LoadUriInternal(nsIURI* aURI);
+ virtual nsresult LaunchDefaultWithFile(nsIFile* aFile);
+
+ private:
+ nsCOMPtr<nsIFile> mDefaultApplication;
+
+ // Given a path to a local handler, return its
+ // nsILocalHandlerApp instance.
+ bool GetLocalHandlerApp(const nsAString& aCommandHandler,
+ nsCOMPtr<nsILocalHandlerApp>& aApp);
+
+ // Return the cleaned up file path associated
+ // with a command verb located in root/Applications.
+ bool GetAppsVerbCommandHandler(const nsAString& appExeName,
+ nsAString& applicationPath, bool bEdit);
+
+ // Return the cleaned up file path associated
+ // with a progid command verb located in root.
+ bool GetProgIDVerbCommandHandler(const nsAString& appProgIDName,
+ nsAString& applicationPath, bool bEdit);
+
+ // Lookup a rundll command handler and return
+ // a populated command template for use with rundll32.exe.
+ bool GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile, nsAString& args,
+ bool bEdit);
+
+ // Helper routine used in tracking app lists
+ void ProcessPath(nsCOMPtr<nsIMutableArray>& appList,
+ nsTArray<nsString>& trackList,
+ const nsAString& appFilesystemCommand);
+
+ // Helper routine to call mozilla::ShellExecuteByExplorer
+ nsresult ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc,
+ const wchar_t** aArgv);
+};
+
+#endif
diff --git a/uriloader/exthandler/win/nsOSHelperAppService.cpp b/uriloader/exthandler/win/nsOSHelperAppService.cpp
new file mode 100644
index 0000000000..e3a2ae9873
--- /dev/null
+++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp
@@ -0,0 +1,609 @@
+/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:set ts=2 sts=2 sw=2 et cin:
+ *
+ * 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 "nsComponentManagerUtils.h"
+#include "nsOSHelperAppService.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsIMIMEInfo.h"
+#include "nsMIMEInfoWin.h"
+#include "nsMimeTypes.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsLocalFile.h"
+#include "nsIWindowsRegKey.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/UniquePtrExtensions.h"
+
+// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN
+#include <shellapi.h>
+#include <shlwapi.h>
+
+#define LOG(...) MOZ_LOG(sLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+// helper methods: forward declarations...
+static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType,
+ nsString& aFileExtension);
+
+nsOSHelperAppService::nsOSHelperAppService()
+ : nsExternalHelperAppService(), mAppAssoc(nullptr) {
+ CoInitialize(nullptr);
+ CoCreateInstance(CLSID_ApplicationAssociationRegistration, nullptr,
+ CLSCTX_INPROC, IID_IApplicationAssociationRegistration,
+ (void**)&mAppAssoc);
+}
+
+nsOSHelperAppService::~nsOSHelperAppService() {
+ if (mAppAssoc) mAppAssoc->Release();
+ mAppAssoc = nullptr;
+ CoUninitialize();
+}
+
+// The windows registry provides a mime database key which lists a set of mime
+// types and corresponding "Extension" values. we can use this to look up our
+// mime type to see if there is a preferred extension for the mime type.
+static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType,
+ nsString& aFileExtension) {
+ nsAutoString mimeDatabaseKey;
+ mimeDatabaseKey.AssignLiteral("MIME\\Database\\Content Type\\");
+
+ AppendASCIItoUTF16(aMimeType, mimeDatabaseKey);
+
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) return NS_ERROR_NOT_AVAILABLE;
+
+ nsresult rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, mimeDatabaseKey,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+
+ if (NS_SUCCEEDED(rv))
+ regKey->ReadStringValue(u"Extension"_ns, aFileExtension);
+
+ return NS_OK;
+}
+
+nsresult nsOSHelperAppService::OSProtocolHandlerExists(
+ const char* aProtocolScheme, bool* aHandlerExists) {
+ // look up the protocol scheme in the windows registry....if we find a match
+ // then we have a handler for it...
+ *aHandlerExists = false;
+ if (aProtocolScheme && *aProtocolScheme) {
+ NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE);
+ wchar_t* pResult = nullptr;
+ NS_ConvertASCIItoUTF16 scheme(aProtocolScheme);
+ // We are responsible for freeing returned strings.
+ HRESULT hr = mAppAssoc->QueryCurrentDefault(scheme.get(), AT_URLPROTOCOL,
+ AL_EFFECTIVE, &pResult);
+ if (SUCCEEDED(hr)) {
+ CoTaskMemFree(pResult);
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ nsDependentString(scheme.get()),
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) {
+ // Open will fail if the registry key path doesn't exist.
+ return NS_OK;
+ }
+
+ bool hasValue;
+ rv = regKey->HasValue(u"URL Protocol"_ns, &hasValue);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!hasValue) {
+ return NS_OK;
+ }
+
+ *aHandlerExists = true;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(
+ const nsACString& aScheme, nsAString& _retval) {
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) return NS_ERROR_NOT_AVAILABLE;
+
+ NS_ConvertASCIItoUTF16 buf(aScheme);
+
+ wchar_t result[1024];
+ DWORD resultSize = 1024;
+ HRESULT hr = AssocQueryString(0x1000 /* ASSOCF_IS_PROTOCOL */,
+ ASSOCSTR_FRIENDLYAPPNAME, buf.get(), NULL,
+ result, &resultSize);
+ if (SUCCEEDED(hr)) {
+ _retval = result;
+ return NS_OK;
+ }
+
+ NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE);
+ wchar_t* pResult = nullptr;
+ // We are responsible for freeing returned strings.
+ hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL, AL_EFFECTIVE,
+ &pResult);
+ if (SUCCEEDED(hr)) {
+ nsCOMPtr<nsIFile> app;
+ nsAutoString appInfo(pResult);
+ CoTaskMemFree(pResult);
+ if (NS_SUCCEEDED(GetDefaultAppInfo(appInfo, _retval, getter_AddRefs(app))))
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
+ const nsACString& aScheme, bool* _retval) {
+ *_retval = false;
+
+ NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE);
+
+ NS_ConvertASCIItoUTF16 buf(aScheme);
+
+ // Find the progID
+ wchar_t* pResult = nullptr;
+ HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL,
+ AL_EFFECTIVE, &pResult);
+ if (FAILED(hr)) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoString progID(pResult);
+ // We are responsible for freeing returned strings.
+ CoTaskMemFree(pResult);
+
+ // Find the default executable.
+ nsAutoString description;
+ nsCOMPtr<nsIFile> appFile;
+ nsresult rv = GetDefaultAppInfo(progID, description, getter_AddRefs(appFile));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Determine if the default executable is our executable.
+ nsCOMPtr<nsIFile> ourBinary;
+ XRE_GetBinaryPath(getter_AddRefs(ourBinary));
+ bool isSame = false;
+ rv = appFile->Equals(ourBinary, &isSame);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *_retval = isSame;
+ return NS_OK;
+}
+
+// GetMIMEInfoFromRegistry: This function obtains the values of some of the
+// nsIMIMEInfo attributes for the mimeType/extension associated with the input
+// registry key. The default entry for that key is the name of a registry key
+// under HKEY_CLASSES_ROOT. The default value for *that* key is the descriptive
+// name of the type. The EditFlags value is a binary value; the low order bit
+// of the third byte of which indicates that the user does not need to be
+// prompted.
+//
+// This function sets only the Description attribute of the input nsIMIMEInfo.
+/* static */
+nsresult nsOSHelperAppService::GetMIMEInfoFromRegistry(const nsString& fileType,
+ nsIMIMEInfo* pInfo) {
+ nsresult rv = NS_OK;
+
+ NS_ENSURE_ARG(pInfo);
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) return NS_ERROR_NOT_AVAILABLE;
+
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileType,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ // OK, the default value here is the description of the type.
+ nsAutoString description;
+ rv = regKey->ReadStringValue(u""_ns, description);
+ if (NS_SUCCEEDED(rv)) pInfo->SetDescription(description);
+
+ return NS_OK;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////////////
+// method overrides used to gather information from the windows registry for
+// various mime types.
+////////////////////////////////////////////////////////////////////////////////////////////////
+
+/// Looks up the type for the extension aExt and compares it to aType
+/* static */
+bool nsOSHelperAppService::typeFromExtEquals(const char16_t* aExt,
+ const char* aType) {
+ if (!aType) return false;
+ nsAutoString fileExtToUse;
+ if (aExt[0] != char16_t('.')) fileExtToUse = char16_t('.');
+
+ fileExtToUse.Append(aExt);
+
+ bool eq = false;
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) return eq;
+
+ nsresult rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return eq;
+
+ nsAutoString type;
+ rv = regKey->ReadStringValue(u"Content Type"_ns, type);
+ if (NS_SUCCEEDED(rv)) eq = type.LowerCaseEqualsASCII(aType);
+
+ return eq;
+}
+
+// The "real" name of a given helper app (as specified by the path to the
+// executable file held in various registry keys) is stored n the VERSIONINFO
+// block in the file's resources. We need to find the path to the executable
+// and then retrieve the "FileDescription" field value from the file.
+nsresult nsOSHelperAppService::GetDefaultAppInfo(
+ const nsAString& aAppInfo, nsAString& aDefaultDescription,
+ nsIFile** aDefaultApplication) {
+ nsAutoString handlerCommand;
+
+ // If all else fails, use the file type key name, which will be
+ // something like "pngfile" for .pngs, "WMVFile" for .wmvs, etc.
+ aDefaultDescription = aAppInfo;
+ *aDefaultApplication = nullptr;
+
+ if (aAppInfo.IsEmpty()) return NS_ERROR_FAILURE;
+
+ // aAppInfo may be a file, file path, program id, or
+ // Applications reference -
+ // c:\dir\app.exe
+ // Applications\appfile.exe/dll (shell\open...)
+ // ProgID.progid (shell\open...)
+
+ nsAutoString handlerKeyName(aAppInfo);
+
+ nsCOMPtr<nsIWindowsRegKey> chkKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!chkKey) return NS_ERROR_FAILURE;
+
+ nsresult rv =
+ chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) {
+ // It's a file system path to a handler
+ handlerCommand.Assign(aAppInfo);
+ } else {
+ handlerKeyName.AppendLiteral("\\shell\\open\\command");
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) return NS_ERROR_FAILURE;
+
+ nsresult rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return NS_ERROR_FAILURE;
+
+ // OK, the default value here is the description of the type.
+ rv = regKey->ReadStringValue(u""_ns, handlerCommand);
+ if (NS_FAILED(rv)) {
+ // Check if there is a DelegateExecute string
+ nsAutoString delegateExecute;
+ rv = regKey->ReadStringValue(u"DelegateExecute"_ns, delegateExecute);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Look for InProcServer32
+ nsAutoString delegateExecuteRegPath;
+ delegateExecuteRegPath.AssignLiteral("CLSID\\");
+ delegateExecuteRegPath.Append(delegateExecute);
+ delegateExecuteRegPath.AppendLiteral("\\InProcServer32");
+ rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ delegateExecuteRegPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_SUCCEEDED(rv)) {
+ rv = chkKey->ReadStringValue(u""_ns, handlerCommand);
+ }
+
+ if (NS_FAILED(rv)) {
+ // Look for LocalServer32
+ delegateExecuteRegPath.AssignLiteral("CLSID\\");
+ delegateExecuteRegPath.Append(delegateExecute);
+ delegateExecuteRegPath.AppendLiteral("\\LocalServer32");
+ rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT,
+ delegateExecuteRegPath,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = chkKey->ReadStringValue(u""_ns, handlerCommand);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ // XXX FIXME: If this fails, the UI will display the full command
+ // string.
+ // There are some rare cases this can happen - ["url.dll" -foo]
+ // for example won't resolve correctly to the system dir. The
+ // subsequent launch of the helper app will work though.
+ nsCOMPtr<nsILocalFileWin> lf = new nsLocalFile();
+ rv = lf->InitWithCommandLine(handlerCommand);
+ NS_ENSURE_SUCCESS(rv, rv);
+ lf.forget(aDefaultApplication);
+
+ wchar_t friendlyName[1024];
+ DWORD friendlyNameSize = 1024;
+ HRESULT hr = AssocQueryString(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME,
+ PromiseFlatString(aAppInfo).get(), NULL,
+ friendlyName, &friendlyNameSize);
+ if (SUCCEEDED(hr) && friendlyNameSize > 1) {
+ aDefaultDescription.Assign(friendlyName, friendlyNameSize - 1);
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<nsMIMEInfoWin> nsOSHelperAppService::GetByExtension(
+ const nsString& aFileExt, const char* aTypeHint) {
+ if (aFileExt.IsEmpty()) return nullptr;
+
+ // Determine the mime type.
+ nsAutoCString typeToUse;
+ if (aTypeHint && *aTypeHint) {
+ typeToUse.Assign(aTypeHint);
+ } else if (!GetMIMETypeFromOSForExtension(NS_ConvertUTF16toUTF8(aFileExt),
+ typeToUse)) {
+ return nullptr;
+ }
+
+ RefPtr<nsMIMEInfoWin> mimeInfo = new nsMIMEInfoWin(typeToUse);
+
+ // Our extension APIs expect extensions without the '.', so normalize:
+ uint32_t dotlessIndex = aFileExt.First() != char16_t('.') ? 0 : 1;
+ nsAutoCString lowerFileExt =
+ NS_ConvertUTF16toUTF8(Substring(aFileExt, dotlessIndex));
+ ToLowerCase(lowerFileExt);
+ mimeInfo->AppendExtension(lowerFileExt);
+ mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+
+ if (NS_FAILED(InternalSetDefaultsOnMIME(mimeInfo))) {
+ return nullptr;
+ }
+
+ return mimeInfo.forget();
+}
+
+nsresult nsOSHelperAppService::InternalSetDefaultsOnMIME(
+ nsMIMEInfoWin* aMIMEInfo) {
+ NS_ENSURE_ARG(aMIMEInfo);
+
+ nsAutoCString primaryExt;
+ aMIMEInfo->GetPrimaryExtension(primaryExt);
+
+ if (primaryExt.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // windows registry assumes your file extension is going to include the '.',
+ // but our APIs don't have it, so add it:
+ nsAutoString assocType = NS_ConvertUTF8toUTF16(primaryExt);
+ if (assocType.First() != char16_t('.')) {
+ assocType.Insert(char16_t('.'), 0);
+ }
+
+ nsAutoString appInfo;
+ bool found;
+
+ // Retrieve the default application for this extension
+ NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE);
+ wchar_t* pResult = nullptr;
+ HRESULT hr = mAppAssoc->QueryCurrentDefault(assocType.get(), AT_FILEEXTENSION,
+ AL_EFFECTIVE, &pResult);
+ if (SUCCEEDED(hr)) {
+ found = true;
+ appInfo.Assign(pResult);
+ CoTaskMemFree(pResult);
+ } else {
+ found = false;
+ }
+
+ // Bug 358297 - ignore the default handler, force the user to choose app
+ if (appInfo.EqualsLiteral("XPSViewer.Document")) found = false;
+
+ if (!found) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Get other nsIMIMEInfo fields from registry, if possible.
+ nsAutoString defaultDescription;
+ nsCOMPtr<nsIFile> defaultApplication;
+
+ if (NS_FAILED(GetDefaultAppInfo(appInfo, defaultDescription,
+ getter_AddRefs(defaultApplication)))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aMIMEInfo->SetDefaultDescription(defaultDescription);
+ aMIMEInfo->SetDefaultApplicationHandler(defaultApplication);
+
+ // Grab the general description
+ GetMIMEInfoFromRegistry(appInfo, aMIMEInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt,
+ bool* aFound, nsIMIMEInfo** aMIMEInfo) {
+ *aFound = false;
+
+ const nsCString& flatType = PromiseFlatCString(aMIMEType);
+ nsAutoString fileExtension;
+ CopyUTF8toUTF16(aFileExt, fileExtension);
+
+ /* XXX The octet-stream check is a gross hack to wallpaper over the most
+ * common Win32 extension issues caused by the fix for bug 116938. See bug
+ * 120327, comment 271 for why this is needed. Not even sure we
+ * want to remove this once we have fixed all this stuff to work
+ * right; any info we get from the OS on this type is pretty much
+ * useless....
+ */
+ bool haveMeaningfulMimeType =
+ !aMIMEType.IsEmpty() &&
+ !aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM);
+ LOG("Extension lookup on '%S' with mimetype '%s'%s\n",
+ static_cast<const wchar_t*>(fileExtension.get()), flatType.get(),
+ haveMeaningfulMimeType ? " (treated as meaningful)" : "");
+
+ RefPtr<nsMIMEInfoWin> mi;
+
+ // We should have *something* to go on here.
+ nsAutoString extensionFromMimeType;
+ if (haveMeaningfulMimeType) {
+ GetExtensionFromWindowsMimeDatabase(aMIMEType, extensionFromMimeType);
+ }
+ if (fileExtension.IsEmpty() && extensionFromMimeType.IsEmpty()) {
+ // Without an extension from the mimetype or the file, we can't
+ // do anything here.
+ mi = new nsMIMEInfoWin(flatType.get());
+ mi.forget(aMIMEInfo);
+ return NS_OK;
+ }
+
+ // Either fileExtension or extensionFromMimeType must now be non-empty.
+
+ *aFound = true;
+
+ // On Windows, we prefer the file extension for lookups over the mimetype,
+ // because that's how windows does things.
+ // If we have no file extension or it doesn't match the mimetype, use the
+ // mime type's default file extension instead.
+ bool usedMimeTypeExtensionForLookup = false;
+ if (fileExtension.IsEmpty() ||
+ (!extensionFromMimeType.IsEmpty() &&
+ !typeFromExtEquals(fileExtension.get(), flatType.get()))) {
+ usedMimeTypeExtensionForLookup = true;
+ fileExtension = extensionFromMimeType;
+ LOG("Now using '%s' mimetype's default file extension '%S' for lookup\n",
+ flatType.get(), static_cast<const wchar_t*>(fileExtension.get()));
+ }
+
+ // If we have an extension, use it for lookup:
+ mi = GetByExtension(fileExtension, flatType.get());
+ LOG("Extension lookup on '%S' found: 0x%p\n",
+ static_cast<const wchar_t*>(fileExtension.get()), mi.get());
+
+ if (mi) {
+ bool hasDefault = false;
+ mi->GetHasDefaultHandler(&hasDefault);
+ // If we don't find a default handler description, see if we can find one
+ // using the mimetype.
+ if (!hasDefault && !usedMimeTypeExtensionForLookup) {
+ RefPtr<nsMIMEInfoWin> miFromMimeType =
+ GetByExtension(extensionFromMimeType, flatType.get());
+ LOG("Mime-based ext. lookup for '%S' found 0x%p\n",
+ static_cast<const wchar_t*>(extensionFromMimeType.get()),
+ miFromMimeType.get());
+ if (miFromMimeType) {
+ nsAutoString desc;
+ miFromMimeType->GetDefaultDescription(desc);
+ mi->SetDefaultDescription(desc);
+ }
+ }
+ mi.forget(aMIMEInfo);
+ return NS_OK;
+ }
+
+ // The extension didn't work. Try the extension from the mimetype if
+ // different:
+ if (!extensionFromMimeType.IsEmpty() && !usedMimeTypeExtensionForLookup) {
+ mi = GetByExtension(extensionFromMimeType, flatType.get());
+ LOG("Mime-based ext. lookup for '%S' found 0x%p\n",
+ static_cast<const wchar_t*>(extensionFromMimeType.get()), mi.get());
+ }
+ if (mi) {
+ mi.forget(aMIMEInfo);
+ return NS_OK;
+ }
+ // This didn't work either, so just return an empty dummy mimeinfo.
+ *aFound = false;
+ mi = new nsMIMEInfoWin(flatType.get());
+ // If we didn't resort to the mime type's extension, we must have had a
+ // valid extension, so stick its lowercase version on the mime info.
+ if (!usedMimeTypeExtensionForLookup) {
+ nsAutoCString lowerFileExt;
+ ToLowerCase(aFileExt, lowerFileExt);
+ mi->AppendExtension(lowerFileExt);
+ }
+ mi.forget(aMIMEInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::UpdateDefaultAppInfo(nsIMIMEInfo* aMIMEInfo) {
+ InternalSetDefaultsOnMIME(static_cast<nsMIMEInfoWin*>(aMIMEInfo));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval) {
+ NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!");
+
+ nsresult rv =
+ OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found);
+ if (NS_FAILED(rv)) return rv;
+
+ nsMIMEInfoWin* handlerInfo =
+ new nsMIMEInfoWin(aScheme, nsMIMEInfoBase::eProtocolInfo);
+ NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(*_retval = handlerInfo);
+
+ if (!*found) {
+ // Code that calls this requires an object regardless if the OS has
+ // something for us, so we return the empty object.
+ return NS_OK;
+ }
+
+ nsAutoString desc;
+ GetApplicationDescription(aScheme, desc);
+ handlerInfo->SetDefaultDescription(desc);
+
+ return NS_OK;
+}
+
+bool nsOSHelperAppService::GetMIMETypeFromOSForExtension(
+ const nsACString& aExtension, nsACString& aMIMEType) {
+ if (aExtension.IsEmpty()) return false;
+
+ // windows registry assumes your file extension is going to include the '.'.
+ // so make sure it's there...
+ nsAutoString fileExtToUse;
+ if (aExtension.First() != '.') fileExtToUse = char16_t('.');
+
+ AppendUTF8toUTF16(aExtension, fileExtToUse);
+
+ // Try to get an entry from the windows registry.
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1");
+ if (!regKey) return false;
+
+ nsresult rv =
+ regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse,
+ nsIWindowsRegKey::ACCESS_QUERY_VALUE);
+ if (NS_FAILED(rv)) return false;
+
+ nsAutoString mimeType;
+ if (NS_FAILED(regKey->ReadStringValue(u"Content Type"_ns, mimeType)) ||
+ mimeType.IsEmpty()) {
+ return false;
+ }
+ // Content-Type is always in ASCII
+ aMIMEType.Truncate();
+ LossyAppendUTF16toASCII(mimeType, aMIMEType);
+ return true;
+}
diff --git a/uriloader/exthandler/win/nsOSHelperAppService.h b/uriloader/exthandler/win/nsOSHelperAppService.h
new file mode 100644
index 0000000000..ff7c597217
--- /dev/null
+++ b/uriloader/exthandler/win/nsOSHelperAppService.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 3; 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 nsOSHelperAppService_h__
+#define nsOSHelperAppService_h__
+
+// The OS helper app service is a subclass of nsExternalHelperAppService and is
+// implemented on each platform. It contains platform specific code for finding
+// helper applications for a given mime type in addition to launching those
+// applications.
+
+#include "nsExternalHelperAppService.h"
+#include "nsCExternalHandlerService.h"
+#include "nsCOMPtr.h"
+#include <windows.h>
+#include <shlobj.h>
+
+class nsMIMEInfoWin;
+class nsIMIMEInfo;
+
+class nsOSHelperAppService : public nsExternalHelperAppService {
+ public:
+ nsOSHelperAppService();
+ virtual ~nsOSHelperAppService();
+
+ // override nsIExternalProtocolService methods
+ NS_IMETHOD OSProtocolHandlerExists(const char* aProtocolScheme,
+ bool* aHandlerExists) override;
+ nsresult LoadUriInternal(nsIURI* aURL);
+ NS_IMETHOD GetApplicationDescription(const nsACString& aScheme,
+ nsAString& _retval) override;
+
+ NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval) override;
+
+ // method overrides for windows registry look up steps....
+ NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) override;
+ NS_IMETHOD UpdateDefaultAppInfo(nsIMIMEInfo* aMIMEInfo) override;
+ NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval);
+ virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension,
+ nsACString& aMIMEType) override;
+
+ /** Get the string value of a registry value and store it in result.
+ * @return true on success, false on failure
+ */
+ static bool GetValueString(HKEY hKey, const char16_t* pValueName,
+ nsAString& result);
+
+ protected:
+ nsresult GetDefaultAppInfo(const nsAString& aTypeName,
+ nsAString& aDefaultDescription,
+ nsIFile** aDefaultApplication);
+ // Lookup a mime info by extension, using an optional type hint
+ already_AddRefed<nsMIMEInfoWin> GetByExtension(
+ const nsString& aFileExt, const char* aTypeHint = nullptr);
+ nsresult InternalSetDefaultsOnMIME(nsMIMEInfoWin* aMIMEInfo);
+ nsresult FindOSMimeInfoForType(const char* aMimeContentType, nsIURI* aURI,
+ char** aFileExtension,
+ nsIMIMEInfo** aMIMEInfo);
+
+ static nsresult GetMIMEInfoFromRegistry(const nsString& fileType,
+ nsIMIMEInfo* pInfo);
+ /// Looks up the type for the extension aExt and compares it to aType
+ static bool typeFromExtEquals(const char16_t* aExt, const char* aType);
+
+ private:
+ IApplicationAssociationRegistration* mAppAssoc;
+};
+
+#endif // nsOSHelperAppService_h__