summaryrefslogtreecommitdiffstats
path: root/uriloader/exthandler
diff options
context:
space:
mode:
Diffstat (limited to 'uriloader/exthandler')
-rw-r--r--uriloader/exthandler/ContentHandlerService.cpp248
-rw-r--r--uriloader/exthandler/ContentHandlerService.h53
-rw-r--r--uriloader/exthandler/DBusHelpers.h84
-rw-r--r--uriloader/exthandler/ExternalHelperAppChild.cpp93
-rw-r--r--uriloader/exthandler/ExternalHelperAppChild.h46
-rw-r--r--uriloader/exthandler/ExternalHelperAppParent.cpp446
-rw-r--r--uriloader/exthandler/ExternalHelperAppParent.h114
-rw-r--r--uriloader/exthandler/HandlerService.js675
-rw-r--r--uriloader/exthandler/HandlerService.manifest2
-rw-r--r--uriloader/exthandler/HandlerServiceChild.h25
-rw-r--r--uriloader/exthandler/HandlerServiceParent.cpp379
-rw-r--r--uriloader/exthandler/HandlerServiceParent.h66
-rw-r--r--uriloader/exthandler/PExternalHelperApp.ipdl28
-rw-r--r--uriloader/exthandler/PHandlerService.ipdl61
-rw-r--r--uriloader/exthandler/WebHandlerApp.jsm150
-rw-r--r--uriloader/exthandler/android/nsAndroidHandlerApp.cpp86
-rw-r--r--uriloader/exthandler/android/nsAndroidHandlerApp.h33
-rw-r--r--uriloader/exthandler/android/nsExternalURLHandlerService.cpp21
-rw-r--r--uriloader/exthandler/android/nsExternalURLHandlerService.h22
-rw-r--r--uriloader/exthandler/android/nsMIMEInfoAndroid.cpp414
-rw-r--r--uriloader/exthandler/android/nsMIMEInfoAndroid.h62
-rw-r--r--uriloader/exthandler/android/nsOSHelperAppService.cpp70
-rw-r--r--uriloader/exthandler/android/nsOSHelperAppService.h38
-rw-r--r--uriloader/exthandler/components.conf14
-rw-r--r--uriloader/exthandler/docs/index.rst76
-rw-r--r--uriloader/exthandler/mac/nsDecodeAppleFile.cpp357
-rw-r--r--uriloader/exthandler/mac/nsDecodeAppleFile.h116
-rw-r--r--uriloader/exthandler/mac/nsLocalHandlerAppMac.h26
-rw-r--r--uriloader/exthandler/mac/nsLocalHandlerAppMac.mm78
-rw-r--r--uriloader/exthandler/mac/nsMIMEInfoMac.h33
-rw-r--r--uriloader/exthandler/mac/nsMIMEInfoMac.mm101
-rw-r--r--uriloader/exthandler/mac/nsOSHelperAppService.h53
-rw-r--r--uriloader/exthandler/mac/nsOSHelperAppService.mm591
-rw-r--r--uriloader/exthandler/moz.build152
-rw-r--r--uriloader/exthandler/nsCExternalHandlerService.idl33
-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.cpp3096
-rw-r--r--uriloader/exthandler/nsExternalHelperAppService.h512
-rw-r--r--uriloader/exthandler/nsExternalProtocolHandler.cpp545
-rw-r--r--uriloader/exthandler/nsExternalProtocolHandler.h37
-rw-r--r--uriloader/exthandler/nsIContentDispatchChooser.idl38
-rw-r--r--uriloader/exthandler/nsIExternalHelperAppService.idl179
-rw-r--r--uriloader/exthandler/nsIExternalProtocolService.idl140
-rw-r--r--uriloader/exthandler/nsIExternalURLHandlerService.idl26
-rw-r--r--uriloader/exthandler/nsIHandlerService.idl162
-rw-r--r--uriloader/exthandler/nsIHelperAppLauncherDialog.idl90
-rw-r--r--uriloader/exthandler/nsISharingHandlerApp.idl12
-rw-r--r--uriloader/exthandler/nsLocalHandlerApp.cpp157
-rw-r--r--uriloader/exthandler/nsLocalHandlerApp.h59
-rw-r--r--uriloader/exthandler/nsMIMEInfoChild.h54
-rw-r--r--uriloader/exthandler/nsMIMEInfoImpl.cpp467
-rw-r--r--uriloader/exthandler/nsMIMEInfoImpl.h213
-rw-r--r--uriloader/exthandler/nsOSHelperAppServiceChild.cpp129
-rw-r--r--uriloader/exthandler/nsOSHelperAppServiceChild.h48
-rw-r--r--uriloader/exthandler/tests/HandlerServiceTestUtils.jsm241
-rw-r--r--uriloader/exthandler/tests/WriteArgument.cpp20
-rw-r--r--uriloader/exthandler/tests/mochitest/.eslintrc.js5
-rw-r--r--uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js38
-rw-r--r--uriloader/exthandler/tests/mochitest/browser.ini51
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_auto_close_window.js271
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js25
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js612
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js70
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_download_urlescape.js75
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_extension_correction.js145
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js70
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js255
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js398
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js754
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js76
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_remember_download_option.js61
-rw-r--r--uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js124
-rw-r--r--uriloader/exthandler/tests/mochitest/download.bin1
-rw-r--r--uriloader/exthandler/tests/mochitest/download.sjs38
-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_nested_protocol_request.html1
-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/handlerApp.xhtml28
-rw-r--r--uriloader/exthandler/tests/mochitest/handlerApps.js118
-rw-r--r--uriloader/exthandler/tests/mochitest/head.js277
-rw-r--r--uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs14
-rw-r--r--uriloader/exthandler/tests/mochitest/mochitest.ini23
-rw-r--r--uriloader/exthandler/tests/mochitest/protocolHandler.html16
-rw-r--r--uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml11
-rw-r--r--uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml47
-rw-r--r--uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml49
-rw-r--r--uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html28
-rw-r--r--uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml72
-rw-r--r--uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs14
-rw-r--r--uriloader/exthandler/tests/moz.build31
-rw-r--r--uriloader/exthandler/tests/unit/handlers.json90
-rw-r--r--uriloader/exthandler/tests/unit/head.js79
-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.js163
-rw-r--r--uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js36
-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.js190
-rw-r--r--uriloader/exthandler/tests/unit/test_handlerService.js474
-rw-r--r--uriloader/exthandler/tests/unit/test_handlerService_store.js771
-rw-r--r--uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js117
-rw-r--r--uriloader/exthandler/tests/unit/test_punycodeURIs.js130
-rw-r--r--uriloader/exthandler/tests/unit/xpcshell.ini28
-rw-r--r--uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h27
-rw-r--r--uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm15
-rw-r--r--uriloader/exthandler/uikit/nsMIMEInfoUIKit.h31
-rw-r--r--uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm12
-rw-r--r--uriloader/exthandler/uikit/nsOSHelperAppService.h54
-rw-r--r--uriloader/exthandler/uikit/nsOSHelperAppService.mm53
-rw-r--r--uriloader/exthandler/unix/nsGNOMERegistry.cpp100
-rw-r--r--uriloader/exthandler/unix/nsGNOMERegistry.h28
-rw-r--r--uriloader/exthandler/unix/nsMIMEInfoUnix.cpp80
-rw-r--r--uriloader/exthandler/unix/nsMIMEInfoUnix.h30
-rw-r--r--uriloader/exthandler/unix/nsOSHelperAppService.cpp1409
-rw-r--r--uriloader/exthandler/unix/nsOSHelperAppService.h125
-rw-r--r--uriloader/exthandler/win/nsMIMEInfoWin.cpp900
-rw-r--r--uriloader/exthandler/win/nsMIMEInfoWin.h72
-rw-r--r--uriloader/exthandler/win/nsOSHelperAppService.cpp589
-rw-r--r--uriloader/exthandler/win/nsOSHelperAppService.h79
140 files changed, 20954 insertions, 0 deletions
diff --git a/uriloader/exthandler/ContentHandlerService.cpp b/uriloader/exthandler/ContentHandlerService.cpp
new file mode 100644
index 0000000000..96eb6a0cbb
--- /dev/null
+++ b/uriloader/exthandler/ContentHandlerService.cpp
@@ -0,0 +1,248 @@
+/* -*- 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"
+
+using mozilla::dom::ContentChild;
+using mozilla::dom::HandlerInfo;
+using mozilla::dom::PHandlerServiceChild;
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(ContentHandlerService, nsIHandlerService)
+
+ContentHandlerService::ContentHandlerService() {}
+
+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_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 CopyHanderInfoTonsIHandlerInfo(
+ 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);
+
+ 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, nsCString(aOverrideType),
+ &returnedInfo);
+ CopyHanderInfoTonsIHandlerInfo(returnedInfo, aHandlerInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP ContentHandlerService::GetMIMEInfoFromOS(
+ nsIHandlerInfo* aHandlerInfo, const nsACString& aMIMEType,
+ const nsACString& aExtension, bool* aFound) {
+ nsresult rv = NS_ERROR_FAILURE;
+ HandlerInfo returnedInfo;
+ if (!mHandlerServiceChild->SendGetMIMEInfoFromOS(nsCString(aMIMEType),
+ nsCString(aExtension), &rv,
+ &returnedInfo, aFound)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ CopyHanderInfoTonsIHandlerInfo(returnedInfo, aHandlerInfo);
+ 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(nsCString(aProtocolScheme),
+ aRetval)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ContentHandlerService::ExistsForProtocol(const nsACString& aProtocolScheme,
+ bool* aRetval) {
+ if (!mHandlerServiceChild->SendExistsForProtocol(nsCString(aProtocolScheme),
+ aRetval)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP ContentHandlerService::GetTypeFromExtension(
+ const nsACString& aFileExtension, nsACString& _retval) {
+ nsCString* cachedType = nullptr;
+ if (!!mExtToTypeMap.Get(aFileExtension, &cachedType) && !!cachedType) {
+ _retval.Assign(*cachedType);
+ return NS_OK;
+ }
+ nsCString type;
+ mHandlerServiceChild->SendGetTypeFromExtension(nsCString(aFileExtension),
+ &type);
+ _retval.Assign(type);
+ mExtToTypeMap.Put(nsCString(aFileExtension), new 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..81d0c44148
--- /dev/null
+++ b/uriloader/exthandler/ContentHandlerService.h
@@ -0,0 +1,53 @@
+/* -*- 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);
+
+ 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/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..228d7e45b3
--- /dev/null
+++ b/uriloader/exthandler/ExternalHelperAppChild.h
@@ -0,0 +1,46 @@
+/* -*- 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 {
+namespace 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 dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ExternalHelperAppChild_h
diff --git a/uriloader/exthandler/ExternalHelperAppParent.cpp b/uriloader/exthandler/ExternalHelperAppParent.cpp
new file mode 100644
index 0000000000..1a88e54df7
--- /dev/null
+++ b/uriloader/exthandler/ExternalHelperAppParent.cpp
@@ -0,0 +1,446 @@
+/* -*- 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 "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 nsCString& aContentDispositionHeader,
+ const uint32_t& aContentDispositionHint,
+ const nsString& 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;
+ }
+}
+
+void ExternalHelperAppParent::Init(
+ const Maybe<mozilla::net::LoadInfoArgs>& aLoadInfoArgs,
+ const nsCString& aMimeContentType, const bool& aForceSave,
+ nsIURI* aReferrer, BrowsingContext* aContext,
+ const bool& aShouldCloseWindow) {
+ mozilla::ipc::LoadInfoArgsToLoadInfo(aLoadInfoArgs,
+ getter_AddRefs(mLoadInfo));
+
+ 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 (aShouldCloseWindow) {
+ RefPtr<nsExternalAppHandler> handler = do_QueryObject(mListener);
+ if (handler) {
+ handler->SetShouldCloseWindow();
+ }
+ }
+}
+
+void ExternalHelperAppParent::ActorDestroy(ActorDestroyReason why) {
+ mIPCClosed = true;
+}
+
+void ExternalHelperAppParent::Delete() {
+ if (!mIPCClosed) {
+ Unused << Send__delete__(this);
+ }
+}
+
+mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnStartRequest(
+ const nsCString& entityID) {
+ mEntityID = entityID;
+ mPending = true;
+ mStatus = mListener->OnStartRequest(this);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnDataAvailable(
+ const nsCString& 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::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(nsISupports** 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::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..71ac04cee8
--- /dev/null
+++ b/uriloader/exthandler/ExternalHelperAppParent.h
@@ -0,0 +1,114 @@
+/* -*- 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/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 nsCString& entityID) override;
+ mozilla::ipc::IPCResult RecvOnDataAvailable(const nsCString& 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 nsCString& aContentDispositionHeader,
+ const uint32_t& aContentDispositionHint,
+ const nsString& aContentDispositionFilename);
+ void Init(const Maybe<mozilla::net::LoadInfoArgs>& aLoadInfoArgs,
+ const nsCString& 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
diff --git a/uriloader/exthandler/HandlerService.js b/uriloader/exthandler/HandlerService.js
new file mode 100644
index 0000000000..085ddab31c
--- /dev/null
+++ b/uriloader/exthandler/HandlerService.js
@@ -0,0 +1,675 @@
+/* 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 { ComponentUtils } = ChromeUtils.import(
+ "resource://gre/modules/ComponentUtils.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "JSONFile",
+ "resource://gre/modules/JSONFile.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gExternalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+
+function HandlerService() {
+ // Observe handlersvc-json-replace so we can switch to the datasource
+ Services.obs.addObserver(this, "handlersvc-json-replace", true);
+}
+
+HandlerService.prototype = {
+ classID: Components.ID("{220cc253-b60f-41f6-b9cf-fdcb325f970f}"),
+ QueryInterface: ChromeUtils.generateQI([
+ "nsISupportsWeakReference",
+ "nsIHandlerService",
+ "nsIObserver",
+ ]),
+
+ __store: null,
+ get _store() {
+ if (!this.__store) {
+ this.__store = new 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: {},
+ };
+ },
+
+ /**
+ * Injects new default protocol handlers if the version in the preferences is
+ * newer than the one in the data store.
+ */
+ _injectDefaultProtocolHandlersIfNeeded() {
+ let prefsDefaultHandlersVersion;
+ try {
+ prefsDefaultHandlersVersion = Services.prefs.getComplexValue(
+ "gecko.handlerService.defaultHandlersVersion",
+ Ci.nsIPrefLocalizedString
+ );
+ } catch (ex) {
+ if (
+ ex instanceof Components.Exception &&
+ ex.result == Cr.NS_ERROR_UNEXPECTED
+ ) {
+ // This platform does not have any default protocol handlers configured.
+ return;
+ }
+ throw ex;
+ }
+
+ try {
+ prefsDefaultHandlersVersion = Number(prefsDefaultHandlersVersion.data);
+ let locale = Services.locale.appLocaleAsBCP47;
+
+ let defaultHandlersVersion =
+ this._store.data.defaultHandlersVersion[locale] || 0;
+ if (defaultHandlersVersion < prefsDefaultHandlersVersion) {
+ this._injectDefaultProtocolHandlers();
+ this._store.data.defaultHandlersVersion[
+ locale
+ ] = prefsDefaultHandlersVersion;
+ // Now save the result:
+ this._store.saveSoon();
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+
+ _injectDefaultProtocolHandlers() {
+ let schemesPrefBranch = Services.prefs.getBranch(
+ "gecko.handlerService.schemes."
+ );
+ let schemePrefList = schemesPrefBranch.getChildList("");
+
+ let schemes = {};
+
+ // read all the scheme prefs into a hash
+ for (let schemePrefName of schemePrefList) {
+ let [scheme, handlerNumber, attribute] = schemePrefName.split(".");
+
+ try {
+ let attrData = schemesPrefBranch.getComplexValue(
+ schemePrefName,
+ Ci.nsIPrefLocalizedString
+ ).data;
+ if (!(scheme in schemes)) {
+ schemes[scheme] = {};
+ }
+
+ if (!(handlerNumber in schemes[scheme])) {
+ schemes[scheme][handlerNumber] = {};
+ }
+
+ schemes[scheme][handlerNumber][attribute] = attrData;
+ } catch (ex) {}
+ }
+
+ // Now drop any entries without a uriTemplate, or with a broken one.
+ // The Array.from calls ensure we can safely delete things without
+ // affecting the iterator.
+ for (let [scheme, handlerObject] of Array.from(Object.entries(schemes))) {
+ let handlers = Array.from(Object.entries(handlerObject));
+ let validHandlers = 0;
+ for (let [key, obj] of handlers) {
+ if (
+ !obj.uriTemplate ||
+ !obj.uriTemplate.startsWith("https://") ||
+ !obj.uriTemplate.toLowerCase().includes("%s")
+ ) {
+ delete handlerObject[key];
+ } else {
+ validHandlers++;
+ }
+ }
+ if (!validHandlers) {
+ delete 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(schemes)) {
+ 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 handlerNumber of Object.keys(schemes[scheme])) {
+ let newHandler = schemes[scheme][handlerNumber];
+ // 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 = gExternalProtocolService.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);
+ }
+ }
+ },
+ };
+ let migrationsToRun = Services.prefs.getCharPref(
+ "browser.handlers.migrations",
+ ""
+ );
+ migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : [];
+ for (let migration of migrationsToRun) {
+ migration.trim();
+ try {
+ kMigrations[migration]();
+ } catch (ex) {
+ Cu.reportError(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(Cu.reportError);
+ },
+
+ // 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 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(Cu.reportError);
+ }
+ },
+
+ // nsIHandlerService
+ enumerate() {
+ let handlers = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ for (let type of Object.keys(this._store.data.mimeTypes)) {
+ let handler = gMIMEService.getFromTypeAndExtension(type, null);
+ 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 = gExternalProtocolService.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 == Ci.nsIHandlerInfo.saveToDisk ||
+ handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault ||
+ handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally
+ ) {
+ storedHandlerInfo.action = handlerInfo.preferredAction;
+ } else {
+ storedHandlerInfo.action = Ci.nsIHandlerInfo.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:
+ gExternalProtocolService.setProtocolHandlerDefaults(
+ handlerInfo,
+ handlerInfo.hasDefaultHandler
+ );
+ if (
+ handlerInfo.preferredAction == Ci.nsIHandlerInfo.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 = Ci.nsIHandlerInfo.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);
+ }
+ }
+ },
+
+ /**
+ * 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 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 "";
+ },
+};
+
+this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]);
diff --git a/uriloader/exthandler/HandlerService.manifest b/uriloader/exthandler/HandlerService.manifest
new file mode 100644
index 0000000000..854d13adab
--- /dev/null
+++ b/uriloader/exthandler/HandlerService.manifest
@@ -0,0 +1,2 @@
+component {220cc253-b60f-41f6-b9cf-fdcb325f970f} HandlerService.js
+contract @mozilla.org/uriloader/handler-service;1 {220cc253-b60f-41f6-b9cf-fdcb325f970f} process=main
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..8f73c7cb8c
--- /dev/null
+++ b/uriloader/exthandler/HandlerServiceParent.cpp
@@ -0,0 +1,379 @@
+/* -*- 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 "ContentHandlerService.h"
+#include "nsStringEnumerator.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 nsCString& 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 nsCString& aMIMEType, const nsCString& 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 nsCString& 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(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 nsCString& aProtocolScheme, bool* aHandlerExists) {
+ if (aProtocolScheme.Length() > MAX_SCHEME_LENGTH) {
+ *aHandlerExists = false;
+ return IPC_OK();
+ }
+#if defined(XP_MACOSX)
+ // 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(aProtocolScheme.get(),
+ aHandlerExists);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aHandlerExists = false;
+ }
+#else
+ MOZ_RELEASE_ASSERT(false, "No implementation on this platform.");
+ *aHandlerExists = false;
+#endif
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult HandlerServiceParent::RecvGetTypeFromExtension(
+ const nsCString& 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 nsCString& 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..e98e2f589e
--- /dev/null
+++ b/uriloader/exthandler/HandlerServiceParent.h
@@ -0,0 +1,66 @@
+/* -*- 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 nsCString& aOverrideType,
+ HandlerInfo* handlerInfoData) override;
+
+ mozilla::ipc::IPCResult RecvGetMIMEInfoFromOS(const nsCString& aMIMEType,
+ const nsCString& aExtension,
+ nsresult* aRv,
+ HandlerInfo* aHandlerInfoData,
+ bool* aFound) override;
+
+ mozilla::ipc::IPCResult RecvExists(const HandlerInfo& aHandlerInfo,
+ bool* exists) override;
+
+ mozilla::ipc::IPCResult RecvGetTypeFromExtension(
+ const nsCString& aFileExtension, nsCString* type) override;
+
+ mozilla::ipc::IPCResult RecvExistsForProtocolOS(
+ const nsCString& aProtocolScheme, bool* aHandlerExists) override;
+
+ mozilla::ipc::IPCResult RecvExistsForProtocol(
+ const nsCString& aProtocolScheme, bool* aHandlerExists) override;
+
+ mozilla::ipc::IPCResult RecvGetApplicationDescription(
+ const nsCString& 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..b3d1e7c736
--- /dev/null
+++ b/uriloader/exthandler/PExternalHelperApp.ipdl
@@ -0,0 +1,28 @@
+/* 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 {
+
+refcounted 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..981c1839c4
--- /dev/null
+++ b/uriloader/exthandler/PHandlerService.ipdl
@@ -0,0 +1,61 @@
+/* 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;
+ long preferredAction;
+};
+
+sync refcounted 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.jsm b/uriloader/exthandler/WebHandlerApp.jsm
new file mode 100644
index 0000000000..bac75230f3
--- /dev/null
+++ b/uriloader/exthandler/WebHandlerApp.jsm
@@ -0,0 +1,150 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+
+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...
+
+ // encode the URI to be handled
+ var escapedUriSpecToHandle = encodeURIComponent(aURI.spec);
+
+ // insert the encoded URI and create the object version.
+ var uriSpecToSend = this.uriTemplate.replace("%s", escapedUriSpecToHandle);
+ var uriToSend = Services.io.newURI(uriSpecToSend);
+
+ let policy = WebExtensionPolicy.getByURI(uriToSend);
+ let privateAllowed = !policy || policy.privateBrowsingAllowed;
+
+ // 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) {
+ let { scheme } = aURI;
+ if (!scheme.startsWith("web+") && !scheme.startsWith("ext+")) {
+ 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(uriSpecToSend, { triggeringPrincipal })
+ );
+ return;
+ }
+
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // If this is an extension handler, check private browsing access.
+ if (!privateAllowed && 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;
+ },
+};
+
+var EXPORTED_SYMBOLS = ["nsWebHandlerApp"];
diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.cpp b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp
new file mode 100644
index 0000000000..de9f6be509
--- /dev/null
+++ b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp
@@ -0,0 +1,86 @@
+/* -*- 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 "mozilla/java/GeckoAppShellWrappers.h"
+#include "nsAndroidHandlerApp.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsAndroidHandlerApp, nsIHandlerApp, nsISharingHandlerApp)
+
+nsAndroidHandlerApp::nsAndroidHandlerApp(const nsAString& aName,
+ const nsAString& aDescription,
+ const nsAString& aPackageName,
+ const nsAString& aClassName,
+ const nsACString& aMimeType,
+ const nsAString& aAction)
+ : mName(aName),
+ mDescription(aDescription),
+ mPackageName(aPackageName),
+ mClassName(aClassName),
+ mMimeType(aMimeType),
+ mAction(aAction) {}
+
+nsAndroidHandlerApp::~nsAndroidHandlerApp() {}
+
+NS_IMETHODIMP
+nsAndroidHandlerApp::GetName(nsAString& aName) {
+ aName.Assign(mName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHandlerApp::SetName(const nsAString& aName) {
+ mName.Assign(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHandlerApp::GetDetailedDescription(nsAString& aDescription) {
+ aDescription.Assign(mDescription);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHandlerApp::SetDetailedDescription(const nsAString& aDescription) {
+ mDescription.Assign(aDescription);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHandlerApp::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(mName) && detailedDescription.Equals(mDescription);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHandlerApp::LaunchWithURI(
+ nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) {
+ nsCString uriSpec;
+ aURI->GetSpec(uriSpec);
+ return java::GeckoAppShell::OpenUriExternal(uriSpec, mMimeType, mPackageName,
+ mClassName, mAction, u""_ns)
+ ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsAndroidHandlerApp::Share(const nsAString& data, const nsAString& title) {
+ return java::GeckoAppShell::OpenUriExternal(data, mMimeType, mPackageName,
+ mClassName, mAction, u""_ns)
+ ? NS_OK
+ : NS_ERROR_FAILURE;
+}
diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.h b/uriloader/exthandler/android/nsAndroidHandlerApp.h
new file mode 100644
index 0000000000..dab4042fd8
--- /dev/null
+++ b/uriloader/exthandler/android/nsAndroidHandlerApp.h
@@ -0,0 +1,33 @@
+/* -*- 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 nsAndroidHandlerApp_h
+#define nsAndroidHandlerApp_h
+
+#include "nsMIMEInfoImpl.h"
+#include "nsISharingHandlerApp.h"
+
+class nsAndroidHandlerApp : public nsISharingHandlerApp {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHANDLERAPP
+ NS_DECL_NSISHARINGHANDLERAPP
+
+ nsAndroidHandlerApp(const nsAString& aName, const nsAString& aDescription,
+ const nsAString& aPackageName,
+ const nsAString& aClassName, const nsACString& aMimeType,
+ const nsAString& aAction);
+
+ private:
+ virtual ~nsAndroidHandlerApp();
+
+ nsString mName;
+ nsString mDescription;
+ nsString mPackageName;
+ nsString mClassName;
+ nsCString mMimeType;
+ nsString mAction;
+};
+#endif
diff --git a/uriloader/exthandler/android/nsExternalURLHandlerService.cpp b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp
new file mode 100644
index 0000000000..60ccbbdfe9
--- /dev/null
+++ b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp
@@ -0,0 +1,21 @@
+/* -*- 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 "nsExternalURLHandlerService.h"
+#include "nsMIMEInfoAndroid.h"
+
+NS_IMPL_ISUPPORTS(nsExternalURLHandlerService, nsIExternalURLHandlerService)
+
+nsExternalURLHandlerService::nsExternalURLHandlerService() {}
+
+nsExternalURLHandlerService::~nsExternalURLHandlerService() {}
+
+NS_IMETHODIMP
+nsExternalURLHandlerService::GetURLHandlerInfoFromOS(nsIURI* aURL, 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/nsExternalURLHandlerService.h b/uriloader/exthandler/android/nsExternalURLHandlerService.h
new file mode 100644
index 0000000000..18389c5286
--- /dev/null
+++ b/uriloader/exthandler/android/nsExternalURLHandlerService.h
@@ -0,0 +1,22 @@
+/* -*- 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 NSEXTERNALURLHANDLERSERVICE_H
+#define NSEXTERNALURLHANDLERSERVICE_H
+
+#include "nsIExternalURLHandlerService.h"
+
+class nsExternalURLHandlerService final : public nsIExternalURLHandlerService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIEXTERNALURLHANDLERSERVICE
+
+ nsExternalURLHandlerService();
+
+ private:
+ ~nsExternalURLHandlerService();
+};
+
+#endif // NSEXTERNALURLHANDLERSERVICE_H
diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp
new file mode 100644
index 0000000000..6d361d32c2
--- /dev/null
+++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp
@@ -0,0 +1,414 @@
+/* -*- 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 "AndroidBridge.h"
+#include "nsAndroidHandlerApp.h"
+#include "nsArrayUtils.h"
+#include "nsISupportsUtils.h"
+#include "nsStringEnumerator.h"
+#include "nsNetUtil.h"
+#include "mozilla/Utf8.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) {
+ nsCString uriSpec;
+ aURI->GetSpec(uriSpec);
+
+ nsCString uriScheme;
+ aURI->GetScheme(uriScheme);
+
+ nsAutoString mimeType;
+ if (mType.Equals(uriScheme) || mType.Equals(uriSpec)) {
+ mimeType.Truncate();
+ } else {
+ CopyUTF8toUTF16(mType, mimeType);
+ }
+
+ if (java::GeckoAppShell::OpenUriExternal(NS_ConvertUTF8toUTF16(uriSpec),
+ mimeType, u""_ns, u""_ns, u""_ns,
+ u""_ns)) {
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+bool nsMIMEInfoAndroid::GetMimeInfoForMimeType(const nsACString& aMimeType,
+ nsMIMEInfoAndroid** aMimeInfo) {
+ RefPtr<nsMIMEInfoAndroid> info = new nsMIMEInfoAndroid(aMimeType);
+ mozilla::AndroidBridge* bridge = mozilla::AndroidBridge::Bridge();
+ // we don't have access to the bridge, so just assume we can handle
+ // the mime type for now and let the system deal with it
+ if (!bridge) {
+ info.forget(aMimeInfo);
+ return false;
+ }
+
+ nsIHandlerApp* systemDefault = nullptr;
+
+ if (!IsUtf8(aMimeType)) return false;
+
+ NS_ConvertUTF8toUTF16 mimeType(aMimeType);
+
+ bridge->GetHandlersForMimeType(mimeType, info->mHandlerApps, &systemDefault);
+
+ if (systemDefault) info->mPrefApp = systemDefault;
+
+ nsAutoCString fileExt;
+ bridge->GetExtensionFromMimeType(aMimeType, fileExt);
+ info->SetPrimaryExtension(fileExt);
+
+ uint32_t len;
+ info->mHandlerApps->GetLength(&len);
+ if (len == 1) {
+ info.forget(aMimeInfo);
+ return false;
+ }
+
+ info.forget(aMimeInfo);
+ return true;
+}
+
+bool nsMIMEInfoAndroid::GetMimeInfoForFileExt(const nsACString& aFileExt,
+ nsMIMEInfoAndroid** aMimeInfo) {
+ nsCString mimeType;
+ if (mozilla::AndroidBridge::Bridge())
+ mozilla::AndroidBridge::Bridge()->GetMimeTypeFromExtensions(aFileExt,
+ mimeType);
+
+ // "*/*" 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 = true;
+
+ mozilla::AndroidBridge* bridge = mozilla::AndroidBridge::Bridge();
+ if (!bridge) {
+ // we don't have access to the bridge, so just assume we can handle
+ // the protocol for now and let the system deal with it
+ return NS_OK;
+ }
+
+ nsIHandlerApp* systemDefault = nullptr;
+ bridge->GetHandlersForURL(NS_ConvertUTF8toUTF16(aURL), mimeinfo->mHandlerApps,
+ &systemDefault);
+
+ if (systemDefault) mimeinfo->mPrefApp = systemDefault;
+
+ nsAutoCString fileExt;
+ nsAutoCString mimeType;
+ mimeinfo->GetType(mimeType);
+ bridge->GetExtensionFromMimeType(mimeType, fileExt);
+ mimeinfo->SetPrimaryExtension(fileExt);
+
+ uint32_t len;
+ mimeinfo->mHandlerApps->GetLength(&len);
+ if (len == 1) {
+ // Code that calls this requires an object regardless if the OS has
+ // something for us, so we return the empty object.
+ *found = false;
+ return NS_OK;
+ }
+
+ 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 (mozilla::AndroidBridge::Bridge()) {
+ mozilla::AndroidBridge::Bridge()->GetMimeTypeFromExtensions(aExtension,
+ mimeType);
+ }
+
+ // "*/*" 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..bdac8f97f9
--- /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 "nsAndroidHandlerApp.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..9849b66075
--- /dev/null
+++ b/uriloader/exthandler/android/nsOSHelperAppService.cpp
@@ -0,0 +1,70 @@
+/* -*- 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"
+#include "AndroidBridge.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;
+}
+
+nsIHandlerApp* nsOSHelperAppService::CreateAndroidHandlerApp(
+ const nsAString& aName, const nsAString& aDescription,
+ const nsAString& aPackageName, const nsAString& aClassName,
+ const nsACString& aMimeType, const nsAString& aAction) {
+ return new nsAndroidHandlerApp(aName, aDescription, aPackageName, aClassName,
+ aMimeType, aAction);
+}
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..1379c13ffd
--- /dev/null
+++ b/uriloader/exthandler/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{8b1ae382-51a9-4972-b930-56977a57919d}',
+ 'contract_ids': ['@mozilla.org/uriloader/web-handler-app;1'],
+ 'jsm': 'resource://gre/modules/WebHandlerApp.jsm',
+ '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..29c1adf941
--- /dev/null
+++ b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp
@@ -0,0 +1,357 @@
+/* -*- 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::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..7cb09a6622
--- /dev/null
+++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm
@@ -0,0 +1,78 @@
+/* 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_ABORT_BLOCK_NSRESULT;
+
+ 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_ABORT_BLOCK_NSRESULT;
+}
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..ab4db322af
--- /dev/null
+++ b/uriloader/exthandler/mac/nsMIMEInfoMac.mm
@@ -0,0 +1,101 @@
+/* -*- 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) {
+ if (mDefaultApplication) {
+ nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(mDefaultApplication);
+ 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_ABORT_BLOCK_NSRESULT;
+
+ nsCOMPtr<nsIFile> application;
+ nsresult rv;
+
+ NS_ASSERTION(mClass == eMIMEInfo, "only MIME infos are currently allowed"
+ "to pass content by value");
+
+ 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 = mDefaultApplication;
+ } 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_ABORT_BLOCK_NSRESULT;
+}
+
+nsresult nsMIMEInfoMac::LoadUriInternal(nsIURI* aURI) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+ 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_ABORT_BLOCK_NSRESULT;
+}
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..5585fb67d2
--- /dev/null
+++ b/uriloader/exthandler/mac/nsOSHelperAppService.mm
@@ -0,0 +1,591 @@
+/* -*- 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 "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 "nsMemory.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_ABORT_BLOCK_NSRESULT;
+
+ 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) {
+ if (@available(macOS 10.10, *)) {
+ *aBundleURL = ::LSCopyDefaultApplicationURLForURL(lookupCFURL, kLSRolesAll, NULL);
+ if (*aBundleURL) {
+ rv = NS_OK;
+ }
+ } else {
+ OSStatus theErr = ::LSGetApplicationForURL(lookupCFURL, kLSRolesAll, NULL, aBundleURL);
+ if (theErr == noErr && *aBundleURL) {
+ rv = NS_OK;
+ }
+ }
+
+ ::CFRelease(lookupCFURL);
+ }
+
+ ::CFRelease(lookupCFString);
+ }
+
+ ::CFRelease(schemeCFString);
+ }
+
+ return rv;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+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_ABORT_BLOCK_NSRESULT;
+
+ 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_ABORT_BLOCK_NSRESULT;
+}
+
+NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+ 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_ABORT_BLOCK_NSRESULT;
+}
+
+nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath,
+ nsIFile** aFile) {
+ NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
+
+ 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_ABORT_BLOCK_NSRESULT;
+}
+
+// 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_ABORT_BLOCK_NSNULL;
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ *aFound = false;
+
+ const nsCString& flatType = PromiseFlatCString(aMIMEType);
+ const nsCString& flatExt = PromiseFlatCString(aFileExt);
+
+ MOZ_LOG(mLog, 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(mLog, 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(mLog, 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::useSystemDefault);
+ } 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);
+ }
+ }
+
+ CFStringRef cfType = ::CFStringCreateWithCString(NULL, mimeType.get(), kCFStringEncodingUTF8);
+ if (cfType) {
+ CFStringRef cfTypeDesc = NULL;
+ if (::LSCopyKindStringForMIMEType(cfType, &cfTypeDesc) == noErr) {
+ 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);
+ }
+ if (cfTypeDesc) {
+ ::CFRelease(cfTypeDesc);
+ }
+ ::CFRelease(cfType);
+ }
+ }
+
+ MOZ_LOG(mLog, 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_ABORT_BLOCK_NSRESULT;
+}
+
+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..0a668f9922
--- /dev/null
+++ b/uriloader/exthandler/moz.build
@@ -0,0 +1,152 @@
+# -*- 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"
+
+TEST_DIRS += ["tests"]
+
+XPIDL_SOURCES += [
+ "nsCExternalHandlerService.idl",
+ "nsIContentDispatchChooser.idl",
+ "nsIExternalHelperAppService.idl",
+ "nsIExternalProtocolService.idl",
+ "nsIExternalURLHandlerService.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",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ EXPORTS += [
+ "%s/%s" % (osdir, f)
+ for f in [
+ "nsExternalURLHandlerService.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_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["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += ["-Wno-error=shadow"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ UNIFIED_SOURCES += [
+ "unix/nsGNOMERegistry.cpp",
+ "unix/nsMIMEInfoUnix.cpp",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ UNIFIED_SOURCES += [
+ "android/nsAndroidHandlerApp.cpp",
+ "android/nsExternalURLHandlerService.cpp",
+ "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_COMPONENTS += [
+ "HandlerService.js",
+ "HandlerService.manifest",
+]
+
+EXTRA_JS_MODULES += [
+ "WebHandlerApp.jsm",
+]
+
+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["TK_CFLAGS"]
+ CXXFLAGS += CONFIG["MOZ_DBUS_CFLAGS"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["TK_CFLAGS"]
+ CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"]
diff --git a/uriloader/exthandler/nsCExternalHandlerService.idl b/uriloader/exthandler/nsCExternalHandlerService.idl
new file mode 100644
index 0000000000..bdf59e9ae6
--- /dev/null
+++ b/uriloader/exthandler/nsCExternalHandlerService.idl
@@ -0,0 +1,33 @@
+/* -*- 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..b78eac71d5
--- /dev/null
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -0,0 +1,3096 @@
+/* -*- 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/WindowGlobalParent.h"
+#include "mozilla/StaticPrefs_security.h"
+#include "nsXULAppAPI.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 "nsString.h"
+#include "nsUnicharUtils.h"
+#include "nsIStringEnumerator.h"
+#include "nsMemory.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"
+
+// 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"
+// 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 "nsPluginHost.h"
+#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"
+
+#ifdef XP_WIN
+# include "nsWindowsHelpers.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;
+
+// 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::mLog("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(args) \
+ MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args)
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(nsExternalHelperAppService::mLog, 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";
+
+// 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, 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;
+}
+
+/**
+ * Given a channel, returns the filename and extension the channel has.
+ * This uses the URL and other sources (nsIMultiPartChannel).
+ * Also gives back whether the channel requested external handling (i.e.
+ * whether Content-Disposition: attachment was sent)
+ * @param aChannel The channel to extract the filename/extension from
+ * @param aFileName [out] Reference to the string where the filename should be
+ * stored. Empty if it could not be retrieved.
+ * WARNING - this filename may contain characters which the OS does not
+ * allow as part of filenames!
+ * @param aExtension [out] Reference to the string where the extension should
+ * be stored. Empty if it could not be retrieved. Stored in UTF-8.
+ * @param aAllowURLExtension (optional) Get the extension from the URL if no
+ * Content-Disposition header is present. Default is true.
+ * @retval true The server sent Content-Disposition:attachment or equivalent
+ * @retval false Content-Disposition: inline or no content-disposition header
+ * was sent.
+ */
+static bool GetFilenameAndExtensionFromChannel(nsIChannel* aChannel,
+ nsString& aFileName,
+ nsCString& aExtension,
+ bool aAllowURLExtension = true) {
+ aExtension.Truncate();
+ /*
+ * If the channel is an http or part of a multipart channel and we
+ * have a content disposition header set, then use the file name
+ * suggested there as the preferred file name to SUGGEST to the
+ * user. we shouldn't actually use that without their
+ * permission... otherwise just use our temp file
+ */
+ bool handleExternally = false;
+ uint32_t disp;
+ nsresult rv = aChannel->GetContentDisposition(&disp);
+ bool gotFileNameFromURI = false;
+ if (NS_SUCCEEDED(rv)) {
+ aChannel->GetContentDispositionFilename(aFileName);
+ if (disp == nsIChannel::DISPOSITION_ATTACHMENT) handleExternally = true;
+ }
+
+ // If the disposition header didn't work, try the filename from nsIURL
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetURI(getter_AddRefs(uri));
+ nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
+ if (url && aFileName.IsEmpty()) {
+ if (aAllowURLExtension) {
+ url->GetFileExtension(aExtension);
+ UnescapeFragment(aExtension, url, aExtension);
+
+ // Windows ignores terminating dots. So we have to as well, so
+ // that our security checks do "the right thing"
+ // In case the aExtension consisted only of the dot, the code below will
+ // extract an aExtension from the filename
+ aExtension.Trim(".", false);
+ }
+
+ // try to extract the file name from the url and use that as a first pass as
+ // the leaf name of our temp file...
+ nsAutoCString leafName;
+ url->GetFileName(leafName);
+ if (!leafName.IsEmpty()) {
+ gotFileNameFromURI = true;
+ rv = UnescapeFragment(leafName, url, aFileName);
+ if (NS_FAILED(rv)) {
+ CopyUTF8toUTF16(leafName, aFileName); // use escaped name
+ }
+ }
+ }
+
+ // If we have a filename and no extension, remove trailing dots from the
+ // filename and extract the extension if that is possible.
+ if (aExtension.IsEmpty() && !aFileName.IsEmpty()) {
+ // Windows ignores terminating dots. So we have to as well, so
+ // that our security checks do "the right thing"
+ aFileName.Trim(".", false);
+ // We can get an extension if the filename is from a header, or if getting
+ // it from the URL was allowed.
+ bool canGetExtensionFromFilename =
+ !gotFileNameFromURI || aAllowURLExtension;
+ // ... , or if the mimetype is meaningless and we have nothing to go on:
+ if (!canGetExtensionFromFilename) {
+ nsAutoCString contentType;
+ if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) {
+ canGetExtensionFromFilename =
+ contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) ||
+ contentType.EqualsIgnoreCase("binary/octet-stream") ||
+ contentType.EqualsIgnoreCase("application/x-msdownload");
+ }
+ }
+
+ if (canGetExtensionFromFilename) {
+ // XXX RFindCharInReadable!!
+ nsAutoString fileNameStr(aFileName);
+ int32_t idx = fileNameStr.RFindChar(char16_t('.'));
+ if (idx != kNotFound)
+ CopyUTF16toUTF8(StringTail(fileNameStr, fileNameStr.Length() - idx - 1),
+ aExtension);
+ }
+ }
+
+ return handleExternally;
+}
+
+/**
+ * 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.jsm.
+ *
+ * Optionally skip availability of the directory and storage.
+ */
+static nsresult GetDownloadDirectory(nsIFile** _directory,
+ bool aSkipChecks = false) {
+ nsCOMPtr<nsIFile> dir;
+#ifdef XP_MACOSX
+ // On OS X, we first 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
+ bool dirExists = false;
+ (void)dir->Exists(&dirExists);
+ if (dirExists) break;
+
+ nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (NS_FAILED(rv)) {
+ dir = nullptr;
+ break;
+ }
+ } break;
+ case NS_FOLDER_VALUE_DOWNLOADS:
+ // This is just the OS default location, so fall out
+ break;
+ }
+
+ if (!dir) {
+ // If not, we default to the OS X default download location.
+ nsresult rv = NS_GetSpecialDirectory(NS_OSX_DEFAULT_DOWNLOAD_DIR,
+ getter_AddRefs(dir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+#elif defined(ANDROID)
+ return NS_ERROR_FAILURE;
+#else
+ // On all other platforms, we default to the systems temporary directory.
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+# if defined(XP_UNIX)
+ // Ensuring that only the current user can read the file names we end up
+ // creating. Note that Creating directories with specified permission only
+ // supported on Unix platform right now. That's why above if 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(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;
+ } else if (rv != NS_ERROR_FILE_ALREADY_EXISTS) {
+ // Unexpected error.
+ return rv;
+ }
+
+ counter++;
+ }
+ }
+
+# endif
+#endif
+
+ NS_ASSERTION(dir, "Somehow we didn't get a download directory!");
+ dir.forget(_directory);
+ 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, "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)"},
+
+ // Note: if you add new image types, please also update the list in
+ // contentAreaUtils.js to match.
+ {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"},
+
+ {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", "iCalendar"},
+ {VIDEO_OGG, "ogv", "Ogg Video"},
+ {VIDEO_OGG, "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", "MPEG Audio"},
+ {VIDEO_MP4, "mp4", "MPEG-4 Video"},
+ {AUDIO_MP4, "m4a", "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[] = {
+ // OpenDocument formats
+ "application/vnd.oasis.opendocument.text",
+ "application/vnd.oasis.opendocument.presentation",
+ "application/vnd.oasis.opendocument.spreadsheet",
+ "application/vnd.oasis.opendocument.graphics",
+
+ // Legacy Microsoft Office
+ "application/msword", "application/vnd.ms-powerpoint",
+ "application/vnd.ms-excel",
+
+ // Office Open XML
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+
+ // Note: zip and json mimetypes are commonly used with a variety of
+ // extensions; don't add them here. It's a similar story for text/xml,
+ // but slightly worse because we can use it when sniffing for a mimetype
+ // if one hasn't been provided, so don't re-add that here either.
+
+ 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", "pdf", "svg", "webp", "xml",
+};
+
+static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton;
+
+/**
+ * On Mac child processes, return an nsOSHelperAppServiceChild for remoting
+ * OS calls to the parent process. On all other platforms use
+ * nsOSHelperAppService.
+ */
+/* static */
+already_AddRefed<nsExternalHelperAppService>
+nsExternalHelperAppService::GetSingleton() {
+ if (!sExtHelperAppSvcSingleton) {
+#ifdef XP_MACOSX
+ if (XRE_IsParentProcess()) {
+ sExtHelperAppSvcSingleton = new nsOSHelperAppService();
+ } else {
+ sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild();
+ }
+#else
+ sExtHelperAppSvcSingleton = new nsOSHelperAppService();
+#endif /* XP_MACOSX */
+ 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, nsIRequest* aRequest,
+ BrowsingContext* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener) {
+ // 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;
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ if (channel) {
+ channel->GetURI(getter_AddRefs(uri));
+ channel->GetContentLength(&contentLength);
+ channel->GetContentDisposition(&contentDisposition);
+ channel->GetContentDispositionFilename(fileName);
+ channel->GetContentDispositionHeader(disp);
+ loadInfo = channel->LoadInfo();
+
+ nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest));
+ wasFileChannel = fileChan != nullptr;
+ }
+
+ nsCOMPtr<nsIURI> referrer;
+ NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer));
+
+ Maybe<mozilla::net::LoadInfoArgs> loadInfoArgs;
+ MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
+
+ nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aRequest));
+ // 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;
+
+ RefPtr<nsExternalAppHandler> handler =
+ new nsExternalAppHandler(nullptr, ""_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, nsIRequest* aRequest,
+ BrowsingContext* aContentContext, bool aForceSave,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aStreamListener) {
+ MOZ_ASSERT(!XRE_IsContentProcess());
+
+ nsAutoString fileName;
+ nsAutoCString fileExtension;
+ uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE;
+ uint32_t contentDisposition = -1;
+
+ // Get the file extension and name that we will need later
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ nsCOMPtr<nsIURI> uri;
+ int64_t contentLength = -1;
+ if (channel) {
+ channel->GetURI(getter_AddRefs(uri));
+ channel->GetContentLength(&contentLength);
+ channel->GetContentDisposition(&contentDisposition);
+ channel->GetContentDispositionFilename(fileName);
+
+ // Check if we have a POST request, in which case we don't want to use
+ // the url's extension
+ bool allowURLExt = !net::ChannelIsPost(channel);
+
+ // 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 (uri && allowURLExt) {
+ nsCOMPtr<nsIURL> url = do_QueryInterface(uri);
+
+ if (url) {
+ nsAutoCString query;
+
+ // We only care about the query for HTTP and HTTPS URLs
+ if (uri->SchemeIs("http") || uri->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();
+ }
+ }
+ // Extract name & extension
+ bool isAttachment = GetFilenameAndExtensionFromChannel(
+ channel, fileName, fileExtension, allowURLExt);
+ LOG(("Found extension '%s' (filename is '%s', handling attachment: %i)",
+ fileExtension.get(), NS_ConvertUTF16toUTF8(fileName).get(),
+ isAttachment));
+ if (isAttachment) {
+ reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST;
+ }
+ }
+
+ LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n",
+ PromiseFlatCString(aMimeContentType).get(), fileExtension.get()));
+
+ // 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> mimeSvc(do_GetService(NS_MIMESERVICE_CONTRACTID));
+ NS_ENSURE_TRUE(mimeSvc, NS_ERROR_FAILURE);
+
+ // Try to find a mime object by looking at the mime type/extension
+ nsCOMPtr<nsIMIMEInfo> mimeInfo;
+ if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT,
+ nsCaseInsensitiveCStringComparator)) {
+ nsAutoCString mimeType;
+ if (!fileExtension.IsEmpty()) {
+ mimeSvc->GetFromTypeAndExtension(""_ns, fileExtension,
+ getter_AddRefs(mimeInfo));
+ if (mimeInfo) {
+ mimeInfo->GetMIMEType(mimeType);
+
+ LOG(("OS-Provided mime type '%s' for extension '%s'\n", mimeType.get(),
+ fileExtension.get()));
+ }
+ }
+
+ if (fileExtension.IsEmpty() || mimeType.IsEmpty()) {
+ // Extension lookup gave us no useful match
+ mimeSvc->GetFromTypeAndExtension(
+ nsLiteralCString(APPLICATION_OCTET_STREAM), fileExtension,
+ getter_AddRefs(mimeInfo));
+ mimeType.AssignLiteral(APPLICATION_OCTET_STREAM);
+ }
+
+ if (channel) {
+ channel->SetContentType(mimeType);
+ }
+
+ // Don't overwrite SERVERREQUEST
+ if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
+ reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED;
+ }
+ } else {
+ mimeSvc->GetFromTypeAndExtension(aMimeContentType, fileExtension,
+ getter_AddRefs(mimeInfo));
+ }
+ 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;
+ }
+
+ *aStreamListener = nullptr;
+ // We want the mimeInfo's primary extension to pass it to
+ // nsExternalAppHandler
+ nsAutoCString buf;
+ mimeInfo->GetPrimaryExtension(buf);
+
+ // NB: ExternalHelperAppParent depends on this listener always being an
+ // nsExternalAppHandler. If this changes, make sure to update that code.
+ nsExternalAppHandler* handler =
+ new nsExternalAppHandler(mimeInfo, buf, 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, nsIRequest* aRequest,
+ 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, aRequest, bc,
+ aForceSave, aWindowContext,
+ aStreamListener);
+ }
+
+ nsresult rv = CreateListener(aMimeContentType, aRequest, 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";
+
+NS_IMETHODIMP
+nsExternalHelperAppService::LoadURI(nsIURI* aURI,
+ nsIPrincipal* aTriggeringPrincipal,
+ BrowsingContext* aBrowsingContext) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ if (XRE_IsContentProcess()) {
+ mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal(
+ aURI, aTriggeringPrincipal, aBrowsingContext);
+ return NS_OK;
+ }
+
+ nsAutoCString spec;
+ aURI->GetSpec(spec);
+
+ if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI;
+
+ spec.ReplaceSubstring("\"", "%22");
+ spec.ReplaceSubstring("`", "%60");
+
+ nsCOMPtr<nsIIOService> ios(do_GetIOService());
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = ios->NewURI(spec, nullptr, nullptr, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString scheme;
+ uri->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, uri, aTriggeringPrincipal,
+ aBrowsingContext);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////
+// 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 nsACString& aTempFileExtension,
+ BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext,
+ nsExternalHelperAppService* aExtProtSvc,
+ const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave)
+ : mMimeInfo(aMIMEInfo),
+ mBrowsingContext(aBrowsingContext),
+ mWindowContext(aWindowContext),
+ mSuggestedFileName(aSuggestedFilename),
+ mForceSave(aForceSave),
+ mCanceled(false),
+ mStopRequestIssued(false),
+ mIsFileChannel(false),
+ mShouldCloseWindow(false),
+ mHandleInternally(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 (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.')
+ mTempFileExtension = char16_t('.');
+ AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension);
+
+ // Get mSuggestedFileName's current file extension.
+ nsAutoString originalFileExt;
+ int32_t pos = mSuggestedFileName.RFindChar('.');
+ if (pos != kNotFound) {
+ mSuggestedFileName.Right(originalFileExt,
+ mSuggestedFileName.Length() - pos);
+ }
+
+ // replace platform specific path separator and illegal characters to avoid
+ // any confusion.
+ // Try to keep the use of spaces or underscores in sync with the Downloads
+ // code sanitization in DownloadPaths.jsm
+ mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
+ mSuggestedFileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
+ mSuggestedFileName.ReplaceChar(char16_t(0), '_');
+ mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS, '_');
+ mTempFileExtension.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' ');
+
+ // Remove unsafe bidi characters which might have spoofing implications (bug
+ // 511521).
+ const char16_t unsafeBidiCharacters[] = {
+ char16_t(0x061c), // Arabic Letter Mark
+ char16_t(0x200e), // Left-to-Right Mark
+ char16_t(0x200f), // Right-to-Left Mark
+ char16_t(0x202a), // Left-to-Right Embedding
+ char16_t(0x202b), // Right-to-Left Embedding
+ char16_t(0x202c), // Pop Directional Formatting
+ char16_t(0x202d), // Left-to-Right Override
+ char16_t(0x202e), // Right-to-Left Override
+ char16_t(0x2066), // Left-to-Right Isolate
+ char16_t(0x2067), // Right-to-Left Isolate
+ char16_t(0x2068), // First Strong Isolate
+ char16_t(0x2069), // Pop Directional Isolate
+ char16_t(0)};
+ mSuggestedFileName.ReplaceChar(unsafeBidiCharacters, '_');
+ mTempFileExtension.ReplaceChar(unsafeBidiCharacters, '_');
+
+ // Remove trailing or leading spaces that we may have generated while
+ // sanitizing.
+ mSuggestedFileName.CompressWhitespace();
+ mTempFileExtension.CompressWhitespace();
+
+ EnsureCorrectExtension(originalFileExt);
+
+ mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096);
+}
+
+nsExternalAppHandler::~nsExternalAppHandler() {
+ MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted");
+}
+
+bool nsExternalAppHandler::ShouldForceExtension(const nsString& aFileExt) {
+ nsAutoCString MIMEType;
+ if (!mMimeInfo || NS_FAILED(mMimeInfo->GetMIMEType(MIMEType))) {
+ return false;
+ }
+
+ bool canForce = StringBeginsWith(MIMEType, "image/"_ns) ||
+ StringBeginsWith(MIMEType, "audio/"_ns) ||
+ StringBeginsWith(MIMEType, "video/"_ns);
+
+ if (!canForce &&
+ StaticPrefs::browser_download_sanitize_non_media_extensions()) {
+ for (const char* mime : forcedExtensionMimetypes) {
+ if (MIMEType.Equals(mime)) {
+ canForce = true;
+ break;
+ }
+ }
+ }
+ if (!canForce) {
+ return false;
+ }
+
+ // If we get here, we know for sure the mimetype allows us to overwrite the
+ // existing extension, if it's wrong. Return whether the extension is wrong:
+
+ bool knownExtension = false;
+ // Note that aFileExt is either empty or consists of an extension
+ // *including the dot* which we remove for ExtensionExists().
+ return (
+ aFileExt.IsEmpty() || aFileExt.EqualsLiteral(".") ||
+ (NS_SUCCEEDED(mMimeInfo->ExtensionExists(
+ Substring(NS_ConvertUTF16toUTF8(aFileExt), 1), &knownExtension)) &&
+ !knownExtension));
+}
+
+void nsExternalAppHandler::EnsureCorrectExtension(const nsString& aFileExt) {
+ // If we don't have an extension (which will include the .),
+ // just short-circuit.
+ if (mTempFileExtension.Length() <= 1) {
+ return;
+ }
+
+ // After removing trailing whitespaces from the name, if we have a
+ // temp file extension, there are broadly 2 cases where we want to
+ // replace the extension.
+ // First, if the file extension contains invalid characters.
+ // Second, for document type mimetypes, if the extension is either
+ // missing or not valid for this mimetype.
+ bool replaceExtension =
+ (aFileExt.FindCharInSet(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS) !=
+ kNotFound) ||
+ ShouldForceExtension(aFileExt);
+
+ if (replaceExtension) {
+ int32_t pos = mSuggestedFileName.RFindChar('.');
+ if (pos != kNotFound) {
+ mSuggestedFileName =
+ Substring(mSuggestedFileName, 0, pos) + mTempFileExtension;
+ } else {
+ mSuggestedFileName.Append(mTempFileExtension);
+ }
+ }
+
+ /*
+ * Ensure we don't double-append the file extension if it matches:
+ */
+ if (replaceExtension ||
+ aFileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator)) {
+ // Matches -> mTempFileExtension can be empty
+ mTempFileExtension.Truncate();
+ }
+}
+
+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->Id();
+ 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.
+ // We will request raw random bytes, and transform that to a base64 string,
+ // as all characters from the base64 set are acceptable for filenames. For
+ // each three bytes of random data, we will get four bytes of ASCII. Request
+ // a bit more, to be safe, and truncate to the length we want in the end.
+
+ const uint32_t wantedFileNameLength = 8;
+ const uint32_t requiredBytesLength =
+ static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3);
+
+ nsCOMPtr<nsIRandomGenerator> rg =
+ do_GetService("@mozilla.org/security/random-generator;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint8_t* buffer;
+ rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString tempLeafName;
+ nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer),
+ requiredBytesLength);
+ rv = Base64Encode(randomData, tempLeafName);
+ free(buffer);
+ buffer = nullptr;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ tempLeafName.Truncate(wantedFileNameLength);
+
+ // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need
+ // to replace illegal characters -- notably '/'
+ tempLeafName.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_');
+
+ // 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));
+ }
+
+ 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);
+
+ // OK, now check why we're here
+ if (!alwaysAsk && mReason != nsIHelperAppLauncherDialog::REASON_CANTHANDLE) {
+ // Force asking if we're not saving. See comment back when we fetched the
+ // alwaysAsk boolean for details.
+ alwaysAsk = (action != nsIMIMEInfo::saveToDisk);
+ }
+
+ // If we're not asking, check we actually know what to do:
+ if (!alwaysAsk) {
+ alwaysAsk = action != nsIMIMEInfo::saveToDisk &&
+ action != nsIMIMEInfo::useHelperApp &&
+ action != nsIMIMEInfo::useSystemDefault;
+ }
+
+ // if we were told that we _must_ save to disk without asking, all the stuff
+ // before this is irrelevant; override it
+ if (mForceSave) {
+ alwaysAsk = false;
+ action = nsIMIMEInfo::saveToDisk;
+ }
+
+ 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();
+ 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.
+#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.
+ * This code mirrors the code in
+ * nsExternalAppHandler::LaunchWithApplication so that what we
+ * test here is as close as possible to what will really be
+ * happening if we decide to execute
+ */
+ 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) ||
+ isExecutable) { // checking NS_FAILED, because paranoia is good
+ action = nsIMIMEInfo::saveToDisk;
+ }
+ } 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! ");
+ action = nsIMIMEInfo::saveToDisk;
+ }
+ }
+
+#endif
+ if (action == nsIMIMEInfo::useHelperApp ||
+ action == nsIMIMEInfo::useSystemDefault) {
+ rv = LaunchWithApplication(mHandleInternally);
+ } else {
+ rv = PromptForSaveDestination();
+ }
+ }
+ return NS_OK;
+}
+
+// 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_DISK_FULL:
+ 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_TARGET_DOES_NOT_EXIST:
+ 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::mLog, 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::mLog, LogLevel::Error,
+ (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get()));
+
+ // Get properties file bundle and extract status string.
+ nsCOMPtr<nsIStringBundleService> stringService =
+ mozilla::services::GetStringBundleService();
+ 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::mLog, 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::mLog, 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::mLog, 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);
+ 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);
+ if (mBrowsingContext) {
+ rv = transfer->InitWithBrowsingContext(
+ mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile,
+ this, channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification, mBrowsingContext, mHandleInternally);
+ } else {
+ rv = transfer->Init(mSourceUrl, target, u""_ns, mMimeInfo,
+ mTimeDownloadStarted, mTempFile, this,
+ channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification);
+ }
+
+ 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);
+
+ // 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);
+
+ nsCOMPtr<nsIURI> pseudoTarget;
+ rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest);
+ if (mBrowsingContext) {
+ rv = transfer->InitWithBrowsingContext(
+ mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted,
+ nullptr, this, channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification, mBrowsingContext, mHandleInternally);
+ } else {
+ rv = transfer->Init(mSourceUrl, pseudoTarget, u""_ns, mMimeInfo,
+ mTimeDownloadStarted, nullptr, this,
+ channel && NS_UsePrivateBrowsing(channel),
+ mDownloadClassification);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Our failed transfer is ready.
+ mTransfer = std::move(transfer);
+
+ return NS_OK;
+}
+
+nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile) {
+ if (aFile)
+ 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;
+
+ mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
+
+ if (mSuggestedFileName.IsEmpty()) {
+ RequestSaveDestination(mTempLeafName, mTempFileExtension);
+ } else {
+ nsAutoString fileExt;
+ int32_t pos = mSuggestedFileName.RFindChar('.');
+ if (pos >= 0) {
+ mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos);
+ }
+ if (fileExt.IsEmpty()) {
+ fileExt = mTempFileExtension;
+ }
+
+ 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");
+
+ 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) {
+ // Get the old leaf name and append .part to it
+ nsAutoString name;
+ mFinalFileDestination->GetLeafName(name);
+ name.AppendLiteral(".part");
+ movedFile->SetLeafName(name);
+
+ 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;
+}
+
+// LaunchWithApplication 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::LaunchWithApplication(
+ bool aHandleInternally) {
+ if (mCanceled) return NS_OK;
+
+ mHandleInternally = aHandleInternally;
+
+ // Now check if the file is local, in which case we won't bother with saving
+ // it to a temporary directory and just launch it from where it is
+ nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl));
+ if (fileUrl && mIsFileChannel) {
+ 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;
+ }
+
+ // 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;
+ (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
+ fileToUse->Append(mSuggestedFileName + mTempFileExtension);
+#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;
+}
+
+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;
+
+ 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_ConvertASCIItoUTF16(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;
+}
+
+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
+ for (auto& entry : defaultMimeEntries) {
+ if (aFileExt.LowerCaseEqualsASCII(entry.mFileExtension)) {
+ aContentType = entry.mMimeType;
+ 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;
+ }
+
+ // Try the plugins
+ RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst();
+ if (pluginHost &&
+ pluginHost->HavePluginForExtension(aFileExt, aContentType)) {
+ 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::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;
+}
diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h
new file mode 100644
index 0000000000..8c87587e9e
--- /dev/null
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
@@ -0,0 +1,512 @@
+/* -*- 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 "nsIMIMEInfo.h"
+#include "nsIMIMEService.h"
+#include "nsINamed.h"
+#include "nsIStreamListener.h"
+#include "nsIFile.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,
+ mozilla::dom::BrowsingContext* aBrowsingContext) 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. 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();
+
+ 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);
+
+ /**
+ * 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 mLog;
+
+ // 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();
+
+ /**
+ * 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, nsIRequest* aRequest,
+ 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 aFileName The filename to use
+ * @param aReason A constant from nsIHelperAppLauncherDialog
+ * indicating why the request is handled by a helper app.
+ */
+ nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsACString& aFileExtension,
+ mozilla::dom::BrowsingContext* aBrowsingContext,
+ nsIInterfaceRequestor* aWindowContext,
+ nsExternalHelperAppService* aExtProtSvc,
+ const nsAString& aFilename, 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:
+ ~nsExternalAppHandler();
+
+ nsCOMPtr<nsIFile> mTempFile;
+ nsCOMPtr<nsIURI> mSourceUrl;
+ nsString mTempFileExtension;
+ 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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 that checks whether we should enforce an extension
+ * for this file.
+ */
+ bool ShouldForceExtension(const nsString& aFileExt);
+
+ /**
+ * 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 mTempFileExtension 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 nsHelperDlgApp.js. 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..30b806d7c2
--- /dev/null
+++ b/uriloader/exthandler/nsExternalProtocolHandler.cpp
@@ -0,0 +1,545 @@
+/* -*- 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 "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(nsISupports** 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> principal = mLoadInfo->TriggeringPrincipal();
+ rv = extProtService->LoadURI(mUrl, principal, ctx);
+
+ 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::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::NotifyFlashPluginStateChanged(
+ nsIHttpChannel::FlashPluginState aState) {
+ // 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::GetDefaultPort(int32_t* aDefaultPort) {
+ *aDefaultPort = 0;
+ 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::GetProtocolFlags(uint32_t* aUritype) {
+ // Make it norelative since it is a simple uri
+ *aUritype = URI_NORELATIVE | URI_NOAUTH | URI_LOADABLE_BY_ANYONE |
+ URI_NON_PERSISTABLE | URI_DOES_NOT_RETURN_DATA;
+ return NS_OK;
+}
+
+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..d12c1aa907
--- /dev/null
+++ b/uriloader/exthandler/nsIContentDispatchChooser.idl
@@ -0,0 +1,38 @@
+/* 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.
+ */
+ void handleURI(in nsIHandlerInfo aHandler,
+ in nsIURI aURI,
+ in nsIPrincipal aTriggeringPrincipal,
+ in BrowsingContext aBrowsingContext);
+};
+
diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl
new file mode 100644
index 0000000000..657e15bc07
--- /dev/null
+++ b/uriloader/exthandler/nsIExternalHelperAppService.idl
@@ -0,0 +1,179 @@
+/* -*- 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 nsIRequest;
+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 aRequest The request 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 nsIRequest aRequest,
+ 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 aRequest The request corresponding to the incoming data
+ * @param aContentContext The BrowsingContext that the request 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 nsIRequest aRequest,
+ 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.
+ */
+ void launchWithApplication(in boolean aHandleInternally);
+
+ /**
+ * 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).
+ */
+ void saveDestinationAvailable(in nsIFile aFile);
+
+ /**
+ * 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..66d459c491
--- /dev/null
+++ b/uriloader/exthandler/nsIExternalProtocolService.idl
@@ -0,0 +1,140 @@
+/* -*- 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 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.
+ *
+ * @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 BrowsingContext aBrowsingContext);
+
+ /**
+ * 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/nsIExternalURLHandlerService.idl b/uriloader/exthandler/nsIExternalURLHandlerService.idl
new file mode 100644
index 0000000000..3573497591
--- /dev/null
+++ b/uriloader/exthandler/nsIExternalURLHandlerService.idl
@@ -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/. */
+
+#include "nsIMIMEInfo.idl"
+
+/**
+ * The external URL handler service is used for finding
+ * platform-specific applications for handling particular URLs.
+ */
+
+[scriptable, uuid(56c5c7d3-6fd3-43f8-9429-4397e111453a)]
+interface nsIExternalURLHandlerService : nsISupports
+{
+ /**
+ * Given a URL, looks up the handler info from the OS. This should be
+ * overridden by each OS's implementation.
+ *
+ * @param aURL The URL we are looking for.
+ * @param aFound Was an OS default handler for this URL found?
+ * @return An nsIHanderInfo for the protocol.
+ */
+ nsIHandlerInfo getURLHandlerInfoFromOS(in nsIURI aURL,
+ out boolean aFound);
+
+};
diff --git a/uriloader/exthandler/nsIHandlerService.idl b/uriloader/exthandler/nsIHandlerService.idl
new file mode 100644
index 0000000000..34519f57cd
--- /dev/null
+++ b/uriloader/exthandler/nsIHandlerService.idl
@@ -0,0 +1,162 @@
+/* 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;
+
+[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);
+
+ /*
+ * Fill in a handler info object using information from the OS, taking into
+ * account the MIME type and file extension. When the OS handler
+ * for the MIME type and extension match, |aFound| is returned as true. If
+ * either the MIME type or extension is the empty string and a handler is
+ * found, |aFound| is returned as true.
+ */
+ void getMIMEInfoFromOS(in nsIHandlerInfo aHandlerInfo,
+ in ACString aMIMEType,
+ in ACString aExtension,
+ out bool 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..14bdee1884
--- /dev/null
+++ b/uriloader/exthandler/nsISharingHandlerApp.idl
@@ -0,0 +1,12 @@
+/* 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..f1a9a779cf
--- /dev/null
+++ b/uriloader/exthandler/nsLocalHandlerApp.cpp
@@ -0,0 +1,157 @@
+/* -*- 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"
+
+// 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..3800d41d57
--- /dev/null
+++ b/uriloader/exthandler/nsMIMEInfoImpl.cpp
@@ -0,0 +1,467 @@
+/* -*- 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 "nsCURILoader.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIExternalProtocolService.h"
+#include "mozilla/StaticPtr.h"
+
+static bool sInitializedOurData = false;
+StaticRefPtr<nsIFile> sOurAppFile;
+
+static already_AddRefed<nsIFile> GetCanonicalExecutable(nsIFile* aFile) {
+ nsCOMPtr<nsIFile> binary = aFile;
+#ifdef XP_MACOSX
+ nsAutoString leafName;
+ if (binary) {
+ binary->GetLeafName(leafName);
+ }
+ while (binary && !StringEndsWith(leafName, u".app"_ns)) {
+ nsCOMPtr<nsIFile> parent;
+ binary->GetParent(getter_AddRefs(parent));
+ binary = 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 = 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),
+ mPreferredAction(nsIMIMEInfo::saveToDisk),
+ mAlwaysAskBeforeHandling(true) {}
+
+nsMIMEInfoBase::nsMIMEInfoBase(const nsACString& aMIMEType)
+ : mSchemeOrType(aMIMEType),
+ mClass(eMIMEInfo),
+ mPreferredAction(nsIMIMEInfo::saveToDisk),
+ mAlwaysAskBeforeHandling(true) {}
+
+// 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),
+ mPreferredAction(nsIMIMEInfo::saveToDisk),
+ mAlwaysAskBeforeHandling(true) {}
+
+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);
+ return NS_OK;
+}
+
+void nsMIMEInfoBase::AddUniqueExtension(const nsACString& aExtension) {
+ 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 (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;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoBase::LaunchWithURI(nsIURI* aURI, 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() && mDefaultApplication) {
+ // Don't want to cache this, just in case someone resets the app
+ // without changing the description....
+ mDefaultApplication->GetLeafName(aDefaultDescription);
+ } else {
+ aDefaultDescription = mDefaultAppDescription;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoImpl::GetHasDefaultHandler(bool* _retval) {
+ *_retval = !mDefaultAppDescription.IsEmpty();
+ if (mDefaultApplication) {
+ bool exists;
+ *_retval = NS_SUCCEEDED(mDefaultApplication->Exists(&exists)) && exists;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMIMEInfoImpl::IsCurrentAppOSDefault(bool* _retval) {
+ *_retval = false;
+ if (mDefaultApplication) {
+ // Determine if the default executable is our executable.
+ EnsureAppDetailsAvailable();
+ bool isSame = false;
+ nsresult rv = mDefaultApplication->Equals(sOurAppFile, &isSame);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ *_retval = isSame;
+ }
+ return NS_OK;
+}
+
+nsresult nsMIMEInfoImpl::LaunchDefaultWithFile(nsIFile* aFile) {
+ if (!mDefaultApplication) return NS_ERROR_FILE_NOT_FOUND;
+
+ return LaunchWithIProcess(mDefaultApplication, 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..f2fd792efb
--- /dev/null
+++ b/uriloader/exthandler/nsMIMEInfoImpl.h
@@ -0,0 +1,213 @@
+/* -*- 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; }
+
+ 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;
+
+ 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; ///< preferred action to associate with this type
+ nsString mPreferredAppDescription;
+ nsString mDefaultAppDescription;
+ bool mAlwaysAskBeforeHandling;
+};
+
+/**
+ * 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; the default application is immutable after it is first set.
+ */
+ void SetDefaultApplication(nsIFile* aApp) {
+ if (!mDefaultApplication) mDefaultApplication = aApp;
+ }
+
+ 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;
+
+ 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..1e1442fac6
--- /dev/null
+++ b/uriloader/exthandler/nsOSHelperAppServiceChild.cpp
@@ -0,0 +1,129 @@
+/* -*- 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 "nsMIMEInfoChild.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsIFile.h"
+#include "nsIHandlerService.h"
+#include "nsMimeTypes.h"
+#include "nsMIMEInfoImpl.h"
+#include "nsMemory.h"
+#include "nsCRT.h"
+#include "nsEmbedCID.h"
+
+#undef LOG
+#define LOG(args) \
+ MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args)
+#undef LOG_ERR
+#define LOG_ERR(args) \
+ MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Error, args)
+#undef LOG_ENABLED
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(nsExternalHelperAppService::mLog, 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) {
+ RefPtr<nsChildProcessMIMEInfo> mimeInfo =
+ new nsChildProcessMIMEInfo(aMIMEType);
+
+ nsCOMPtr<nsIHandlerService> handlerSvc =
+ do_GetService(NS_HANDLERSERVICE_CONTRACTID);
+ if (handlerSvc) {
+ nsresult rv =
+ handlerSvc->GetMIMEInfoFromOS(mimeInfo, aMIMEType, aFileExt, aFound);
+ 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));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ } else {
+ LOG_ERR(("nsOSHelperAppServiceChild error: no handler service"));
+ *aFound = false;
+ }
+
+ mimeInfo.forget(aMIMEInfo);
+ return NS_OK;
+}
+
+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..f37caf010d
--- /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() : nsExternalHelperAppService(){};
+ 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.jsm b/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm
new file mode 100644
index 0000000000..72c551d654
--- /dev/null
+++ b/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm
@@ -0,0 +1,241 @@
+/* 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.
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["HandlerServiceTestUtils"];
+
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gExternalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gMIMEService",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gHandlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+
+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(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 = 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 (gHandlerService.exists(handlerInfo)) {
+ 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 = gExternalProtocolService.getProtocolHandlerInfoFromOS(
+ type,
+ osDefaultHandlerFound
+ );
+ if (gHandlerService.exists(handlerInfo)) {
+ gHandlerService.fillHandlerInfo(handlerInfo, "");
+ } else {
+ 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;
+ 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.
+ preferredAction = type.includes("/")
+ ? Ci.nsIHandlerInfo.saveToDisk
+ : Ci.nsIHandlerInfo.alwaysAsk;
+ preferredApplicationHandler = null;
+ }
+
+ this.assertHandlerInfoMatches(handlerInfo, {
+ type,
+ preferredAction,
+ alwaysAskBeforeHandling: true,
+ 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/mochitest/.eslintrc.js b/uriloader/exthandler/tests/mochitest/.eslintrc.js
new file mode 100644
index 0000000000..7612459de1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test", "plugin:mozilla/mochitest-test"],
+};
diff --git a/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js
new file mode 100644
index 0000000000..d08d72b048
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js
@@ -0,0 +1,38 @@
+const { ComponentUtils } = ChromeUtils.import(
+ "resource://gre/modules/ComponentUtils.jsm"
+);
+
+const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1";
+const HELPERAPP_DIALOG_CID = Components.ID(
+ Cc[HELPERAPP_DIALOG_CONTRACT].number
+);
+
+const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator)
+ .generateUUID();
+/* eslint-env mozilla/frame-script */
+function HelperAppLauncherDialog() {}
+HelperAppLauncherDialog.prototype = {
+ show(aLauncher, aWindowContext, aReason) {
+ sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]),
+};
+
+var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(
+ FAKE_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT,
+ ComponentUtils._getFactory(HelperAppLauncherDialog)
+);
+
+addMessageListener("unregister", function() {
+ registrar.registerFactory(
+ HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT,
+ null
+ );
+ sendAsyncMessage("unregistered");
+});
diff --git a/uriloader/exthandler/tests/mochitest/browser.ini b/uriloader/exthandler/tests/mochitest/browser.ini
new file mode 100644
index 0000000000..0d5b20bef8
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+head = head.js
+support-files =
+ protocolHandler.html
+
+[browser_auto_close_window.js]
+run-if = e10s # test relies on e10s behavior
+support-files =
+ download_page.html
+ download.bin
+ download.sjs
+[browser_download_always_ask_preferred_app.js]
+[browser_download_privatebrowsing.js]
+[browser_download_open_with_internal_handler.js]
+support-files =
+ file_pdf_application_pdf.pdf
+ file_pdf_application_pdf.pdf^headers^
+ file_pdf_application_unknown.pdf
+ file_pdf_application_unknown.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^
+[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_open_internal_choice_persistence.js]
+support-files =
+ file_pdf_application_pdf.pdf
+ file_pdf_application_pdf.pdf^headers^
+[browser_protocol_ask_dialog.js]
+support-files =
+ file_nested_protocol_request.html
+[browser_first_prompt_not_blocked_without_user_interaction.js]
+support-files =
+ file_external_protocol_iframe.html
+[browser_protocol_ask_dialog_permission.js]
+[browser_protocolhandler_loop.js]
+[browser_remember_download_option.js]
+[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..2e1d17e139
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js
@@ -0,0 +1,271 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ComponentUtils } = ChromeUtils.import(
+ "resource://gre/modules/ComponentUtils.jsm"
+);
+
+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_task(async function setup() {
+ // Replace the real helper app dialog with our own.
+ mockHelperAppService = ComponentUtils._getFactory(HelperAppLauncherDialog);
+ registrar.registerFactory(
+ MOCK_HELPERAPP_DIALOG_CID,
+ "",
+ HELPERAPP_DIALOG_CONTRACT_ID,
+ mockHelperAppService
+ );
+});
+
+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");
+ });
+});
+
+// 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_download_always_ask_preferred_app.js b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js
new file mode 100644
index 0000000000..bd421d51f3
--- /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_open_with_internal_handler.js b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js
new file mode 100644
index 0000000000..ef7174f30f
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js
@@ -0,0 +1,612 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Downloads.jsm", this);
+const { DownloadIntegration } = ChromeUtils.import(
+ "resource://gre/modules/DownloadIntegration.jsm"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+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) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["pdfjs.eventBusDispatchToDOM", true]],
+ });
+ // Runs tests after all "load" event handlers have fired off
+ let loadPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "documentloaded",
+ false,
+ null,
+ true
+ );
+ await SpecialPowers.spawn(browser, [url], contentUrl => {
+ content.location = contentUrl;
+ });
+ return loadPromise;
+}
+
+add_task(async function setup() {
+ // 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 mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+ );
+ 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");
+});
+
+/**
+ * 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() {
+ 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,
+ TEST_PATH + file
+ );
+ // 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.loadURI(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();
+
+ 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"
+ );
+ subDialogWindow = await subdialogPromise;
+ 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);
+ subInternalHandlerRadio = subDoc.querySelector("#handleInternally");
+ ok(
+ subInternalHandlerRadio.hidden,
+ "The option should be hidden when the dialog is opened from pdf.js"
+ );
+ subDoc.querySelector("#open").click();
+
+ let tabOpenListener = () => {
+ ok(
+ false,
+ "A new tab should not be opened when accepting the dialog with 'open-with-external-app' chosen"
+ );
+ };
+ gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener);
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let waitForLaunchFileCalled = new Promise(resolve => {
+ DownloadIntegration.launchFile = async () => {
+ ok(true, "The file should be launched with an external application");
+ resolve();
+ };
+ });
+
+ downloadFinishedPromise = promiseDownloadFinished(publicList);
+
+ info("Accepting the dialog");
+ subDoc.querySelector("#unknownContentType").acceptDialog();
+ info("Waiting until DownloadIntegration.launchFile is called");
+ await waitForLaunchFileCalled;
+ DownloadIntegration.launchFile = oldLaunchFile;
+
+ // 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 */
+ }
+ }
+
+ gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener);
+ BrowserTestUtils.removeTab(loadingTab);
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(extraTab);
+
+ // Remove the remaining file once complete.
+ download = await downloadFinishedPromise;
+ if (download?.target.exists) {
+ try {
+ info("removing " + download.target.path);
+ await IOUtils.remove(download.target.path);
+ } catch (ex) {
+ /* ignore */
+ }
+ }
+ await publicList.removeFinished();
+ }
+});
+
+/**
+ * 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() {
+ 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,
+ TEST_PATH + file
+ );
+ 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();
+ }
+});
+
+/**
+ * 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 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");
+ 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,
+ TEST_PATH + file
+ );
+ 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,
+ TEST_PATH + file
+ );
+ 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() {
+ 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_xml_attachment_test.xml", true],
+ ]) {
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ 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);
+
+ ok(!internalHandlerRadio.hidden, "The option should be visible for XML");
+ if (checkDefault) {
+ ok(internalHandlerRadio.selected, "The option should be selected");
+ }
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.cancelDialog();
+ BrowserTestUtils.removeTab(loadingTab);
+ }
+ }
+);
+
+/**
+ * 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() {
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_txt_attachment_test.txt"
+ );
+ 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);
+});
+
+/**
+ * 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() {
+ 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,
+ TEST_PATH + file
+ );
+ 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);
+ }
+});
+
+/**
+ * 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() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.helperApps.showOpenOptionForViewableInternally", false]],
+ });
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + "file_xml_attachment_test.xml"
+ );
+ 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);
+ }
+);
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..02cf6c3941
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js
@@ -0,0 +1,70 @@
+/* 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";
+
+ChromeUtils.import("resource://gre/modules/Downloads.jsm", this);
+ChromeUtils.import("resource://gre/modules/DownloadPaths.jsm", this);
+ChromeUtils.import("resource://testing-common/FileTestUtils.jsm", this);
+ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", this);
+
+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_urlescape.js b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js
new file mode 100644
index 0000000000..ffab8146b6
--- /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 = PromiseUtils.defer();
+ // First try to save the browser
+ saveBrowser(browser);
+ await pendingPromise.promise;
+
+ // Next, try the context menu:
+ pendingTest = "save from context menu";
+ pendingPromise = PromiseUtils.defer();
+ 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..b806ee9ace
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js
@@ -0,0 +1,145 @@
+/* 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_task(async function setup() {
+ 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 });
+}
+
+async function checkDownloadWithExtensionState(
+ task,
+ { type, shouldHaveExtension, expectedName = null }
+) {
+ let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+
+ await task();
+
+ info("Waiting for dialog.");
+ let win = await winPromise;
+
+ let actualName = win.document.getElementById("location").value;
+ if (shouldHaveExtension) {
+ expectedName ??= "somefile." + getMIMEInfoForType(type).primaryExtension;
+ is(actualName, expectedName, `${type} should get an extension`);
+ } else {
+ expectedName ??= "somefile";
+ is(actualName, expectedName, `${type} should not get an extension`);
+ }
+
+ let closedPromise = BrowserTestUtils.windowClosed(win);
+
+ if (shouldHaveExtension) {
+ // Wait for the download.
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+
+ // Then pick "save" in the dialog.
+ let dialog = win.document.getElementById("unknownContentType");
+ win.document.getElementById("save").click();
+ let button = dialog.getButton("accept");
+ button.disabled = false;
+ dialog.acceptDialog();
+
+ // Wait for the download to finish and check the extension is correct.
+ let download = await downloadFinishedPromise;
+ is(
+ PathUtils.filename(download.target.path),
+ expectedName,
+ `Downloaded file should also 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 {
+ // 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/msword", true);
+ await testLinkWithoutExtension("application/pdf", true);
+
+ await testLinkWithoutExtension("application/x-gobbledygook", 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");
+ 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,
+ TEST_PATH + "file_as.exe?foo=bar"
+ ).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_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..b6f401e5e1
--- /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.loadURI(
+ 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_open_internal_choice_persistence.js b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js
new file mode 100644
index 0000000000..a36443edd1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js
@@ -0,0 +1,255 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import("resource://gre/modules/Downloads.jsm", this);
+const { DownloadIntegration } = ChromeUtils.import(
+ "resource://gre/modules/DownloadIntegration.jsm"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+
+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) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["pdfjs.eventBusDispatchToDOM", true]],
+ });
+ // Runs tests after all "load" event handlers have fired off
+ let loadPromise = BrowserTestUtils.waitForContentEvent(
+ browser,
+ "documentloaded",
+ false,
+ null,
+ true
+ );
+ await SpecialPowers.spawn(browser, [url], contentUrl => {
+ content.location = contentUrl;
+ });
+ return loadPromise;
+}
+
+add_task(async function setup() {
+ // 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 = gMimeSvc.getFromTypeAndExtension(type, ext);
+ const existed = gHandlerSvc.exists(mimeInfo);
+ registerCleanupFunction(() => {
+ if (existed) {
+ gHandlerSvc.store(mimeInfo);
+ } else {
+ gHandlerSvc.remove(mimeInfo);
+ }
+ });
+ };
+ registerRestoreHandler("application/pdf", "pdf");
+});
+
+const { handleInternally, saveToDisk, useSystemDefault } = Ci.nsIHandlerInfo;
+
+const kTestCases = [
+ {
+ description:
+ "Saving to disk when internal handling is the default shouldn't change prefs.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: 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: handleInternally,
+ expectedAsk: false,
+ },
+ {
+ description:
+ "Opening externally when internal handling is the default shouldn't change prefs.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: false },
+ dialogActions(doc) {
+ let openItem = doc.querySelector("#open");
+ openItem.click();
+ ok(openItem.selected, "The 'save' option should now be selected");
+ },
+ expectTab: false,
+ expectLaunch: true,
+ expectedPreferredAction: handleInternally,
+ expectedAsk: false,
+ },
+ {
+ description:
+ "Saving to disk when internal handling is the default *should* change prefs if checkbox is ticked.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: false },
+ dialogActions(doc) {
+ let saveItem = doc.querySelector("#save");
+ saveItem.click();
+ ok(saveItem.selected, "The 'save' option should now be selected");
+ let checkbox = doc.querySelector("#rememberChoice");
+ checkbox.checked = true;
+ checkbox.doCommand();
+ },
+ expectTab: false,
+ expectLaunch: false,
+ expectedPreferredAction: saveToDisk,
+ expectedAsk: false,
+ },
+ {
+ description:
+ "Saving to disk when asking is the default should change persisted default.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: true },
+ 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,
+ expectedAsk: true,
+ },
+ {
+ description:
+ "Opening externally when asking is the default should change persisted default.",
+ preDialogState: { preferredAction: handleInternally, alwaysAsk: true },
+ dialogActions(doc) {
+ let openItem = doc.querySelector("#open");
+ openItem.click();
+ ok(openItem.selected, "The 'save' option should now be selected");
+ },
+ expectTab: false,
+ expectLaunch: true,
+ expectedPreferredAction: useSystemDefault,
+ expectedAsk: true,
+ },
+];
+
+function ensureMIMEState({ preferredAction, alwaysAsk }) {
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+ mimeInfo.preferredAction = preferredAction;
+ mimeInfo.alwaysAskBeforeHandling = alwaysAsk;
+ gHandlerSvc.store(mimeInfo);
+}
+
+/**
+ * Test that if we have PDFs set to handle internally, and the user chooses to
+ * do something else with it, we do not alter the saved state.
+ */
+add_task(async function test_check_saving_handler_choices() {
+ let publicList = await Downloads.getList(Downloads.PUBLIC);
+ registerCleanupFunction(async () => {
+ await publicList.removeFinished();
+ });
+ for (let testCase of kTestCases) {
+ let file = "file_pdf_application_pdf.pdf";
+ info("Testing with " + file + "; " + testCase.description);
+ ensureMIMEState(testCase.preDialogState);
+
+ let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded();
+ let loadingTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_PATH + file
+ );
+ 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 Firefox option should be selected by default"
+ );
+
+ const { expectTab, expectLaunch, description } = testCase;
+ // Prep to intercept things so we only see the results we want.
+ let tabOpenListener = ev => {
+ ok(
+ expectTab,
+ `A new tab should ${expectTab ? "" : "not "}be opened - ${description}`
+ );
+ BrowserTestUtils.removeTab(ev.target);
+ };
+ gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener);
+
+ let oldLaunchFile = DownloadIntegration.launchFile;
+ let fileLaunched = PromiseUtils.defer();
+ DownloadIntegration.launchFile = () => {
+ ok(
+ expectLaunch,
+ `The file should ${
+ expectLaunch ? "" : "not "
+ }be launched with an external application - ${description}`
+ );
+ fileLaunched.resolve();
+ };
+ let downloadFinishedPromise = promiseDownloadFinished(publicList);
+
+ await testCase.dialogActions(doc);
+
+ let dialog = doc.querySelector("#unknownContentType");
+ dialog.acceptDialog();
+
+ let download = await downloadFinishedPromise;
+ if (expectLaunch) {
+ await fileLaunched.promise;
+ }
+ DownloadIntegration.launchFile = oldLaunchFile;
+ gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener);
+
+ is(
+ (await publicList.getAll()).length,
+ 1,
+ "download should appear in public list"
+ );
+
+ // Check mime info:
+ const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf");
+ gHandlerSvc.fillHandlerInfo(mimeInfo, "");
+ is(
+ mimeInfo.preferredAction,
+ testCase.expectedPreferredAction,
+ "preferredAction - " + description
+ );
+ is(
+ mimeInfo.alwaysAskBeforeHandling,
+ testCase.expectedAsk,
+ "alwaysAsk - " + description
+ );
+
+ BrowserTestUtils.removeTab(loadingTab);
+ 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_protocol_ask_dialog.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js
new file mode 100644
index 0000000000..5f60e39bb1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js
@@ -0,0 +1,398 @@
+/* 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"
+);
+
+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.loadURI(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.loadURI(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);
+});
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..7050616dc3
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js
@@ -0,0 +1,754 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+ChromeUtils.import(
+ "resource://testing-common/HandlerServiceTestUtils.jsm",
+ this
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+// Testing multiple protocol / origin combinations takes long on debug.
+requestLongerTimeout(3);
+
+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
+);
+
+let testExtension;
+
+/**
+ * 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
+ );
+}
+
+/**
+ * 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."
+ );
+}
+
+/**
+ * Open a test URL with the desired scheme.
+ * By default the load is triggered by the content principal of the browser.
+ * @param {MozBrowser} browser - Browser to load the test URL in.
+ * @param {string} scheme - Scheme of the test URL.
+ * @param {Object} [opts] - Options for the triggering principal.
+ * @param {nsIPrincipal} [opts.triggeringPrincipal] - Principal to trigger the
+ * load with. Defaults to the browsers content principal.
+ * @param {boolean} [opts.useNullPrincipal] - If true, we will trigger the load
+ * with a null principal.
+ * @param {boolean} [opts.useExtensionPrincipal] - If true, we will trigger the
+ * load with an extension.
+ * @param {boolean} [opts.omitTriggeringPrincipal] - If true, we will directly
+ * call the protocol handler dialogs without a principal.
+ */
+async function triggerOpenProto(
+ browser,
+ scheme,
+ {
+ triggeringPrincipal = browser.contentPrincipal,
+ useNullPrincipal = false,
+ useExtensionPrincipal = false,
+ omitTriggeringPrincipal = false,
+ } = {}
+) {
+ let uri = `${scheme}://test`;
+
+ if (useNullPrincipal) {
+ // Create and load iframe with data URI.
+ // This will be a null principal.
+ 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);
+ });
+ return;
+ }
+
+ if (useExtensionPrincipal) {
+ 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();
+ return;
+ }
+
+ if (omitTriggeringPrincipal) {
+ // Directly call ContentDispatchChooser without a triggering principal
+ 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
+ );
+ return;
+ }
+
+ info("Loading uri: " + uri);
+ browser.loadURI(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 {Object} [options.loadOptions] - Options for triggering the protocol
+ * load which causes the dialog to show.
+ * @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, loadOptions } = {}
+) {
+ 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 triggerOpenProto(browser, scheme, loadOptions);
+ 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,
+ hasChangeApp,
+ chooserIsNext,
+ actionCheckbox,
+ actionConfirm,
+ actionChangeApp,
+ } = permDialogOptions;
+
+ if (actionChangeApp) {
+ actionConfirm = false;
+ }
+
+ await testCheckbox(dialogEl, dialogType, {
+ hasCheckbox,
+ actionCheckbox,
+ });
+
+ // 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 (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");
+ }
+
+ // Clean up test extension if needed.
+ await testExtension?.unload();
+}
+
+/**
+ * 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 }
+) {
+ let checkbox = dialogEl.ownerDocument.getElementById("remember");
+ if (typeof hasCheckbox == "boolean") {
+ let hiddenEl;
+ if (dialogType == "permission") {
+ hiddenEl = checkbox.parentElement;
+ } else {
+ hiddenEl = checkbox;
+ }
+ is(
+ checkbox && !hiddenEl.hidden,
+ hasCheckbox,
+ "Dialog checkbox has correct visibility."
+ );
+ }
+
+ if (typeof hasCheckboxState == "boolean") {
+ is(checkbox.checked, hasCheckboxState, "Dialog checkbox has correct state");
+ }
+
+ if (typeof actionCheckbox == "boolean") {
+ checkbox.focus();
+ await EventUtils.synthesizeKey("VK_SPACE", undefined, dialogEl.ownerWindow);
+ }
+}
+
+/**
+ * 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_task(async function setup() {
+ 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
+ * skip the permission dialog.
+ */
+add_task(async function test_permission_system_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ chooserDialogOptions: { hasCheckbox: true, actionConfirm: false },
+ loadOptions: {
+ triggeringPrincipal: 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, {
+ loadOptions: {
+ useNullPrincipal: true,
+ },
+ 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, {
+ loadOptions: {
+ omitTriggeringPrincipal: true,
+ },
+ permDialogOptions: {
+ hasCheckbox: false,
+ chooserIsNext: true,
+ hasChangeApp: false,
+ actionConfirm: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
+
+/**
+ * Tests that we skip the permission dialog for extension callers.
+ */
+add_task(async function test_extension_principal() {
+ let scheme = TEST_PROTOS[0];
+ await BrowserTestUtils.withNewTab(ORIGIN1, async browser => {
+ await testOpenProto(browser, scheme, {
+ loadOptions: {
+ useExtensionPrincipal: true,
+ },
+ chooserDialogOptions: {
+ hasCheckbox: true,
+ actionConfirm: false, // Cancel dialog
+ },
+ });
+ });
+});
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..b9ba4a7955
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js
@@ -0,0 +1,76 @@
+/* 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 = gBrowser.loadOneTab = oldAddTab)
+ );
+ let wrongThingHappenedPromise = new Promise(resolve => {
+ gBrowser.addTab = gBrowser.loadOneTab = 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);
+
+ BrowserTestUtils.loadURI(browser, kProt + ":test");
+ 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..28bd50a120
--- /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/SharedPromptUtils.jsm#53
+ 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_web_protocol_handlers.js b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
new file mode 100644
index 0000000000..66f461834c
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js
@@ -0,0 +1,124 @@
+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.loadURI(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.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,
+ 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,
+ 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 != testURL);
+ is(
+ gURLBar.value,
+ 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..bee7bd7015
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/download.sjs
@@ -0,0 +1,38 @@
+"use strict";
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+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_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_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..37823166a4
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^
@@ -0,0 +1,2 @@
+Content-Disposition: attachment; filename=file_text_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/handlerApp.xhtml b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml
new file mode 100644
index 0000000000..e519e80029
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml
@@ -0,0 +1,28 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Pseudo Web Handler App</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="onLoad()">
+Pseudo Web Handler App
+
+<script class="testbody" type="text/javascript">
+<![CDATA[
+function onLoad() {
+ // if we have a window.opener, this must be the windowContext
+ // instance of this test. check that we got the URI right and clean up.
+ if (window.opener) {
+ window.opener.is(location.search,
+ "?uri=" + encodeURIComponent(window.opener.testURI),
+ "uri passed to web-handler app");
+ window.opener.SimpleTest.finish();
+ }
+
+ window.close();
+}
+]]>
+</script>
+
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/handlerApps.js b/uriloader/exthandler/tests/mochitest/handlerApps.js
new file mode 100644
index 0000000000..aa841f13be
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/handlerApps.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+// handlerApp.xhtml grabs this for verification purposes via window.opener
+var testURI = "webcal://127.0.0.1/rheeeeet.html";
+
+const Cc = SpecialPowers.Cc;
+
+function test() {
+ // set up the web handler object
+ var webHandler = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(SpecialPowers.Ci.nsIWebHandlerApp);
+ webHandler.name = "Test Web Handler App";
+ webHandler.uriTemplate =
+ "https://example.com/tests/uriloader/exthandler/tests/mochitest/" +
+ "handlerApp.xhtml?uri=%s";
+
+ // set up the uri to test with
+ /* eslint-disable mozilla/use-services */
+
+ var ioService = Cc["@mozilla.org/network/io-service;1"].getService(
+ SpecialPowers.Ci.nsIIOService
+ );
+
+ var uri = ioService.newURI(testURI);
+
+ // create a window, and launch the handler in it
+ var newWindow = window.open("", "handlerWindow", "height=300,width=300");
+ var windowContext = SpecialPowers.wrap(newWindow).docShell;
+
+ webHandler.launchWithURI(uri, windowContext);
+
+ // if we get this far without an exception, we've at least partly passed
+ // (remaining check in handlerApp.xhtml)
+ ok(true, "webHandler launchWithURI (existing window/tab) started");
+
+ // make the web browser launch in its own window/tab
+ webHandler.launchWithURI(uri);
+
+ // if we get this far without an exception, we've passed
+ ok(true, "webHandler launchWithURI (new window/tab) test started");
+
+ // set up the local handler object
+ var localHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(SpecialPowers.Ci.nsILocalHandlerApp);
+ localHandler.name = "Test Local Handler App";
+
+ // get a local app that we know will be there and do something sane
+ /* eslint-disable mozilla/use-services */
+
+ var osString = Cc["@mozilla.org/xre/app-info;1"].getService(
+ SpecialPowers.Ci.nsIXULRuntime
+ ).OS;
+
+ var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ SpecialPowers.Ci.nsIDirectoryServiceProvider
+ );
+ if (osString == "WINNT") {
+ var windowsDir = dirSvc.getFile("WinD", {});
+ var exe = windowsDir.clone().QueryInterface(SpecialPowers.Ci.nsIFile);
+ exe.appendRelativePath("SYSTEM32\\HOSTNAME.EXE");
+ } else if (osString == "Darwin") {
+ var localAppsDir = dirSvc.getFile("LocApp", {});
+ exe = localAppsDir.clone();
+ exe.append("iCal.app"); // lingers after the tests finish, but this seems
+ // seems better than explicitly killing it, since
+ // developers who run the tests locally may well
+ // information in their running copy of iCal
+
+ if (navigator.userAgent.match(/ SeaMonkey\//)) {
+ // SeaMonkey tinderboxes don't like to have iCal lingering (and focused)
+ // on next test suite run(s).
+ todo(false, "On SeaMonkey, testing OS X as generic Unix. (Bug 749872)");
+
+ // assume a generic UNIX variant
+ exe = Cc["@mozilla.org/file/local;1"].createInstance(
+ SpecialPowers.Ci.nsIFile
+ );
+ exe.initWithPath("/bin/echo");
+ }
+ } else {
+ // assume a generic UNIX variant
+ exe = Cc["@mozilla.org/file/local;1"].createInstance(
+ SpecialPowers.Ci.nsIFile
+ );
+ exe.initWithPath("/bin/echo");
+ }
+
+ localHandler.executable = exe;
+ localHandler.launchWithURI(ioService.newURI(testURI));
+
+ // if we get this far without an exception, we've passed
+ ok(true, "localHandler launchWithURI test");
+
+ // if we ever decide that killing iCal is the right thing to do, change
+ // the if statement below from "NOTDarwin" to "Darwin"
+ if (osString == "NOTDarwin") {
+ var killall = Cc["@mozilla.org/file/local;1"].createInstance(
+ SpecialPowers.Ci.nsIFile
+ );
+ killall.initWithPath("/usr/bin/killall");
+
+ var process = Cc["@mozilla.org/process/util;1"].createInstance(
+ SpecialPowers.Ci.nsIProcess
+ );
+ process.init(killall);
+
+ var args = ["iCal"];
+ process.run(false, args, args.length);
+ }
+
+ SimpleTest.waitForExplicitFinish();
+}
+
+test();
diff --git a/uriloader/exthandler/tests/mochitest/head.js b/uriloader/exthandler/tests/mochitest/head.js
new file mode 100644
index 0000000000..3b0d8a4072
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/head.js
@@ -0,0 +1,277 @@
+var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+var { HandlerServiceTestUtils } = ChromeUtils.import(
+ "resource://testing-common/HandlerServiceTestUtils.jsm"
+);
+
+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 = FileUtils.getFile("TmpD", ["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() {},
+ launchWithApplication() {},
+ 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;
+}
+
+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
+ );
+ Cu.reportError(ex);
+ }
+ let dlg = await helperAppDialogShownPromise;
+
+ is(
+ dlg.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Got correct dialog"
+ );
+
+ return dlg;
+}
+
+async function waitForSubDialog(browser, url, state) {
+ let eventStr = state ? "dialogopen" : "dialogclose";
+
+ let tabDialogBox = gBrowser.getTabDialogBox(browser);
+ let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack;
+
+ let checkFn;
+
+ if (state) {
+ checkFn = dialogEvent => dialogEvent.detail.dialog?._openedURL == url;
+ }
+
+ let event = await BrowserTestUtils.waitForEvent(
+ dialogStack,
+ 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) {
+ return new Promise(resolve => {
+ list.addView({
+ onDownloadChanged(download) {
+ 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 = await PathUtils.getTempDir();
+ tmpDir = PathUtils.join(
+ tmpDir,
+ "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() {
+ try {
+ await IOUtils.remove(tmpDir, { recursive: true });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ });
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ Services.prefs.setCharPref("browser.download.dir", tmpDir);
+ return tmpDir;
+}
+
+add_task(async function test_common_initialize() {
+ gDownloadDir = await setDownloadDir();
+ Services.prefs.setCharPref("browser.download.loglevel", "Debug");
+});
diff --git a/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs
new file mode 100644
index 0000000000..d12e2904d9
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs
@@ -0,0 +1,14 @@
+/* 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/mochitest.ini b/uriloader/exthandler/tests/mochitest/mochitest.ini
new file mode 100644
index 0000000000..be3b6bb37e
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/mochitest.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+ handlerApp.xhtml
+ handlerApps.js
+
+[test_handlerApps.xhtml]
+skip-if = (toolkit == 'android' || os == 'mac') || e10s # OS X: bug 786938
+scheme = https
+[test_invalidCharFileExtension.xhtml]
+skip-if = toolkit == 'android' && !is_fennec # Bug 1525959
+support-files =
+ HelperAppLauncherDialog_chromeScript.js
+ invalidCharFileExtension.sjs
+[test_nullCharFile.xhtml]
+skip-if = toolkit == 'android' && !is_fennec # Bug 1525959
+support-files =
+ HelperAppLauncherDialog_chromeScript.js
+[test_unknown_ext_protocol_handlers.html]
+[test_unsafeBidiChars.xhtml]
+skip-if = toolkit == 'android' && !is_fennec # Bug 1525959
+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/test_handlerApps.xhtml b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml
new file mode 100644
index 0000000000..d6166fd270
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml
@@ -0,0 +1,11 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Handler Apps </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="handlerApps.js"/>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+</body>
+</html>
diff --git a/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml
new file mode 100644
index 0000000000..4ee1a6a1c1
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml
@@ -0,0 +1,47 @@
+<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() {
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ for (let [name, expected] of tests) {
+ let promiseName = new Promise(function(resolve) {
+ chromeScript.addMessageListener("suggestedFileName",
+ function listener(data) {
+ chromeScript.removeMessageListener("suggestedFileName", listener);
+ resolve(data);
+ });
+ });
+ document.getElementById("test").src =
+ "invalidCharFileExtension.sjs?name=" + encodeURIComponent(name);
+ is((await promiseName), expected, "got the expected sanitized name");
+ }
+
+ let promise = new Promise(function(resolve) {
+ chromeScript.addMessageListener("unregistered", function listener() {
+ chromeScript.removeMessageListener("unregistered", listener);
+ resolve();
+ });
+ });
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ 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..9bb1140718
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml
@@ -0,0 +1,49 @@
+<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() {
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ for (let [name, expected] of tests) {
+ let promiseName = new Promise(function(resolve) {
+ chromeScript.addMessageListener("suggestedFileName",
+ function listener(data) {
+ chromeScript.removeMessageListener("suggestedFileName", listener);
+ resolve(data);
+ });
+ });
+ 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");
+ }
+
+ let promise = new Promise(function(resolve) {
+ chromeScript.addMessageListener("unregistered", function listener() {
+ chromeScript.removeMessageListener("unregistered", listener);
+ resolve();
+ });
+ });
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ chromeScript.destroy();
+});
+</script>
+</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..4f62b32d99
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml
@@ -0,0 +1,72 @@
+<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() {
+ let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js");
+ let chromeScript = SpecialPowers.loadChromeScript(url);
+
+ for (let test of tests) {
+ for (let char of unsafeBidiChars) {
+ let promiseName = new Promise(function(resolve) {
+ chromeScript.addMessageListener("suggestedFileName",
+ function listener(data) {
+ chromeScript.removeMessageListener("suggestedFileName", listener);
+ resolve(data);
+ });
+ });
+ 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");
+ }
+ }
+
+ let promise = new Promise(function(resolve) {
+ chromeScript.addMessageListener("unregistered", function listener() {
+ chromeScript.removeMessageListener("unregistered", listener);
+ resolve();
+ });
+ });
+ chromeScript.sendAsyncMessage("unregister");
+ await promise;
+
+ 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..48301be5b4
--- /dev/null
+++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs
@@ -0,0 +1,14 @@
+/* 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..3d96aaa448
--- /dev/null
+++ b/uriloader/exthandler/tests/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ["mochitest/mochitest.ini"]
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
+
+BROWSER_CHROME_MANIFESTS += ["mochitest/browser.ini"]
+
+TESTING_JS_MODULES += [
+ "HandlerServiceTestUtils.jsm",
+]
+
+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..40e88f930a
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/handlers.json
@@ -0,0 +1,90 @@
+{
+ "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"
+ }
+ ]
+ }
+ }
+}
diff --git a/uriloader/exthandler/tests/unit/head.js b/uriloader/exthandler/tests/unit/head.js
new file mode 100644
index 0000000000..3330a309be
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/head.js
@@ -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/. */
+
+/*
+ * Initialization for tests related to invoking external handler applications.
+ */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+var { OS, require } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.import(
+ "resource://testing-common/HandlerServiceTestUtils.jsm",
+ this
+);
+var { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gHandlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+
+do_get_profile();
+
+let jsonPath = OS.Path.join(OS.Constants.Path.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 OS.File.remove(jsonPath, { ignoreAbsent: true });
+};
+
+/**
+ * Unloads the data store and replaces it with the test data file.
+ */
+let copyTestDataToHandlerStore = async function() {
+ await unloadHandlerStore();
+
+ await OS.File.copy(do_get_file("handlers.json").path, jsonPath);
+};
+
+/**
+ * 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..f9f9feda23
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js
@@ -0,0 +1,163 @@
+/* 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"
+);
+
+const kDefaultHandlerList = Services.prefs
+ .getChildList("gecko.handlerService.schemes")
+ .filter(p => {
+ try {
+ let val = Services.prefs.getComplexValue(p, Ci.nsIPrefLocalizedString)
+ .data;
+ return !!val;
+ } catch (ex) {
+ return false;
+ }
+ });
+
+add_task(async function test_check_defaults_get_added() {
+ let protocols = new Set(
+ kDefaultHandlerList.map(p => p.match(/schemes\.(\w+)/)[1])
+ );
+ for (let protocol of protocols) {
+ const kPrefStr = `schemes.${protocol}.`;
+ let matchingPrefs = kDefaultHandlerList.filter(p => p.includes(kPrefStr));
+ let protocolHandlerCount = matchingPrefs.length / 2;
+ Assert.ok(
+ protocolHandlerCount,
+ `Prefs for ${protocol} have at least 1 protocol handler`
+ );
+ 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() {
+ let mailtoHandlerCount =
+ kDefaultHandlerList.filter(p => p.includes("mailto")).length / 2;
+ Assert.ok(mailtoHandlerCount, "Prefs have at least 1 mailto handler");
+ 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();
+});
+
+/**
+ * Check that we don't add bogus handlers.
+ */
+add_task(async function test_check_restrictions() {
+ const kTestData = {
+ testdeleteme: [
+ ["Delete me", ""],
+ ["Delete me insecure", "http://example.com/%s"],
+ ["Delete me no substitution", "https://example.com/"],
+ ["Keep me", "https://example.com/%s"],
+ ],
+ testreallydeleteme: [
+ // used to check we remove the entire entry.
+ ["Delete me", "http://example.com/%s"],
+ ],
+ };
+ for (let [scheme, handlers] of Object.entries(kTestData)) {
+ let count = 1;
+ for (let [name, uriTemplate] of handlers) {
+ let pref = `gecko.handlerService.schemes.${scheme}.${count}.`;
+ let obj = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(
+ Ci.nsIPrefLocalizedString
+ );
+ obj.data = name;
+ Services.prefs.setComplexValue(
+ pref + "name",
+ Ci.nsIPrefLocalizedString,
+ obj
+ );
+ obj.data = uriTemplate;
+ Services.prefs.setComplexValue(
+ pref + "uriTemplate",
+ Ci.nsIPrefLocalizedString,
+ obj
+ );
+ count++;
+ }
+ }
+
+ gHandlerService.wrappedJSObject._injectDefaultProtocolHandlers();
+ let schemeData = gHandlerService.wrappedJSObject._store.data.schemes;
+
+ Assert.ok(schemeData.testdeleteme, "Expect an entry for testdeleteme");
+ Assert.ok(
+ schemeData.testdeleteme.stubEntry,
+ "Expect a stub entry for testdeleteme"
+ );
+
+ Assert.deepEqual(
+ schemeData.testdeleteme.handlers,
+ [null, { name: "Keep me", uriTemplate: "https://example.com/%s" }],
+ "Expect only one handler is kept."
+ );
+
+ Assert.ok(!schemeData.testreallydeleteme, "No entry for reallydeleteme");
+});
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..62031da1e6
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js
@@ -0,0 +1,36 @@
+/* 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"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gBundleService",
+ "@mozilla.org/intl/stringbundle;1",
+ "nsIStringBundleService"
+);
+
+// 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 = gBundleService.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..dad5530856
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js
@@ -0,0 +1,190 @@
+/* -*- 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;
+
+ info("Create a mock RegKey factory");
+ let originalRegKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ let mockWindowsRegKeyFactory = {
+ createInstance(outer, iid) {
+ if (outer != null) {
+ throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION);
+ }
+ 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
+ );
+ });
+}
diff --git a/uriloader/exthandler/tests/unit/test_handlerService.js b/uriloader/exthandler/tests/unit/test_handlerService.js
new file mode 100644
index 0000000000..610eb5b749
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_handlerService.js
@@ -0,0 +1,474 @@
+/* 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;
+
+ const env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+
+ const rootPrefBranch = prefSvc.getBranch("");
+
+ 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.ok(handlerInfo.alwaysAskBeforeHandling);
+
+ // 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, "");
+
+ // test some default protocol info properties
+ var haveDefaultHandlersVersion = false;
+ try {
+ // If we have a defaultHandlersVersion pref, then assume that we're in the
+ // firefox tree and that we'll also have default handlers.
+ // Bug 395131 has been filed to make this test work more generically
+ // by providing our own prefs for this test rather than this icky
+ // special casing.
+ rootPrefBranch.getCharPref("gecko.handlerService.defaultHandlersVersion");
+ haveDefaultHandlersVersion = true;
+ } catch (ex) {}
+
+ 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);
+ 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 (haveDefaultHandlersVersion) {
+ Assert.equal(2, protoInfo.possibleApplicationHandlers.length);
+ } else {
+ Assert.equal(0, 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 (haveDefaultHandlersVersion) {
+ Assert.equal(2, 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.
+ } else {
+ Assert.equal(0, protoInfo.possibleApplicationHandlers.length);
+ Assert.ok(protoInfo.alwaysAskBeforeHandling);
+ }
+
+ if (haveDefaultHandlersVersion) {
+ // 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");
+ Assert.equal(2, 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"];
+ if (haveDefaultHandlersVersion) {
+ handlerTypes.push("mailto");
+ handlerTypes.push("irc");
+ handlerTypes.push("ircs");
+ }
+ 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") {
+ env.set("PERSONAL_MAILCAP", do_get_file("mailcap").path);
+ handlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", null);
+ Assert.equal(
+ handlerInfo.preferredAction,
+ Ci.nsIHandlerInfo.useSystemDefault
+ );
+ 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..aa2efde822
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_handlerService_store.js
@@ -0,0 +1,771 @@
+/* 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 = FileUtils.getFile("TmpD", []);
+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 alwaysAsk or has an unknown value, but the
+ * action always becomes useHelperApp when reloading.
+ */
+add_task(async function test_store_preferredAction() {
+ await deleteHandlerStore();
+
+ let handlerInfo = getKnownHandlerInfo("example/new");
+
+ for (let preferredAction of [Ci.nsIHandlerInfo.alwaysAsk, 999]) {
+ handlerInfo.preferredAction = preferredAction;
+ gHandlerService.store(handlerInfo);
+ gHandlerService.fillHandlerInfo(handlerInfo, "");
+ Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp);
+ }
+});
+
+/**
+ * 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 = FileUtils.getFile("TmpD", ["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();
+
+ for (let type of ["irc", "ircs"]) {
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type,
+ preferredActionOSDependent: true,
+ possibleApplicationHandlers: [
+ {
+ name: "Mibbit",
+ uriTemplate: "https://www.mibbit.com/?url=%s",
+ },
+ ],
+ });
+ }
+
+ HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), {
+ type: "mailto",
+ preferredActionOSDependent: true,
+ possibleApplicationHandlers: [
+ {
+ name: "Yahoo! Mail",
+ uriTemplate: "https://compose.mail.yahoo.com/?To=%s",
+ },
+ {
+ 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(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(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();
+
+ // Remove the "irc" handler so we can verify that the injection is repeated.
+ let ircHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("irc");
+ gHandlerService.remove(ircHandlerInfo);
+
+ let originalDefaultHandlersVersion = Services.prefs.getComplexValue(
+ "gecko.handlerService.defaultHandlersVersion",
+ Ci.nsIPrefLocalizedString
+ );
+
+ // Set the preference to an arbitrarily high number to force injecting again.
+ Services.prefs.setStringPref(
+ "gecko.handlerService.defaultHandlersVersion",
+ "999"
+ );
+
+ await unloadHandlerStore();
+
+ // Check that "irc" exists to make sure that the injection was repeated.
+ Assert.ok(gHandlerService.exists(ircHandlerInfo));
+
+ // There should be no duplicate handlers in the protocols.
+ await assertAllHandlerInfosMatchDefaultHandlers();
+
+ Services.prefs.setStringPref(
+ "gecko.handlerService.defaultHandlersVersion",
+ originalDefaultHandlersVersion
+ );
+});
+
+/**
+ * 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 = JSON.parse(new TextDecoder().decode(await OS.File.read(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 = FileUtils.getFile("TmpD", ["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 OS.File.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_protocol_ask_dialog_telemetry.js b/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js
new file mode 100644
index 0000000000..161cce8d33
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ContentDispatchChooserTelemetry } = ChromeUtils.import(
+ "resource://gre/modules/ContentDispatchChooser.jsm"
+);
+
+let telemetryLabels = Services.telemetry.getCategoricalLabels()
+ .EXTERNAL_PROTOCOL_HANDLER_DIALOG_CONTEXT_SCHEME;
+
+let schemeToLabel = ContentDispatchChooserTelemetry.SCHEME_TO_LABEL;
+let schemePrefixToLabel =
+ ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL;
+
+/**
+ * Test for scheme-label mappings of protocol ask dialog telemetry.
+ */
+add_task(async function test_telemetry_label_maps() {
+ let mapValues = Object.values(schemeToLabel).concat(
+ Object.values(schemePrefixToLabel)
+ );
+
+ // Scheme - label maps must have valid label values.
+ mapValues.forEach(label => {
+ // Mapped labels must be valid.
+ Assert.ok(telemetryLabels.includes(label), `Exists label: ${label}`);
+ });
+
+ // Uppercase labels must have a mapping.
+ telemetryLabels.forEach(label => {
+ Assert.equal(
+ label == "OTHER" || mapValues.includes(label),
+ label == label.toUpperCase(),
+ `Exists label: ${label}`
+ );
+ });
+
+ Object.keys(schemeToLabel).forEach(key => {
+ // Schemes which have a mapping must not exist as as label.
+ Assert.ok(!telemetryLabels.includes(key), `Not exists label: ${key}`);
+
+ // There must be no key duplicates across the two maps.
+ Assert.ok(!schemePrefixToLabel[key], `No duplicate key: ${key}`);
+ });
+});
+
+/**
+ * Tests the getTelemetryLabel method.
+ */
+add_task(async function test_telemetry_getTelemetryLabel() {
+ // Method should return the correct mapping.
+ Object.keys(schemeToLabel).forEach(scheme => {
+ Assert.equal(
+ schemeToLabel[scheme],
+ ContentDispatchChooserTelemetry._getTelemetryLabel(scheme)
+ );
+ });
+
+ // Passing null to _getTelemetryLabel should throw.
+ Assert.throws(() => {
+ ContentDispatchChooserTelemetry._getTelemetryLabel(null);
+ }, /Invalid scheme/);
+
+ // Replace maps with test data
+ ContentDispatchChooserTelemetry.SCHEME_TO_LABEL = {
+ foo: "FOOLABEL",
+ bar: "BARLABEL",
+ };
+
+ ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL = {
+ fooPrefix: "FOOPREFIXLABEL",
+ barPrefix: "BARPREFIXLABEL",
+ fo: "PREFIXLABEL",
+ };
+
+ Assert.equal(
+ ContentDispatchChooserTelemetry._getTelemetryLabel("foo"),
+ "FOOLABEL",
+ "Non prefix mapping should have priority"
+ );
+
+ Assert.equal(
+ ContentDispatchChooserTelemetry._getTelemetryLabel("bar"),
+ "BARLABEL",
+ "Should return the correct label"
+ );
+
+ Assert.equal(
+ ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix"),
+ "FOOPREFIXLABEL",
+ "Should return the correct label"
+ );
+
+ Assert.equal(
+ ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix1"),
+ "FOOPREFIXLABEL",
+ "Should return the correct label"
+ );
+
+ Assert.equal(
+ ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix2"),
+ "FOOPREFIXLABEL",
+ "Should return the correct label"
+ );
+
+ Assert.equal(
+ ContentDispatchChooserTelemetry._getTelemetryLabel("doesnotexist"),
+ "OTHER",
+ "Should return the correct label for unknown scheme"
+ );
+
+ // Restore maps
+ ContentDispatchChooserTelemetry.SCHEME_TO_LABEL = schemeToLabel;
+ ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL = schemePrefixToLabel;
+});
diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js
new file mode 100644
index 0000000000..638128d11b
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.js
@@ -0,0 +1,130 @@
+/* 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
+ var envSvc = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+
+ // 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);
+
+ envSvc.set("DYLD_LIBRARY_PATH", greDir.path);
+ // For Linux
+ envSvc.set("LD_LIBRARY_PATH", greDir.path);
+ // XXX: handle windows
+
+ // Now tell it where we want the file.
+ envSvc.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.ini b/uriloader/exthandler/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..d7bf9e54d6
--- /dev/null
+++ b/uriloader/exthandler/tests/unit/xpcshell.ini
@@ -0,0 +1,28 @@
+[DEFAULT]
+head = head.js
+run-sequentially = Bug 912235 - Intermittent failures
+firefox-appdir = 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_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_badMIMEType.js]
+[test_handlerService.js]
+skip-if = (verify && (os == 'win'))
+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]
+[test_protocol_ask_dialog_telemetry.js]
+skip-if = os == "android" # Desktop telemetry
diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h
new file mode 100644
index 0000000000..35672ed20c
--- /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,
+ 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..b9fe2766ba
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm
@@ -0,0 +1,15 @@
+/* -*- 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, nsIInterfaceRequestor* aWindowContext) {
+ 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..8638b94f75
--- /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);
+#ifdef DEBUG
+ virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) {
+ 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..2ed0c1eb2e
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm
@@ -0,0 +1,12 @@
+/* -*- 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..8c1b1fb6b3
--- /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);
+ NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme,
+ bool* _retval);
+
+ // method overrides --> used to hook the mime service into internet config....
+ NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType,
+ const nsACString& aFileExt,
+ nsIMIMEInfo** aMIMEInfo);
+ NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType,
+ const nsACString& aFileExt, bool* aFound,
+ nsIMIMEInfo** aMIMEInfo) override;
+ NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
+ bool* found,
+ nsIHandlerInfo** _retval);
+
+ // 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);
+
+ 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..c0eb14d28d
--- /dev/null
+++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm
@@ -0,0 +1,53 @@
+/* -*- 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 char* 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..6136709d91
--- /dev/null
+++ b/uriloader/exthandler/unix/nsGNOMERegistry.cpp
@@ -0,0 +1,100 @@
+/* -*- 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::useSystemDefault);
+ 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..7cbefcce3e
--- /dev/null
+++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp
@@ -0,0 +1,80 @@
+/* -*- 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 mDefaultApplication 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 (mDefaultApplication) 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 mDefaultApplication 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 (mDefaultApplication) 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..8ffa98c40b
--- /dev/null
+++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp
@@ -0,0 +1,1409 @@
+/* -*- 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"
+#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/Preferences.h"
+#include "nsMimeTypes.h"
+
+using namespace mozilla;
+
+#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(mLog, 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;
+}
+
+// static
+nsresult nsOSHelperAppService::GetFileLocation(const char* aPrefName,
+ const char* aEnvVarName,
+ nsAString& aFileLocation) {
+ LOG(("-- GetFileLocation. Pref: '%s' EnvVar: '%s'\n", aPrefName,
+ aEnvVarName));
+ MOZ_ASSERT(aPrefName, "Null pref name passed; don't do that!");
+
+ aFileLocation.Truncate();
+ /* The lookup order is:
+ 1) user pref
+ 2) env var
+ 3) pref
+ */
+ NS_ENSURE_TRUE(Preferences::GetRootBranch(), NS_ERROR_FAILURE);
+
+ /*
+ 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(aPrefName) &&
+ NS_SUCCEEDED(Preferences::GetString(aPrefName, aFileLocation))) {
+ return NS_OK;
+ }
+
+ if (aEnvVarName && *aEnvVarName) {
+ char* prefValue = PR_GetEnv(aEnvVarName);
+ if (prefValue && *prefValue) {
+ // 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(prefValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = file->GetPath(aFileLocation);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+ }
+
+ return Preferences::GetString(aPrefName, aFileLocation);
+}
+
+/* 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;
+
+ const char* filenamePref = aUserData ? "helpers.private_mime_types_file"
+ : "helpers.global_mime_types_file";
+
+ nsresult rv = GetFileLocation(filenamePref, nullptr, 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("helpers.private_mime_types_file", nullptr, 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("helpers.global_mime_types_file", nullptr,
+ 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 char* filenamePref = aUserData ? "helpers.private_mailcap_file"
+ : "helpers.global_mailcap_file";
+ const char* filenameEnvVar = aUserData ? "PERSONAL_MAILCAP" : "MAILCAP";
+
+ nsresult rv = GetFileLocation(filenamePref, filenameEnvVar, mailcapFileName);
+ if (NS_SUCCEEDED(rv) && !mailcapFileName.IsEmpty()) {
+ rv = GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, aMajorType,
+ aMinorType, aHandler,
+ aDescription, aMozillaFlags);
+ } else {
+ rv = NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return rv;
+}
+
+// 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;
+ 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::useSystemDefault);
+ 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::useSystemDefault);
+ 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..ffa83f6a23
--- /dev/null
+++ b/uriloader/exthandler/unix/nsOSHelperAppService.h
@@ -0,0 +1,125 @@
+/* -*- 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 GetFileLocation(const char* aPrefName,
+ const char* aEnvVarName,
+ nsAString& aFileLocation);
+ 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..9f36a97ff9
--- /dev/null
+++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp
@@ -0,0 +1,900 @@
+/* -*- 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 "nsCOMArray.h"
+#include "nsLocalFile.h"
+#include "nsMIMEInfoWin.h"
+#include "nsNetUtil.h"
+#include <windows.h>
+#include <shellapi.h>
+#include "nsIMutableArray.h"
+#include "nsTArray.h"
+#include "shlobj.h"
+#include "windows.h"
+#include "nsIWindowsRegKey.h"
+#include "nsUnicharUtils.h"
+#include "nsITextToSubURI.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 (mPreferredAction == useSystemDefault) {
+ if (mDefaultApplication &&
+ 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 = mDefaultApplication->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(mDefaultApplication,
+ 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;
+ if (mDefaultApplication &&
+ aName.EqualsLiteral(PROPERTY_DEFAULT_APP_ICON_URL)) {
+ rv = GetIconURLVariant(mDefaultApplication, _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;
+
+ nsCOMPtr<nsITextToSubURI> textToSubURI =
+ do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (NS_FAILED(textToSubURI->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.
+ LauncherResult<_bstr_t> validatedUri = 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;
+}
+
+// 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(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;
+ if (mDefaultApplication) {
+ // Determine if the default executable is our executable.
+ nsCOMPtr<nsIFile> ourBinary;
+ XRE_GetBinaryPath(getter_AddRefs(ourBinary));
+ bool isSame = false;
+ nsresult rv = mDefaultApplication->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..1de1f152ad
--- /dev/null
+++ b/uriloader/exthandler/win/nsMIMEInfoWin.h
@@ -0,0 +1,72 @@
+/* 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;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIPROPERTYBAG
+
+ void SetDefaultApplicationHandler(nsIFile* aDefaultApplication) {
+ mDefaultApplication = aDefaultApplication;
+ }
+
+ protected:
+ 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..48c5ec64d8
--- /dev/null
+++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp
@@ -0,0 +1,589 @@
+/* -*- 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 "plstr.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsLocalFile.h"
+#include "nsIWindowsRegKey.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/WindowsVersion.h"
+
+// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN
+#include <shellapi.h>
+#include <shlwapi.h>
+
+#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, 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);
+
+ if (mozilla::IsWin8OrLater()) {
+ 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.
+ HRESULT 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);
+
+ // windows registry assumes your file extension is going to include the '.',
+ // but our APIs expect it to not be there, so make sure we normalize that bit.
+ nsAutoString fileExtToUse;
+ if (aFileExt.First() != char16_t('.')) fileExtToUse = char16_t('.');
+
+ fileExtToUse.Append(aFileExt);
+
+ // don't append the '.' for our APIs.
+ nsAutoCString lowerFileExt =
+ NS_ConvertUTF16toUTF8(Substring(fileExtToUse, 1));
+ ToLowerCase(lowerFileExt);
+ mimeInfo->AppendExtension(lowerFileExt);
+ mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault);
+
+ nsAutoString appInfo;
+ bool found;
+
+ // Retrieve the default application for this extension
+ NS_ENSURE_TRUE(mAppAssoc, nullptr);
+ nsString assocType(fileExtToUse);
+ 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 nullptr;
+ }
+
+ // Get other nsIMIMEInfo fields from registry, if possible.
+ nsAutoString defaultDescription;
+ nsCOMPtr<nsIFile> defaultApplication;
+
+ if (NS_FAILED(GetDefaultAppInfo(appInfo, defaultDescription,
+ getter_AddRefs(defaultApplication)))) {
+ return nullptr;
+ }
+
+ mimeInfo->SetDefaultDescription(defaultDescription);
+ mimeInfo->SetDefaultApplicationHandler(defaultApplication);
+
+ // Grab the general description
+ GetMIMEInfoFromRegistry(appInfo, mimeInfo);
+
+ return mimeInfo.forget();
+}
+
+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", 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(), 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", 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",
+ 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",
+ 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::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..0b5cbc21e5
--- /dev/null
+++ b/uriloader/exthandler/win/nsOSHelperAppService.h
@@ -0,0 +1,79 @@
+/* -*- 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>
+
+#ifdef _WIN32_WINNT
+# undef _WIN32_WINNT
+#endif
+#define _WIN32_WINNT 0x0600
+#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 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 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__