summaryrefslogtreecommitdiffstats
path: root/uriloader
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /uriloader
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--uriloader/base/moz.build37
-rw-r--r--uriloader/base/nsCURILoader.idl36
-rw-r--r--uriloader/base/nsDocLoader.cpp1574
-rw-r--r--uriloader/base/nsDocLoader.h407
-rw-r--r--uriloader/base/nsIContentHandler.idl35
-rw-r--r--uriloader/base/nsIDocumentLoader.idl36
-rw-r--r--uriloader/base/nsITransfer.idl137
-rw-r--r--uriloader/base/nsIURIContentListener.idl124
-rw-r--r--uriloader/base/nsIURILoader.idl140
-rw-r--r--uriloader/base/nsIWebProgress.idl164
-rw-r--r--uriloader/base/nsIWebProgressListener.idl547
-rw-r--r--uriloader/base/nsIWebProgressListener2.idl69
-rw-r--r--uriloader/base/nsURILoader.cpp795
-rw-r--r--uriloader/base/nsURILoader.h219
-rw-r--r--uriloader/docs/index.rst10
-rw-r--r--uriloader/docs/uriloader.rst46
-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
-rw-r--r--uriloader/moz.build17
-rw-r--r--uriloader/prefetch/OfflineCacheUpdateChild.cpp472
-rw-r--r--uriloader/prefetch/OfflineCacheUpdateChild.h93
-rw-r--r--uriloader/prefetch/OfflineCacheUpdateGlue.cpp220
-rw-r--r--uriloader/prefetch/OfflineCacheUpdateGlue.h124
-rw-r--r--uriloader/prefetch/OfflineCacheUpdateParent.cpp287
-rw-r--r--uriloader/prefetch/OfflineCacheUpdateParent.h64
-rw-r--r--uriloader/prefetch/POfflineCacheUpdate.ipdl28
-rw-r--r--uriloader/prefetch/moz.build44
-rw-r--r--uriloader/prefetch/nsIOfflineCacheUpdate.idl291
-rw-r--r--uriloader/prefetch/nsIPrefetchService.idl54
-rw-r--r--uriloader/prefetch/nsOfflineCacheUpdate.cpp2332
-rw-r--r--uriloader/prefetch/nsOfflineCacheUpdate.h369
-rw-r--r--uriloader/prefetch/nsOfflineCacheUpdateService.cpp646
-rw-r--r--uriloader/prefetch/nsPrefetchService.cpp889
-rw-r--r--uriloader/prefetch/nsPrefetchService.h130
-rw-r--r--uriloader/preload/FetchPreloader.cpp339
-rw-r--r--uriloader/preload/FetchPreloader.h99
-rw-r--r--uriloader/preload/PreloadHashKey.cpp213
-rw-r--r--uriloader/preload/PreloadHashKey.h105
-rw-r--r--uriloader/preload/PreloadService.cpp290
-rw-r--r--uriloader/preload/PreloadService.h126
-rw-r--r--uriloader/preload/PreloaderBase.cpp380
-rw-r--r--uriloader/preload/PreloaderBase.h200
-rw-r--r--uriloader/preload/gtest/TestFetchPreloader.cpp937
-rw-r--r--uriloader/preload/gtest/moz.build18
-rw-r--r--uriloader/preload/moz.build28
183 files changed, 34125 insertions, 0 deletions
diff --git a/uriloader/base/moz.build b/uriloader/base/moz.build
new file mode 100644
index 0000000000..f0ced2555b
--- /dev/null
+++ b/uriloader/base/moz.build
@@ -0,0 +1,37 @@
+# -*- 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/.
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+XPIDL_SOURCES += [
+ "nsCURILoader.idl",
+ "nsIContentHandler.idl",
+ "nsIDocumentLoader.idl",
+ "nsITransfer.idl",
+ "nsIURIContentListener.idl",
+ "nsIURILoader.idl",
+ "nsIWebProgress.idl",
+ "nsIWebProgressListener.idl",
+ "nsIWebProgressListener2.idl",
+]
+
+XPIDL_MODULE = "uriloader"
+
+EXPORTS += [
+ "nsDocLoader.h",
+ "nsURILoader.h",
+]
+
+UNIFIED_SOURCES += [
+ "nsDocLoader.cpp",
+ "nsURILoader.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/netwerk/base",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/uriloader/base/nsCURILoader.idl b/uriloader/base/nsCURILoader.idl
new file mode 100644
index 0000000000..b77422abe9
--- /dev/null
+++ b/uriloader/base/nsCURILoader.idl
@@ -0,0 +1,36 @@
+/* -*- 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 "nsIURILoader.idl"
+
+/*
+nsCURILoader implements:
+-------------------------
+nsIURILoader
+*/
+
+%{ C++
+#define NS_CONTENT_HANDLER_CONTRACTID "@mozilla.org/uriloader/content-handler;1"
+#define NS_CONTENT_HANDLER_CONTRACTID_PREFIX NS_CONTENT_HANDLER_CONTRACTID "?type="
+
+/**
+ * A category where content listeners can register. The name of the entry must
+ * be the content that this listener wants to handle, the value must be a
+ * contract ID for the listener. It will be created using createInstance (not
+ * getService).
+ *
+ * Listeners added this way are tried after the initial target of the load and
+ * after explicitly registered listeners (nsIURILoader::registerContentListener).
+ *
+ * These listeners must implement at least nsIURIContentListener (and
+ * nsISupports).
+ *
+ * @see nsICategoryManager
+ * @see nsIURIContentListener
+ */
+#define NS_CONTENT_LISTENER_CATEGORYMANAGER_ENTRY "external-uricontentlisteners"
+
+%}
diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp
new file mode 100644
index 0000000000..bf7902f5dd
--- /dev/null
+++ b/uriloader/base/nsDocLoader.cpp
@@ -0,0 +1,1574 @@
+/* -*- 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 "nspr.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/Components.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/Logging.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/PresShell.h"
+
+#include "nsDocLoader.h"
+#include "nsDocShell.h"
+#include "nsLoadGroup.h"
+#include "nsNetUtil.h"
+#include "nsIHttpChannel.h"
+#include "nsIWebNavigation.h"
+#include "nsIWebProgressListener2.h"
+
+#include "nsString.h"
+
+#include "nsCOMPtr.h"
+#include "nscore.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsQueryObject.h"
+
+#include "nsPIDOMWindow.h"
+#include "nsGlobalWindow.h"
+
+#include "nsIStringBundle.h"
+
+#include "nsIDocShell.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocGroup.h"
+#include "nsPresContext.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsIBrowserDOMWindow.h"
+#include "nsGlobalWindow.h"
+#include "mozilla/ThrottledEventQueue.h"
+using namespace mozilla;
+using mozilla::DebugOnly;
+using mozilla::eLoad;
+using mozilla::EventDispatcher;
+using mozilla::LogLevel;
+using mozilla::WidgetEvent;
+using mozilla::dom::BrowserChild;
+using mozilla::dom::BrowsingContext;
+using mozilla::dom::Document;
+
+//
+// Log module for nsIDocumentLoader logging...
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=DocLoader:5
+// set MOZ_LOG_FILE=debug.log
+//
+// this enables LogLevel::Debug level information and places all output in
+// the file 'debug.log'.
+//
+mozilla::LazyLogModule gDocLoaderLog("DocLoader");
+
+#if defined(DEBUG)
+void GetURIStringFromRequest(nsIRequest* request, nsACString& name) {
+ if (request)
+ request->GetName(name);
+ else
+ name.AssignLiteral("???");
+}
+#endif /* DEBUG */
+
+void nsDocLoader::RequestInfoHashInitEntry(PLDHashEntryHdr* entry,
+ const void* key) {
+ // Initialize the entry with placement new
+ new (entry) nsRequestInfo(key);
+}
+
+void nsDocLoader::RequestInfoHashClearEntry(PLDHashTable* table,
+ PLDHashEntryHdr* entry) {
+ nsRequestInfo* info = static_cast<nsRequestInfo*>(entry);
+ info->~nsRequestInfo();
+}
+
+// this is used for mListenerInfoList.Contains()
+template <>
+class nsDefaultComparator<nsDocLoader::nsListenerInfo,
+ nsIWebProgressListener*> {
+ public:
+ bool Equals(const nsDocLoader::nsListenerInfo& aInfo,
+ nsIWebProgressListener* const& aListener) const {
+ nsCOMPtr<nsIWebProgressListener> listener =
+ do_QueryReferent(aInfo.mWeakListener);
+ return aListener == listener;
+ }
+};
+
+/* static */ const PLDHashTableOps nsDocLoader::sRequestInfoHashOps = {
+ PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub,
+ PLDHashTable::MoveEntryStub, nsDocLoader::RequestInfoHashClearEntry,
+ nsDocLoader::RequestInfoHashInitEntry};
+
+nsDocLoader::nsDocLoader()
+ : mParent(nullptr),
+ mProgressStateFlags(0),
+ mCurrentSelfProgress(0),
+ mMaxSelfProgress(0),
+ mCurrentTotalProgress(0),
+ mMaxTotalProgress(0),
+ mRequestInfoHash(&sRequestInfoHashOps, sizeof(nsRequestInfo)),
+ mCompletedTotalProgress(0),
+ mIsLoadingDocument(false),
+ mIsRestoringDocument(false),
+ mDontFlushLayout(false),
+ mIsFlushingLayout(false),
+ mTreatAsBackgroundLoad(false),
+ mHasFakeOnLoadDispatched(false),
+ mIsReadyToHandlePostMessage(false),
+ mDocumentOpenedButNotLoaded(false) {
+ ClearInternalProgress();
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader:%p: created.\n", this));
+}
+
+nsresult nsDocLoader::SetDocLoaderParent(nsDocLoader* aParent) {
+ mParent = aParent;
+ return NS_OK;
+}
+
+nsresult nsDocLoader::Init() {
+ nsresult rv = NS_NewLoadGroup(getter_AddRefs(mLoadGroup), this);
+ if (NS_FAILED(rv)) return rv;
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: load group %p.\n", this, mLoadGroup.get()));
+
+ return NS_OK;
+}
+
+nsresult nsDocLoader::InitWithBrowsingContext(
+ BrowsingContext* aBrowsingContext) {
+ RefPtr<net::nsLoadGroup> loadGroup = new net::nsLoadGroup();
+ if (!aBrowsingContext->GetRequestContextId()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ nsresult rv = loadGroup->InitWithRequestContextId(
+ aBrowsingContext->GetRequestContextId());
+ if (NS_FAILED(rv)) return rv;
+
+ rv = loadGroup->SetGroupObserver(this);
+ if (NS_FAILED(rv)) return rv;
+
+ mLoadGroup = loadGroup;
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: load group %p.\n", this, mLoadGroup.get()));
+
+ return NS_OK;
+}
+
+nsDocLoader::~nsDocLoader() {
+ /*
+ |ClearWeakReferences()| here is intended to prevent people holding
+ weak references from re-entering this destructor since |QueryReferent()|
+ will |AddRef()| me, and the subsequent |Release()| will try to destroy me.
+ At this point there should be only weak references remaining (otherwise, we
+ wouldn't be getting destroyed).
+
+ An alternative would be incrementing our refcount (consider it a
+ compressed flag saying "Don't re-destroy."). I haven't yet decided which
+ is better. [scc]
+ */
+ // XXXbz now that NS_IMPL_RELEASE stabilizes by setting refcount to 1, is
+ // this needed?
+ ClearWeakReferences();
+
+ Destroy();
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader:%p: deleted.\n", this));
+}
+
+/*
+ * Implementation of ISupports methods...
+ */
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDocLoader)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDocLoader)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDocLoader)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDocumentLoader)
+ NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIDocumentLoader)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY(nsIWebProgress)
+ NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink)
+ NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor)
+ NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsPriority)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(nsDocLoader)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_WEAK(nsDocLoader, mChildrenInOnload)
+
+/*
+ * Implementation of nsIInterfaceRequestor methods...
+ */
+NS_IMETHODIMP nsDocLoader::GetInterface(const nsIID& aIID, void** aSink) {
+ nsresult rv = NS_ERROR_NO_INTERFACE;
+
+ NS_ENSURE_ARG_POINTER(aSink);
+
+ if (aIID.Equals(NS_GET_IID(nsILoadGroup))) {
+ *aSink = mLoadGroup;
+ NS_IF_ADDREF((nsISupports*)*aSink);
+ rv = NS_OK;
+ } else {
+ rv = QueryInterface(aIID, aSink);
+ }
+
+ return rv;
+}
+
+/* static */
+already_AddRefed<nsDocLoader> nsDocLoader::GetAsDocLoader(
+ nsISupports* aSupports) {
+ RefPtr<nsDocLoader> ret = do_QueryObject(aSupports);
+ return ret.forget();
+}
+
+/* static */
+nsresult nsDocLoader::AddDocLoaderAsChildOfRoot(nsDocLoader* aDocLoader) {
+ nsCOMPtr<nsIDocumentLoader> docLoaderService =
+ components::DocLoader::Service();
+ NS_ENSURE_TRUE(docLoaderService, NS_ERROR_UNEXPECTED);
+
+ RefPtr<nsDocLoader> rootDocLoader = GetAsDocLoader(docLoaderService);
+ NS_ENSURE_TRUE(rootDocLoader, NS_ERROR_UNEXPECTED);
+
+ return rootDocLoader->AddChildLoader(aDocLoader);
+}
+
+NS_IMETHODIMP
+nsDocLoader::Stop(void) {
+ nsresult rv = NS_OK;
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: Stop() called\n", this));
+
+ NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, Stop, ());
+
+ if (mLoadGroup) rv = mLoadGroup->Cancel(NS_BINDING_ABORTED);
+
+ // Don't report that we're flushing layout so IsBusy returns false after a
+ // Stop call.
+ mIsFlushingLayout = false;
+
+ // Clear out mChildrenInOnload. We're not going to fire our onload
+ // anyway at this point, and there's no issue with mChildrenInOnload
+ // after this, since mDocumentRequest will be null after the
+ // DocLoaderIsEmpty() call.
+ mChildrenInOnload.Clear();
+ mOOPChildrenLoading.Clear();
+
+ // Make sure to call DocLoaderIsEmpty now so that we reset mDocumentRequest,
+ // etc, as needed. We could be getting into here from a subframe onload, in
+ // which case the call to DocLoaderIsEmpty() is coming but hasn't quite
+ // happened yet, Canceling the loadgroup did nothing (because it was already
+ // empty), and we're about to start a new load (which is what triggered this
+ // Stop() call).
+
+ // XXXbz If the child frame loadgroups were requests in mLoadgroup, I suspect
+ // we wouldn't need the call here....
+
+ NS_ASSERTION(!IsBusy(), "Shouldn't be busy here");
+
+ // If Cancelling the load group only had pending subresource requests, then
+ // the group status will still be success, and we would fire the load event.
+ // We want to avoid that when we're aborting the load, so override the status
+ // with an explicit NS_BINDING_ABORTED value.
+ DocLoaderIsEmpty(false, Some(NS_BINDING_ABORTED));
+
+ return rv;
+}
+
+bool nsDocLoader::TreatAsBackgroundLoad() { return mTreatAsBackgroundLoad; }
+
+void nsDocLoader::SetBackgroundLoadIframe() { mTreatAsBackgroundLoad = true; }
+
+bool nsDocLoader::IsBusy() {
+ nsresult rv;
+
+ //
+ // A document loader is busy if either:
+ //
+ // 1. One of its children is in the middle of an onload handler. Note that
+ // the handler may have already removed this child from mChildList!
+ // 2. It is currently loading a document and either has parts of it still
+ // loading, or has a busy child docloader.
+ // 3. It's currently flushing layout in DocLoaderIsEmpty().
+ //
+
+ if (!mChildrenInOnload.IsEmpty() || !mOOPChildrenLoading.IsEmpty() ||
+ mIsFlushingLayout) {
+ return true;
+ }
+
+ /* Is this document loader busy? */
+ if (!IsBlockingLoadEvent()) {
+ return false;
+ }
+
+ // Check if any in-process sub-document is awaiting its 'load' event:
+ bool busy;
+ rv = mLoadGroup->IsPending(&busy);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ if (busy) {
+ return true;
+ }
+
+ /* check its child document loaders... */
+ uint32_t count = mChildList.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ nsIDocumentLoader* loader = ChildAt(i);
+
+ // If 'dom.cross_origin_iframes_loaded_in_background' is set, the parent
+ // document treats cross domain iframes as background loading frame
+ if (loader && static_cast<nsDocLoader*>(loader)->TreatAsBackgroundLoad()) {
+ continue;
+ }
+ // This is a safe cast, because we only put nsDocLoader objects into the
+ // array
+ if (loader && static_cast<nsDocLoader*>(loader)->IsBusy()) return true;
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP
+nsDocLoader::GetContainer(nsISupports** aResult) {
+ NS_ADDREF(*aResult = static_cast<nsIDocumentLoader*>(this));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocLoader::GetLoadGroup(nsILoadGroup** aResult) {
+ nsresult rv = NS_OK;
+
+ if (nullptr == aResult) {
+ rv = NS_ERROR_NULL_POINTER;
+ } else {
+ *aResult = mLoadGroup;
+ NS_IF_ADDREF(*aResult);
+ }
+ return rv;
+}
+
+void nsDocLoader::Destroy() {
+ Stop();
+
+ // Remove the document loader from the parent list of loaders...
+ if (mParent) {
+ DebugOnly<nsresult> rv = mParent->RemoveChildLoader(this);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RemoveChildLoader failed");
+ }
+
+ // Release all the information about network requests...
+ ClearRequestInfoHash();
+
+ mListenerInfoList.Clear();
+ mListenerInfoList.Compact();
+
+ mDocumentRequest = nullptr;
+
+ if (mLoadGroup) mLoadGroup->SetGroupObserver(nullptr);
+
+ DestroyChildren();
+}
+
+void nsDocLoader::DestroyChildren() {
+ uint32_t count = mChildList.Length();
+ // if the doc loader still has children...we need to enumerate the
+ // children and make them null out their back ptr to the parent doc
+ // loader
+ for (uint32_t i = 0; i < count; i++) {
+ nsIDocumentLoader* loader = ChildAt(i);
+
+ if (loader) {
+ // This is a safe cast, as we only put nsDocLoader objects into the
+ // array
+ DebugOnly<nsresult> rv =
+ static_cast<nsDocLoader*>(loader)->SetDocLoaderParent(nullptr);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetDocLoaderParent failed");
+ }
+ }
+ mChildList.Clear();
+}
+
+NS_IMETHODIMP
+nsDocLoader::OnStartRequest(nsIRequest* request) {
+ // called each time a request is added to the group.
+
+ if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) {
+ nsAutoCString name;
+ request->GetName(name);
+
+ uint32_t count = 0;
+ if (mLoadGroup) mLoadGroup->GetActiveCount(&count);
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: OnStartRequest[%p](%s) mIsLoadingDocument=%s, %u "
+ "active URLs",
+ this, request, name.get(), (mIsLoadingDocument ? "true" : "false"),
+ count));
+ }
+
+ bool bJustStartedLoading = false;
+
+ nsLoadFlags loadFlags = 0;
+ request->GetLoadFlags(&loadFlags);
+
+ if (!mIsLoadingDocument && (loadFlags & nsIChannel::LOAD_DOCUMENT_URI)) {
+ bJustStartedLoading = true;
+ mIsLoadingDocument = true;
+ mDocumentOpenedButNotLoaded = false;
+ ClearInternalProgress(); // only clear our progress if we are starting a
+ // new load....
+ }
+
+ //
+ // Create a new nsRequestInfo for the request that is starting to
+ // load...
+ //
+ AddRequestInfo(request);
+
+ //
+ // Only fire a doStartDocumentLoad(...) if the document loader
+ // has initiated a load... Otherwise, this notification has
+ // resulted from a request being added to the load group.
+ //
+ if (mIsLoadingDocument) {
+ if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) {
+ //
+ // Make sure that the document channel is null at this point...
+ // (unless its been redirected)
+ //
+ NS_ASSERTION(
+ (loadFlags & nsIChannel::LOAD_REPLACE) || !(mDocumentRequest.get()),
+ "Overwriting an existing document channel!");
+
+ // This request is associated with the entire document...
+ mDocumentRequest = request;
+ mLoadGroup->SetDefaultLoadRequest(request);
+
+ // Only fire the start document load notification for the first
+ // document URI... Do not fire it again for redirections
+ //
+ if (bJustStartedLoading) {
+ // Update the progress status state
+ mProgressStateFlags = nsIWebProgressListener::STATE_START;
+
+ // Fire the start document load notification
+ doStartDocumentLoad();
+ return NS_OK;
+ }
+ }
+ }
+
+ NS_ASSERTION(!mIsLoadingDocument || mDocumentRequest,
+ "mDocumentRequest MUST be set for the duration of a page load!");
+
+ // This is the only way to catch document request start event after a redirect
+ // has occured without changing inherited Firefox behaviour significantly.
+ // Problem description:
+ // The combination of |STATE_START + STATE_IS_DOCUMENT| is only sent for
+ // initial request (see |doStartDocumentLoad| call above).
+ // And |STATE_REDIRECTING + STATE_IS_DOCUMENT| is sent with old channel, which
+ // makes it impossible to filter by destination URL (see
+ // |AsyncOnChannelRedirect| implementation).
+ // Fixing any of those bugs may cause unpredictable consequences in any part
+ // of the browser, so we just add a custom flag for this exact situation.
+ int32_t extraFlags = 0;
+ if (mIsLoadingDocument && !bJustStartedLoading &&
+ (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) &&
+ (loadFlags & nsIChannel::LOAD_REPLACE)) {
+ extraFlags = nsIWebProgressListener::STATE_IS_REDIRECTED_DOCUMENT;
+ }
+ doStartURLLoad(request, extraFlags);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocLoader::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ nsresult rv = NS_OK;
+
+ if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) {
+ nsAutoCString name;
+ aRequest->GetName(name);
+
+ uint32_t count = 0;
+ if (mLoadGroup) mLoadGroup->GetActiveCount(&count);
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: OnStopRequest[%p](%s) status=%" PRIx32
+ " mIsLoadingDocument=%s, mDocumentOpenedButNotLoaded=%s,"
+ " %u active URLs",
+ this, aRequest, name.get(), static_cast<uint32_t>(aStatus),
+ (mIsLoadingDocument ? "true" : "false"),
+ (mDocumentOpenedButNotLoaded ? "true" : "false"), count));
+ }
+
+ bool bFireTransferring = false;
+
+ //
+ // Set the Maximum progress to the same value as the current progress.
+ // Since the URI has finished loading, all the data is there. Also,
+ // this will allow a more accurate estimation of the max progress (in case
+ // the old value was unknown ie. -1)
+ //
+ nsRequestInfo* info = GetRequestInfo(aRequest);
+ if (info) {
+ // Null out mLastStatus now so we don't find it when looking for
+ // status from now on. This destroys the nsStatusInfo and hence
+ // removes it from our list.
+ info->mLastStatus = nullptr;
+
+ int64_t oldMax = info->mMaxProgress;
+
+ info->mMaxProgress = info->mCurrentProgress;
+
+ //
+ // If a request whose content-length was previously unknown has just
+ // finished loading, then use this new data to try to calculate a
+ // mMaxSelfProgress...
+ //
+ if ((oldMax < int64_t(0)) && (mMaxSelfProgress < int64_t(0))) {
+ mMaxSelfProgress = CalculateMaxProgress();
+ }
+
+ // As we know the total progress of this request now, save it to be part
+ // of CalculateMaxProgress() result. We need to remove the info from the
+ // hash, see bug 480713.
+ mCompletedTotalProgress += info->mMaxProgress;
+
+ //
+ // Determine whether a STATE_TRANSFERRING notification should be
+ // 'synthesized'.
+ //
+ // If nsRequestInfo::mMaxProgress (as stored in oldMax) and
+ // nsRequestInfo::mCurrentProgress are both 0, then the
+ // STATE_TRANSFERRING notification has not been fired yet...
+ //
+ if ((oldMax == 0) && (info->mCurrentProgress == 0)) {
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+
+ // Only fire a TRANSFERRING notification if the request is also a
+ // channel -- data transfer requires a nsIChannel!
+ //
+ if (channel) {
+ if (NS_SUCCEEDED(aStatus)) {
+ bFireTransferring = true;
+ }
+ //
+ // If the request failed (for any reason other than being
+ // redirected or retargeted), the TRANSFERRING notification can
+ // still be fired if a HTTP connection was established to a server.
+ //
+ else if (aStatus != NS_BINDING_REDIRECTED &&
+ aStatus != NS_BINDING_RETARGETED) {
+ //
+ // Only if the load has been targeted (see bug 268483)...
+ //
+ uint32_t lf;
+ channel->GetLoadFlags(&lf);
+ if (lf & nsIChannel::LOAD_TARGETED) {
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aRequest));
+ if (httpChannel) {
+ uint32_t responseCode;
+ rv = httpChannel->GetResponseStatus(&responseCode);
+ if (NS_SUCCEEDED(rv)) {
+ //
+ // A valid server status indicates that a connection was
+ // established to the server... So, fire the notification
+ // even though a failure occurred later...
+ //
+ bFireTransferring = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (bFireTransferring) {
+ // Send a STATE_TRANSFERRING notification for the request.
+ int32_t flags;
+
+ flags = nsIWebProgressListener::STATE_TRANSFERRING |
+ nsIWebProgressListener::STATE_IS_REQUEST;
+ //
+ // Move the WebProgress into the STATE_TRANSFERRING state if necessary...
+ //
+ if (mProgressStateFlags & nsIWebProgressListener::STATE_START) {
+ mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING;
+
+ // Send STATE_TRANSFERRING for the document too...
+ flags |= nsIWebProgressListener::STATE_IS_DOCUMENT;
+ }
+
+ FireOnStateChange(this, aRequest, flags, NS_OK);
+ }
+
+ //
+ // Fire the OnStateChange(...) notification for stop request
+ //
+ doStopURLLoad(aRequest, aStatus);
+
+ // Clear this request out of the hash to avoid bypass of FireOnStateChange
+ // when address of the request is reused.
+ RemoveRequestInfo(aRequest);
+
+ //
+ // Only fire the DocLoaderIsEmpty(...) if we may need to fire onload.
+ //
+ if (IsBlockingLoadEvent()) {
+ nsCOMPtr<nsIDocShell> ds =
+ do_QueryInterface(static_cast<nsIRequestObserver*>(this));
+ bool doNotFlushLayout = false;
+ if (ds) {
+ // Don't do unexpected layout flushes while we're in process of restoring
+ // a document from the bfcache.
+ ds->GetRestoringDocument(&doNotFlushLayout);
+ }
+ DocLoaderIsEmpty(!doNotFlushLayout);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsDocLoader::RemoveChildLoader(nsDocLoader* aChild) {
+ nsresult rv = mChildList.RemoveElement(aChild) ? NS_OK : NS_ERROR_FAILURE;
+ if (NS_SUCCEEDED(rv)) {
+ rv = aChild->SetDocLoaderParent(nullptr);
+ }
+ return rv;
+}
+
+nsresult nsDocLoader::AddChildLoader(nsDocLoader* aChild) {
+ mChildList.AppendElement(aChild);
+ return aChild->SetDocLoaderParent(this);
+}
+
+NS_IMETHODIMP nsDocLoader::GetDocumentChannel(nsIChannel** aChannel) {
+ if (!mDocumentRequest) {
+ *aChannel = nullptr;
+ return NS_OK;
+ }
+
+ return CallQueryInterface(mDocumentRequest, aChannel);
+}
+
+void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout,
+ const Maybe<nsresult>& aOverrideStatus) {
+ if (IsBlockingLoadEvent()) {
+ /* In the unimagineably rude circumstance that onload event handlers
+ triggered by this function actually kill the window ... ok, it's
+ not unimagineable; it's happened ... this deathgrip keeps this object
+ alive long enough to survive this function call. */
+ nsCOMPtr<nsIDocumentLoader> kungFuDeathGrip(this);
+
+ // Don't flush layout if we're still busy.
+ if (IsBusy()) {
+ return;
+ }
+
+ NS_ASSERTION(!mIsFlushingLayout, "Someone screwed up");
+ // We may not have a document request if we are in a
+ // document.open() situation.
+ NS_ASSERTION(mDocumentRequest || mDocumentOpenedButNotLoaded,
+ "No Document Request!");
+
+ // The load group for this DocumentLoader is idle. Flush if we need to.
+ if (aFlushLayout && !mDontFlushLayout) {
+ nsCOMPtr<Document> doc = do_GetInterface(GetAsSupports(this));
+ if (doc) {
+ // We start loads from style resolution, so we need to flush out style
+ // no matter what. If we have user fonts, we also need to flush layout,
+ // since the reflow is what starts font loads.
+ mozilla::FlushType flushType = mozilla::FlushType::Style;
+ // Be safe in case this presshell is in teardown now
+ doc->FlushUserFontSet();
+ if (doc->GetUserFontSet()) {
+ flushType = mozilla::FlushType::Layout;
+ }
+ mDontFlushLayout = mIsFlushingLayout = true;
+ doc->FlushPendingNotifications(flushType);
+ mDontFlushLayout = mIsFlushingLayout = false;
+ }
+ }
+
+ // And now check whether we're really busy; that might have changed with
+ // the layout flush.
+ //
+ // Note, mDocumentRequest can be null while mDocumentOpenedButNotLoaded is
+ // false if the flushing above re-entered this method.
+ if (IsBusy() || (!mDocumentRequest && !mDocumentOpenedButNotLoaded)) {
+ return;
+ }
+
+ if (mDocumentRequest) {
+ // Clear out our request info hash, now that our load really is done and
+ // we don't need it anymore to CalculateMaxProgress().
+ ClearInternalProgress();
+
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: Is now idle...\n", this));
+
+ nsCOMPtr<nsIRequest> docRequest = mDocumentRequest;
+
+ mDocumentRequest = nullptr;
+ mIsLoadingDocument = false;
+
+ // Update the progress status state - the document is done
+ mProgressStateFlags = nsIWebProgressListener::STATE_STOP;
+
+ nsresult loadGroupStatus = NS_OK;
+ if (aOverrideStatus) {
+ loadGroupStatus = *aOverrideStatus;
+ } else {
+ mLoadGroup->GetStatus(&loadGroupStatus);
+ }
+
+ //
+ // New code to break the circular reference between
+ // the load group and the docloader...
+ //
+ mLoadGroup->SetDefaultLoadRequest(nullptr);
+
+ // Take a ref to our parent now so that we can call ChildDoneWithOnload()
+ // on it even if our onload handler removes us from the docloader tree.
+ RefPtr<nsDocLoader> parent = mParent;
+
+ // Note that if calling ChildEnteringOnload() on the parent returns false
+ // then calling our onload handler is not safe. That can only happen on
+ // OOM, so that's ok.
+ if (!parent || parent->ChildEnteringOnload(this)) {
+ // Do nothing with our state after firing the
+ // OnEndDocumentLoad(...). The document loader may be loading a *new*
+ // document - if LoadDocument() was called from a handler!
+ //
+ doStopDocumentLoad(docRequest, loadGroupStatus);
+
+ NotifyDoneWithOnload(parent);
+ }
+ } else {
+ MOZ_ASSERT(mDocumentOpenedButNotLoaded);
+ mDocumentOpenedButNotLoaded = false;
+
+ // Make sure we do the ChildEnteringOnload/ChildDoneWithOnload even if we
+ // plan to skip firing our own load event, because otherwise we might
+ // never end up firing our parent's load event.
+ RefPtr<nsDocLoader> parent = mParent;
+ if (!parent || parent->ChildEnteringOnload(this)) {
+ nsresult loadGroupStatus = NS_OK;
+ mLoadGroup->GetStatus(&loadGroupStatus);
+
+ // Can "doc" or "window" ever come back null here? Our state machine
+ // is complicated enough I wouldn't bet against it...
+ nsCOMPtr<Document> doc = do_GetInterface(GetAsSupports(this));
+ if (doc) {
+ // Make sure we're not canceling the loadgroup. If we are, then just
+ // like the normal navigation case we should not fire a load event.
+ if (NS_SUCCEEDED(loadGroupStatus) ||
+ loadGroupStatus == NS_ERROR_PARSED_DATA_CACHED) {
+ // The readyState change is required to pass
+ // dom/html/test/test_bug347174_write.html
+ doc->SetReadyStateInternal(Document::READYSTATE_COMPLETE,
+ /* updateTimingInformation = */ false);
+ doc->StopDocumentLoad();
+
+ nsCOMPtr<nsPIDOMWindowOuter> window = doc->GetWindow();
+ if (window && !doc->SkipLoadEventAfterClose()) {
+ if (!mozilla::dom::DocGroup::TryToLoadIframesInBackground() ||
+ (mozilla::dom::DocGroup::TryToLoadIframesInBackground() &&
+ !HasFakeOnLoadDispatched())) {
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: Firing load event for document.open\n",
+ this));
+
+ // This is a very cut-down version of
+ // nsDocumentViewer::LoadComplete that doesn't do various things
+ // that are not relevant here because this wasn't an actual
+ // navigation.
+ WidgetEvent event(true, eLoad);
+ event.mFlags.mBubbles = false;
+ event.mFlags.mCancelable = false;
+ // Dispatching to |window|, but using |document| as the target,
+ // per spec.
+ event.mTarget = doc;
+ nsEventStatus unused = nsEventStatus_eIgnore;
+ doc->SetLoadEventFiring(true);
+ EventDispatcher::Dispatch(window, nullptr, &event, nullptr,
+ &unused);
+ doc->SetLoadEventFiring(false);
+
+ // Now unsuppress painting on the presshell, if we
+ // haven't done that yet.
+ RefPtr<PresShell> presShell = doc->GetPresShell();
+ if (presShell && !presShell->IsDestroying()) {
+ presShell->UnsuppressPainting();
+
+ if (!presShell->IsDestroying()) {
+ presShell->LoadComplete();
+ }
+ }
+ }
+ }
+ } else if (loadGroupStatus == NS_BINDING_ABORTED) {
+ doc->NotifyAbortedLoad();
+ }
+
+ if (doc->IsCurrentActiveDocument() && !doc->IsShowing() &&
+ loadGroupStatus != NS_BINDING_ABORTED) {
+ nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(this);
+ bool isInUnload;
+ if (docShell &&
+ NS_SUCCEEDED(docShell->GetIsInUnload(&isInUnload)) &&
+ !isInUnload) {
+ doc->OnPageShow(false, nullptr);
+ }
+ }
+ }
+ NotifyDoneWithOnload(parent);
+ }
+ }
+ }
+}
+
+void nsDocLoader::NotifyDoneWithOnload(nsDocLoader* aParent) {
+ if (aParent) {
+ // In-process parent:
+ aParent->ChildDoneWithOnload(this);
+ }
+ nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(this);
+ if (!docShell) {
+ return;
+ }
+ BrowsingContext* bc = nsDocShell::Cast(docShell)->GetBrowsingContext();
+ if (bc->IsContentSubframe() && !bc->GetParent()->IsInProcess()) {
+ if (BrowserChild* browserChild = BrowserChild::GetFrom(docShell)) {
+ mozilla::Unused << browserChild->SendMaybeFireEmbedderLoadEvents(
+ dom::EmbedderElementEventType::NoEvent);
+ }
+ }
+}
+
+void nsDocLoader::doStartDocumentLoad(void) {
+#if defined(DEBUG)
+ nsAutoCString buffer;
+
+ GetURIStringFromRequest(mDocumentRequest, buffer);
+ MOZ_LOG(
+ gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: ++ Firing OnStateChange for start document load (...)."
+ "\tURI: %s \n",
+ this, buffer.get()));
+#endif /* DEBUG */
+
+ // Fire an OnStatus(...) notification STATE_START. This indicates
+ // that the document represented by mDocumentRequest has started to
+ // load...
+ FireOnStateChange(this, mDocumentRequest,
+ nsIWebProgressListener::STATE_START |
+ nsIWebProgressListener::STATE_IS_DOCUMENT |
+ nsIWebProgressListener::STATE_IS_REQUEST |
+ nsIWebProgressListener::STATE_IS_WINDOW |
+ nsIWebProgressListener::STATE_IS_NETWORK,
+ NS_OK);
+}
+
+void nsDocLoader::doStartURLLoad(nsIRequest* request, int32_t aExtraFlags) {
+#if defined(DEBUG)
+ nsAutoCString buffer;
+
+ GetURIStringFromRequest(request, buffer);
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: ++ Firing OnStateChange start url load (...)."
+ "\tURI: %s\n",
+ this, buffer.get()));
+#endif /* DEBUG */
+
+ FireOnStateChange(this, request,
+ nsIWebProgressListener::STATE_START |
+ nsIWebProgressListener::STATE_IS_REQUEST | aExtraFlags,
+ NS_OK);
+}
+
+void nsDocLoader::doStopURLLoad(nsIRequest* request, nsresult aStatus) {
+#if defined(DEBUG)
+ nsAutoCString buffer;
+
+ GetURIStringFromRequest(request, buffer);
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: ++ Firing OnStateChange for end url load (...)."
+ "\tURI: %s status=%" PRIx32 "\n",
+ this, buffer.get(), static_cast<uint32_t>(aStatus)));
+#endif /* DEBUG */
+
+ FireOnStateChange(this, request,
+ nsIWebProgressListener::STATE_STOP |
+ nsIWebProgressListener::STATE_IS_REQUEST,
+ aStatus);
+
+ // Fire a status change message for the most recent unfinished
+ // request to make sure that the displayed status is not outdated.
+ if (!mStatusInfoList.isEmpty()) {
+ nsStatusInfo* statusInfo = mStatusInfoList.getFirst();
+ FireOnStatusChange(this, statusInfo->mRequest, statusInfo->mStatusCode,
+ statusInfo->mStatusMessage.get());
+ }
+}
+
+void nsDocLoader::doStopDocumentLoad(nsIRequest* request, nsresult aStatus) {
+#if defined(DEBUG)
+ nsAutoCString buffer;
+
+ GetURIStringFromRequest(request, buffer);
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: ++ Firing OnStateChange for end document load (...)."
+ "\tURI: %s Status=%" PRIx32 "\n",
+ this, buffer.get(), static_cast<uint32_t>(aStatus)));
+#endif /* DEBUG */
+
+ // Firing STATE_STOP|STATE_IS_DOCUMENT will fire onload handlers.
+ // Grab our parent chain before doing that so we can still dispatch
+ // STATE_STOP|STATE_IS_WINDW_STATE_IS_NETWORK to them all, even if
+ // the onload handlers rearrange the docshell tree.
+ WebProgressList list;
+ GatherAncestorWebProgresses(list);
+
+ //
+ // Fire an OnStateChange(...) notification indicating the the
+ // current document has finished loading...
+ //
+ int32_t flags = nsIWebProgressListener::STATE_STOP |
+ nsIWebProgressListener::STATE_IS_DOCUMENT;
+ for (uint32_t i = 0; i < list.Length(); ++i) {
+ list[i]->DoFireOnStateChange(this, request, flags, aStatus);
+ }
+
+ //
+ // Fire a final OnStateChange(...) notification indicating the the
+ // current document has finished loading...
+ //
+ flags = nsIWebProgressListener::STATE_STOP |
+ nsIWebProgressListener::STATE_IS_WINDOW |
+ nsIWebProgressListener::STATE_IS_NETWORK;
+ for (uint32_t i = 0; i < list.Length(); ++i) {
+ list[i]->DoFireOnStateChange(this, request, flags, aStatus);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// The following section contains support for nsIWebProgress and related stuff
+////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsDocLoader::AddProgressListener(nsIWebProgressListener* aListener,
+ uint32_t aNotifyMask) {
+ if (mListenerInfoList.Contains(aListener)) {
+ // The listener is already registered!
+ return NS_ERROR_FAILURE;
+ }
+
+ nsWeakPtr listener = do_GetWeakReference(aListener);
+ if (!listener) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mListenerInfoList.AppendElement(nsListenerInfo(listener, aNotifyMask));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocLoader::RemoveProgressListener(nsIWebProgressListener* aListener) {
+ return mListenerInfoList.RemoveElement(aListener) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsDocLoader::GetDOMWindow(mozIDOMWindowProxy** aResult) {
+ return CallGetInterface(this, aResult);
+}
+
+NS_IMETHODIMP
+nsDocLoader::GetIsTopLevel(bool* aResult) {
+ nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(this);
+ *aResult = docShell && docShell->GetBrowsingContext()->IsTop();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocLoader::GetIsLoadingDocument(bool* aIsLoadingDocument) {
+ *aIsLoadingDocument = mIsLoadingDocument;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocLoader::GetLoadType(uint32_t* aLoadType) {
+ *aLoadType = 0;
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsDocLoader::GetTarget(nsIEventTarget** aTarget) {
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ nsresult rv = GetDOMWindow(getter_AddRefs(window));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(window);
+ NS_ENSURE_STATE(global);
+
+ nsCOMPtr<nsIEventTarget> target =
+ global->EventTargetFor(mozilla::TaskCategory::Other);
+ target.forget(aTarget);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocLoader::SetTarget(nsIEventTarget* aTarget) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+int64_t nsDocLoader::GetMaxTotalProgress() {
+ int64_t newMaxTotal = 0;
+
+ uint32_t count = mChildList.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ int64_t individualProgress = 0;
+ nsIDocumentLoader* docloader = ChildAt(i);
+ if (docloader) {
+ // Cast is safe since all children are nsDocLoader too
+ individualProgress = ((nsDocLoader*)docloader)->GetMaxTotalProgress();
+ }
+ if (individualProgress < int64_t(0)) // if one of the elements doesn't know
+ // it's size then none of them do
+ {
+ newMaxTotal = int64_t(-1);
+ break;
+ } else
+ newMaxTotal += individualProgress;
+ }
+
+ int64_t progress = -1;
+ if (mMaxSelfProgress >= int64_t(0) && newMaxTotal >= int64_t(0))
+ progress = newMaxTotal + mMaxSelfProgress;
+
+ return progress;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// The following section contains support for nsIProgressEventSink which is used
+// to pass progress and status between the actual request and the doc loader.
+// The doc loader then turns around and makes the right web progress calls based
+// on this information.
+////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsDocLoader::OnProgress(nsIRequest* aRequest, int64_t aProgress,
+ int64_t aProgressMax) {
+ int64_t progressDelta = 0;
+
+ //
+ // Update the RequestInfo entry with the new progress data
+ //
+ if (nsRequestInfo* info = GetRequestInfo(aRequest)) {
+ // Update info->mCurrentProgress before we call FireOnStateChange,
+ // since that can make the "info" pointer invalid.
+ int64_t oldCurrentProgress = info->mCurrentProgress;
+ progressDelta = aProgress - oldCurrentProgress;
+ info->mCurrentProgress = aProgress;
+
+ // suppress sending STATE_TRANSFERRING if this is upload progress (see bug
+ // 240053)
+ if (!info->mUploading && (int64_t(0) == oldCurrentProgress) &&
+ (int64_t(0) == info->mMaxProgress)) {
+ //
+ // If we receive an OnProgress event from a toplevel channel that the URI
+ // Loader has not yet targeted, then we must suppress the event. This is
+ // necessary to ensure that webprogresslisteners do not get confused when
+ // the channel is finally targeted. See bug 257308.
+ //
+ nsLoadFlags lf = 0;
+ aRequest->GetLoadFlags(&lf);
+ if ((lf & nsIChannel::LOAD_DOCUMENT_URI) &&
+ !(lf & nsIChannel::LOAD_TARGETED)) {
+ MOZ_LOG(
+ gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p Ignoring OnProgress while load is not targeted\n",
+ this));
+ return NS_OK;
+ }
+
+ //
+ // This is the first progress notification for the entry. If
+ // (aMaxProgress != -1) then the content-length of the data is known,
+ // so update mMaxSelfProgress... Otherwise, set it to -1 to indicate
+ // that the content-length is no longer known.
+ //
+ if (aProgressMax != -1) {
+ mMaxSelfProgress += aProgressMax;
+ info->mMaxProgress = aProgressMax;
+ } else {
+ mMaxSelfProgress = int64_t(-1);
+ info->mMaxProgress = int64_t(-1);
+ }
+
+ // Send a STATE_TRANSFERRING notification for the request.
+ int32_t flags;
+
+ flags = nsIWebProgressListener::STATE_TRANSFERRING |
+ nsIWebProgressListener::STATE_IS_REQUEST;
+ //
+ // Move the WebProgress into the STATE_TRANSFERRING state if necessary...
+ //
+ if (mProgressStateFlags & nsIWebProgressListener::STATE_START) {
+ mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING;
+
+ // Send STATE_TRANSFERRING for the document too...
+ flags |= nsIWebProgressListener::STATE_IS_DOCUMENT;
+ }
+
+ FireOnStateChange(this, aRequest, flags, NS_OK);
+ }
+
+ // Update our overall current progress count.
+ mCurrentSelfProgress += progressDelta;
+ }
+ //
+ // The request is not part of the load group, so ignore its progress
+ // information...
+ //
+ else {
+#if defined(DEBUG)
+ nsAutoCString buffer;
+
+ GetURIStringFromRequest(aRequest, buffer);
+ MOZ_LOG(
+ gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p OOPS - No Request Info for: %s\n", this, buffer.get()));
+#endif /* DEBUG */
+
+ return NS_OK;
+ }
+
+ //
+ // Fire progress notifications out to any registered nsIWebProgressListeners
+ //
+ FireOnProgressChange(this, aRequest, aProgress, aProgressMax, progressDelta,
+ mCurrentTotalProgress, mMaxTotalProgress);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDocLoader::OnStatus(nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aStatusArg) {
+ //
+ // Fire progress notifications out to any registered nsIWebProgressListeners
+ //
+ if (aStatus != NS_OK) {
+ // Remember the current status for this request
+ nsRequestInfo* info;
+ info = GetRequestInfo(aRequest);
+ if (info) {
+ bool uploading = (aStatus == NS_NET_STATUS_WRITING ||
+ aStatus == NS_NET_STATUS_SENDING_TO);
+ // If switching from uploading to downloading (or vice versa), then we
+ // need to reset our progress counts. This is designed with HTTP form
+ // submission in mind, where an upload is performed followed by download
+ // of possibly several documents.
+ if (info->mUploading != uploading) {
+ mCurrentSelfProgress = mMaxSelfProgress = 0;
+ mCurrentTotalProgress = mMaxTotalProgress = 0;
+ mCompletedTotalProgress = 0;
+ info->mUploading = uploading;
+ info->mCurrentProgress = 0;
+ info->mMaxProgress = 0;
+ }
+ }
+
+ nsCOMPtr<nsIStringBundleService> sbs =
+ mozilla::services::GetStringBundleService();
+ if (!sbs) return NS_ERROR_FAILURE;
+ nsAutoString msg;
+ nsresult rv = sbs->FormatStatusMessage(aStatus, aStatusArg, msg);
+ if (NS_FAILED(rv)) return rv;
+
+ // Keep around the message. In case a request finishes, we need to make sure
+ // to send the status message of another request to our user to that we
+ // don't display, for example, "Transferring" messages for requests that are
+ // already done.
+ if (info) {
+ if (!info->mLastStatus) {
+ info->mLastStatus = MakeUnique<nsStatusInfo>(aRequest);
+ } else {
+ // We're going to move it to the front of the list, so remove
+ // it from wherever it is now.
+ info->mLastStatus->remove();
+ }
+ info->mLastStatus->mStatusMessage = msg;
+ info->mLastStatus->mStatusCode = aStatus;
+ // Put the info at the front of the list
+ mStatusInfoList.insertFront(info->mLastStatus.get());
+ }
+ FireOnStatusChange(this, aRequest, aStatus, msg.get());
+ }
+ return NS_OK;
+}
+
+void nsDocLoader::ClearInternalProgress() {
+ ClearRequestInfoHash();
+
+ mCurrentSelfProgress = mMaxSelfProgress = 0;
+ mCurrentTotalProgress = mMaxTotalProgress = 0;
+ mCompletedTotalProgress = 0;
+
+ mProgressStateFlags = nsIWebProgressListener::STATE_STOP;
+}
+
+/**
+ * |_code| is executed for every listener matching |_flag|
+ * |listener| should be used inside |_code| as the nsIWebProgressListener var.
+ */
+#define NOTIFY_LISTENERS(_flag, _code) \
+ PR_BEGIN_MACRO \
+ nsCOMPtr<nsIWebProgressListener> listener; \
+ ListenerArray::BackwardIterator iter(mListenerInfoList); \
+ while (iter.HasMore()) { \
+ nsListenerInfo& info = iter.GetNext(); \
+ if (!(info.mNotifyMask & (_flag))) { \
+ continue; \
+ } \
+ listener = do_QueryReferent(info.mWeakListener); \
+ if (!listener) { \
+ iter.Remove(); \
+ continue; \
+ } \
+ _code \
+ } \
+ mListenerInfoList.Compact(); \
+ PR_END_MACRO
+
+void nsDocLoader::FireOnProgressChange(nsDocLoader* aLoadInitiator,
+ nsIRequest* request, int64_t aProgress,
+ int64_t aProgressMax,
+ int64_t aProgressDelta,
+ int64_t aTotalProgress,
+ int64_t aMaxTotalProgress) {
+ if (mIsLoadingDocument) {
+ mCurrentTotalProgress += aProgressDelta;
+ mMaxTotalProgress = GetMaxTotalProgress();
+
+ aTotalProgress = mCurrentTotalProgress;
+ aMaxTotalProgress = mMaxTotalProgress;
+ }
+
+#if defined(DEBUG)
+ nsAutoCString buffer;
+
+ GetURIStringFromRequest(request, buffer);
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: Progress (%s): curSelf: %" PRId64 " maxSelf: %" PRId64
+ " curTotal: %" PRId64 " maxTotal %" PRId64 "\n",
+ this, buffer.get(), aProgress, aProgressMax, aTotalProgress,
+ aMaxTotalProgress));
+#endif /* DEBUG */
+
+ NOTIFY_LISTENERS(
+ nsIWebProgress::NOTIFY_PROGRESS,
+ // XXX truncates 64-bit to 32-bit
+ listener->OnProgressChange(aLoadInitiator, request, int32_t(aProgress),
+ int32_t(aProgressMax), int32_t(aTotalProgress),
+ int32_t(aMaxTotalProgress)););
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ mParent->FireOnProgressChange(aLoadInitiator, request, aProgress,
+ aProgressMax, aProgressDelta, aTotalProgress,
+ aMaxTotalProgress);
+ }
+}
+
+void nsDocLoader::GatherAncestorWebProgresses(WebProgressList& aList) {
+ for (nsDocLoader* loader = this; loader; loader = loader->mParent) {
+ aList.AppendElement(loader);
+ }
+}
+
+void nsDocLoader::FireOnStateChange(nsIWebProgress* aProgress,
+ nsIRequest* aRequest, int32_t aStateFlags,
+ nsresult aStatus) {
+ WebProgressList list;
+ GatherAncestorWebProgresses(list);
+ for (uint32_t i = 0; i < list.Length(); ++i) {
+ list[i]->DoFireOnStateChange(aProgress, aRequest, aStateFlags, aStatus);
+ }
+}
+
+void nsDocLoader::DoFireOnStateChange(nsIWebProgress* const aProgress,
+ nsIRequest* const aRequest,
+ int32_t& aStateFlags,
+ const nsresult aStatus) {
+ //
+ // Remove the STATE_IS_NETWORK bit if necessary.
+ //
+ // The rule is to remove this bit, if the notification has been passed
+ // up from a child WebProgress, and the current WebProgress is already
+ // active...
+ //
+ if (mIsLoadingDocument &&
+ (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) &&
+ (this != aProgress)) {
+ aStateFlags &= ~nsIWebProgressListener::STATE_IS_NETWORK;
+ }
+
+ // Add the STATE_RESTORING bit if necessary.
+ if (mIsRestoringDocument)
+ aStateFlags |= nsIWebProgressListener::STATE_RESTORING;
+
+#if defined(DEBUG)
+ nsAutoCString buffer;
+
+ GetURIStringFromRequest(aRequest, buffer);
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: Status (%s): code: %x\n", this, buffer.get(),
+ aStateFlags));
+#endif /* DEBUG */
+
+ NS_ASSERTION(aRequest,
+ "Firing OnStateChange(...) notification with a NULL request!");
+
+ NOTIFY_LISTENERS(
+ ((aStateFlags >> 16) & nsIWebProgress::NOTIFY_STATE_ALL),
+ listener->OnStateChange(aProgress, aRequest, aStateFlags, aStatus););
+}
+
+void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* aUri,
+ uint32_t aFlags) {
+ NOTIFY_LISTENERS(
+ nsIWebProgress::NOTIFY_LOCATION,
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader [%p] calling %p->OnLocationChange to %s %x", this,
+ listener.get(), aUri->GetSpecOrDefault().get(), aFlags));
+ listener->OnLocationChange(aWebProgress, aRequest, aUri, aFlags););
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ mParent->FireOnLocationChange(aWebProgress, aRequest, aUri, aFlags);
+ }
+}
+
+void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ NOTIFY_LISTENERS(
+ nsIWebProgress::NOTIFY_STATUS,
+ listener->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage););
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ mParent->FireOnStatusChange(aWebProgress, aRequest, aStatus, aMessage);
+ }
+}
+
+bool nsDocLoader::RefreshAttempted(nsIWebProgress* aWebProgress, nsIURI* aURI,
+ int32_t aDelay, bool aSameURI) {
+ /*
+ * Returns true if the refresh may proceed,
+ * false if the refresh should be blocked.
+ */
+ bool allowRefresh = true;
+
+ NOTIFY_LISTENERS(
+ nsIWebProgress::NOTIFY_REFRESH,
+ nsCOMPtr<nsIWebProgressListener2> listener2 =
+ do_QueryReferent(info.mWeakListener);
+ if (!listener2) continue;
+
+ bool listenerAllowedRefresh;
+ nsresult listenerRV = listener2->OnRefreshAttempted(
+ aWebProgress, aURI, aDelay, aSameURI, &listenerAllowedRefresh);
+ if (NS_FAILED(listenerRV)) continue;
+
+ allowRefresh = allowRefresh && listenerAllowedRefresh;);
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ allowRefresh = allowRefresh && mParent->RefreshAttempted(aWebProgress, aURI,
+ aDelay, aSameURI);
+ }
+
+ return allowRefresh;
+}
+
+nsresult nsDocLoader::AddRequestInfo(nsIRequest* aRequest) {
+ if (!mRequestInfoHash.Add(aRequest, mozilla::fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+void nsDocLoader::RemoveRequestInfo(nsIRequest* aRequest) {
+ mRequestInfoHash.Remove(aRequest);
+}
+
+nsDocLoader::nsRequestInfo* nsDocLoader::GetRequestInfo(
+ nsIRequest* aRequest) const {
+ return static_cast<nsRequestInfo*>(mRequestInfoHash.Search(aRequest));
+}
+
+void nsDocLoader::ClearRequestInfoHash(void) { mRequestInfoHash.Clear(); }
+
+int64_t nsDocLoader::CalculateMaxProgress() {
+ int64_t max = mCompletedTotalProgress;
+ for (auto iter = mRequestInfoHash.Iter(); !iter.Done(); iter.Next()) {
+ auto info = static_cast<const nsRequestInfo*>(iter.Get());
+
+ if (info->mMaxProgress < info->mCurrentProgress) {
+ return int64_t(-1);
+ }
+ max += info->mMaxProgress;
+ }
+ return max;
+}
+
+NS_IMETHODIMP nsDocLoader::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* cb) {
+ if (aOldChannel) {
+ nsLoadFlags loadFlags = 0;
+ int32_t stateFlags = nsIWebProgressListener::STATE_REDIRECTING |
+ nsIWebProgressListener::STATE_IS_REQUEST;
+
+ aOldChannel->GetLoadFlags(&loadFlags);
+ // If the document channel is being redirected, then indicate that the
+ // document is being redirected in the notification...
+ if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) {
+ stateFlags |= nsIWebProgressListener::STATE_IS_DOCUMENT;
+
+#if defined(DEBUG)
+ // We only set mDocumentRequest in OnStartRequest(), but its possible
+ // to get a redirect before that for service worker interception.
+ if (mDocumentRequest) {
+ nsCOMPtr<nsIRequest> request(aOldChannel);
+ NS_ASSERTION(request == mDocumentRequest, "Wrong Document Channel");
+ }
+#endif /* DEBUG */
+ }
+
+ OnRedirectStateChange(aOldChannel, aNewChannel, aFlags, stateFlags);
+ FireOnStateChange(this, aOldChannel, stateFlags, NS_OK);
+ }
+
+ cb->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+void nsDocLoader::OnSecurityChange(nsISupports* aContext, uint32_t aState) {
+ //
+ // Fire progress notifications out to any registered nsIWebProgressListeners.
+ //
+
+ nsCOMPtr<nsIRequest> request = do_QueryInterface(aContext);
+ nsIWebProgress* webProgress = static_cast<nsIWebProgress*>(this);
+
+ NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_SECURITY,
+ listener->OnSecurityChange(webProgress, request, aState););
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ mParent->OnSecurityChange(aContext, aState);
+ }
+}
+
+/*
+ * Implementation of nsISupportsPriority methods...
+ *
+ * The priority of the DocLoader _is_ the priority of its LoadGroup.
+ *
+ * XXX(darin): Once we start storing loadgroups in loadgroups, this code will
+ * go away.
+ */
+
+NS_IMETHODIMP nsDocLoader::GetPriority(int32_t* aPriority) {
+ nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup);
+ if (p) return p->GetPriority(aPriority);
+
+ *aPriority = 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDocLoader::SetPriority(int32_t aPriority) {
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: SetPriority(%d) called\n", this, aPriority));
+
+ nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup);
+ if (p) p->SetPriority(aPriority);
+
+ NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, SetPriority,
+ (aPriority));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDocLoader::AdjustPriority(int32_t aDelta) {
+ MOZ_LOG(gDocLoaderLog, LogLevel::Debug,
+ ("DocLoader:%p: AdjustPriority(%d) called\n", this, aDelta));
+
+ nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup);
+ if (p) p->AdjustPriority(aDelta);
+
+ NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, AdjustPriority,
+ (aDelta));
+
+ return NS_OK;
+}
+
+#if 0
+void nsDocLoader::DumpChannelInfo()
+{
+ nsChannelInfo *info;
+ int32_t i, count;
+ int32_t current=0, max=0;
+
+
+ printf("==== DocLoader=%x\n", this);
+
+ count = mChannelInfoList.Count();
+ for(i=0; i<count; i++) {
+ info = (nsChannelInfo *)mChannelInfoList.ElementAt(i);
+
+# if defined(DEBUG)
+ nsAutoCString buffer;
+ nsresult rv = NS_OK;
+ if (info->mURI) {
+ rv = info->mURI->GetSpec(buffer);
+ }
+
+ printf(" [%d] current=%d max=%d [%s]\n", i,
+ info->mCurrentProgress,
+ info->mMaxProgress, buffer.get());
+# endif /* DEBUG */
+
+ current += info->mCurrentProgress;
+ if (max >= 0) {
+ if (info->mMaxProgress < info->mCurrentProgress) {
+ max = -1;
+ } else {
+ max += info->mMaxProgress;
+ }
+ }
+ }
+
+ printf("\nCurrent=%d Total=%d\n====\n", current, max);
+}
+#endif /* 0 */
diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h
new file mode 100644
index 0000000000..9f39fca7f2
--- /dev/null
+++ b/uriloader/base/nsDocLoader.h
@@ -0,0 +1,407 @@
+/* -*- 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 nsDocLoader_h__
+#define nsDocLoader_h__
+
+#include "nsIDocumentLoader.h"
+#include "nsIWebProgress.h"
+#include "nsIWebProgressListener.h"
+#include "nsIRequestObserver.h"
+#include "nsWeakReference.h"
+#include "nsILoadGroup.h"
+#include "nsCOMArray.h"
+#include "nsTObserverArray.h"
+#include "nsString.h"
+#include "nsIChannel.h"
+#include "nsIProgressEventSink.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIChannelEventSink.h"
+#include "nsISupportsPriority.h"
+#include "nsCOMPtr.h"
+#include "PLDHashTable.h"
+#include "nsCycleCollectionParticipant.h"
+
+#include "mozilla/LinkedList.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+namespace dom {
+class BrowserBridgeChild;
+class BrowsingContext;
+} // namespace dom
+} // namespace mozilla
+
+/****************************************************************************
+ * nsDocLoader implementation...
+ ****************************************************************************/
+
+#define NS_THIS_DOCLOADER_IMPL_CID \
+ { /* b4ec8387-98aa-4c08-93b6-6d23069c06f2 */ \
+ 0xb4ec8387, 0x98aa, 0x4c08, { \
+ 0x93, 0xb6, 0x6d, 0x23, 0x06, 0x9c, 0x06, 0xf2 \
+ } \
+ }
+
+class nsDocLoader : public nsIDocumentLoader,
+ public nsIRequestObserver,
+ public nsSupportsWeakReference,
+ public nsIProgressEventSink,
+ public nsIWebProgress,
+ public nsIInterfaceRequestor,
+ public nsIChannelEventSink,
+ public nsISupportsPriority {
+ public:
+ using BrowserBridgeChild = mozilla::dom::BrowserBridgeChild;
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_THIS_DOCLOADER_IMPL_CID)
+
+ nsDocLoader();
+
+ [[nodiscard]] virtual nsresult Init();
+ [[nodiscard]] nsresult InitWithBrowsingContext(
+ mozilla::dom::BrowsingContext* aBrowsingContext);
+
+ static already_AddRefed<nsDocLoader> GetAsDocLoader(nsISupports* aSupports);
+ // Needed to deal with ambiguous inheritance from nsISupports...
+ static nsISupports* GetAsSupports(nsDocLoader* aDocLoader) {
+ return static_cast<nsIDocumentLoader*>(aDocLoader);
+ }
+
+ // Add aDocLoader as a child to the docloader service.
+ [[nodiscard]] static nsresult AddDocLoaderAsChildOfRoot(
+ nsDocLoader* aDocLoader);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsDocLoader, nsIDocumentLoader)
+
+ NS_DECL_NSIDOCUMENTLOADER
+
+ // nsIProgressEventSink
+ NS_DECL_NSIPROGRESSEVENTSINK
+
+ // nsIRequestObserver methods: (for observing the load group)
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSIWEBPROGRESS
+
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSISUPPORTSPRIORITY; // semicolon for clang-format bug 1629756
+
+ // Implementation specific methods...
+
+ // Remove aChild from our childlist. This nulls out the child's mParent
+ // pointer.
+ [[nodiscard]] nsresult RemoveChildLoader(nsDocLoader* aChild);
+
+ // Add aChild to our child list. This will set aChild's mParent pointer to
+ // |this|.
+ [[nodiscard]] nsresult AddChildLoader(nsDocLoader* aChild);
+ nsDocLoader* GetParent() const { return mParent; }
+
+ struct nsListenerInfo {
+ nsListenerInfo(nsIWeakReference* aListener, unsigned long aNotifyMask)
+ : mWeakListener(aListener), mNotifyMask(aNotifyMask) {}
+
+ // Weak pointer for the nsIWebProgressListener...
+ nsWeakPtr mWeakListener;
+
+ // Mask indicating which notifications the listener wants to receive.
+ unsigned long mNotifyMask;
+ };
+
+ /**
+ * Fired when a security change occurs due to page transitions,
+ * or end document load. This interface should be called by
+ * a security package (eg Netscape Personal Security Manager)
+ * to notify nsIWebProgressListeners that security state has
+ * changed. State flags are in nsIWebProgressListener.idl
+ */
+ void OnSecurityChange(nsISupports* aContext, uint32_t aState);
+
+ void SetDocumentOpenedButNotLoaded() { mDocumentOpenedButNotLoaded = true; }
+
+ bool TreatAsBackgroundLoad();
+
+ void SetFakeOnLoadDispatched() { mHasFakeOnLoadDispatched = true; };
+
+ bool HasFakeOnLoadDispatched() { return mHasFakeOnLoadDispatched; };
+
+ void ResetToFirstLoad() {
+ mHasFakeOnLoadDispatched = false;
+ mIsReadyToHandlePostMessage = false;
+ mTreatAsBackgroundLoad = false;
+ };
+
+ // Inform a parent docloader that a BrowserBridgeChild has been created for
+ // an OOP sub-document.
+ // (This is the OOP counterpart to ChildEnteringOnload below.)
+ void OOPChildLoadStarted(BrowserBridgeChild* aChild) {
+ MOZ_DIAGNOSTIC_ASSERT(!mOOPChildrenLoading.Contains(aChild));
+ mOOPChildrenLoading.AppendElement(aChild);
+ }
+
+ // Inform a parent docloader that the BrowserBridgeChild for one of its
+ // OOP sub-documents is done calling its onload handler.
+ // (This is the OOP counterpart to ChildDoneWithOnload below.)
+ void OOPChildLoadDone(BrowserBridgeChild* aChild) {
+ // aChild will not be in the list if nsDocLoader::Stop() was called, since
+ // that clears mOOPChildrenLoading. It also dispatches the 'load' event,
+ // so we don't need to call DocLoaderIsEmpty in that case.
+ if (mOOPChildrenLoading.RemoveElement(aChild)) {
+ DocLoaderIsEmpty(true);
+ }
+ }
+
+ uint32_t ChildCount() const { return mChildList.Length(); }
+
+ protected:
+ virtual ~nsDocLoader();
+
+ [[nodiscard]] virtual nsresult SetDocLoaderParent(nsDocLoader* aLoader);
+
+ bool IsBusy();
+
+ void SetBackgroundLoadIframe();
+
+ void Destroy();
+ virtual void DestroyChildren();
+
+ nsIDocumentLoader* ChildAt(int32_t i) {
+ return mChildList.SafeElementAt(i, nullptr);
+ }
+
+ void FireOnProgressChange(nsDocLoader* aLoadInitiator, nsIRequest* request,
+ int64_t aProgress, int64_t aProgressMax,
+ int64_t aProgressDelta, int64_t aTotalProgress,
+ int64_t aMaxTotalProgress);
+
+ // This should be at least 2 long since we'll generally always
+ // have the current page and the global docloader on the ancestor
+ // list. But to deal with frames it's better to make it a bit
+ // longer, and it's always a stack temporary so there's no real
+ // reason not to.
+ typedef AutoTArray<RefPtr<nsDocLoader>, 8> WebProgressList;
+ void GatherAncestorWebProgresses(WebProgressList& aList);
+
+ void FireOnStateChange(nsIWebProgress* aProgress, nsIRequest* request,
+ int32_t aStateFlags, nsresult aStatus);
+
+ // The guts of FireOnStateChange, but does not call itself on our ancestors.
+ // The arguments that are const are const so that we can detect cases when
+ // DoFireOnStateChange wants to propagate changes to the next web progress
+ // at compile time. The ones that are not, are references so that such
+ // changes can be propagated.
+ void DoFireOnStateChange(nsIWebProgress* const aProgress,
+ nsIRequest* const request, int32_t& aStateFlags,
+ const nsresult aStatus);
+
+ void FireOnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ nsresult aStatus, const char16_t* aMessage);
+
+ void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
+ nsIURI* aUri, uint32_t aFlags);
+
+ [[nodiscard]] bool RefreshAttempted(nsIWebProgress* aWebProgress,
+ nsIURI* aURI, int32_t aDelay,
+ bool aSameURI);
+
+ // this function is overridden by the docshell, it is provided so that we
+ // can pass more information about redirect state (the normal OnStateChange
+ // doesn't get the new channel).
+ // @param aRedirectFlags The flags being sent to OnStateChange that
+ // indicate the type of redirect.
+ // @param aStateFlags The channel flags normally sent to OnStateChange.
+ virtual void OnRedirectStateChange(nsIChannel* aOldChannel,
+ nsIChannel* aNewChannel,
+ uint32_t aRedirectFlags,
+ uint32_t aStateFlags) {}
+
+ void doStartDocumentLoad();
+ void doStartURLLoad(nsIRequest* request, int32_t aExtraFlags);
+ void doStopURLLoad(nsIRequest* request, nsresult aStatus);
+ void doStopDocumentLoad(nsIRequest* request, nsresult aStatus);
+
+ void NotifyDoneWithOnload(nsDocLoader* aParent);
+
+ // Inform a parent docloader that aChild is about to call its onload
+ // handler.
+ [[nodiscard]] bool ChildEnteringOnload(nsIDocumentLoader* aChild) {
+ // It's ok if we're already in the list -- we'll just be in there twice
+ // and then the RemoveObject calls from ChildDoneWithOnload will remove
+ // us.
+ return mChildrenInOnload.AppendObject(aChild);
+ }
+
+ // Inform a parent docloader that aChild is done calling its onload
+ // handler.
+ void ChildDoneWithOnload(nsIDocumentLoader* aChild) {
+ mChildrenInOnload.RemoveObject(aChild);
+ DocLoaderIsEmpty(true);
+ }
+
+ // DocLoaderIsEmpty should be called whenever the docloader may be empty.
+ // This method is idempotent and does nothing if the docloader is not in
+ // fact empty. This method _does_ make sure that layout is flushed if our
+ // loadgroup has no active requests before checking for "real" emptiness if
+ // aFlushLayout is true.
+ // @param aOverrideStatus An optional status to use when notifying listeners
+ // of the completed load, instead of using the load group's status.
+ void DocLoaderIsEmpty(
+ bool aFlushLayout,
+ const mozilla::Maybe<nsresult>& aOverrideStatus = mozilla::Nothing());
+
+ protected:
+ struct nsStatusInfo : public mozilla::LinkedListElement<nsStatusInfo> {
+ nsString mStatusMessage;
+ nsresult mStatusCode;
+ // Weak mRequest is ok; we'll be told if it decides to go away.
+ nsIRequest* const mRequest;
+
+ explicit nsStatusInfo(nsIRequest* aRequest)
+ : mStatusCode(NS_ERROR_NOT_INITIALIZED), mRequest(aRequest) {
+ MOZ_COUNT_CTOR(nsStatusInfo);
+ }
+ MOZ_COUNTED_DTOR(nsStatusInfo)
+ };
+
+ struct nsRequestInfo : public PLDHashEntryHdr {
+ explicit nsRequestInfo(const void* key)
+ : mKey(key),
+ mCurrentProgress(0),
+ mMaxProgress(0),
+ mUploading(false),
+ mLastStatus(nullptr) {
+ MOZ_COUNT_CTOR(nsRequestInfo);
+ }
+
+ MOZ_COUNTED_DTOR(nsRequestInfo)
+
+ nsIRequest* Request() {
+ return static_cast<nsIRequest*>(const_cast<void*>(mKey));
+ }
+
+ const void* mKey; // Must be first for the PLDHashTable stubs to work
+ int64_t mCurrentProgress;
+ int64_t mMaxProgress;
+ bool mUploading;
+
+ mozilla::UniquePtr<nsStatusInfo> mLastStatus;
+ };
+
+ static void RequestInfoHashInitEntry(PLDHashEntryHdr* entry, const void* key);
+ static void RequestInfoHashClearEntry(PLDHashTable* table,
+ PLDHashEntryHdr* entry);
+
+ // IMPORTANT: The ownership implicit in the following member
+ // variables has been explicitly checked and set using nsCOMPtr
+ // for owning pointers and raw COM interface pointers for weak
+ // (ie, non owning) references. If you add any members to this
+ // class, please make the ownership explicit (pinkerton, scc).
+
+ nsCOMPtr<nsIRequest> mDocumentRequest; // [OWNER] ???compare with document
+
+ nsDocLoader* mParent; // [WEAK]
+
+ typedef nsAutoTObserverArray<nsListenerInfo, 8> ListenerArray;
+ ListenerArray mListenerInfoList;
+
+ nsCOMPtr<nsILoadGroup> mLoadGroup;
+ // We hold weak refs to all our kids
+ nsTObserverArray<nsDocLoader*> mChildList;
+
+ // The following member variables are related to the new nsIWebProgress
+ // feedback interfaces that travis cooked up.
+ int32_t mProgressStateFlags;
+
+ int64_t mCurrentSelfProgress;
+ int64_t mMaxSelfProgress;
+
+ int64_t mCurrentTotalProgress;
+ int64_t mMaxTotalProgress;
+
+ PLDHashTable mRequestInfoHash;
+ int64_t mCompletedTotalProgress;
+
+ mozilla::LinkedList<nsStatusInfo> mStatusInfoList;
+
+ /*
+ * This flag indicates that the loader is loading a document. It is set
+ * from the call to LoadDocument(...) until the OnConnectionsComplete(...)
+ * notification is fired...
+ */
+ bool mIsLoadingDocument;
+
+ /* Flag to indicate that we're in the process of restoring a document. */
+ bool mIsRestoringDocument;
+
+ /* Flag to indicate that we're in the process of flushing layout
+ under DocLoaderIsEmpty() and should not do another flush. */
+ bool mDontFlushLayout;
+
+ /* Flag to indicate whether we should consider ourselves as currently
+ flushing layout for the purposes of IsBusy. For example, if Stop has
+ been called then IsBusy should return false even if we are still
+ flushing. */
+ bool mIsFlushingLayout;
+
+ bool mTreatAsBackgroundLoad;
+
+ private:
+ bool mHasFakeOnLoadDispatched;
+
+ bool mIsReadyToHandlePostMessage;
+ /**
+ * This flag indicates that the loader is waiting for completion of
+ * a document.open-triggered "document load". This is set when
+ * document.open() happens and sets up a new parser and cleared out
+ * when we go to fire our load event or end up with a new document
+ * channel.
+ */
+ bool mDocumentOpenedButNotLoaded;
+
+ static const PLDHashTableOps sRequestInfoHashOps;
+
+ // A list of kids that are in the middle of their onload calls and will let
+ // us know once they're done. We don't want to fire onload for "normal"
+ // DocLoaderIsEmpty calls (those coming from requests finishing in our
+ // loadgroup) unless this is empty.
+ nsCOMArray<nsIDocumentLoader> mChildrenInOnload;
+
+ // The OOP counterpart to mChildrenInOnload.
+ // Not holding strong refs here since we don't actually use the BBCs.
+ nsTArray<const BrowserBridgeChild*> mOOPChildrenLoading;
+
+ int64_t GetMaxTotalProgress();
+
+ nsresult AddRequestInfo(nsIRequest* aRequest);
+ void RemoveRequestInfo(nsIRequest* aRequest);
+ nsRequestInfo* GetRequestInfo(nsIRequest* aRequest) const;
+ void ClearRequestInfoHash();
+ int64_t CalculateMaxProgress();
+ /// void DumpChannelInfo(void);
+
+ // used to clear our internal progress state between loads...
+ void ClearInternalProgress();
+
+ /**
+ * Used to test whether we might need to fire a load event. This
+ * can happen when we have a document load going on, or when we've
+ * had document.open() called and haven't fired the corresponding
+ * load event yet.
+ */
+ bool IsBlockingLoadEvent() const {
+ return mIsLoadingDocument || mDocumentOpenedButNotLoaded;
+ }
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsDocLoader, NS_THIS_DOCLOADER_IMPL_CID)
+
+static inline nsISupports* ToSupports(nsDocLoader* aDocLoader) {
+ return static_cast<nsIDocumentLoader*>(aDocLoader);
+}
+
+#endif /* nsDocLoader_h__ */
diff --git a/uriloader/base/nsIContentHandler.idl b/uriloader/base/nsIContentHandler.idl
new file mode 100644
index 0000000000..31ef87a8ba
--- /dev/null
+++ b/uriloader/base/nsIContentHandler.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 2; 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 nsIRequest;
+interface nsIInterfaceRequestor;
+
+[scriptable, uuid(49439df2-b3d2-441c-bf62-866bdaf56fd2)]
+interface nsIContentHandler : nsISupports
+{
+ /**
+ * Tells the content handler to take over handling the content. If this
+ * function succeeds, the URI Loader will leave this request alone, ignoring
+ * progress notifications. Failure of this method will cause the request to be
+ * cancelled, unless the error code is NS_ERROR_WONT_HANDLE_CONTENT (see
+ * below).
+ *
+ * @param aWindowContext
+ * Window context, used to get things like the current nsIDOMWindow
+ * for this request. May be null.
+ * @param aContentType
+ * The content type of aRequest
+ * @param aRequest
+ * A request whose content type is already known.
+ *
+ * @throw NS_ERROR_WONT_HANDLE_CONTENT Indicates that this handler does not
+ * want to handle this content. A different way for handling this
+ * content should be tried.
+ */
+ void handleContent(in string aContentType,
+ in nsIInterfaceRequestor aWindowContext,
+ in nsIRequest aRequest);
+};
diff --git a/uriloader/base/nsIDocumentLoader.idl b/uriloader/base/nsIDocumentLoader.idl
new file mode 100644
index 0000000000..3bd960ac84
--- /dev/null
+++ b/uriloader/base/nsIDocumentLoader.idl
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; 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 nsILoadGroup;
+interface nsIChannel;
+interface nsIURI;
+interface nsIWebProgress;
+interface nsIRequest;
+
+/**
+ * An nsIDocumentLoader is an interface responsible for tracking groups of
+ * loads that belong together (images, external scripts, etc) and subdocuments
+ * (<iframe>, <frame>, etc). It is also responsible for sending
+ * nsIWebProgressListener notifications.
+ * XXXbz this interface should go away, we think...
+ */
+[scriptable, uuid(bbe961ee-59e9-42bb-be50-0331979bb79f)]
+interface nsIDocumentLoader : nsISupports
+{
+ // Stop all loads in the loadgroup of this docloader
+ void stop();
+
+ // XXXbz is this needed? For embedding? What this does is does is not
+ // defined by this interface!
+ readonly attribute nsISupports container;
+
+ // The loadgroup associated with this docloader
+ readonly attribute nsILoadGroup loadGroup;
+
+ // The defaultLoadRequest of the loadgroup associated with this docloader
+ readonly attribute nsIChannel documentChannel;
+};
+
diff --git a/uriloader/base/nsITransfer.idl b/uriloader/base/nsITransfer.idl
new file mode 100644
index 0000000000..34f855ff3d
--- /dev/null
+++ b/uriloader/base/nsITransfer.idl
@@ -0,0 +1,137 @@
+/* -*- 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 "nsIWebProgressListener2.idl"
+
+interface nsIArray;
+interface nsIURI;
+interface nsICancelable;
+interface nsIMIMEInfo;
+interface nsIFile;
+webidl BrowsingContext;
+
+[scriptable, uuid(37ec75d3-97ad-4da8-afaa-eabe5b4afd73)]
+interface nsITransfer : nsIWebProgressListener2 {
+
+ const unsigned long DOWNLOAD_ACCEPTABLE = 0;
+ const unsigned long DOWNLOAD_FORBIDDEN = 1;
+ const unsigned long DOWNLOAD_POTENTIALLY_UNSAFE = 2;
+
+ /**
+ * Initializes the transfer with certain properties. This function must
+ * be called prior to accessing any properties on this interface.
+ *
+ * @param aSource The source URI of the transfer. Must not be null.
+ *
+ * @param aTarget The target URI of the transfer. Must not be null.
+ *
+ * @param aDisplayName The user-readable description of the transfer.
+ * Can be empty.
+ *
+ * @param aMIMEInfo The MIME info associated with the target,
+ * including MIME type and helper app when appropriate.
+ * This parameter is optional.
+ *
+ * @param startTime Time when the download started (ie, when the first
+ * response from the server was received)
+ * XXX presumably wbp and exthandler do this differently
+ *
+ * @param aTempFile The location of a temporary file; i.e. a file in which
+ * the received data will be stored, but which is not
+ * equal to the target file. (will be moved to the real
+ * target by the caller, when the download is finished)
+ * May be null.
+ *
+ * @param aCancelable An object that can be used to abort the download.
+ * Must not be null.
+ * Implementations are expected to hold a strong
+ * reference to this object until the download is
+ * finished, at which point they should release the
+ * reference.
+ *
+ * @param aIsPrivate Used to determine the privacy status of the new transfer.
+ * If true, indicates that the transfer was initiated from
+ * a source that desires privacy.
+ *
+ * @param aDownloadClassification Indicates wheter the dowload is unwanted,
+ * should be considered dangerous or insecure.
+ */
+ void init(in nsIURI aSource,
+ in nsIURI aTarget,
+ in AString aDisplayName,
+ in nsIMIMEInfo aMIMEInfo,
+ in PRTime startTime,
+ in nsIFile aTempFile,
+ in nsICancelable aCancelable,
+ in boolean aIsPrivate,
+ in long aDownloadClassification);
+
+ /**
+ * Same as init, but allows for passing the browsingContext
+ * which will allow for opening the download with the same
+ * userContextId
+ *
+ * @param aBrowsingContext BrowsingContext of the initiating document.
+ *
+ * @param aHandleInternally Set to true if the download should be opened within
+ * the browser.
+ */
+ void initWithBrowsingContext(in nsIURI aSource,
+ in nsIURI aTarget,
+ in AString aDisplayName,
+ in nsIMIMEInfo aMIMEInfo,
+ in PRTime startTime,
+ in nsIFile aTempFile,
+ in nsICancelable aCancelable,
+ in boolean aIsPrivate,
+ in long aDownloadClassification,
+ in BrowsingContext aBrowsingContext,
+ in boolean aHandleInternally);
+
+ /*
+ * Used to notify the transfer object of the hash of the downloaded file.
+ * Must be called on the main thread, only after the download has finished
+ * successfully.
+ * @param aHash The SHA-256 hash in raw bytes of the downloaded file.
+ */
+ void setSha256Hash(in ACString aHash);
+
+ /*
+ * Used to notify the transfer object of the signature of the downloaded
+ * file. Must be called on the main thread, only after the download has
+ * finished successfully.
+ * @param aSignatureInfo The Array of Array of Array of bytes
+ * certificates of the downloaded file.
+ */
+ void setSignatureInfo(in Array<Array<Array<uint8_t> > > aSignatureInfo);
+
+ /*
+ * Used to notify the transfer object of the redirects associated with the
+ * channel that terminated in the downloaded file. Must be called on the
+ * main thread, only after the download has finished successfully.
+ * @param aRedirects The nsIArray of nsIPrincipal of redirected URIs
+ * associated with the downloaded file.
+ */
+ void setRedirects(in nsIArray aRedirects);
+};
+
+%{C++
+/**
+ * A component with this contract ID will be created each time a download is
+ * started, and nsITransfer::Init will be called on it and an observer will be set.
+ *
+ * Notifications of the download progress will happen via
+ * nsIWebProgressListener/nsIWebProgressListener2.
+ *
+ * INTERFACES THAT MUST BE IMPLEMENTED:
+ * nsITransfer
+ * nsIWebProgressListener
+ * nsIWebProgressListener2
+ *
+ * XXX move this to nsEmbedCID.h once the interfaces (and the contract ID) are
+ * frozen.
+ */
+#define NS_TRANSFER_CONTRACTID "@mozilla.org/transfer;1"
+%}
diff --git a/uriloader/base/nsIURIContentListener.idl b/uriloader/base/nsIURIContentListener.idl
new file mode 100644
index 0000000000..35ff3a9c91
--- /dev/null
+++ b/uriloader/base/nsIURIContentListener.idl
@@ -0,0 +1,124 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIRequest;
+interface nsIStreamListener;
+interface nsIURI;
+
+/**
+ * nsIURIContentListener is an interface used by components which
+ * want to know (and have a chance to handle) a particular content type.
+ * Typical usage scenarios will include running applications which register
+ * a nsIURIContentListener for each of its content windows with the uri
+ * dispatcher service.
+ */
+[scriptable, uuid(10a28f38-32e8-4c63-8aa1-12eaaebc369a)]
+interface nsIURIContentListener : nsISupports
+{
+ /**
+ * Notifies the content listener to hook up an nsIStreamListener capable of
+ * consuming the data stream.
+ *
+ * @param aContentType Content type of the data.
+ * @param aIsContentPreferred Indicates whether the content should be
+ * preferred by this listener.
+ * @param aRequest Request that is providing the data.
+ * @param aContentHandler nsIStreamListener that will consume the data.
+ * This should be set to <code>nullptr</code> if
+ * this content listener can't handle the content
+ * type; in this case, doContent should also fail
+ * (i.e., return failure nsresult).
+ *
+ * @return <code>true</code> if the load should
+ * be aborted and consumer wants to
+ * handle the load completely by itself. This
+ * causes the URI Loader do nothing else...
+ * <code>false</code> if the URI Loader should
+ * continue handling the load and call the
+ * returned streamlistener's methods.
+ */
+ boolean doContent(in ACString aContentType,
+ in boolean aIsContentPreferred,
+ in nsIRequest aRequest,
+ out nsIStreamListener aContentHandler);
+
+ /**
+ * When given a uri to dispatch, if the URI is specified as 'preferred
+ * content' then the uri loader tries to find a preferred content handler
+ * for the content type. The thought is that many content listeners may
+ * be able to handle the same content type if they have to. i.e. the mail
+ * content window can handle text/html just like a browser window content
+ * listener. However, if the user clicks on a link with text/html content,
+ * then the browser window should handle that content and not the mail
+ * window where the user may have clicked the link. This is the difference
+ * between isPreferred and canHandleContent.
+ *
+ * @param aContentType Content type of the data.
+ * @param aDesiredContentType Indicates that aContentType must be converted
+ * to aDesiredContentType before processing the
+ * data. This causes a stream converted to be
+ * inserted into the nsIStreamListener chain.
+ * This argument can be <code>nullptr</code> if
+ * the content should be consumed directly as
+ * aContentType.
+ *
+ * @return <code>true</code> if this is a preferred
+ * content handler for aContentType;
+ * <code>false<code> otherwise.
+ */
+ boolean isPreferred(in string aContentType, out string aDesiredContentType);
+
+ /**
+ * When given a uri to dispatch, if the URI is not specified as 'preferred
+ * content' then the uri loader calls canHandleContent to see if the content
+ * listener is capable of handling the content.
+ *
+ * @param aContentType Content type of the data.
+ * @param aIsContentPreferred Indicates whether the content should be
+ * preferred by this listener.
+ * @param aDesiredContentType Indicates that aContentType must be converted
+ * to aDesiredContentType before processing the
+ * data. This causes a stream converted to be
+ * inserted into the nsIStreamListener chain.
+ * This argument can be <code>nullptr</code> if
+ * the content should be consumed directly as
+ * aContentType.
+ *
+ * @return <code>true</code> if the data can be consumed.
+ * <code>false</code> otherwise.
+ *
+ * Note: I really envision canHandleContent as a method implemented
+ * by the docshell as the implementation is generic to all doc
+ * shells. The isPreferred decision is a decision made by a top level
+ * application content listener that sits at the top of the docshell
+ * hierarchy.
+ */
+ boolean canHandleContent(in string aContentType,
+ in boolean aIsContentPreferred,
+ out string aDesiredContentType);
+
+ /**
+ * The load context associated with a particular content listener.
+ * The URI Loader stores and accesses this value as needed.
+ */
+ attribute nsISupports loadCookie;
+
+ /**
+ * The parent content listener if this particular listener is part of a chain
+ * of content listeners (i.e. a docshell!)
+ *
+ * @note If this attribute is set to an object that implements
+ * nsISupportsWeakReference, the implementation should get the
+ * nsIWeakReference and hold that. Otherwise, the implementation
+ * should not refcount this interface; it should assume that a non
+ * null value is always valid. In that case, the caller is
+ * responsible for explicitly setting this value back to null if the
+ * parent content listener is destroyed.
+ */
+ attribute nsIURIContentListener parentContentListener;
+};
+
diff --git a/uriloader/base/nsIURILoader.idl b/uriloader/base/nsIURILoader.idl
new file mode 100644
index 0000000000..273c0f4cdc
--- /dev/null
+++ b/uriloader/base/nsIURILoader.idl
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 2; 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 nsIURIContentListener;
+interface nsIURI;
+interface nsILoadGroup;
+interface nsIProgressEventSink;
+interface nsIChannel;
+interface nsIRequest;
+interface nsIStreamListener;
+interface nsIInputStream;
+interface nsIInterfaceRequestor;
+
+/**
+ * The uri dispatcher is responsible for taking uri's, determining
+ * the content and routing the opened url to the correct content
+ * handler.
+ *
+ * When you encounter a url you want to open, you typically call
+ * openURI, passing it the content listener for the window the uri is
+ * originating from. The uri dispatcher opens the url to discover the
+ * content type. It then gives the content listener first crack at
+ * handling the content. If it doesn't want it, the dispatcher tries
+ * to hand it off one of the registered content listeners. This allows
+ * running applications the chance to jump in and handle the content.
+ *
+ * If that also fails, then the uri dispatcher goes to the registry
+ * looking for the preferred content handler for the content type
+ * of the uri. The content handler may create an app instance
+ * or it may hand the contents off to a platform specific plugin
+ * or helper app. Or it may hand the url off to an OS registered
+ * application.
+ */
+[scriptable, uuid(8762c4e7-be35-4958-9b81-a05685bb516d)]
+interface nsIURILoader : nsISupports
+{
+ /**
+ * @name Flags for opening URIs.
+ */
+ /* @{ */
+ /**
+ * Should the content be displayed in a container that prefers the
+ * content-type, or will any container do.
+ */
+ const unsigned long IS_CONTENT_PREFERRED = 1 << 0;
+ /**
+ * If this flag is set, only the listener of the specified window context will
+ * be considered for content handling; if it refuses the load, an error will
+ * be indicated.
+ */
+ const unsigned long DONT_RETARGET = 1 << 1;
+ /* @} */
+
+ /**
+ * As applications such as messenger and the browser are instantiated,
+ * they register content listener's with the uri dispatcher corresponding
+ * to content windows within that application.
+ *
+ * Note to self: we may want to optimize things a bit more by requiring
+ * the content types the registered content listener cares about.
+ *
+ * @param aContentListener
+ * The listener to register. This listener must implement
+ * nsISupportsWeakReference.
+ *
+ * @see the nsIURILoader class description
+ */
+ void registerContentListener (in nsIURIContentListener aContentListener);
+ void unRegisterContentListener (in nsIURIContentListener aContentListener);
+
+ /**
+ * OpenURI requires the following parameters.....
+ * @param aChannel
+ * The channel that should be opened. This must not be asyncOpen'd yet!
+ * If a loadgroup is set on the channel, it will get replaced with a
+ * different one.
+ * @param aFlags
+ * Combination (bitwise OR) of the flags specified above. 0 indicates
+ * default handling.
+ * @param aWindowContext
+ * If you are running the url from a doc shell or a web shell, this is
+ * your window context. If you have a content listener you want to
+ * give first crack to, the uri loader needs to be able to get it
+ * from the window context. We will also be using the window context
+ * to get at the progress event sink interface.
+ * <b>Must not be null!</b>
+ */
+ void openURI(in nsIChannel aChannel,
+ in unsigned long aFlags,
+ in nsIInterfaceRequestor aWindowContext);
+
+ /**
+ * Loads data from a channel. This differs from openURI in that the channel
+ * may already be opened, and that it returns a stream listener into which the
+ * caller should pump data. The caller is responsible for opening the channel
+ * and pumping the channel's data into the returned stream listener.
+ *
+ * Note: If the channel already has a loadgroup, it will be replaced with the
+ * window context's load group, or null if the context doesn't have one.
+ *
+ * If the window context's nsIURIContentListener refuses the load immediately
+ * (e.g. in nsIURIContentListener::onStartURIOpen), this method will return
+ * NS_ERROR_WONT_HANDLE_CONTENT. At that point, the caller should probably
+ * cancel the channel if it's already open (this method will not cancel the
+ * channel).
+ *
+ * If flags include DONT_RETARGET, and the content listener refuses the load
+ * during onStartRequest (e.g. in canHandleContent/isPreferred), then the
+ * returned stream listener's onStartRequest method will return
+ * NS_ERROR_WONT_HANDLE_CONTENT.
+ *
+ * @param aChannel
+ * The channel that should be loaded. The channel may already be
+ * opened. It must not be closed (i.e. this must be called before the
+ * channel calls onStopRequest on its stream listener).
+ * @param aFlags
+ * Combination (bitwise OR) of the flags specified above. 0 indicates
+ * default handling.
+ * @param aWindowContext
+ * If you are running the url from a doc shell or a web shell, this is
+ * your window context. If you have a content listener you want to
+ * give first crack to, the uri loader needs to be able to get it
+ * from the window context. We will also be using the window context
+ * to get at the progress event sink interface.
+ * <b>Must not be null!</b>
+ */
+ nsIStreamListener openChannel(in nsIChannel aChannel,
+ in unsigned long aFlags,
+ in nsIInterfaceRequestor aWindowContext);
+
+ /**
+ * Stops an in progress load
+ */
+ void stop(in nsISupports aLoadCookie);
+};
+
diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl
new file mode 100644
index 0000000000..70079adfff
--- /dev/null
+++ b/uriloader/base/nsIWebProgress.idl
@@ -0,0 +1,164 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+interface nsIEventTarget;
+interface nsIWebProgressListener;
+
+/**
+ * The nsIWebProgress interface is used to add or remove nsIWebProgressListener
+ * instances to observe the loading of asynchronous requests (usually in the
+ * context of a DOM window).
+ *
+ * nsIWebProgress instances may be arranged in a parent-child configuration,
+ * corresponding to the parent-child configuration of their respective DOM
+ * windows. However, in some cases a nsIWebProgress instance may not have an
+ * associated DOM window. The parent-child relationship of nsIWebProgress
+ * instances is not made explicit by this interface, but the relationship may
+ * exist in some implementations.
+ *
+ * A nsIWebProgressListener instance receives notifications for the
+ * nsIWebProgress instance to which it added itself, and it may also receive
+ * notifications from any nsIWebProgress instances that are children of that
+ * nsIWebProgress instance.
+ */
+[scriptable, uuid(c4d64640-b332-4db6-a2a5-e08566000dc9)]
+interface nsIWebProgress : nsISupports
+{
+ /**
+ * The following flags may be combined to form the aNotifyMask parameter for
+ * the addProgressListener method. They limit the set of events that are
+ * delivered to an nsIWebProgressListener instance.
+ */
+
+ /**
+ * These flags indicate the state transistions to observe, corresponding to
+ * nsIWebProgressListener::onStateChange.
+ *
+ * NOTIFY_STATE_REQUEST
+ * Only receive the onStateChange event if the aStateFlags parameter
+ * includes nsIWebProgressListener::STATE_IS_REQUEST.
+ *
+ * NOTIFY_STATE_DOCUMENT
+ * Only receive the onStateChange event if the aStateFlags parameter
+ * includes nsIWebProgressListener::STATE_IS_DOCUMENT.
+ *
+ * NOTIFY_STATE_NETWORK
+ * Only receive the onStateChange event if the aStateFlags parameter
+ * includes nsIWebProgressListener::STATE_IS_NETWORK.
+ *
+ * NOTIFY_STATE_WINDOW
+ * Only receive the onStateChange event if the aStateFlags parameter
+ * includes nsIWebProgressListener::STATE_IS_WINDOW.
+ *
+ * NOTIFY_STATE_ALL
+ * Receive all onStateChange events.
+ */
+ const unsigned long NOTIFY_STATE_REQUEST = 0x00000001;
+ const unsigned long NOTIFY_STATE_DOCUMENT = 0x00000002;
+ const unsigned long NOTIFY_STATE_NETWORK = 0x00000004;
+ const unsigned long NOTIFY_STATE_WINDOW = 0x00000008;
+ const unsigned long NOTIFY_STATE_ALL = 0x0000000f;
+
+ /**
+ * These flags indicate the other events to observe, corresponding to the
+ * other four methods defined on nsIWebProgressListener.
+ *
+ * NOTIFY_PROGRESS
+ * Receive onProgressChange events.
+ *
+ * NOTIFY_STATUS
+ * Receive onStatusChange events.
+ *
+ * NOTIFY_SECURITY
+ * Receive onSecurityChange events.
+ *
+ * NOTIFY_LOCATION
+ * Receive onLocationChange events.
+ *
+ * NOTIFY_CONTENT_BLOCKING
+ * Receive onContentBlockingEvent events.
+ *
+ * NOTIFY_REFRESH
+ * Receive onRefreshAttempted events.
+ * This is defined on nsIWebProgressListener2.
+ */
+ const unsigned long NOTIFY_PROGRESS = 0x00000010;
+ const unsigned long NOTIFY_STATUS = 0x00000020;
+ const unsigned long NOTIFY_SECURITY = 0x00000040;
+ const unsigned long NOTIFY_LOCATION = 0x00000080;
+ const unsigned long NOTIFY_REFRESH = 0x00000100;
+ const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200;
+
+ /**
+ * This flag enables all notifications.
+ */
+ const unsigned long NOTIFY_ALL = 0x000003ff;
+
+ /**
+ * Registers a listener to receive web progress events.
+ *
+ * @param aListener
+ * The listener interface to be called when a progress event occurs.
+ * This object must also implement nsISupportsWeakReference.
+ * @param aNotifyMask
+ * The types of notifications to receive.
+ *
+ * @throw NS_ERROR_INVALID_ARG
+ * Indicates that aListener was either null or that it does not
+ * support weak references.
+ * @throw NS_ERROR_FAILURE
+ * Indicates that aListener was already registered.
+ */
+ void addProgressListener(in nsIWebProgressListener aListener,
+ in unsigned long aNotifyMask);
+
+ /**
+ * Removes a previously registered listener of progress events.
+ *
+ * @param aListener
+ * The listener interface previously registered with a call to
+ * addProgressListener.
+ *
+ * @throw NS_ERROR_FAILURE
+ * Indicates that aListener was not registered.
+ */
+ void removeProgressListener(in nsIWebProgressListener aListener);
+
+ /**
+ * The DOM window associated with this nsIWebProgress instance.
+ *
+ * @throw NS_ERROR_FAILURE
+ * Indicates that there is no associated DOM window.
+ */
+ readonly attribute mozIDOMWindowProxy DOMWindow;
+
+ /**
+ * Indicates whether DOMWindow.top == DOMWindow.
+ */
+ readonly attribute boolean isTopLevel;
+
+ /**
+ * Indicates whether or not a document is currently being loaded
+ * in the context of this nsIWebProgress instance.
+ */
+ readonly attribute boolean isLoadingDocument;
+
+ /**
+ * Contains a load type as specified by the load* constants in
+ * nsIDocShellLoadInfo.idl.
+ */
+ readonly attribute unsigned long loadType;
+
+ /**
+ * Main thread event target to which progress updates should be
+ * dispatched. This typically will be a SchedulerEventTarget
+ * corresponding to the tab requesting updates.
+ */
+ attribute nsIEventTarget target;
+};
diff --git a/uriloader/base/nsIWebProgressListener.idl b/uriloader/base/nsIWebProgressListener.idl
new file mode 100644
index 0000000000..4b46e1f5e7
--- /dev/null
+++ b/uriloader/base/nsIWebProgressListener.idl
@@ -0,0 +1,547 @@
+/* -*- Mode: IDL; 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 "nsISupports.idl"
+
+interface nsIWebProgress;
+interface nsIRequest;
+interface nsIURI;
+
+/**
+ * The nsIWebProgressListener interface is implemented by clients wishing to
+ * listen in on the progress associated with the loading of asynchronous
+ * requests in the context of a nsIWebProgress instance as well as any child
+ * nsIWebProgress instances. nsIWebProgress.idl describes the parent-child
+ * relationship of nsIWebProgress instances.
+ */
+[scriptable, uuid(a9df523b-efe2-421e-9d8e-3d7f807dda4c)]
+interface nsIWebProgressListener : nsISupports
+{
+ /**
+ * State Transition Flags
+ *
+ * These flags indicate the various states that requests may transition
+ * through as they are being loaded. These flags are mutually exclusive.
+ *
+ * For any given request, onStateChange is called once with the STATE_START
+ * flag, zero or more times with the STATE_TRANSFERRING flag or once with the
+ * STATE_REDIRECTING flag, and then finally once with the STATE_STOP flag.
+ * NOTE: For document requests, a second STATE_STOP is generated (see the
+ * description of STATE_IS_WINDOW for more details).
+ *
+ * STATE_START
+ * This flag indicates the start of a request. This flag is set when a
+ * request is initiated. The request is complete when onStateChange is
+ * called for the same request with the STATE_STOP flag set.
+ *
+ * STATE_REDIRECTING
+ * This flag indicates that a request is being redirected. The request
+ * passed to onStateChange is the request that is being redirected. When a
+ * redirect occurs, a new request is generated automatically to process the
+ * new request. Expect a corresponding STATE_START event for the new
+ * request, and a STATE_STOP for the redirected request.
+ *
+ * STATE_TRANSFERRING
+ * This flag indicates that data for a request is being transferred to an
+ * end consumer. This flag indicates that the request has been targeted,
+ * and that the user may start seeing content corresponding to the request.
+ *
+ * STATE_NEGOTIATING
+ * This flag is not used.
+ *
+ * STATE_STOP
+ * This flag indicates the completion of a request. The aStatus parameter
+ * to onStateChange indicates the final status of the request.
+ */
+ const unsigned long STATE_START = 0x00000001;
+ const unsigned long STATE_REDIRECTING = 0x00000002;
+ const unsigned long STATE_TRANSFERRING = 0x00000004;
+ const unsigned long STATE_NEGOTIATING = 0x00000008;
+ const unsigned long STATE_STOP = 0x00000010;
+
+
+ /**
+ * State Type Flags
+ *
+ * These flags further describe the entity for which the state transition is
+ * occuring. These flags are NOT mutually exclusive (i.e., an onStateChange
+ * event may indicate some combination of these flags).
+ *
+ * STATE_IS_REQUEST
+ * This flag indicates that the state transition is for a request, which
+ * includes but is not limited to document requests. (See below for a
+ * description of document requests.) Other types of requests, such as
+ * requests for inline content (e.g., images and stylesheets) are
+ * considered normal requests.
+ *
+ * STATE_IS_DOCUMENT
+ * This flag indicates that the state transition is for a document request.
+ * This flag is set in addition to STATE_IS_REQUEST. A document request
+ * supports the nsIChannel interface and its loadFlags attribute includes
+ * the nsIChannel::LOAD_DOCUMENT_URI flag.
+ *
+ * A document request does not complete until all requests associated with
+ * the loading of its corresponding document have completed. This includes
+ * other document requests (e.g., corresponding to HTML <iframe> elements).
+ * The document corresponding to a document request is available via the
+ * DOMWindow attribute of onStateChange's aWebProgress parameter.
+ *
+ * STATE_IS_NETWORK
+ * This flag indicates that the state transition corresponds to the start
+ * or stop of activity in the indicated nsIWebProgress instance. This flag
+ * is accompanied by either STATE_START or STATE_STOP, and it may be
+ * combined with other State Type Flags.
+ *
+ * Unlike STATE_IS_WINDOW, this flag is only set when activity within the
+ * nsIWebProgress instance being observed starts or stops. If activity
+ * only occurs in a child nsIWebProgress instance, then this flag will be
+ * set to indicate the start and stop of that activity.
+ *
+ * For example, in the case of navigation within a single frame of a HTML
+ * frameset, a nsIWebProgressListener instance attached to the
+ * nsIWebProgress of the frameset window will receive onStateChange calls
+ * with the STATE_IS_NETWORK flag set to indicate the start and stop of
+ * said navigation. In other words, an observer of an outer window can
+ * determine when activity, that may be constrained to a child window or
+ * set of child windows, starts and stops.
+ *
+ * STATE_IS_WINDOW
+ * This flag indicates that the state transition corresponds to the start
+ * or stop of activity in the indicated nsIWebProgress instance. This flag
+ * is accompanied by either STATE_START or STATE_STOP, and it may be
+ * combined with other State Type Flags.
+ *
+ * This flag is similar to STATE_IS_DOCUMENT. However, when a document
+ * request completes, two onStateChange calls with STATE_STOP are
+ * generated. The document request is passed as aRequest to both calls.
+ * The first has STATE_IS_REQUEST and STATE_IS_DOCUMENT set, and the second
+ * has the STATE_IS_WINDOW flag set (and possibly the STATE_IS_NETWORK flag
+ * set as well -- see above for a description of when the STATE_IS_NETWORK
+ * flag may be set). This second STATE_STOP event may be useful as a way
+ * to partition the work that occurs when a document request completes.
+ *
+ * STATE_IS_REDIRECTED_DOCUMENT
+ * Same as STATE_IS_DOCUMENT, but sent only after a redirect has occured.
+ * Introduced in order not to confuse existing code with extra state change
+ * events. See |nsDocLoader::OnStartRequest| for more info.
+ */
+ const unsigned long STATE_IS_REQUEST = 0x00010000;
+ const unsigned long STATE_IS_DOCUMENT = 0x00020000;
+ const unsigned long STATE_IS_NETWORK = 0x00040000;
+ const unsigned long STATE_IS_WINDOW = 0x00080000;
+ const unsigned long STATE_IS_REDIRECTED_DOCUMENT = 0x00100000;
+
+ /**
+ * State Modifier Flags
+ *
+ * These flags further describe the transition which is occuring. These
+ * flags are NOT mutually exclusive (i.e., an onStateChange event may
+ * indicate some combination of these flags).
+ *
+ * STATE_RESTORING
+ * This flag indicates that the state transition corresponds to the start
+ * or stop of activity for restoring a previously-rendered presentation.
+ * As such, there is no actual network activity associated with this
+ * request, and any modifications made to the document or presentation
+ * when it was originally loaded will still be present.
+ */
+ const unsigned long STATE_RESTORING = 0x01000000;
+
+ /**
+ * State Security Flags
+ *
+ * These flags describe the security state reported by a call to the
+ * onSecurityChange method. These flags are mutually exclusive.
+ *
+ * STATE_IS_INSECURE
+ * This flag indicates that the data corresponding to the request
+ * was received over an insecure channel.
+ *
+ * STATE_IS_BROKEN
+ * This flag indicates an unknown security state. This may mean that the
+ * request is being loaded as part of a page in which some content was
+ * received over an insecure channel.
+ *
+ * STATE_IS_SECURE
+ * This flag indicates that the data corresponding to the request was
+ * received over a secure channel. The degree of security is expressed by
+ * STATE_SECURE_HIGH, STATE_SECURE_MED, or STATE_SECURE_LOW.
+ */
+ const unsigned long STATE_IS_INSECURE = 0x00000004;
+ const unsigned long STATE_IS_BROKEN = 0x00000001;
+ const unsigned long STATE_IS_SECURE = 0x00000002;
+
+ /**
+ * Mixed active content flags
+ *
+ * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT
+ * nsSecureBrowserUIImpl::CheckForBlockedContent().
+ *
+ * May be set in addition to the State Security Flags, to indicate that
+ * mixed active content has been encountered.
+ *
+ * STATE_BLOCKED_MIXED_ACTIVE_CONTENT
+ * Mixed active content has been blocked from loading.
+ *
+ * STATE_LOADED_MIXED_ACTIVE_CONTENT
+ * Mixed active content has been loaded. State should be STATE_IS_BROKEN.
+ */
+ const unsigned long STATE_BLOCKED_MIXED_ACTIVE_CONTENT = 0x00000010;
+ const unsigned long STATE_LOADED_MIXED_ACTIVE_CONTENT = 0x00000020;
+
+ /**
+ * Mixed display content flags
+ *
+ * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT
+ * nsSecureBrowserUIImpl::CheckForBlockedContent().
+ *
+ * May be set in addition to the State Security Flags, to indicate that
+ * mixed display content has been encountered.
+ *
+ * STATE_BLOCKED_MIXED_DISPLAY_CONTENT
+ * Mixed display content has been blocked from loading.
+ *
+ * STATE_LOADED_MIXED_DISPLAY_CONTENT
+ * Mixed display content has been loaded. State should be STATE_IS_BROKEN.
+ */
+ const unsigned long STATE_BLOCKED_MIXED_DISPLAY_CONTENT = 0x00000100;
+ const unsigned long STATE_LOADED_MIXED_DISPLAY_CONTENT = 0x00000200;
+
+ /**
+ * Diagnostic flags
+ *
+ * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT
+ * nsSecureBrowserUIImpl::CheckForBlockedContent().
+ *
+ * May be set in addition to other security state flags to indicate that
+ * some state is countered that deserves a warning or error, but does not
+ * change the top level security state of the connection.
+ *
+ * STATE_CERT_DISTRUST_IMMINENT
+ * The certificate in use will be distrusted in the near future.
+ */
+ const unsigned long STATE_CERT_DISTRUST_IMMINENT = 0x00010000;
+
+ /**
+ * State bits for EV == Extended Validation == High Assurance
+ *
+ * These flags describe the level of identity verification
+ * in a call to the onSecurityChange method.
+ *
+ * STATE_IDENTITY_EV_TOPLEVEL
+ * The topmost document uses an EV cert.
+ * NOTE: Available since Gecko 1.9
+ */
+
+ const unsigned long STATE_IDENTITY_EV_TOPLEVEL = 0x00100000;
+
+ /**
+ * Broken state flags
+ *
+ * These flags describe the reason of the broken state.
+ *
+ * STATE_USES_SSL_3
+ * The topmost document uses SSL 3.0.
+ *
+ * STATE_USES_WEAK_CRYPTO
+ * The topmost document uses a weak cipher suite such as RC4.
+ *
+ * STATE_CERT_USER_OVERRIDDEN
+ * The user has added a security exception for the site.
+ */
+ const unsigned long STATE_USES_SSL_3 = 0x01000000;
+ const unsigned long STATE_USES_WEAK_CRYPTO = 0x02000000;
+ const unsigned long STATE_CERT_USER_OVERRIDDEN = 0x04000000;
+
+ /**
+ * Content Blocking Event flags
+ *
+ * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT
+ * nsSecureBrowserUIImpl::CheckForBlockedContent() AND UPDATE THE
+ * CORRESPONDING LIST IN ContentBlockingController.java
+ *
+ * These flags describe the reason of cookie jar rejection.
+ *
+ * STATE_BLOCKED_TRACKING_CONTENT
+ * Tracking content has been blocked from loading.
+ *
+ * STATE_LOADED_LEVEL_1_TRACKING_CONTENT
+ * Tracking content from the Disconnect Level 1 list has been loaded.
+ *
+ * STATE_LOADED_LEVEL_2_TRACKING_CONTENT
+ * Tracking content from the Disconnect Level 2 list has been loaded.
+ *
+ * STATE_BLOCKED_FINGERPRINTING_CONTENT
+ * Fingerprinting content has been blocked from loading.
+ *
+ * STATE_LOADED_FINGERPRINTING_CONTENT
+ * Fingerprinting content has been loaded.
+ *
+ * STATE_BLOCKED_CRYPTOMINING_CONTENT
+ * Cryptomining content has been blocked from loading.
+ *
+ * STATE_LOADED_CRYPTOMINING_CONTENT
+ * Cryptomining content has been loaded.
+ *
+ * STATE_BLOCKED_UNSAFE_CONTENT
+ * Content which againts SafeBrowsing list has been blocked from loading.
+ *
+ * STATE_COOKIES_LOADED
+ * Performed a storage access check, which usually means something like a
+ * cookie or a storage item was loaded/stored on the current tab.
+ * Alternatively this could indicate that something in the current tab
+ * attempted to communicate with its same-origin counterparts in other
+ * tabs.
+ *
+ * STATE_COOKIES_LOADED_TRACKER
+ * Similar to STATE_COOKIES_LOADED, but only sent if the subject of the
+ * action was a third-party tracker when the active cookie policy imposes
+ * restrictions on such content.
+ *
+ * STATE_COOKIES_LOADED_SOCIALTRACKER
+ * Similar to STATE_COOKIES_LOADED, but only sent if the subject of the
+ * action was a third-party social tracker when the active cookie policy
+ * imposes restrictions on such content.
+ *
+ * STATE_COOKIES_BLOCKED_BY_PERMISSION
+ * Rejected for custom site permission.
+ *
+ * STATE_COOKIES_BLOCKED_TRACKER
+ * Rejected because the resource is a tracker and cookie policy doesn't
+ * allow its loading.
+ *
+ * STATE_COOKIES_BLOCKED_SOCIALTRACKER
+ * Rejected because the resource is a tracker from a social origin and
+ * cookie policy doesn't allow its loading.
+ *
+ * STATE_COOKIES_PARTITIONED_FOREIGN
+ * Rejected because the resource is a third-party and cookie policy forces
+ * third-party resources to be partitioned.
+ *
+ * STATE_COOKIES_BLOCKED_ALL
+ * Rejected because cookie policy blocks all cookies.
+ *
+ * STATE_COOKIES_BLOCKED_FOREIGN
+ * Rejected because cookie policy blocks 3rd party cookies.
+ *
+ * STATE_BLOCKED_SOCIALTRACKING_CONTENT
+ * SocialTracking content has been blocked from loading.
+ *
+ * STATE_LOADED_SOCIALTRACKING_CONTENT
+ * SocialTracking content has been loaded.
+ *
+ * STATE_UNBLOCKED_TRACKING_CONTENT
+ * Tracking content should be blocked from loading was unblocked.
+ *
+ */
+ const unsigned long STATE_BLOCKED_TRACKING_CONTENT = 0x00001000;
+ const unsigned long STATE_LOADED_LEVEL_1_TRACKING_CONTENT = 0x00002000;
+ const unsigned long STATE_LOADED_LEVEL_2_TRACKING_CONTENT = 0x00100000;
+ const unsigned long STATE_BLOCKED_FINGERPRINTING_CONTENT = 0x00000040;
+ const unsigned long STATE_LOADED_FINGERPRINTING_CONTENT = 0x00000400;
+ const unsigned long STATE_BLOCKED_CRYPTOMINING_CONTENT = 0x00000800;
+ const unsigned long STATE_LOADED_CRYPTOMINING_CONTENT = 0x00200000;
+ const unsigned long STATE_BLOCKED_UNSAFE_CONTENT = 0x00004000;
+ const unsigned long STATE_COOKIES_LOADED = 0x00008000;
+ const unsigned long STATE_COOKIES_LOADED_TRACKER = 0x00040000;
+ const unsigned long STATE_COOKIES_LOADED_SOCIALTRACKER = 0x00080000;
+ const unsigned long STATE_COOKIES_BLOCKED_BY_PERMISSION = 0x10000000;
+ const unsigned long STATE_COOKIES_BLOCKED_TRACKER = 0x20000000;
+ const unsigned long STATE_COOKIES_BLOCKED_SOCIALTRACKER = 0x01000000;
+ const unsigned long STATE_COOKIES_BLOCKED_ALL = 0x40000000;
+ const unsigned long STATE_COOKIES_PARTITIONED_FOREIGN = 0x80000000;
+ const unsigned long STATE_COOKIES_BLOCKED_FOREIGN = 0x00000080;
+ const unsigned long STATE_BLOCKED_SOCIALTRACKING_CONTENT = 0x00010000;
+ const unsigned long STATE_LOADED_SOCIALTRACKING_CONTENT = 0x00020000;
+ const unsigned long STATE_UNBLOCKED_TRACKING_CONTENT = 0x00000010;
+
+ /**
+ * Flag for HTTPS-Only Mode upgrades
+ *
+ * STATE_HTTPS_ONLY_MODE_UPGRADED
+ * When a request has been upgraded by HTTPS-Only Mode
+ *
+ * STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED
+ * When an upgraded request failed.
+ */
+ const unsigned long STATE_HTTPS_ONLY_MODE_UPGRADED = 0x00400000;
+ const unsigned long STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED = 0x00800000;
+
+ /**
+ * Notification indicating the state has changed for one of the requests
+ * associated with aWebProgress.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification
+ * @param aRequest
+ * The nsIRequest that has changed state.
+ * @param aStateFlags
+ * Flags indicating the new state. This value is a combination of one
+ * of the State Transition Flags and one or more of the State Type
+ * Flags defined above. Any undefined bits are reserved for future
+ * use.
+ * @param aStatus
+ * Error status code associated with the state change. This parameter
+ * should be ignored unless aStateFlags includes the STATE_STOP bit.
+ * The status code indicates success or failure of the request
+ * associated with the state change. NOTE: aStatus may be a success
+ * code even for server generated errors, such as the HTTP 404 error.
+ * In such cases, the request itself should be queried for extended
+ * error information (e.g., for HTTP requests see nsIHttpChannel).
+ */
+ void onStateChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in unsigned long aStateFlags,
+ in nsresult aStatus);
+
+ /**
+ * Notification that the progress has changed for one of the requests
+ * associated with aWebProgress. Progress totals are reset to zero when all
+ * requests in aWebProgress complete (corresponding to onStateChange being
+ * called with aStateFlags including the STATE_STOP and STATE_IS_WINDOW
+ * flags).
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The nsIRequest that has new progress.
+ * @param aCurSelfProgress
+ * The current progress for aRequest.
+ * @param aMaxSelfProgress
+ * The maximum progress for aRequest.
+ * @param aCurTotalProgress
+ * The current progress for all requests associated with aWebProgress.
+ * @param aMaxTotalProgress
+ * The total progress for all requests associated with aWebProgress.
+ *
+ * NOTE: If any progress value is unknown, or if its value would exceed the
+ * maximum value of type long, then its value is replaced with -1.
+ *
+ * NOTE: If the object also implements nsIWebProgressListener2 and the caller
+ * knows about that interface, this function will not be called. Instead,
+ * nsIWebProgressListener2::onProgressChange64 will be called.
+ */
+ void onProgressChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in long aCurSelfProgress,
+ in long aMaxSelfProgress,
+ in long aCurTotalProgress,
+ in long aMaxTotalProgress);
+
+ /**
+ * Flags for onLocationChange
+ *
+ * LOCATION_CHANGE_SAME_DOCUMENT
+ * This flag is on when |aWebProgress| did not load a new document.
+ * For example, the location change is due to an anchor scroll or a
+ * pushState/popState/replaceState.
+ *
+ * LOCATION_CHANGE_ERROR_PAGE
+ * This flag is on when |aWebProgress| redirected from the requested
+ * contents to an internal page to show error status, such as
+ * <about:neterror>, <about:certerror> and so on.
+ *
+ * Generally speaking, |aURI| and |aRequest| are the original data. DOM
+ * |window.location.href| is also the original location, while
+ * |document.documentURI| is the redirected location. Sometimes |aURI| is
+ * <about:blank> and |aRequest| is null when the original data does not
+ + remain.
+ *
+ * |aWebProgress| does NOT set this flag when it did not try to load a new
+ * document. In this case, it should set LOCATION_CHANGE_SAME_DOCUMENT.
+ *
+ * LOCATION_CHANGE_RELOAD
+ * This flag is on when reloading the current page, either from
+ * location.reload() or the browser UI.
+ */
+ const unsigned long LOCATION_CHANGE_SAME_DOCUMENT = 0x00000001;
+ const unsigned long LOCATION_CHANGE_ERROR_PAGE = 0x00000002;
+ const unsigned long LOCATION_CHANGE_RELOAD = 0x00000004;
+
+ /**
+ * Called when the location of the window being watched changes. This is not
+ * when a load is requested, but rather once it is verified that the load is
+ * going to occur in the given window. For instance, a load that starts in a
+ * window might send progress and status messages for the new site, but it
+ * will not send the onLocationChange until we are sure that we are loading
+ * this new page here.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The associated nsIRequest. This may be null in some cases.
+ * @param aLocation
+ * The URI of the location that is being loaded.
+ * @param aFlags
+ * This is a value which explains the situation or the reason why
+ * the location has changed.
+ */
+ void onLocationChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in nsIURI aLocation,
+ [optional] in unsigned long aFlags);
+
+ /**
+ * Notification that the status of a request has changed. The status message
+ * is intended to be displayed to the user (e.g., in the status bar of the
+ * browser).
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The nsIRequest that has new status.
+ * @param aStatus
+ * This value is not an error code. Instead, it is a numeric value
+ * that indicates the current status of the request. This interface
+ * does not define the set of possible status codes. NOTE: Some
+ * status values are defined by nsITransport and nsISocketTransport.
+ * @param aMessage
+ * Localized text corresponding to aStatus.
+ */
+ void onStatusChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in nsresult aStatus,
+ in wstring aMessage);
+
+ /**
+ * Notification called for security progress. This method will be called on
+ * security transitions (eg HTTP -> HTTPS, HTTPS -> HTTP, FOO -> HTTPS) and
+ * after document load completion. It might also be called if an error
+ * occurs during network loading.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The nsIRequest that has new security state.
+ * @param aState
+ * A value composed of the Security State Flags and the Security
+ * Strength Flags listed above. Any undefined bits are reserved for
+ * future use.
+ *
+ * NOTE: These notifications will only occur if a security package is
+ * installed.
+ */
+ void onSecurityChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in unsigned long aState);
+
+ /**
+ * Notification called for content blocking events. This method will be
+ * called when content gets allowed/blocked for various reasons per the
+ * Content Blocking rules.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The nsIRequest that has new security state.
+ * @param aEvent
+ * A value composed of the Content Blocking Event Flags listed above.
+ * Any undefined bits are reserved for future use.
+ */
+ void onContentBlockingEvent(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in unsigned long aEvent);
+};
diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl
new file mode 100644
index 0000000000..87701f8d2c
--- /dev/null
+++ b/uriloader/base/nsIWebProgressListener2.idl
@@ -0,0 +1,69 @@
+/* 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 "nsIWebProgressListener.idl"
+
+/**
+ * An extended version of nsIWebProgressListener.
+ */
+[scriptable, uuid(dde39de0-e4e0-11da-8ad9-0800200c9a66)]
+interface nsIWebProgressListener2 : nsIWebProgressListener {
+ /**
+ * Notification that the progress has changed for one of the requests
+ * associated with aWebProgress. Progress totals are reset to zero when all
+ * requests in aWebProgress complete (corresponding to onStateChange being
+ * called with aStateFlags including the STATE_STOP and STATE_IS_WINDOW
+ * flags).
+ *
+ * This function is identical to nsIWebProgressListener::onProgressChange,
+ * except that this function supports 64-bit values.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The nsIRequest that has new progress.
+ * @param aCurSelfProgress
+ * The current progress for aRequest.
+ * @param aMaxSelfProgress
+ * The maximum progress for aRequest.
+ * @param aCurTotalProgress
+ * The current progress for all requests associated with aWebProgress.
+ * @param aMaxTotalProgress
+ * The total progress for all requests associated with aWebProgress.
+ *
+ * NOTE: If any progress value is unknown, then its value is replaced with -1.
+ *
+ * @see nsIWebProgressListener2::onProgressChange64
+ */
+ void onProgressChange64(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in long long aCurSelfProgress,
+ in long long aMaxSelfProgress,
+ in long long aCurTotalProgress,
+ in long long aMaxTotalProgress);
+
+ /**
+ * Notification that a refresh or redirect has been requested in aWebProgress
+ * For example, via a <meta http-equiv="refresh"> or an HTTP Refresh: header
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRefreshURI
+ * The new URI that aWebProgress has requested redirecting to.
+ * @param aMillis
+ * The delay (in milliseconds) before refresh.
+ * @param aSameURI
+ * True if aWebProgress is requesting a refresh of the
+ * current URI.
+ * False if aWebProgress is requesting a redirection to
+ * a different URI.
+ *
+ * @return True if the refresh may proceed.
+ * False if the refresh should be aborted.
+ */
+ boolean onRefreshAttempted(in nsIWebProgress aWebProgress,
+ in nsIURI aRefreshURI,
+ in long aMillis,
+ in boolean aSameURI);
+};
diff --git a/uriloader/base/nsURILoader.cpp b/uriloader/base/nsURILoader.cpp
new file mode 100644
index 0000000000..21aa14aa8f
--- /dev/null
+++ b/uriloader/base/nsURILoader.cpp
@@ -0,0 +1,795 @@
+/* -*- 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 "nsURILoader.h"
+#include "nsIURIContentListener.h"
+#include "nsIContentHandler.h"
+#include "nsILoadGroup.h"
+#include "nsIDocumentLoader.h"
+#include "nsIStreamListener.h"
+#include "nsIURI.h"
+#include "nsIChannel.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIInputStream.h"
+#include "nsIStreamConverterService.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIHttpChannel.h"
+#include "netCore.h"
+#include "nsCRT.h"
+#include "nsIDocShell.h"
+#include "nsIThreadRetargetableStreamListener.h"
+#include "nsIChildChannel.h"
+#include "nsExternalHelperAppService.h"
+
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsReadableUtils.h"
+#include "nsError.h"
+
+#include "nsICategoryManager.h"
+#include "nsCExternalHandlerService.h"
+
+#include "nsNetCID.h"
+
+#include "nsMimeTypes.h"
+
+#include "nsDocLoader.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Unused.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_general.h"
+#include "nsContentUtils.h"
+
+mozilla::LazyLogModule nsURILoader::mLog("URILoader");
+
+#define LOG(args) MOZ_LOG(nsURILoader::mLog, mozilla::LogLevel::Debug, args)
+#define LOG_ERROR(args) \
+ MOZ_LOG(nsURILoader::mLog, mozilla::LogLevel::Error, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(nsURILoader::mLog, mozilla::LogLevel::Debug)
+
+NS_IMPL_ADDREF(nsDocumentOpenInfo)
+NS_IMPL_RELEASE(nsDocumentOpenInfo)
+
+NS_INTERFACE_MAP_BEGIN(nsDocumentOpenInfo)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRequestObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIRequestObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
+ NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener)
+NS_INTERFACE_MAP_END
+
+nsDocumentOpenInfo::nsDocumentOpenInfo(nsIInterfaceRequestor* aWindowContext,
+ uint32_t aFlags, nsURILoader* aURILoader)
+ : m_originalContext(aWindowContext),
+ mFlags(aFlags),
+ mURILoader(aURILoader),
+ mDataConversionDepthLimit(
+ StaticPrefs::general_document_open_conversion_depth_limit()) {}
+
+nsDocumentOpenInfo::nsDocumentOpenInfo(uint32_t aFlags,
+ bool aAllowListenerConversions)
+ : m_originalContext(nullptr),
+ mFlags(aFlags),
+ mURILoader(nullptr),
+ mDataConversionDepthLimit(
+ StaticPrefs::general_document_open_conversion_depth_limit()),
+ mAllowListenerConversions(aAllowListenerConversions) {}
+
+nsDocumentOpenInfo::~nsDocumentOpenInfo() {}
+
+nsresult nsDocumentOpenInfo::Prepare() {
+ LOG(("[0x%p] nsDocumentOpenInfo::Prepare", this));
+
+ nsresult rv;
+
+ // ask our window context if it has a uri content listener...
+ m_contentListener = do_GetInterface(m_originalContext, &rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsDocumentOpenInfo::OnStartRequest(nsIRequest* request) {
+ LOG(("[0x%p] nsDocumentOpenInfo::OnStartRequest", this));
+ MOZ_ASSERT(request);
+ if (!request) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult rv = NS_OK;
+
+ //
+ // Deal with "special" HTTP responses:
+ //
+ // - In the case of a 204 (No Content) or 205 (Reset Content) response, do
+ // not try to find a content handler. Return NS_BINDING_ABORTED to cancel
+ // the request. This has the effect of ensuring that the DocLoader does
+ // not try to interpret this as a real request.
+ //
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request, &rv));
+
+ if (NS_SUCCEEDED(rv)) {
+ uint32_t responseCode = 0;
+
+ rv = httpChannel->GetResponseStatus(&responseCode);
+
+ if (NS_FAILED(rv)) {
+ LOG_ERROR((" Failed to get HTTP response status"));
+
+ // behave as in the canceled case
+ return NS_OK;
+ }
+
+ LOG((" HTTP response status: %d", responseCode));
+
+ if (204 == responseCode || 205 == responseCode) {
+ return NS_BINDING_ABORTED;
+ }
+ }
+
+ //
+ // Make sure that the transaction has succeeded, so far...
+ //
+ nsresult status;
+
+ rv = request->GetStatus(&status);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Unable to get request status!");
+ if (NS_FAILED(rv)) return rv;
+
+ if (NS_FAILED(status)) {
+ LOG_ERROR((" Request failed, status: 0x%08" PRIX32,
+ static_cast<uint32_t>(status)));
+
+ //
+ // The transaction has already reported an error - so it will be torn
+ // down. Therefore, it is not necessary to return an error code...
+ //
+ return NS_OK;
+ }
+
+ rv = DispatchContent(request, nullptr);
+
+ LOG((" After dispatch, m_targetStreamListener: 0x%p, rv: 0x%08" PRIX32,
+ m_targetStreamListener.get(), static_cast<uint32_t>(rv)));
+
+ NS_ASSERTION(
+ NS_SUCCEEDED(rv) || !m_targetStreamListener,
+ "Must not have an m_targetStreamListener with a failure return!");
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (m_targetStreamListener)
+ rv = m_targetStreamListener->OnStartRequest(request);
+
+ LOG((" OnStartRequest returning: 0x%08" PRIX32, static_cast<uint32_t>(rv)));
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDocumentOpenInfo::CheckListenerChain() {
+ NS_ASSERTION(NS_IsMainThread(), "Should be on the main thread!");
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
+ do_QueryInterface(m_targetStreamListener, &rv);
+ if (retargetableListener) {
+ rv = retargetableListener->CheckListenerChain();
+ }
+ LOG(
+ ("[0x%p] nsDocumentOpenInfo::CheckListenerChain %s listener %p rv "
+ "%" PRIx32,
+ this, (NS_SUCCEEDED(rv) ? "success" : "failure"),
+ (nsIStreamListener*)m_targetStreamListener, static_cast<uint32_t>(rv)));
+ return rv;
+}
+
+NS_IMETHODIMP
+nsDocumentOpenInfo::OnDataAvailable(nsIRequest* request, nsIInputStream* inStr,
+ uint64_t sourceOffset, uint32_t count) {
+ // if we have retarged to the end stream listener, then forward the call....
+ // otherwise, don't do anything
+
+ nsresult rv = NS_OK;
+
+ if (m_targetStreamListener)
+ rv = m_targetStreamListener->OnDataAvailable(request, inStr, sourceOffset,
+ count);
+ return rv;
+}
+
+NS_IMETHODIMP nsDocumentOpenInfo::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ LOG(("[0x%p] nsDocumentOpenInfo::OnStopRequest", this));
+
+ if (m_targetStreamListener) {
+ nsCOMPtr<nsIStreamListener> listener(m_targetStreamListener);
+
+ // If this is a multipart stream, we could get another
+ // OnStartRequest after this... reset state.
+ m_targetStreamListener = nullptr;
+ mContentType.Truncate();
+ listener->OnStopRequest(request, aStatus);
+ }
+ mUsedContentHandler = false;
+
+ // Remember...
+ // In the case of multiplexed streams (such as multipart/x-mixed-replace)
+ // these stream listener methods could be called again :-)
+ //
+ return NS_OK;
+}
+
+nsresult nsDocumentOpenInfo::DispatchContent(nsIRequest* request,
+ nsISupports* aCtxt) {
+ LOG(("[0x%p] nsDocumentOpenInfo::DispatchContent for type '%s'", this,
+ mContentType.get()));
+
+ MOZ_ASSERT(!m_targetStreamListener,
+ "Why do we already have a target stream listener?");
+
+ nsresult rv;
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+ if (!aChannel) {
+ LOG_ERROR((" Request is not a channel. Bailing."));
+ return NS_ERROR_FAILURE;
+ }
+
+ constexpr auto anyType = "*/*"_ns;
+ if (mContentType.IsEmpty() || mContentType == anyType) {
+ rv = aChannel->GetContentType(mContentType);
+ if (NS_FAILED(rv)) return rv;
+ LOG((" Got type from channel: '%s'", mContentType.get()));
+ }
+
+ bool isGuessFromExt =
+ mContentType.LowerCaseEqualsASCII(APPLICATION_GUESS_FROM_EXT);
+ if (isGuessFromExt) {
+ // Reset to application/octet-stream for now; no one other than the
+ // external helper app service should see APPLICATION_GUESS_FROM_EXT.
+ mContentType = APPLICATION_OCTET_STREAM;
+ aChannel->SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM));
+ }
+
+ // Check whether the data should be forced to be handled externally. This
+ // could happen because the Content-Disposition header is set so, or, in the
+ // future, because the user has specified external handling for the MIME
+ // type.
+ bool forceExternalHandling = false;
+ uint32_t disposition;
+ rv = aChannel->GetContentDisposition(&disposition);
+
+ if (NS_SUCCEEDED(rv) && disposition == nsIChannel::DISPOSITION_ATTACHMENT) {
+ forceExternalHandling = true;
+ }
+
+ LOG((" forceExternalHandling: %s", forceExternalHandling ? "yes" : "no"));
+
+ if (!forceExternalHandling) {
+ //
+ // First step: See whether m_contentListener wants to handle this
+ // content type.
+ //
+ if (TryDefaultContentListener(aChannel)) {
+ LOG((" Success! Our default listener likes this type"));
+ // All done here
+ return NS_OK;
+ }
+
+ // If we aren't allowed to try other listeners, just skip through to
+ // trying to convert the data.
+ if (!(mFlags & nsIURILoader::DONT_RETARGET)) {
+ //
+ // Second step: See whether some other registered listener wants
+ // to handle this content type.
+ //
+ int32_t count = mURILoader ? mURILoader->m_listeners.Count() : 0;
+ nsCOMPtr<nsIURIContentListener> listener;
+ for (int32_t i = 0; i < count; i++) {
+ listener = do_QueryReferent(mURILoader->m_listeners[i]);
+ if (listener) {
+ if (TryContentListener(listener, aChannel)) {
+ LOG((" Found listener registered on the URILoader"));
+ return NS_OK;
+ }
+ } else {
+ // remove from the listener list, reset i and update count
+ mURILoader->m_listeners.RemoveObjectAt(i--);
+ --count;
+ }
+ }
+
+ //
+ // Third step: Try to find a content listener that has not yet had
+ // the chance to register, as it is contained in a not-yet-loaded
+ // module, but which has registered a contract ID.
+ //
+ nsCOMPtr<nsICategoryManager> catman =
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID);
+ if (catman) {
+ nsCString contractidString;
+ rv = catman->GetCategoryEntry(NS_CONTENT_LISTENER_CATEGORYMANAGER_ENTRY,
+ mContentType, contractidString);
+ if (NS_SUCCEEDED(rv) && !contractidString.IsEmpty()) {
+ LOG((" Listener contractid for '%s' is '%s'", mContentType.get(),
+ contractidString.get()));
+
+ listener = do_CreateInstance(contractidString.get());
+ LOG((" Listener from category manager: 0x%p", listener.get()));
+
+ if (listener && TryContentListener(listener, aChannel)) {
+ LOG((" Listener from category manager likes this type"));
+ return NS_OK;
+ }
+ }
+ }
+
+ //
+ // Fourth step: try to find an nsIContentHandler for our type.
+ //
+ nsAutoCString handlerContractID(NS_CONTENT_HANDLER_CONTRACTID_PREFIX);
+ handlerContractID += mContentType;
+
+ nsCOMPtr<nsIContentHandler> contentHandler =
+ do_CreateInstance(handlerContractID.get());
+ if (contentHandler) {
+ LOG((" Content handler found"));
+ // Note that m_originalContext can be nullptr when running this in
+ // the parent process on behalf on a docshell in the content process,
+ // and in that case we only support content handlers that don't need
+ // the context.
+ rv = contentHandler->HandleContent(mContentType.get(),
+ m_originalContext, request);
+ // XXXbz returning an error code to represent handling the
+ // content is just bizarre!
+ if (rv != NS_ERROR_WONT_HANDLE_CONTENT) {
+ if (NS_FAILED(rv)) {
+ // The content handler has unexpectedly failed. Cancel the request
+ // just in case the handler didn't...
+ LOG((" Content handler failed. Aborting load"));
+ request->Cancel(rv);
+ } else {
+ LOG((" Content handler taking over load"));
+ mUsedContentHandler = true;
+ }
+
+ return rv;
+ }
+ }
+ } else {
+ LOG(
+ (" DONT_RETARGET flag set, so skipped over random other content "
+ "listeners and content handlers"));
+ }
+
+ //
+ // Fifth step: If no listener prefers this type, see if any stream
+ // converters exist to transform this content type into
+ // some other.
+ //
+ // Don't do this if the server sent us a MIME type of "*/*" because they saw
+ // it in our Accept header and got confused.
+ // XXXbz have to be careful here; may end up in some sort of bizarre
+ // infinite decoding loop.
+ if (mContentType != anyType) {
+ rv = TryStreamConversion(aChannel);
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+ }
+ }
+
+ NS_ASSERTION(!m_targetStreamListener,
+ "If we found a listener, why are we not using it?");
+
+ if (mFlags & nsIURILoader::DONT_RETARGET) {
+ LOG(
+ (" External handling forced or (listener not interested and no "
+ "stream converter exists), and retargeting disallowed -> aborting"));
+ return NS_ERROR_WONT_HANDLE_CONTENT;
+ }
+
+ // Before dispatching to the external helper app service, check for an HTTP
+ // error page. If we got one, we don't want to handle it with a helper app,
+ // really.
+ // The WPT a-download-click-404.html requires us to silently handle this
+ // without displaying an error page, so we just return early here.
+ // See bug 1604308 for discussion around what the ideal behaviour is.
+ nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request));
+ if (httpChannel) {
+ bool requestSucceeded;
+ rv = httpChannel->GetRequestSucceeded(&requestSucceeded);
+ if (NS_FAILED(rv) || !requestSucceeded) {
+ return NS_OK;
+ }
+ }
+
+ // Sixth step:
+ //
+ // All attempts to dispatch this content have failed. Just pass it off to
+ // the helper app service.
+ //
+
+ nsCOMPtr<nsIExternalHelperAppService> helperAppService =
+ do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &rv);
+ if (helperAppService) {
+ LOG((" Passing load off to helper app service"));
+
+ // Set these flags to indicate that the channel has been targeted and that
+ // we are not using the original consumer.
+ nsLoadFlags loadFlags = 0;
+ request->GetLoadFlags(&loadFlags);
+ request->SetLoadFlags(loadFlags | nsIChannel::LOAD_RETARGETED_DOCUMENT_URI |
+ nsIChannel::LOAD_TARGETED);
+
+ if (isGuessFromExt) {
+ mContentType = APPLICATION_GUESS_FROM_EXT;
+ aChannel->SetContentType(nsLiteralCString(APPLICATION_GUESS_FROM_EXT));
+ }
+
+ rv = TryExternalHelperApp(helperAppService, aChannel);
+ if (NS_FAILED(rv)) {
+ request->SetLoadFlags(loadFlags);
+ m_targetStreamListener = nullptr;
+ }
+ }
+
+ NS_ASSERTION(m_targetStreamListener || NS_FAILED(rv),
+ "There is no way we should be successful at this point without "
+ "a m_targetStreamListener");
+ return rv;
+}
+
+nsresult nsDocumentOpenInfo::TryExternalHelperApp(
+ nsIExternalHelperAppService* aHelperAppService, nsIChannel* aChannel) {
+ return aHelperAppService->DoContent(mContentType, aChannel, m_originalContext,
+ false, nullptr,
+ getter_AddRefs(m_targetStreamListener));
+}
+
+nsresult nsDocumentOpenInfo::ConvertData(nsIRequest* request,
+ nsIURIContentListener* aListener,
+ const nsACString& aSrcContentType,
+ const nsACString& aOutContentType) {
+ LOG(("[0x%p] nsDocumentOpenInfo::ConvertData from '%s' to '%s'", this,
+ PromiseFlatCString(aSrcContentType).get(),
+ PromiseFlatCString(aOutContentType).get()));
+
+ if (mDataConversionDepthLimit == 0) {
+ LOG(
+ ("[0x%p] nsDocumentOpenInfo::ConvertData - reached the recursion "
+ "limit!",
+ this));
+ // This will fall back to external helper app handling.
+ return NS_ERROR_ABORT;
+ }
+
+ MOZ_ASSERT(aSrcContentType != aOutContentType,
+ "ConvertData called when the two types are the same!");
+
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIStreamConverterService> StreamConvService =
+ do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ LOG((" Got converter service"));
+
+ // When applying stream decoders, it is necessary to "insert" an
+ // intermediate nsDocumentOpenInfo instance to handle the targeting of
+ // the "final" stream or streams.
+ //
+ // For certain content types (ie. multi-part/x-mixed-replace) the input
+ // stream is split up into multiple destination streams. This
+ // intermediate instance is used to target these "decoded" streams...
+ //
+ RefPtr<nsDocumentOpenInfo> nextLink = Clone();
+
+ LOG((" Downstream DocumentOpenInfo would be: 0x%p", nextLink.get()));
+
+ // Decrease the conversion recursion limit by one to prevent infinite loops.
+ nextLink->mDataConversionDepthLimit = mDataConversionDepthLimit - 1;
+
+ // Make sure nextLink starts with the contentListener that said it wanted
+ // the results of this decode.
+ nextLink->m_contentListener = aListener;
+ // Also make sure it has to look for a stream listener to pump data into.
+ nextLink->m_targetStreamListener = nullptr;
+
+ // Make sure that nextLink treats the data as aOutContentType when
+ // dispatching; that way even if the stream converters don't change the type
+ // on the channel we will still do the right thing. If aOutContentType is
+ // */*, that's OK -- that will just indicate to nextLink that it should get
+ // the type off the channel.
+ nextLink->mContentType = aOutContentType;
+
+ // The following call sets m_targetStreamListener to the input end of the
+ // stream converter and sets the output end of the stream converter to
+ // nextLink. As we pump data into m_targetStreamListener the stream
+ // converter will convert it and pass the converted data to nextLink.
+ return StreamConvService->AsyncConvertData(
+ PromiseFlatCString(aSrcContentType).get(),
+ PromiseFlatCString(aOutContentType).get(), nextLink, request,
+ getter_AddRefs(m_targetStreamListener));
+}
+
+nsresult nsDocumentOpenInfo::TryStreamConversion(nsIChannel* aChannel) {
+ constexpr auto anyType = "*/*"_ns;
+ nsresult rv = ConvertData(aChannel, m_contentListener, mContentType, anyType);
+ if (NS_FAILED(rv)) {
+ m_targetStreamListener = nullptr;
+ } else if (m_targetStreamListener) {
+ // We found a converter for this MIME type. We'll just pump data into
+ // it and let the downstream nsDocumentOpenInfo handle things.
+ LOG((" Converter taking over now"));
+ }
+ return rv;
+}
+
+bool nsDocumentOpenInfo::TryContentListener(nsIURIContentListener* aListener,
+ nsIChannel* aChannel) {
+ LOG(("[0x%p] nsDocumentOpenInfo::TryContentListener; mFlags = 0x%x", this,
+ mFlags));
+
+ MOZ_ASSERT(aListener, "Must have a non-null listener");
+ MOZ_ASSERT(aChannel, "Must have a channel");
+
+ bool listenerWantsContent = false;
+ nsCString typeToUse;
+
+ if (mFlags & nsIURILoader::IS_CONTENT_PREFERRED) {
+ aListener->IsPreferred(mContentType.get(), getter_Copies(typeToUse),
+ &listenerWantsContent);
+ } else {
+ aListener->CanHandleContent(mContentType.get(), false,
+ getter_Copies(typeToUse),
+ &listenerWantsContent);
+ }
+ if (!listenerWantsContent) {
+ LOG((" Listener is not interested"));
+ return false;
+ }
+
+ if (!typeToUse.IsEmpty() && typeToUse != mContentType) {
+ // Need to do a conversion here.
+
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+ if (mAllowListenerConversions) {
+ rv = ConvertData(aChannel, aListener, mContentType, typeToUse);
+ }
+
+ if (NS_FAILED(rv)) {
+ // No conversion path -- we don't want this listener, if we got one
+ m_targetStreamListener = nullptr;
+ }
+
+ LOG((" Found conversion: %s", m_targetStreamListener ? "yes" : "no"));
+
+ // m_targetStreamListener is now the input end of the converter, and we can
+ // just pump the data in there, if it exists. If it does not, we need to
+ // try other nsIURIContentListeners.
+ return m_targetStreamListener != nullptr;
+ }
+
+ // At this point, aListener wants data of type mContentType. Let 'em have
+ // it. But first, if we are retargeting, set an appropriate flag on the
+ // channel
+ nsLoadFlags loadFlags = 0;
+ aChannel->GetLoadFlags(&loadFlags);
+
+ // Set this flag to indicate that the channel has been targeted at a final
+ // consumer. This load flag is tested in nsDocLoader::OnProgress.
+ nsLoadFlags newLoadFlags = nsIChannel::LOAD_TARGETED;
+
+ nsCOMPtr<nsIURIContentListener> originalListener =
+ do_GetInterface(m_originalContext);
+ if (originalListener != aListener) {
+ newLoadFlags |= nsIChannel::LOAD_RETARGETED_DOCUMENT_URI;
+ }
+ aChannel->SetLoadFlags(loadFlags | newLoadFlags);
+
+ bool abort = false;
+ bool isPreferred = (mFlags & nsIURILoader::IS_CONTENT_PREFERRED) != 0;
+ nsresult rv =
+ aListener->DoContent(mContentType, isPreferred, aChannel,
+ getter_AddRefs(m_targetStreamListener), &abort);
+
+ if (NS_FAILED(rv)) {
+ LOG_ERROR((" DoContent failed"));
+
+ // Unset the RETARGETED_DOCUMENT_URI flag if we set it...
+ aChannel->SetLoadFlags(loadFlags);
+ m_targetStreamListener = nullptr;
+ return false;
+ }
+
+ if (abort) {
+ // Nothing else to do here -- aListener is handling it all. Make
+ // sure m_targetStreamListener is null so we don't do anything
+ // after this point.
+ LOG((" Listener has aborted the load"));
+ m_targetStreamListener = nullptr;
+ }
+
+ NS_ASSERTION(abort || m_targetStreamListener,
+ "DoContent returned no listener?");
+
+ // aListener is handling the load from this point on.
+ return true;
+}
+
+bool nsDocumentOpenInfo::TryDefaultContentListener(nsIChannel* aChannel) {
+ if (m_contentListener) {
+ return TryContentListener(m_contentListener, aChannel);
+ }
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////
+// Implementation of nsURILoader
+///////////////////////////////////////////////////////////////////////////////////////////////
+
+nsURILoader::nsURILoader() {}
+
+nsURILoader::~nsURILoader() {}
+
+NS_IMPL_ADDREF(nsURILoader)
+NS_IMPL_RELEASE(nsURILoader)
+
+NS_INTERFACE_MAP_BEGIN(nsURILoader)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURILoader)
+ NS_INTERFACE_MAP_ENTRY(nsIURILoader)
+NS_INTERFACE_MAP_END
+
+NS_IMETHODIMP nsURILoader::RegisterContentListener(
+ nsIURIContentListener* aContentListener) {
+ nsresult rv = NS_OK;
+
+ nsWeakPtr weakListener = do_GetWeakReference(aContentListener);
+ NS_ASSERTION(weakListener,
+ "your URIContentListener must support weak refs!\n");
+
+ if (weakListener) m_listeners.AppendObject(weakListener);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsURILoader::UnRegisterContentListener(
+ nsIURIContentListener* aContentListener) {
+ nsWeakPtr weakListener = do_GetWeakReference(aContentListener);
+ if (weakListener) m_listeners.RemoveObject(weakListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsURILoader::OpenURI(nsIChannel* channel, uint32_t aFlags,
+ nsIInterfaceRequestor* aWindowContext) {
+ NS_ENSURE_ARG_POINTER(channel);
+
+ if (LOG_ENABLED()) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ nsAutoCString spec;
+ uri->GetAsciiSpec(spec);
+ LOG(("nsURILoader::OpenURI for %s", spec.get()));
+ }
+
+ nsCOMPtr<nsIStreamListener> loader;
+ nsresult rv = OpenChannel(channel, aFlags, aWindowContext, false,
+ getter_AddRefs(loader));
+ if (NS_FAILED(rv)) {
+ if (rv == NS_ERROR_WONT_HANDLE_CONTENT) {
+ // Not really an error, from this method's point of view
+ return NS_OK;
+ }
+ }
+
+ // This method is not complete. Eventually, we should first go
+ // to the content listener and ask them for a protocol handler...
+ // if they don't give us one, we need to go to the registry and get
+ // the preferred protocol handler.
+
+ // But for now, I'm going to let necko do the work for us....
+ rv = channel->AsyncOpen(loader);
+
+ // no content from this load - that's OK.
+ if (rv == NS_ERROR_NO_CONTENT) {
+ LOG((" rv is NS_ERROR_NO_CONTENT -- doing nothing"));
+ return NS_OK;
+ }
+ return rv;
+}
+
+nsresult nsURILoader::OpenChannel(nsIChannel* channel, uint32_t aFlags,
+ nsIInterfaceRequestor* aWindowContext,
+ bool aChannelIsOpen,
+ nsIStreamListener** aListener) {
+ NS_ASSERTION(channel, "Trying to open a null channel!");
+ NS_ASSERTION(aWindowContext, "Window context must not be null");
+
+ if (LOG_ENABLED()) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ nsAutoCString spec;
+ uri->GetAsciiSpec(spec);
+ LOG(("nsURILoader::OpenChannel for %s", spec.get()));
+ }
+
+ // we need to create a DocumentOpenInfo object which will go ahead and open
+ // the url and discover the content type....
+ RefPtr<nsDocumentOpenInfo> loader =
+ new nsDocumentOpenInfo(aWindowContext, aFlags, this);
+
+ // Set the correct loadgroup on the channel
+ nsCOMPtr<nsILoadGroup> loadGroup(do_GetInterface(aWindowContext));
+
+ if (!loadGroup) {
+ // XXXbz This context is violating what we'd like to be the new uriloader
+ // api.... Set up a nsDocLoader to handle the loadgroup for this context.
+ // This really needs to go away!
+ nsCOMPtr<nsIURIContentListener> listener(do_GetInterface(aWindowContext));
+ if (listener) {
+ nsCOMPtr<nsISupports> cookie;
+ listener->GetLoadCookie(getter_AddRefs(cookie));
+ if (!cookie) {
+ RefPtr<nsDocLoader> newDocLoader = new nsDocLoader();
+ nsresult rv = newDocLoader->Init();
+ if (NS_FAILED(rv)) return rv;
+ rv = nsDocLoader::AddDocLoaderAsChildOfRoot(newDocLoader);
+ if (NS_FAILED(rv)) return rv;
+ cookie = nsDocLoader::GetAsSupports(newDocLoader);
+ listener->SetLoadCookie(cookie);
+ }
+ loadGroup = do_GetInterface(cookie);
+ }
+ }
+
+ // If the channel is pending, then we need to remove it from its current
+ // loadgroup
+ nsCOMPtr<nsILoadGroup> oldGroup;
+ channel->GetLoadGroup(getter_AddRefs(oldGroup));
+ if (aChannelIsOpen && !SameCOMIdentity(oldGroup, loadGroup)) {
+ // It is important to add the channel to the new group before
+ // removing it from the old one, so that the load isn't considered
+ // done as soon as the request is removed.
+ loadGroup->AddRequest(channel, nullptr);
+
+ if (oldGroup) {
+ oldGroup->RemoveRequest(channel, nullptr, NS_BINDING_RETARGETED);
+ }
+ }
+
+ channel->SetLoadGroup(loadGroup);
+
+ // prepare the loader for receiving data
+ nsresult rv = loader->Prepare();
+ if (NS_SUCCEEDED(rv)) NS_ADDREF(*aListener = loader);
+ return rv;
+}
+
+NS_IMETHODIMP nsURILoader::OpenChannel(nsIChannel* channel, uint32_t aFlags,
+ nsIInterfaceRequestor* aWindowContext,
+ nsIStreamListener** aListener) {
+ bool pending;
+ if (NS_FAILED(channel->IsPending(&pending))) {
+ pending = false;
+ }
+
+ return OpenChannel(channel, aFlags, aWindowContext, pending, aListener);
+}
+
+NS_IMETHODIMP nsURILoader::Stop(nsISupports* aLoadCookie) {
+ nsresult rv;
+ nsCOMPtr<nsIDocumentLoader> docLoader;
+
+ NS_ENSURE_ARG_POINTER(aLoadCookie);
+
+ docLoader = do_GetInterface(aLoadCookie, &rv);
+ if (docLoader) {
+ rv = docLoader->Stop();
+ }
+ return rv;
+}
diff --git a/uriloader/base/nsURILoader.h b/uriloader/base/nsURILoader.h
new file mode 100644
index 0000000000..5d9eaf97c8
--- /dev/null
+++ b/uriloader/base/nsURILoader.h
@@ -0,0 +1,219 @@
+/* -*- 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 nsURILoader_h__
+#define nsURILoader_h__
+
+#include "nsCURILoader.h"
+#include "nsISupportsUtils.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsString.h"
+#include "nsIWeakReference.h"
+#include "mozilla/Attributes.h"
+#include "nsIStreamListener.h"
+#include "nsIThreadRetargetableStreamListener.h"
+#include "nsIExternalHelperAppService.h"
+
+#include "mozilla/Logging.h"
+
+class nsDocumentOpenInfo;
+
+class nsURILoader final : public nsIURILoader {
+ public:
+ NS_DECL_NSIURILOADER
+ NS_DECL_ISUPPORTS
+
+ nsURILoader();
+
+ protected:
+ ~nsURILoader();
+
+ /**
+ * Equivalent to nsIURILoader::openChannel, but allows specifying whether the
+ * channel is opened already.
+ */
+ [[nodiscard]] nsresult OpenChannel(nsIChannel* channel, uint32_t aFlags,
+ nsIInterfaceRequestor* aWindowContext,
+ bool aChannelOpen,
+ nsIStreamListener** aListener);
+
+ /**
+ * we shouldn't need to have an owning ref count on registered
+ * content listeners because they are supposed to unregister themselves
+ * when they go away. This array stores weak references
+ */
+ nsCOMArray<nsIWeakReference> m_listeners;
+
+ /**
+ * Logging. The module is called "URILoader"
+ */
+ static mozilla::LazyLogModule mLog;
+
+ friend class nsDocumentOpenInfo;
+};
+
+/**
+ * The nsDocumentOpenInfo contains the state required when a single
+ * document is being opened in order to discover the content type...
+ * Each instance remains alive until its target URL has been loaded
+ * (or aborted).
+ */
+class nsDocumentOpenInfo : public nsIStreamListener,
+ public nsIThreadRetargetableStreamListener {
+ public:
+ // Real constructor
+ // aFlags is a combination of the flags on nsIURILoader
+ nsDocumentOpenInfo(nsIInterfaceRequestor* aWindowContext, uint32_t aFlags,
+ nsURILoader* aURILoader);
+ nsDocumentOpenInfo(uint32_t aFlags, bool aAllowListenerConversions);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /**
+ * Prepares this object for receiving data. The stream
+ * listener methods of this class must not be called before calling this
+ * method.
+ */
+ nsresult Prepare();
+
+ // Call this (from OnStartRequest) to attempt to find an nsIStreamListener to
+ // take the data off our hands.
+ nsresult DispatchContent(nsIRequest* request, nsISupports* aCtxt);
+
+ // Call this if we need to insert a stream converter from aSrcContentType to
+ // aOutContentType into the StreamListener chain. DO NOT call it if the two
+ // types are the same, since no conversion is needed in that case.
+ nsresult ConvertData(nsIRequest* request, nsIURIContentListener* aListener,
+ const nsACString& aSrcContentType,
+ const nsACString& aOutContentType);
+
+ /**
+ * Function to attempt to use aListener to handle the load. If
+ * true is returned, nothing else needs to be done; if false
+ * is returned, then a different way of handling the load should be
+ * tried.
+ */
+ bool TryContentListener(nsIURIContentListener* aListener,
+ nsIChannel* aChannel);
+
+ /**
+ * Virtual helper functions for content that we expect to be
+ * overriden when running in the parent process on behalf of
+ * a content process docshell.
+ * We also expect nsIStreamListener functions to be overriden
+ * to add functionality.
+ */
+
+ /**
+ * Attempt to create a steam converter converting from the
+ * current mContentType into something else.
+ * Sets m_targetStreamListener if it succeeds.
+ */
+ virtual nsresult TryStreamConversion(nsIChannel* aChannel);
+
+ /**
+ * Attempt to use the default content listener as our stream
+ * listener.
+ * Sets m_targetStreamListener if it succeeds.
+ */
+ virtual bool TryDefaultContentListener(nsIChannel* aChannel);
+
+ /**
+ * Attempt to pass aChannel onto the external helper app service.
+ * Sets m_targetStreamListener if it succeeds.
+ */
+ virtual nsresult TryExternalHelperApp(
+ nsIExternalHelperAppService* aHelperAppService, nsIChannel* aChannel);
+
+ /**
+ * Create another nsDocumentOpenInfo like this one, so that we can chain
+ * them together when we use a stream converter and don't know what the
+ * converted content type is until the converter outputs OnStartRequest.
+ */
+ virtual nsDocumentOpenInfo* Clone() {
+ return new nsDocumentOpenInfo(m_originalContext, mFlags, mURILoader);
+ }
+
+ // nsIRequestObserver methods:
+ NS_DECL_NSIREQUESTOBSERVER
+
+ // nsIStreamListener methods:
+ NS_DECL_NSISTREAMLISTENER
+
+ // nsIThreadRetargetableStreamListener
+ NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
+
+ protected:
+ virtual ~nsDocumentOpenInfo();
+
+ protected:
+ /**
+ * The first content listener to try dispatching data to. Typically
+ * the listener associated with the entity that originated the load.
+ * This can be nullptr when running in the parent process for a content
+ * process docshell.
+ */
+ nsCOMPtr<nsIURIContentListener> m_contentListener;
+
+ /**
+ * The stream listener to forward nsIStreamListener notifications
+ * to. This is set once the load is dispatched.
+ */
+ nsCOMPtr<nsIStreamListener> m_targetStreamListener;
+
+ /**
+ * A pointer to the entity that originated the load. We depend on getting
+ * things like nsIURIContentListeners, nsIDOMWindows, etc off of it.
+ * This can be nullptr when running in the parent process for a content
+ * process docshell.
+ */
+ nsCOMPtr<nsIInterfaceRequestor> m_originalContext;
+
+ /**
+ * IS_CONTENT_PREFERRED is used for the boolean to pass to CanHandleContent
+ * (also determines whether we use CanHandleContent or IsPreferred).
+ * DONT_RETARGET means that we will only try m_originalContext, no other
+ * listeners.
+ */
+ uint32_t mFlags;
+
+ /**
+ * The type of the data we will be trying to dispatch.
+ */
+ nsCString mContentType;
+
+ /**
+ * Reference to the URILoader service so we can access its list of
+ * nsIURIContentListeners.
+ * This can be nullptr when running in the parent process for a content
+ * process docshell.
+ */
+ RefPtr<nsURILoader> mURILoader;
+
+ /**
+ * Limit of data conversion depth to prevent infinite conversion loops
+ */
+ uint32_t mDataConversionDepthLimit;
+
+ /**
+ * Set to true if OnStartRequest handles the content using an
+ * nsIContentHandler, and the content is consumed despite
+ * m_targetStreamListener being nullptr.
+ */
+ bool mUsedContentHandler = false;
+
+ /**
+ * True if we allow nsIURIContentListeners to return a requested
+ * input typeToUse, and attempt to create a matching stream converter.
+ * This is false when running in the parent process for a content process
+ * docshell
+ */
+ bool mAllowListenerConversions = true;
+};
+
+#endif /* nsURILoader_h__ */
diff --git a/uriloader/docs/index.rst b/uriloader/docs/index.rst
new file mode 100644
index 0000000000..4943040b84
--- /dev/null
+++ b/uriloader/docs/index.rst
@@ -0,0 +1,10 @@
+File Handling
+=============
+
+This covers how files requested for display are loaded.
+
+.. toctree::
+ :maxdepth: 2
+
+ uriloader
+ exthandler/index
diff --git a/uriloader/docs/uriloader.rst b/uriloader/docs/uriloader.rst
new file mode 100644
index 0000000000..e0763233e5
--- /dev/null
+++ b/uriloader/docs/uriloader.rst
@@ -0,0 +1,46 @@
+.. _uri_loader_service:
+
+URI Loader Service
+==================
+
+As its name might suggest the URI loader service is responsible for loading URIs
+but it is also responsible for deciding how to handle that content, whether to
+display it as part of a DOM window or hand it off to something else.
+
+It is generally used when loading content for display to the user, normally from
+``nsDocShell`` for display as a webpage or ``nsObjectLoadingContent`` for display inside
+a webpage's ``<object>`` tag. The normal entrypoint is through ``nsIURILoader::OpenURI``.
+
+The URI loader starts the load and registers an ``nsDocumentOpenInfo`` as a stream
+listener for the content. Once headers have been received `DispatchContent <https://searchfox.org/mozilla-central/search?q=nsDocumentOpenInfo%3A%3ADispatchContent&path=>`_
+then decides what to do with the content as it may need to be handled by something
+other than the caller. It uses a few criteria to decide this including:
+
+* Content-Type header.
+* Content-Disposition header.
+* Load flags.
+
+Part of this handling may include running the content through a registered stream
+converter to convert the content type from one to another. This is done through
+the `stream converter service <https://searchfox.org/mozilla-central/source/netwerk/streamconv>`_.
+When this happens a new ``nsDocumentOpenInfo`` is created to handle the new content
+in the same way as the current content.
+
+The rough flow goes as follows (though note that this can vary depending on the
+flags passed to the loader service):
+
+1. The caller may provide an ``nsIURIContentListener`` which can offer to handle
+ the content type or a content type that we can convert the original type to).
+ If so the load is passed off to the listener.
+2. Global instances of ``nsIURIContentListener`` can be registered with the URI
+ loader service so these are consulted in the same way.
+3. Global instances of ``nsIURIContentListener`` can be registered in the category
+ manager so these are consulted in the same way.
+4. Global instances of ``nsIContentHandler`` can be registered. If one agrees to
+ handle the content then the load is handed over to it.
+5. We attempt to convert the content to a different type.
+6. The load is handed over to the :ref:`External Helper App Service <external_helper_app_service>`.
+
+For the most part the process ends at step 1 because nsDocShell passes a ``nsDSURIContentListener``
+for the ``nsIURIContentListener`` consulted first and it accepts most of the
+`web content types <https://searchfox.org/mozilla-central/search?q=CONTENTDLF_CATEGORIES&redirect=false>`_.
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__
diff --git a/uriloader/moz.build b/uriloader/moz.build
new file mode 100644
index 0000000000..07659656c6
--- /dev/null
+++ b/uriloader/moz.build
@@ -0,0 +1,17 @@
+# -*- 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"] = "docs"
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "File Handling")
+
+DIRS += [
+ "base",
+ "exthandler",
+ "prefetch",
+ "preload",
+]
diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.cpp b/uriloader/prefetch/OfflineCacheUpdateChild.cpp
new file mode 100644
index 0000000000..10907acc34
--- /dev/null
+++ b/uriloader/prefetch/OfflineCacheUpdateChild.cpp
@@ -0,0 +1,472 @@
+/* -*- 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 "BackgroundUtils.h"
+#include "OfflineCacheUpdateChild.h"
+#include "nsOfflineCacheUpdate.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/OfflineResourceListBinding.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/net/NeckoCommon.h"
+
+#include "nsIApplicationCacheChannel.h"
+#include "nsIDocShell.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "nsIObserverService.h"
+#include "nsIBrowserChild.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+#include "mozilla/Logging.h"
+#include "nsApplicationCache.h"
+
+using namespace mozilla::ipc;
+using namespace mozilla::net;
+using mozilla::dom::BrowserChild;
+using mozilla::dom::ContentChild;
+using mozilla::dom::Document;
+
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=nsOfflineCacheUpdate:5
+// set MOZ_LOG_FILE=offlineupdate.log
+//
+// this enables LogLevel::Debug level information and places all output in
+// the file offlineupdate.log
+//
+extern mozilla::LazyLogModule gOfflineCacheUpdateLog;
+
+#undef LOG
+#define LOG(args) \
+ MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args)
+
+#undef LOG_ENABLED
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug)
+
+namespace mozilla {
+namespace docshell {
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateChild::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_INTERFACE_MAP_BEGIN(OfflineCacheUpdateChild)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY(nsIOfflineCacheUpdate)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(OfflineCacheUpdateChild)
+NS_IMPL_RELEASE(OfflineCacheUpdateChild)
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateChild <public>
+//-----------------------------------------------------------------------------
+
+OfflineCacheUpdateChild::OfflineCacheUpdateChild(nsPIDOMWindowInner* aWindow)
+ : mState(STATE_UNINITIALIZED),
+ mIsUpgrade(false),
+ mSucceeded(false),
+ mWindow(aWindow),
+ mByteProgress(0) {}
+
+OfflineCacheUpdateChild::~OfflineCacheUpdateChild() {
+ LOG(("OfflineCacheUpdateChild::~OfflineCacheUpdateChild [%p]", this));
+}
+
+void OfflineCacheUpdateChild::GatherObservers(
+ nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers) {
+ for (int32_t i = 0; i < mWeakObservers.Count(); i++) {
+ nsCOMPtr<nsIOfflineCacheUpdateObserver> observer =
+ do_QueryReferent(mWeakObservers[i]);
+ if (observer)
+ aObservers.AppendObject(observer);
+ else
+ mWeakObservers.RemoveObjectAt(i--);
+ }
+
+ for (int32_t i = 0; i < mObservers.Count(); i++) {
+ aObservers.AppendObject(mObservers[i]);
+ }
+}
+
+void OfflineCacheUpdateChild::SetDocument(Document* aDocument) {
+ // The design is one document for one cache update on the content process.
+ NS_ASSERTION(
+ !mDocument,
+ "Setting more then a single document on a child offline cache update");
+
+ LOG(("Document %p added to update child %p", aDocument, this));
+
+ // Add document only if it was not loaded from an offline cache.
+ // If it were loaded from an offline cache then it has already
+ // been associated with it and must not be again cached as
+ // implicit (which are the reasons we collect documents here).
+ if (!aDocument) return;
+
+ mCookieJarSettings = aDocument->CookieJarSettings();
+
+ nsIChannel* channel = aDocument->GetChannel();
+ nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel =
+ do_QueryInterface(channel);
+ if (!appCacheChannel) return;
+
+ bool loadedFromAppCache;
+ appCacheChannel->GetLoadedFromApplicationCache(&loadedFromAppCache);
+ if (loadedFromAppCache) return;
+
+ mDocument = aDocument;
+}
+
+nsresult OfflineCacheUpdateChild::AssociateDocument(
+ Document* aDocument, nsIApplicationCache* aApplicationCache) {
+ // Check that the document that requested this update was
+ // previously associated with an application cache. If not, it
+ // should be associated with the new one.
+ nsCOMPtr<nsIApplicationCache> existingCache;
+ nsresult rv = aDocument->GetApplicationCache(getter_AddRefs(existingCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!existingCache) {
+ if (LOG_ENABLED()) {
+ nsAutoCString clientID;
+ if (aApplicationCache) {
+ aApplicationCache->GetClientID(clientID);
+ }
+ LOG(("Update %p: associating app cache %s to document %p", this,
+ clientID.get(), aDocument));
+ }
+
+ rv = aDocument->SetApplicationCache(aApplicationCache);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateChild::nsIOfflineCacheUpdate
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::Init(nsIURI* aManifestURI, nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ Document* aDocument, nsIFile* aCustomProfileDir) {
+ nsresult rv;
+
+ // Make sure the service has been initialized
+ nsOfflineCacheUpdateService* service =
+ nsOfflineCacheUpdateService::EnsureService();
+ if (!service) return NS_ERROR_FAILURE;
+
+ if (aCustomProfileDir) {
+ NS_ERROR("Custom Offline Cache Update not supported on child process");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ LOG(("OfflineCacheUpdateChild::Init [%p]", this));
+
+ // Only http and https applications are supported.
+ if (!aManifestURI->SchemeIs("http") && !aManifestURI->SchemeIs("https")) {
+ return NS_ERROR_ABORT;
+ }
+
+ mManifestURI = aManifestURI;
+
+ rv = mManifestURI->GetAsciiHost(mUpdateDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mDocumentURI = aDocumentURI;
+ mLoadingPrincipal = aLoadingPrincipal;
+
+ mState = STATE_INITIALIZED;
+
+ if (aDocument) SetDocument(aDocument);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::InitPartial(nsIURI* aManifestURI,
+ const nsACString& clientID,
+ nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ nsICookieJarSettings* aCookieJarSettings) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Not expected to do partial offline cache updates"
+ " on the child process");
+ // For now leaving this method, we may discover we need it.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::InitForUpdateCheck(nsIURI* aManifestURI,
+ nsIPrincipal* aLoadingPrincipal,
+ nsIObserver* aObserver) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Not expected to do only update checks"
+ " from the child process");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetUpdateDomain(nsACString& aUpdateDomain) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ aUpdateDomain = mUpdateDomain;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetStatus(uint16_t* aStatus) {
+ switch (mState) {
+ case STATE_CHECKING:
+ *aStatus = mozilla::dom::OfflineResourceList_Binding::CHECKING;
+ return NS_OK;
+ case STATE_DOWNLOADING:
+ *aStatus = mozilla::dom::OfflineResourceList_Binding::DOWNLOADING;
+ return NS_OK;
+ default:
+ *aStatus = mozilla::dom::OfflineResourceList_Binding::IDLE;
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetPartial(bool* aPartial) {
+ *aPartial = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetLoadingPrincipal(nsIPrincipal** aLoadingPrincipal) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ NS_IF_ADDREF(*aLoadingPrincipal = mLoadingPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetManifestURI(nsIURI** aManifestURI) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ NS_IF_ADDREF(*aManifestURI = mManifestURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetSucceeded(bool* aSucceeded) {
+ NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE);
+
+ *aSucceeded = mSucceeded;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetIsUpgrade(bool* aIsUpgrade) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ *aIsUpgrade = mIsUpgrade;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::AddDynamicURI(nsIURI* aURI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::Cancel() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::AddObserver(nsIOfflineCacheUpdateObserver* aObserver,
+ bool aHoldWeak) {
+ LOG(("OfflineCacheUpdateChild::AddObserver [%p]", this));
+
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ if (aHoldWeak) {
+ nsWeakPtr weakRef = do_GetWeakReference(aObserver);
+ mWeakObservers.AppendObject(weakRef);
+ } else {
+ mObservers.AppendObject(aObserver);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::RemoveObserver(
+ nsIOfflineCacheUpdateObserver* aObserver) {
+ LOG(("OfflineCacheUpdateChild::RemoveObserver [%p]", this));
+
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ for (int32_t i = 0; i < mWeakObservers.Count(); i++) {
+ nsCOMPtr<nsIOfflineCacheUpdateObserver> observer =
+ do_QueryReferent(mWeakObservers[i]);
+ if (observer == aObserver) {
+ mWeakObservers.RemoveObjectAt(i);
+ return NS_OK;
+ }
+ }
+
+ for (int32_t i = 0; i < mObservers.Count(); i++) {
+ if (mObservers[i] == aObserver) {
+ mObservers.RemoveObjectAt(i);
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::GetByteProgress(uint64_t* _result) {
+ NS_ENSURE_ARG(_result);
+
+ *_result = mByteProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateChild::Schedule() {
+ LOG(("OfflineCacheUpdateChild::Schedule [%p]", this));
+
+ NS_ASSERTION(mWindow,
+ "Window must be provided to the offline cache update child");
+
+ nsCOMPtr<nsPIDOMWindowInner> window = std::move(mWindow);
+ nsCOMPtr<nsIDocShell> docshell = window->GetDocShell();
+ if (!docshell) {
+ NS_WARNING("doc shell tree item is null");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = NS_OK;
+ PrincipalInfo loadingPrincipalInfo;
+ rv = PrincipalToPrincipalInfo(mLoadingPrincipal, &loadingPrincipalInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ LOG(("Calling offline-cache-update-added"));
+ observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this),
+ "offline-cache-update-added", nullptr);
+ LOG(("Done offline-cache-update-added"));
+ }
+
+ // mDocument is non-null if both:
+ // 1. this update was initiated by a document that referred a manifest
+ // 2. the document has not already been loaded from the application cache
+ // This tells the update to cache this document even in case the manifest
+ // has not been changed since the last fetch.
+ // See also nsOfflineCacheUpdate::ScheduleImplicit.
+ bool stickDocument = mDocument != nullptr;
+
+ CookieJarSettingsArgs csArgs;
+ if (mCookieJarSettings) {
+ CookieJarSettings::Cast(mCookieJarSettings)->Serialize(csArgs);
+ }
+
+ ContentChild::GetSingleton()->SendPOfflineCacheUpdateConstructor(
+ this, mManifestURI, mDocumentURI, loadingPrincipalInfo, stickDocument,
+ csArgs);
+
+ return NS_OK;
+}
+
+mozilla::ipc::IPCResult OfflineCacheUpdateChild::RecvAssociateDocuments(
+ const nsCString& cacheGroupId, const nsCString& cacheClientId) {
+ LOG(("OfflineCacheUpdateChild::RecvAssociateDocuments [%p, cache=%s]", this,
+ cacheClientId.get()));
+
+ nsCOMPtr<nsIApplicationCache> cache = new nsApplicationCache();
+
+ cache->InitAsHandle(cacheGroupId, cacheClientId);
+
+ if (mDocument) {
+ AssociateDocument(mDocument, cache);
+ }
+
+ nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+ GatherObservers(observers);
+
+ for (int32_t i = 0; i < observers.Count(); i++)
+ observers[i]->ApplicationCacheAvailable(cache);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult OfflineCacheUpdateChild::RecvNotifyStateEvent(
+ const uint32_t& event, const uint64_t& byteProgress) {
+ LOG(("OfflineCacheUpdateChild::RecvNotifyStateEvent [%p]", this));
+
+ mByteProgress = byteProgress;
+
+ // Convert the public observer state to our internal state
+ switch (event) {
+ case nsIOfflineCacheUpdateObserver::STATE_CHECKING:
+ mState = STATE_CHECKING;
+ break;
+
+ case nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING:
+ mState = STATE_DOWNLOADING;
+ break;
+
+ default:
+ break;
+ }
+
+ nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+ GatherObservers(observers);
+
+ for (int32_t i = 0; i < observers.Count(); i++)
+ observers[i]->UpdateStateChanged(this, event);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult OfflineCacheUpdateChild::RecvFinish(
+ const bool& succeeded, const bool& isUpgrade) {
+ LOG(("OfflineCacheUpdateChild::RecvFinish [%p]", this));
+
+ RefPtr<OfflineCacheUpdateChild> kungFuDeathGrip(this);
+
+ mState = STATE_FINISHED;
+ mSucceeded = succeeded;
+ mIsUpgrade = isUpgrade;
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ LOG(("Calling offline-cache-update-completed"));
+ observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this),
+ "offline-cache-update-completed", nullptr);
+ LOG(("Done offline-cache-update-completed"));
+ }
+
+ // This is by contract the last notification from the parent, release
+ // us now. This is corresponding to AddRef in Schedule().
+ // BrowserChild::DeallocPOfflineCacheUpdate will call Release.
+ OfflineCacheUpdateChild::Send__delete__(this);
+
+ return IPC_OK();
+}
+
+} // namespace docshell
+} // namespace mozilla
diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.h b/uriloader/prefetch/OfflineCacheUpdateChild.h
new file mode 100644
index 0000000000..4099c9dfdb
--- /dev/null
+++ b/uriloader/prefetch/OfflineCacheUpdateChild.h
@@ -0,0 +1,93 @@
+/* -*- 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 nsOfflineCacheUpdateChild_h
+#define nsOfflineCacheUpdateChild_h
+
+#include "mozilla/docshell/POfflineCacheUpdateChild.h"
+#include "nsIOfflineCacheUpdate.h"
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIURI.h"
+#include "nsIWeakReference.h"
+#include "nsString.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+
+namespace docshell {
+
+class OfflineCacheUpdateChild : public nsIOfflineCacheUpdate,
+ public POfflineCacheUpdateChild {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOFFLINECACHEUPDATE
+
+ mozilla::ipc::IPCResult RecvNotifyStateEvent(const uint32_t& stateEvent,
+ const uint64_t& byteProgress);
+
+ mozilla::ipc::IPCResult RecvAssociateDocuments(
+ const nsCString& cacheGroupId, const nsCString& cacheClientId);
+
+ mozilla::ipc::IPCResult RecvFinish(const bool& succeeded,
+ const bool& isUpgrade);
+
+ explicit OfflineCacheUpdateChild(nsPIDOMWindowInner* aWindow);
+
+ void SetDocument(dom::Document* aDocument);
+
+ private:
+ ~OfflineCacheUpdateChild();
+
+ nsresult AssociateDocument(dom::Document* aDocument,
+ nsIApplicationCache* aApplicationCache);
+ void GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers);
+ nsresult Finish();
+
+ enum {
+ STATE_UNINITIALIZED,
+ STATE_INITIALIZED,
+ STATE_CHECKING,
+ STATE_DOWNLOADING,
+ STATE_CANCELLED,
+ STATE_FINISHED
+ } mState;
+
+ bool mIsUpgrade;
+ bool mSucceeded;
+
+ nsCString mUpdateDomain;
+ nsCOMPtr<nsIURI> mManifestURI;
+ nsCOMPtr<nsIURI> mDocumentURI;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+ nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;
+
+ nsCOMPtr<nsIObserverService> mObserverService;
+
+ /* Clients watching this update for changes */
+ nsCOMArray<nsIWeakReference> mWeakObservers;
+ nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers;
+
+ /* Document that requested this update */
+ nsCOMPtr<dom::Document> mDocument;
+
+ /* Keep reference to the window that owns this update to call the
+ parent offline cache update construcor */
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+
+ uint64_t mByteProgress;
+};
+
+} // namespace docshell
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/prefetch/OfflineCacheUpdateGlue.cpp b/uriloader/prefetch/OfflineCacheUpdateGlue.cpp
new file mode 100644
index 0000000000..85515b5547
--- /dev/null
+++ b/uriloader/prefetch/OfflineCacheUpdateGlue.cpp
@@ -0,0 +1,220 @@
+/* -*- 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 "OfflineCacheUpdateGlue.h"
+#include "nsOfflineCacheUpdate.h"
+#include "mozilla/Services.h"
+
+#include "nsIApplicationCache.h"
+#include "nsIApplicationCacheChannel.h"
+#include "nsIChannel.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/Logging.h"
+
+using mozilla::dom::Document;
+
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=nsOfflineCacheUpdate:5
+// set MOZ_LOG_FILE=offlineupdate.log
+//
+// this enables LogLevel::Info level information and places all output in
+// the file offlineupdate.log
+//
+extern mozilla::LazyLogModule gOfflineCacheUpdateLog;
+
+#undef LOG
+#define LOG(args) \
+ MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args)
+
+#undef LOG_ENABLED
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug)
+
+namespace mozilla {
+namespace docshell {
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateGlue::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(OfflineCacheUpdateGlue, nsIOfflineCacheUpdate,
+ nsIOfflineCacheUpdateObserver, nsISupportsWeakReference)
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateGlue <public>
+//-----------------------------------------------------------------------------
+
+OfflineCacheUpdateGlue::OfflineCacheUpdateGlue() : mCoalesced(false) {
+ LOG(("OfflineCacheUpdateGlue::OfflineCacheUpdateGlue [%p]", this));
+}
+
+OfflineCacheUpdateGlue::~OfflineCacheUpdateGlue() {
+ LOG(("OfflineCacheUpdateGlue::~OfflineCacheUpdateGlue [%p]", this));
+}
+
+nsIOfflineCacheUpdate* OfflineCacheUpdateGlue::EnsureUpdate() {
+ if (!mUpdate) {
+ mUpdate = new nsOfflineCacheUpdate();
+ LOG(("OfflineCacheUpdateGlue [%p] is using update [%p]", this,
+ mUpdate.get()));
+
+ mUpdate->SetCookieJarSettings(mCookieJarSettings);
+ }
+
+ return mUpdate;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateGlue::Schedule() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ LOG(("Calling offline-cache-update-added"));
+ observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this),
+ "offline-cache-update-added", nullptr);
+ LOG(("Done offline-cache-update-added"));
+ }
+
+ if (!EnsureUpdate()) return NS_ERROR_NULL_POINTER;
+
+ // Do not use weak reference, we must survive!
+ mUpdate->AddObserver(this, false);
+
+ if (mCoalesced) // already scheduled
+ return NS_OK;
+
+ return mUpdate->Schedule();
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateGlue::Init(nsIURI* aManifestURI, nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ Document* aDocument, nsIFile* aCustomProfileDir) {
+ nsresult rv;
+
+ nsAutoCString originSuffix;
+ rv = aLoadingPrincipal->GetOriginSuffix(originSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsOfflineCacheUpdateService* service =
+ nsOfflineCacheUpdateService::EnsureService();
+ if (service) {
+ service->FindUpdate(aManifestURI, originSuffix, aCustomProfileDir,
+ getter_AddRefs(mUpdate));
+ mCoalesced = !!mUpdate;
+ }
+
+ if (!EnsureUpdate()) return NS_ERROR_NULL_POINTER;
+
+ mDocumentURI = aDocumentURI;
+ mLoadingPrincipal = aLoadingPrincipal;
+
+ if (aDocument) SetDocument(aDocument);
+
+ if (mCoalesced) { // already initialized
+ LOG(("OfflineCacheUpdateGlue %p coalesced with update %p", this,
+ mUpdate.get()));
+ return NS_OK;
+ }
+
+ rv = mUpdate->Init(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr,
+ aCustomProfileDir);
+
+ mUpdate->SetCookieJarSettings(mCookieJarSettings);
+
+ return rv;
+}
+
+void OfflineCacheUpdateGlue::SetDocument(Document* aDocument) {
+ // The design is one document for one cache update on the content process.
+ NS_ASSERTION(!mDocument,
+ "Setting more then a single document on an instance of "
+ "OfflineCacheUpdateGlue");
+
+ LOG(("Document %p added to update glue %p", aDocument, this));
+
+ // Add document only if it was not loaded from an offline cache.
+ // If it were loaded from an offline cache then it has already
+ // been associated with it and must not be again cached as
+ // implicit (which are the reasons we collect documents here).
+ if (!aDocument) return;
+
+ mCookieJarSettings = aDocument->CookieJarSettings();
+
+ nsIChannel* channel = aDocument->GetChannel();
+ nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel =
+ do_QueryInterface(channel);
+ if (!appCacheChannel) return;
+
+ bool loadedFromAppCache;
+ appCacheChannel->GetLoadedFromApplicationCache(&loadedFromAppCache);
+ if (loadedFromAppCache) return;
+
+ if (EnsureUpdate()) {
+ mUpdate->StickDocument(mDocumentURI);
+ }
+
+ mDocument = aDocument;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateGlue::UpdateStateChanged(nsIOfflineCacheUpdate* aUpdate,
+ uint32_t state) {
+ if (state == nsIOfflineCacheUpdateObserver::STATE_FINISHED) {
+ LOG(("OfflineCacheUpdateGlue got STATE_FINISHED [%p]", this));
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ LOG(("Calling offline-cache-update-completed"));
+ observerService->NotifyObservers(
+ static_cast<nsIOfflineCacheUpdate*>(this),
+ "offline-cache-update-completed", nullptr);
+ LOG(("Done offline-cache-update-completed"));
+ }
+
+ aUpdate->RemoveObserver(this);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateGlue::ApplicationCacheAvailable(
+ nsIApplicationCache* aApplicationCache) {
+ NS_ENSURE_ARG(aApplicationCache);
+
+ // Check that the document that requested this update was
+ // previously associated with an application cache. If not, it
+ // should be associated with the new one.
+ if (!mDocument) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIApplicationCache> existingCache;
+ nsresult rv = mDocument->GetApplicationCache(getter_AddRefs(existingCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!existingCache) {
+ if (LOG_ENABLED()) {
+ nsAutoCString clientID;
+ if (aApplicationCache) {
+ aApplicationCache->GetClientID(clientID);
+ }
+ LOG(("Update %p: associating app cache %s to document %p", this,
+ clientID.get(), mDocument.get()));
+ }
+
+ rv = mDocument->SetApplicationCache(aApplicationCache);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+} // namespace docshell
+} // namespace mozilla
diff --git a/uriloader/prefetch/OfflineCacheUpdateGlue.h b/uriloader/prefetch/OfflineCacheUpdateGlue.h
new file mode 100644
index 0000000000..7c1d08cc73
--- /dev/null
+++ b/uriloader/prefetch/OfflineCacheUpdateGlue.h
@@ -0,0 +1,124 @@
+/* -*- 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 nsOfflineCacheUpdateGlue_h
+#define nsOfflineCacheUpdateGlue_h
+
+#include "nsIOfflineCacheUpdate.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "mozilla/Attributes.h"
+
+class nsOfflineCacheUpdate;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+
+namespace docshell {
+
+// Like FORWARD_SAFE except methods:
+// Schedule
+// Init
+#define NS_ADJUSTED_FORWARD_NSIOFFLINECACHEUPDATE(_to) \
+ NS_IMETHOD GetStatus(uint16_t* aStatus) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->GetStatus(aStatus); \
+ } \
+ NS_IMETHOD GetPartial(bool* aPartial) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->GetPartial(aPartial); \
+ } \
+ NS_IMETHOD GetIsUpgrade(bool* aIsUpgrade) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->GetIsUpgrade(aIsUpgrade); \
+ } \
+ NS_IMETHOD GetUpdateDomain(nsACString& aUpdateDomain) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->GetUpdateDomain(aUpdateDomain); \
+ } \
+ NS_IMETHOD GetLoadingPrincipal(nsIPrincipal** aLoadingPrincipal) override { \
+ return !_to ? NS_ERROR_NULL_POINTER \
+ : _to->GetLoadingPrincipal(aLoadingPrincipal); \
+ } \
+ NS_IMETHOD GetManifestURI(nsIURI** aManifestURI) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->GetManifestURI(aManifestURI); \
+ } \
+ NS_IMETHOD GetSucceeded(bool* aSucceeded) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->GetSucceeded(aSucceeded); \
+ } \
+ NS_IMETHOD InitPartial(nsIURI* aManifestURI, const nsACString& aClientID, \
+ nsIURI* aDocumentURI, \
+ nsIPrincipal* aLoadingPrincipal, \
+ nsICookieJarSettings* aCookieJarSettings) override { \
+ return !_to ? NS_ERROR_NULL_POINTER \
+ : _to->InitPartial(aManifestURI, aClientID, aDocumentURI, \
+ aLoadingPrincipal, aCookieJarSettings); \
+ } \
+ NS_IMETHOD InitForUpdateCheck(nsIURI* aManifestURI, \
+ nsIPrincipal* aLoadingPrincipal, \
+ nsIObserver* aObserver) override { \
+ return !_to ? NS_ERROR_NULL_POINTER \
+ : _to->InitForUpdateCheck(aManifestURI, aLoadingPrincipal, \
+ aObserver); \
+ } \
+ NS_IMETHOD AddDynamicURI(nsIURI* aURI) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->AddDynamicURI(aURI); \
+ } \
+ NS_IMETHOD AddObserver(nsIOfflineCacheUpdateObserver* aObserver, \
+ bool aHoldWeak) override { \
+ return !_to ? NS_ERROR_NULL_POINTER \
+ : _to->AddObserver(aObserver, aHoldWeak); \
+ } \
+ NS_IMETHOD RemoveObserver(nsIOfflineCacheUpdateObserver* aObserver) \
+ override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->RemoveObserver(aObserver); \
+ } \
+ NS_IMETHOD GetByteProgress(uint64_t* _result) override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->GetByteProgress(_result); \
+ } \
+ NS_IMETHOD Cancel() override { \
+ return !_to ? NS_ERROR_NULL_POINTER : _to->Cancel(); \
+ }
+
+class OfflineCacheUpdateGlue final : public nsSupportsWeakReference,
+ public nsIOfflineCacheUpdate,
+ public nsIOfflineCacheUpdateObserver {
+ public:
+ NS_DECL_ISUPPORTS
+
+ private:
+ nsIOfflineCacheUpdate* EnsureUpdate();
+
+ public:
+ NS_ADJUSTED_FORWARD_NSIOFFLINECACHEUPDATE(EnsureUpdate())
+ NS_IMETHOD Schedule(void) override;
+ NS_IMETHOD Init(nsIURI* aManifestURI, nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ mozilla::dom::Document* aDocument,
+ nsIFile* aCustomProfileDir) override;
+
+ NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER
+
+ OfflineCacheUpdateGlue();
+
+ void SetDocument(mozilla::dom::Document* aDocument);
+
+ private:
+ ~OfflineCacheUpdateGlue();
+
+ RefPtr<nsOfflineCacheUpdate> mUpdate;
+ bool mCoalesced;
+
+ /* Document that requested this update */
+ RefPtr<mozilla::dom::Document> mDocument;
+ nsCOMPtr<nsIURI> mDocumentURI;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+ nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;
+};
+
+} // namespace docshell
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/prefetch/OfflineCacheUpdateParent.cpp b/uriloader/prefetch/OfflineCacheUpdateParent.cpp
new file mode 100644
index 0000000000..3414617385
--- /dev/null
+++ b/uriloader/prefetch/OfflineCacheUpdateParent.cpp
@@ -0,0 +1,287 @@
+/* -*- 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 "OfflineCacheUpdateParent.h"
+
+#include "BackgroundUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsOfflineCacheUpdate.h"
+#include "nsIApplicationCache.h"
+#include "nsNetUtil.h"
+
+using namespace mozilla::ipc;
+using mozilla::BasePrincipal;
+using mozilla::OriginAttributes;
+using mozilla::dom::BrowserParent;
+
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=nsOfflineCacheUpdate:5
+// set MOZ_LOG_FILE=offlineupdate.log
+//
+// this enables LogLevel::Debug level information and places all output in
+// the file offlineupdate.log
+//
+extern mozilla::LazyLogModule gOfflineCacheUpdateLog;
+
+#undef LOG
+#define LOG(args) \
+ MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args)
+
+#undef LOG_ENABLED
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug)
+
+namespace mozilla {
+namespace docshell {
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateParent::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(OfflineCacheUpdateParent, nsIOfflineCacheUpdateObserver,
+ nsILoadContext)
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateParent <public>
+//-----------------------------------------------------------------------------
+
+OfflineCacheUpdateParent::OfflineCacheUpdateParent() : mIPCClosed(false) {
+ // Make sure the service has been initialized
+ nsOfflineCacheUpdateService::EnsureService();
+
+ LOG(("OfflineCacheUpdateParent::OfflineCacheUpdateParent [%p]", this));
+}
+
+OfflineCacheUpdateParent::~OfflineCacheUpdateParent() {
+ LOG(("OfflineCacheUpdateParent::~OfflineCacheUpdateParent [%p]", this));
+}
+
+void OfflineCacheUpdateParent::ActorDestroy(ActorDestroyReason why) {
+ mIPCClosed = true;
+}
+
+nsresult OfflineCacheUpdateParent::Schedule(
+ nsIURI* aManifestURI, nsIURI* aDocumentURI,
+ const PrincipalInfo& aLoadingPrincipalInfo, const bool& stickDocument,
+ const CookieJarSettingsArgs& aCookieJarSettingsArgs) {
+ LOG(("OfflineCacheUpdateParent::RecvSchedule [%p]", this));
+
+ RefPtr<nsOfflineCacheUpdate> update;
+ if (!aManifestURI) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto loadingPrincipalOrErr = PrincipalInfoToPrincipal(aLoadingPrincipalInfo);
+
+ if (NS_WARN_IF(loadingPrincipalOrErr.isErr())) {
+ return loadingPrincipalOrErr.unwrapErr();
+ }
+
+ mLoadingPrincipal = loadingPrincipalOrErr.unwrap();
+
+ nsOfflineCacheUpdateService* service =
+ nsOfflineCacheUpdateService::EnsureService();
+ if (!service) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool offlinePermissionAllowed = false;
+
+ nsresult rv =
+ service->OfflineAppAllowed(mLoadingPrincipal, &offlinePermissionAllowed);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!offlinePermissionAllowed) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ if (!aDocumentURI) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!NS_SecurityCompareURIs(aManifestURI, aDocumentURI, false)) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ nsAutoCString originSuffix;
+ rv = mLoadingPrincipal->GetOriginSuffix(originSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ service->FindUpdate(aManifestURI, originSuffix, nullptr,
+ getter_AddRefs(update));
+ if (!update) {
+ update = new nsOfflineCacheUpdate();
+
+ // Leave aDocument argument null. Only glues and children keep
+ // document instances.
+ rv = update->Init(aManifestURI, aDocumentURI, mLoadingPrincipal, nullptr,
+ nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ update->SetCookieJarSettingsArgs(aCookieJarSettingsArgs);
+
+ // Must add before Schedule() call otherwise we would miss
+ // oncheck event notification.
+ update->AddObserver(this, false);
+
+ rv = update->Schedule();
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ update->AddObserver(this, false);
+ }
+
+ if (stickDocument) {
+ update->StickDocument(aDocumentURI);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::UpdateStateChanged(nsIOfflineCacheUpdate* aUpdate,
+ uint32_t state) {
+ if (mIPCClosed) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ LOG(("OfflineCacheUpdateParent::StateEvent [%p]", this));
+
+ uint64_t byteProgress;
+ aUpdate->GetByteProgress(&byteProgress);
+ Unused << SendNotifyStateEvent(state, byteProgress);
+
+ if (state == nsIOfflineCacheUpdateObserver::STATE_FINISHED) {
+ // Tell the child the particulars after the update has finished.
+ // Sending the Finish event will release the child side of the protocol
+ // and notify "offline-cache-update-completed" on the child process.
+ bool isUpgrade;
+ aUpdate->GetIsUpgrade(&isUpgrade);
+ bool succeeded;
+ aUpdate->GetSucceeded(&succeeded);
+
+ Unused << SendFinish(succeeded, isUpgrade);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::ApplicationCacheAvailable(
+ nsIApplicationCache* aApplicationCache) {
+ if (mIPCClosed) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ NS_ENSURE_ARG(aApplicationCache);
+
+ nsCString cacheClientId;
+ aApplicationCache->GetClientID(cacheClientId);
+ nsCString cacheGroupId;
+ aApplicationCache->GetGroupID(cacheGroupId);
+
+ Unused << SendAssociateDocuments(cacheGroupId, cacheClientId);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// OfflineCacheUpdateParent::nsILoadContext
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetAssociatedWindow(
+ mozIDOMWindowProxy** aAssociatedWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetTopWindow(mozIDOMWindowProxy** aTopWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetTopFrameElement(dom::Element** aElement) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetIsContent(bool* aIsContent) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetUsePrivateBrowsing(bool* aUsePrivateBrowsing) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+NS_IMETHODIMP
+OfflineCacheUpdateParent::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::SetPrivateBrowsing(bool aUsePrivateBrowsing) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetUseRemoteTabs(bool* aUseRemoteTabs) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::SetRemoteTabs(bool aUseRemoteTabs) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetUseRemoteSubframes(bool* aUseRemoteSubframes) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::SetRemoteSubframes(bool aUseRemoteSubframes) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetScriptableOriginAttributes(
+ JSContext* aCx, JS::MutableHandleValue aAttrs) {
+ NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED);
+
+ nsresult rv = mLoadingPrincipal->GetOriginAttributes(aCx, aAttrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+OfflineCacheUpdateParent::GetOriginAttributes(
+ mozilla::OriginAttributes& aAttrs) {
+ if (mLoadingPrincipal) {
+ aAttrs = mLoadingPrincipal->OriginAttributesRef();
+ }
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::GetUseTrackingProtection(
+ bool* aUseTrackingProtection) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+OfflineCacheUpdateParent::SetUseTrackingProtection(
+ bool aUseTrackingProtection) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace docshell
+} // namespace mozilla
diff --git a/uriloader/prefetch/OfflineCacheUpdateParent.h b/uriloader/prefetch/OfflineCacheUpdateParent.h
new file mode 100644
index 0000000000..b1bc3d90cc
--- /dev/null
+++ b/uriloader/prefetch/OfflineCacheUpdateParent.h
@@ -0,0 +1,64 @@
+/* -*- 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 nsOfflineCacheUpdateParent_h
+#define nsOfflineCacheUpdateParent_h
+
+#include "mozilla/docshell/POfflineCacheUpdateParent.h"
+#include "mozilla/BasePrincipal.h"
+#include "nsIOfflineCacheUpdate.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsILoadContext.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+
+namespace ipc {
+class URIParams;
+} // namespace ipc
+
+namespace net {
+class CookieJarSettingsArgs;
+}
+
+namespace docshell {
+
+class OfflineCacheUpdateParent : public POfflineCacheUpdateParent,
+ public nsIOfflineCacheUpdateObserver,
+ public nsILoadContext {
+ typedef mozilla::ipc::URIParams URIParams;
+ typedef mozilla::ipc::PrincipalInfo PrincipalInfo;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER
+ NS_DECL_NSILOADCONTEXT
+
+ nsresult Schedule(nsIURI* manifestURI, nsIURI* documentURI,
+ const PrincipalInfo& loadingPrincipalInfo,
+ const bool& stickDocument,
+ const net::CookieJarSettingsArgs& aCookieJarSettingsArgs);
+
+ void StopSendingMessagesToChild() { mIPCClosed = true; }
+
+ explicit OfflineCacheUpdateParent();
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ ~OfflineCacheUpdateParent();
+
+ bool mIPCClosed;
+
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+};
+
+} // namespace docshell
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/prefetch/POfflineCacheUpdate.ipdl b/uriloader/prefetch/POfflineCacheUpdate.ipdl
new file mode 100644
index 0000000000..5b4a0961a0
--- /dev/null
+++ b/uriloader/prefetch/POfflineCacheUpdate.ipdl
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 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 PContent;
+
+namespace mozilla {
+namespace docshell {
+
+//-------------------------------------------------------------------
+refcounted protocol POfflineCacheUpdate
+{
+ manager PContent;
+
+parent:
+ async __delete__();
+
+child:
+ async NotifyStateEvent(uint32_t stateEvent, uint64_t byteProgress);
+ async AssociateDocuments(nsCString cacheGroupId, nsCString cacheClientId);
+ async Finish(bool succeeded, bool isUpgrade);
+};
+
+}
+}
diff --git a/uriloader/prefetch/moz.build b/uriloader/prefetch/moz.build
new file mode 100644
index 0000000000..9c3a00bac4
--- /dev/null
+++ b/uriloader/prefetch/moz.build
@@ -0,0 +1,44 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Networking: Cache")
+
+XPIDL_SOURCES += [
+ "nsIOfflineCacheUpdate.idl",
+ "nsIPrefetchService.idl",
+]
+
+XPIDL_MODULE = "prefetch"
+
+EXPORTS.mozilla.docshell += [
+ "OfflineCacheUpdateChild.h",
+ "OfflineCacheUpdateParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "nsOfflineCacheUpdate.cpp",
+ "nsOfflineCacheUpdateService.cpp",
+ "nsPrefetchService.cpp",
+ "OfflineCacheUpdateChild.cpp",
+ "OfflineCacheUpdateGlue.cpp",
+ "OfflineCacheUpdateParent.cpp",
+]
+
+IPDL_SOURCES += [
+ "POfflineCacheUpdate.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+]
+
+if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += ["-Wno-error=shadow"]
diff --git a/uriloader/prefetch/nsIOfflineCacheUpdate.idl b/uriloader/prefetch/nsIOfflineCacheUpdate.idl
new file mode 100644
index 0000000000..00dc8c6e19
--- /dev/null
+++ b/uriloader/prefetch/nsIOfflineCacheUpdate.idl
@@ -0,0 +1,291 @@
+/* -*- 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 mozIDOMWindow;
+interface nsIURI;
+interface nsIOfflineCacheUpdate;
+interface nsIPrincipal;
+interface nsIPrefBranch;
+interface nsIApplicationCache;
+interface nsIFile;
+interface nsIObserver;
+interface nsICookieJarSettings;
+webidl Document;
+
+[scriptable, uuid(47360d57-8ef4-4a5d-8865-1a27a739ad1a)]
+interface nsIOfflineCacheUpdateObserver : nsISupports {
+ const unsigned long STATE_ERROR = 1;
+ const unsigned long STATE_CHECKING = 2;
+ const unsigned long STATE_NOUPDATE = 3;
+ const unsigned long STATE_OBSOLETE = 4;
+ const unsigned long STATE_DOWNLOADING = 5;
+ const unsigned long STATE_ITEMSTARTED = 6;
+ const unsigned long STATE_ITEMCOMPLETED = 7;
+ const unsigned long STATE_ITEMPROGRESS = 8;
+ const unsigned long STATE_FINISHED = 10;
+
+ /**
+ * aUpdate has changed its state.
+ *
+ * @param aUpdate
+ * The nsIOfflineCacheUpdate being processed.
+ * @param event
+ * See enumeration above
+ */
+ void updateStateChanged(in nsIOfflineCacheUpdate aUpdate, in uint32_t state);
+
+ /**
+ * Informs the observer about an application being available to associate.
+ *
+ * @param applicationCache
+ * The application cache instance that has been created or found by the
+ * update to associate with
+ */
+ void applicationCacheAvailable(in nsIApplicationCache applicationCache);
+};
+
+/**
+ * An nsIOfflineCacheUpdate is used to update an application's offline
+ * resources.
+ *
+ * It can be used to perform partial or complete updates.
+ *
+ * One update object will be updating at a time. The active object will
+ * load its items one by one, sending itemCompleted() to any registered
+ * observers.
+ */
+[scriptable, uuid(6e3e26ea-45b2-4db7-9e4a-93b965679298)]
+interface nsIOfflineCacheUpdate : nsISupports {
+ /**
+ * Fetch the status of the running update. This will return a value
+ * defined in OfflineResourceList.webidl.
+ */
+ readonly attribute unsigned short status;
+
+ /**
+ * TRUE if the update is being used to add specific resources.
+ * FALSE if the complete cache update process is happening.
+ */
+ readonly attribute boolean partial;
+
+ /**
+ * TRUE if this is an upgrade attempt, FALSE if it is a new cache
+ * attempt.
+ */
+ readonly attribute boolean isUpgrade;
+
+ /**
+ * The domain being updated, and the domain that will own any URIs added
+ * with this update.
+ */
+ readonly attribute ACString updateDomain;
+
+ /**
+ * The manifest for the offline application being updated.
+ */
+ readonly attribute nsIURI manifestURI;
+
+ /**
+ * The principal of the page that is requesting the update.
+ */
+ readonly attribute nsIPrincipal loadingPrincipal;
+
+ /**
+ * TRUE if the cache update completed successfully.
+ */
+ readonly attribute boolean succeeded;
+
+ /**
+ * Initialize the update.
+ *
+ * @param aManifestURI
+ * The manifest URI to be checked.
+ * @param aDocumentURI
+ * The page that is requesting the update.
+ * @param aLoadingPrincipal
+ * The principal of the page that is requesting the update.
+ */
+ void init(in nsIURI aManifestURI,
+ in nsIURI aDocumentURI,
+ in nsIPrincipal aLoadingPrincipal,
+ in Document aDocument,
+ [optional] in nsIFile aCustomProfileDir);
+
+ /**
+ * Initialize the update for partial processing.
+ *
+ * @param aManifestURI
+ * The manifest URI of the related cache.
+ * @param aClientID
+ * Client ID of the cache to store resource to. This ClientID
+ * must be ID of cache in the cache group identified by
+ * the manifest URI passed in the first parameter.
+ * @param aDocumentURI
+ * The page that is requesting the update. May be null
+ * when this information is unknown.
+ * @param aCookieJarSettings
+ * The cookie jar settings belonging to the page that is requesting
+ * the update.
+ */
+ void initPartial(in nsIURI aManifestURI, in ACString aClientID,
+ in nsIURI aDocumentURI, in nsIPrincipal aPrincipal,
+ in nsICookieJarSettings aCookieJarSettings);
+
+ /**
+ * Initialize the update to only check whether there is an update
+ * to the manifest available (if it has actually changed on the server).
+ *
+ * @param aManifestURI
+ * The manifest URI of the related cache.
+ * @param aObserver
+ * nsIObserver implementation that receives the result.
+ * When aTopic == "offline-cache-update-available" there is an update to
+ * to download. Update of the app cache will lead to a new version
+ * download.
+ * When aTopic == "offline-cache-update-unavailable" then there is no
+ * update available (the manifest has not changed on the server).
+ */
+ void initForUpdateCheck(in nsIURI aManifestURI,
+ in nsIPrincipal aLoadingPrincipal,
+ in nsIObserver aObserver);
+
+ /**
+ * Add a dynamic URI to the offline cache as part of the update.
+ *
+ * @param aURI
+ * The URI to add.
+ */
+ void addDynamicURI(in nsIURI aURI);
+
+ /**
+ * Add the update to the offline update queue. An offline-cache-update-added
+ * event will be sent to the observer service.
+ */
+ void schedule();
+
+ /**
+ * Observe loads that are added to the update.
+ *
+ * @param aObserver
+ * object that notifications will be sent to.
+ * @param aHoldWeak
+ * TRUE if you want the update to hold a weak reference to the
+ * observer, FALSE for a strong reference.
+ */
+ void addObserver(in nsIOfflineCacheUpdateObserver aObserver,
+ [optional] in boolean aHoldWeak);
+
+ /**
+ * Remove an observer from the update.
+ *
+ * @param aObserver
+ * the observer to remove.
+ */
+ void removeObserver(in nsIOfflineCacheUpdateObserver aObserver);
+
+ /**
+ * Cancel the update when still in progress. This stops all running resource
+ * downloads and discards the downloaded cache version. Throws when update
+ * has already finished and made the new cache version active.
+ */
+ void cancel();
+
+ /**
+ * Return the number of bytes downloaded so far
+ */
+ readonly attribute uint64_t byteProgress;
+};
+
+[scriptable, uuid(44971e74-37e4-4140-8677-a4cf213a3f4b)]
+interface nsIOfflineCacheUpdateService : nsISupports {
+ /**
+ * Constants for the offline-app permission.
+ *
+ * XXX: This isn't a great place for this, but it's really the only
+ * private offline-app-related interface
+ */
+
+ /**
+ * Allow the domain to use offline APIs, and don't warn about excessive
+ * usage.
+ */
+ const unsigned long ALLOW_NO_WARN = 3;
+
+ /**
+ * Access to the list of cache updates that have been scheduled.
+ */
+ readonly attribute unsigned long numUpdates;
+ nsIOfflineCacheUpdate getUpdate(in unsigned long index);
+
+ /**
+ * Schedule a cache update for a given offline manifest. If an
+ * existing update is scheduled or running, that update will be returned.
+ * Otherwise a new update will be scheduled.
+ */
+ nsIOfflineCacheUpdate scheduleUpdate(in nsIURI aManifestURI,
+ in nsIURI aDocumentURI,
+ in nsIPrincipal aLoadingPrincipal,
+ in mozIDOMWindow aWindow);
+
+ /**
+ * Schedule a cache update for a given offline manifest using app cache
+ * bound to the given appID flag. If an existing update is scheduled or
+ * running, that update will be returned. Otherwise a new update will be
+ * scheduled.
+ */
+ nsIOfflineCacheUpdate scheduleAppUpdate(in nsIURI aManifestURI,
+ in nsIURI aDocumentURI,
+ in nsIPrincipal aLoadingPrincipal,
+ in nsIFile aProfileDir);
+
+ /**
+ * Schedule a cache update for a manifest when the document finishes
+ * loading.
+ */
+ void scheduleOnDocumentStop(in nsIURI aManifestURI,
+ in nsIURI aDocumentURI,
+ in nsIPrincipal aLoadingPrincipal,
+ in Document aDocument);
+
+ /**
+ * Schedule a check to see if an update is available.
+ *
+ * This will not update or make any changes to the appcache.
+ * It only notifies the observer to indicate whether the manifest has
+ * changed on the server (or not): a changed manifest means that an
+ * update is available.
+ *
+ * For arguments see nsIOfflineCacheUpdate.initForUpdateCheck() method
+ * description.
+ */
+ void checkForUpdate(in nsIURI aManifestURI,
+ in nsIPrincipal aLoadingPrincipal,
+ in nsIObserver aObserver);
+
+ /**
+ * Checks whether a principal should have access to the offline
+ * cache.
+ * @param aPrincipal
+ * The principal to check.
+ */
+ boolean offlineAppAllowed(in nsIPrincipal aPrincipal);
+
+ /**
+ * Checks whether a document at the given URI should have access
+ * to the offline cache.
+ * @param aURI
+ * The URI to check
+ */
+ boolean offlineAppAllowedForURI(in nsIURI aURI);
+
+ /**
+ * Sets the "offline-app" permission for the principal.
+ * In the single process model calls directly on permission manager.
+ * In the multi process model dispatches to the parent process.
+ */
+ void allowOfflineApp(in nsIPrincipal aPrincipal);
+};
diff --git a/uriloader/prefetch/nsIPrefetchService.idl b/uriloader/prefetch/nsIPrefetchService.idl
new file mode 100644
index 0000000000..160d051116
--- /dev/null
+++ b/uriloader/prefetch/nsIPrefetchService.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+#include "nsIContentPolicy.idl"
+
+interface nsIURI;
+interface nsISimpleEnumerator;
+interface nsIReferrerInfo;
+
+webidl Node;
+
+[scriptable, uuid(422a1807-4e7f-463d-b8d7-ca2ceb9b5d53)]
+interface nsIPrefetchService : nsISupports
+{
+ /**
+ * Enqueue a request to prefetch the specified URI.
+ *
+ * @param aURI the URI of the document to prefetch
+ * @param aReferrerInfo the referrerInfo of the request
+ * @param aSource the DOM node (such as a <link> tag) that requested this
+ * fetch, or null if the prefetch was not requested by a DOM node.
+ * @param aExplicit the link element has an explicit prefetch link type
+ */
+ void prefetchURI(in nsIURI aURI,
+ in nsIReferrerInfo aReferrerInfo,
+ in Node aSource,
+ in boolean aExplicit);
+
+ /**
+ * Start a preload of the specified URI.
+ *
+ * @param aURI the URI of the document to preload
+ * @param aReferrerInfo the referrerInfo of the request
+ * @param aSource the DOM node (such as a <link> tag) that requested this
+ * fetch, or null if the prefetch was not requested by a DOM node.
+ * @param aPolicyType content policy to be used for this load.
+ */
+ void preloadURI(in nsIURI aURI,
+ in nsIReferrerInfo aReferrerInfo,
+ in Node aSource,
+ in nsContentPolicyType aPolicyType);
+
+ /**
+ * Find out if there are any prefetches running or queued
+ */
+ boolean hasMoreElements();
+
+ /**
+ * Cancel prefetch or preload for a Node.
+ */
+ void cancelPrefetchPreloadURI(in nsIURI aURI, in Node aSource);
+};
diff --git a/uriloader/prefetch/nsOfflineCacheUpdate.cpp b/uriloader/prefetch/nsOfflineCacheUpdate.cpp
new file mode 100644
index 0000000000..62561cdaa3
--- /dev/null
+++ b/uriloader/prefetch/nsOfflineCacheUpdate.cpp
@@ -0,0 +1,2332 @@
+/* -*- 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 "nsOfflineCacheUpdate.h"
+
+#include "nsCURILoader.h"
+#include "nsIApplicationCacheChannel.h"
+#include "nsIApplicationCacheService.h"
+#include "nsICachingChannel.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/OfflineResourceListBinding.h"
+#include "mozilla/dom/Document.h"
+#include "nsIURL.h"
+#include "nsICryptoHash.h"
+#include "nsICacheEntry.h"
+#include "nsIHttpChannel.h"
+#include "nsIPrincipal.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+#include "nsIConsoleService.h"
+#include "mozilla/Logging.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Attributes.h"
+#include "nsContentUtils.h"
+#include "nsIPrincipal.h"
+#include "nsDiskCacheDeviceSQL.h"
+#include "ReferrerInfo.h"
+
+#include "nsXULAppAPI.h"
+
+using namespace mozilla;
+
+static const uint32_t kRescheduleLimit = 3;
+// Max number of retries for every entry of pinned app.
+static const uint32_t kPinnedEntryRetriesLimit = 3;
+// Maximum number of parallel items loads
+static const uint32_t kParallelLoadLimit = 15;
+
+// Quota for offline apps when preloading
+static const int32_t kCustomProfileQuota = 512000;
+
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=nsOfflineCacheUpdate:5
+// set MOZ_LOG_FILE=offlineupdate.log
+//
+// this enables LogLevel::Debug level information and places all output in
+// the file offlineupdate.log
+//
+extern LazyLogModule gOfflineCacheUpdateLog;
+
+#undef LOG
+#define LOG(args) \
+ MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args)
+
+#undef LOG_ENABLED
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug)
+
+namespace {
+
+nsresult DropReferenceFromURL(nsCOMPtr<nsIURI>& aURI) {
+ // XXXdholbert If this SetRef fails, callers of this method probably
+ // want to call aURI->CloneIgnoringRef() and use the result of that.
+ nsCOMPtr<nsIURI> uri(aURI);
+ return NS_GetURIWithoutRef(uri, getter_AddRefs(aURI));
+}
+
+void LogToConsole(const char* message,
+ nsOfflineCacheUpdateItem* item = nullptr) {
+ nsCOMPtr<nsIConsoleService> consoleService =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ if (consoleService) {
+ nsAutoString messageUTF16 = NS_ConvertUTF8toUTF16(message);
+ if (item && item->mURI) {
+ messageUTF16.AppendLiteral(", URL=");
+ messageUTF16.Append(
+ NS_ConvertUTF8toUTF16(item->mURI->GetSpecOrDefault()));
+ }
+ consoleService->LogStringMessage(messageUTF16.get());
+ }
+}
+
+} // namespace
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck
+//-----------------------------------------------------------------------------
+
+class nsManifestCheck final : public nsIStreamListener,
+ public nsIChannelEventSink,
+ public nsIInterfaceRequestor {
+ public:
+ nsManifestCheck(nsOfflineCacheUpdate* aUpdate, nsIURI* aURI,
+ nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal)
+ : mUpdate(aUpdate),
+ mURI(aURI),
+ mReferrerURI(aReferrerURI),
+ mLoadingPrincipal(aLoadingPrincipal) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIINTERFACEREQUESTOR
+
+ nsresult Begin();
+
+ private:
+ ~nsManifestCheck() {}
+
+ static nsresult ReadManifest(nsIInputStream* aInputStream, void* aClosure,
+ const char* aFromSegment, uint32_t aOffset,
+ uint32_t aCount, uint32_t* aBytesConsumed);
+
+ RefPtr<nsOfflineCacheUpdate> mUpdate;
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIURI> mReferrerURI;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+ nsCOMPtr<nsICryptoHash> mManifestHash;
+ nsCOMPtr<nsIChannel> mChannel;
+};
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsISupports
+//-----------------------------------------------------------------------------
+NS_IMPL_ISUPPORTS(nsManifestCheck, nsIRequestObserver, nsIStreamListener,
+ nsIChannelEventSink, nsIInterfaceRequestor)
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck <public>
+//-----------------------------------------------------------------------------
+
+nsresult nsManifestCheck::Begin() {
+ nsresult rv;
+ mManifestHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mManifestHash->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NS_NewChannel(getter_AddRefs(mChannel), mURI, mLoadingPrincipal,
+ nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED,
+ nsIContentPolicy::TYPE_OTHER, mUpdate->CookieJarSettings(),
+ nullptr, // PerformanceStorage
+ nullptr, // loadGroup
+ nullptr, // aCallbacks
+ nsIRequest::LOAD_BYPASS_CACHE);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // configure HTTP specific stuff
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ nsCOMPtr<nsIReferrerInfo> referrerInfo =
+ new mozilla::dom::ReferrerInfo(mReferrerURI);
+ rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv =
+ httpChannel->SetRequestHeader("X-Moz"_ns, "offline-resource"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ return mChannel->AsyncOpen(this);
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck <public>
+//-----------------------------------------------------------------------------
+
+/* static */
+nsresult nsManifestCheck::ReadManifest(nsIInputStream* aInputStream,
+ void* aClosure, const char* aFromSegment,
+ uint32_t aOffset, uint32_t aCount,
+ uint32_t* aBytesConsumed) {
+ nsManifestCheck* manifestCheck = static_cast<nsManifestCheck*>(aClosure);
+
+ nsresult rv;
+ *aBytesConsumed = aCount;
+
+ rv = manifestCheck->mManifestHash->Update(
+ reinterpret_cast<const uint8_t*>(aFromSegment), aCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsIStreamListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsManifestCheck::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
+
+NS_IMETHODIMP
+nsManifestCheck::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t bytesRead;
+ aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsManifestCheck::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ nsAutoCString manifestHash;
+ if (NS_SUCCEEDED(aStatus)) {
+ mManifestHash->Finish(true, manifestHash);
+ }
+
+ mUpdate->ManifestCheckCompleted(aStatus, manifestHash);
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsIInterfaceRequestor
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsManifestCheck::GetInterface(const nsIID& aIID, void** aResult) {
+ if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
+ NS_ADDREF_THIS();
+ *aResult = static_cast<nsIChannelEventSink*>(this);
+ return NS_OK;
+ }
+
+ return NS_ERROR_NO_INTERFACE;
+}
+
+//-----------------------------------------------------------------------------
+// nsManifestCheck::nsIChannelEventSink
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsManifestCheck::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* callback) {
+ // Redirects should cause the load (and therefore the update) to fail.
+ if (aFlags & nsIChannelEventSink::REDIRECT_INTERNAL) {
+ callback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+ }
+
+ LogToConsole("Manifest check failed because its response is a redirect");
+
+ aOldChannel->Cancel(NS_ERROR_ABORT);
+ return NS_ERROR_ABORT;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateItem::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateItem, nsIRequestObserver,
+ nsIStreamListener, nsIRunnable, nsIInterfaceRequestor,
+ nsIChannelEventSink)
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateItem <public>
+//-----------------------------------------------------------------------------
+
+nsOfflineCacheUpdateItem::nsOfflineCacheUpdateItem(
+ nsIURI* aURI, nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal,
+ nsIApplicationCache* aApplicationCache,
+ nsIApplicationCache* aPreviousApplicationCache, uint32_t type,
+ uint32_t loadFlags)
+ : mURI(aURI),
+ mReferrerURI(aReferrerURI),
+ mLoadingPrincipal(aLoadingPrincipal),
+ mApplicationCache(aApplicationCache),
+ mPreviousApplicationCache(aPreviousApplicationCache),
+ mItemType(type),
+ mLoadFlags(loadFlags),
+ mChannel(nullptr),
+ mState(LoadStatus::UNINITIALIZED),
+ mBytesRead(0) {}
+
+nsOfflineCacheUpdateItem::~nsOfflineCacheUpdateItem() {}
+
+nsresult nsOfflineCacheUpdateItem::OpenChannel(nsOfflineCacheUpdate* aUpdate) {
+ if (LOG_ENABLED()) {
+ LOG(("%p: Opening channel for %s", this, mURI->GetSpecOrDefault().get()));
+ }
+
+ if (mUpdate) {
+ // Holding a reference to the update means this item is already
+ // in progress (has a channel, or is just in between OnStopRequest()
+ // and its Run() call. We must never open channel on this item again.
+ LOG((" %p is already running! ignoring", this));
+ return NS_ERROR_ALREADY_OPENED;
+ }
+
+ nsresult rv = nsOfflineCacheUpdate::GetCacheKey(mURI, mCacheKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t flags =
+ nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED;
+
+ if (mApplicationCache == mPreviousApplicationCache) {
+ // Same app cache to read from and to write to is used during
+ // an only-update-check procedure. Here we protect the existing
+ // cache from being modified.
+ flags |= nsIRequest::INHIBIT_CACHING;
+ }
+
+ flags |= mLoadFlags;
+
+ rv = NS_NewChannel(getter_AddRefs(mChannel), mURI, mLoadingPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER, aUpdate->CookieJarSettings(),
+ nullptr, // PerformanceStorage
+ nullptr, // aLoadGroup
+ this, // aCallbacks
+ flags);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel =
+ do_QueryInterface(mChannel, &rv);
+
+ // Support for nsIApplicationCacheChannel is required.
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Use the existing application cache as the cache to check.
+ rv = appCacheChannel->SetApplicationCache(mPreviousApplicationCache);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set the new application cache as the target for write.
+ rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // configure HTTP specific stuff
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ nsCOMPtr<nsIReferrerInfo> referrerInfo =
+ new mozilla::dom::ReferrerInfo(mReferrerURI);
+ rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv =
+ httpChannel->SetRequestHeader("X-Moz"_ns, "offline-resource"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ rv = mChannel->AsyncOpen(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mUpdate = aUpdate;
+
+ mState = LoadStatus::REQUESTED;
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdateItem::Cancel() {
+ if (mChannel) {
+ mChannel->Cancel(NS_ERROR_ABORT);
+ mChannel = nullptr;
+ }
+
+ mState = LoadStatus::UNINITIALIZED;
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateItem::nsIStreamListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateItem::OnStartRequest(nsIRequest* aRequest) {
+ mState = LoadStatus::RECEIVING;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateItem::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t bytesRead = 0;
+ aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead);
+ mBytesRead += bytesRead;
+ LOG(("loaded %u bytes into offline cache [offset=%" PRIu64 "]\n", bytesRead,
+ aOffset));
+
+ mUpdate->OnByteProgress(bytesRead);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateItem::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatus) {
+ if (LOG_ENABLED()) {
+ LOG(("%p: Done fetching offline item %s [status=%" PRIx32 "]\n", this,
+ mURI->GetSpecOrDefault().get(), static_cast<uint32_t>(aStatus)));
+ }
+
+ if (mBytesRead == 0 && aStatus == NS_OK) {
+ // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was
+ // specified), but the object should report loadedSize as if it
+ // did.
+ mChannel->GetContentLength(&mBytesRead);
+ mUpdate->OnByteProgress(mBytesRead);
+ }
+
+ if (NS_FAILED(aStatus)) {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ bool isNoStore;
+ if (NS_SUCCEEDED(httpChannel->IsNoStoreResponse(&isNoStore)) &&
+ isNoStore) {
+ LogToConsole(
+ "Offline cache manifest item has Cache-control: no-store header",
+ this);
+ }
+ }
+ }
+
+ // We need to notify the update that the load is complete, but we
+ // want to give the channel a chance to close the cache entries.
+ NS_DispatchToCurrentThread(this);
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateItem::nsIRunnable
+//-----------------------------------------------------------------------------
+NS_IMETHODIMP
+nsOfflineCacheUpdateItem::Run() {
+ // Set mState to LOADED here rather than in OnStopRequest to prevent
+ // race condition when checking state of all mItems in ProcessNextURI().
+ // If state would have been set in OnStopRequest we could mistakenly
+ // take this item as already finished and finish the update process too
+ // early when ProcessNextURI() would get called between OnStopRequest()
+ // and Run() of this item. Finish() would then have been called twice.
+ mState = LoadStatus::LOADED;
+
+ RefPtr<nsOfflineCacheUpdate> update;
+ update.swap(mUpdate);
+ update->LoadCompleted(this);
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateItem::nsIInterfaceRequestor
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateItem::GetInterface(const nsIID& aIID, void** aResult) {
+ if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
+ NS_ADDREF_THIS();
+ *aResult = static_cast<nsIChannelEventSink*>(this);
+ return NS_OK;
+ }
+
+ return NS_ERROR_NO_INTERFACE;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateItem::nsIChannelEventSink
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateItem::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* cb) {
+ if (!(aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)) {
+ // Don't allow redirect in case of non-internal redirect and cancel
+ // the channel to clean the cache entry.
+ LogToConsole("Offline cache manifest failed because an item redirects",
+ this);
+
+ aOldChannel->Cancel(NS_ERROR_ABORT);
+ return NS_ERROR_ABORT;
+ }
+
+ nsCOMPtr<nsIURI> newURI;
+ nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel =
+ do_QueryInterface(aNewChannel);
+ if (appCacheChannel) {
+ rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsAutoCString oldScheme;
+ mURI->GetScheme(oldScheme);
+
+ if (!newURI->SchemeIs(oldScheme.get())) {
+ LOG(("rejected: redirected to a different scheme\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ // HTTP request headers are not automatically forwarded to the new channel.
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);
+ NS_ENSURE_STATE(httpChannel);
+
+ rv = httpChannel->SetRequestHeader("X-Moz"_ns, "offline-resource"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ mChannel = aNewChannel;
+
+ cb->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdateItem::GetRequestSucceeded(bool* succeeded) {
+ *succeeded = false;
+
+ if (!mChannel) return NS_OK;
+
+ nsresult rv;
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool reqSucceeded;
+ rv = httpChannel->GetRequestSucceeded(&reqSucceeded);
+ if (NS_ERROR_NOT_AVAILABLE == rv) return NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!reqSucceeded) {
+ LOG(("Request failed"));
+ return NS_OK;
+ }
+
+ nsresult channelStatus;
+ rv = httpChannel->GetStatus(&channelStatus);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (NS_FAILED(channelStatus)) {
+ LOG(("Channel status=0x%08" PRIx32, static_cast<uint32_t>(channelStatus)));
+ return NS_OK;
+ }
+
+ *succeeded = true;
+ return NS_OK;
+}
+
+bool nsOfflineCacheUpdateItem::IsScheduled() {
+ return mState == LoadStatus::UNINITIALIZED;
+}
+
+bool nsOfflineCacheUpdateItem::IsInProgress() {
+ return mState == LoadStatus::REQUESTED || mState == LoadStatus::RECEIVING;
+}
+
+bool nsOfflineCacheUpdateItem::IsCompleted() {
+ return mState == LoadStatus::LOADED;
+}
+
+nsresult nsOfflineCacheUpdateItem::GetStatus(uint16_t* aStatus) {
+ if (!mChannel) {
+ *aStatus = 0;
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t httpStatus;
+ rv = httpChannel->GetResponseStatus(&httpStatus);
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ *aStatus = 0;
+ return NS_OK;
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aStatus = uint16_t(httpStatus);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineManifestItem
+//-----------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+// nsOfflineManifestItem <public>
+//-----------------------------------------------------------------------------
+
+nsOfflineManifestItem::nsOfflineManifestItem(
+ nsIURI* aURI, nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal,
+ nsIApplicationCache* aApplicationCache,
+ nsIApplicationCache* aPreviousApplicationCache)
+ : nsOfflineCacheUpdateItem(aURI, aReferrerURI, aLoadingPrincipal,
+ aApplicationCache, aPreviousApplicationCache,
+ nsIApplicationCache::ITEM_MANIFEST, 0),
+ mParserState(PARSE_INIT),
+ mNeedsUpdate(true),
+ mStrictFileOriginPolicy(false),
+ mManifestHashInitialized(false) {
+ ReadStrictFileOriginPolicyPref();
+}
+
+nsOfflineManifestItem::~nsOfflineManifestItem() {}
+
+//-----------------------------------------------------------------------------
+// nsOfflineManifestItem <private>
+//-----------------------------------------------------------------------------
+
+/* static */
+nsresult nsOfflineManifestItem::ReadManifest(nsIInputStream* aInputStream,
+ void* aClosure,
+ const char* aFromSegment,
+ uint32_t aOffset, uint32_t aCount,
+ uint32_t* aBytesConsumed) {
+ nsOfflineManifestItem* manifest =
+ static_cast<nsOfflineManifestItem*>(aClosure);
+
+ nsresult rv;
+
+ *aBytesConsumed = aCount;
+
+ if (manifest->mParserState == PARSE_ERROR) {
+ // parse already failed, ignore this
+ return NS_OK;
+ }
+
+ if (!manifest->mManifestHashInitialized) {
+ // Avoid re-creation of crypto hash when it fails from some reason the first
+ // time
+ manifest->mManifestHashInitialized = true;
+
+ manifest->mManifestHash =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ rv = manifest->mManifestHash->Init(nsICryptoHash::MD5);
+ if (NS_FAILED(rv)) {
+ manifest->mManifestHash = nullptr;
+ LOG(
+ ("Could not initialize manifest hash for byte-to-byte check, "
+ "rv=%08" PRIx32,
+ static_cast<uint32_t>(rv)));
+ }
+ }
+ }
+
+ if (manifest->mManifestHash) {
+ rv = manifest->mManifestHash->Update(
+ reinterpret_cast<const uint8_t*>(aFromSegment), aCount);
+ if (NS_FAILED(rv)) {
+ manifest->mManifestHash = nullptr;
+ LOG(("Could not update manifest hash, rv=%08" PRIx32,
+ static_cast<uint32_t>(rv)));
+ }
+ }
+
+ manifest->mReadBuf.Append(aFromSegment, aCount);
+
+ nsCString::const_iterator begin, iter, end;
+ manifest->mReadBuf.BeginReading(begin);
+ manifest->mReadBuf.EndReading(end);
+
+ for (iter = begin; iter != end; iter++) {
+ if (*iter == '\r' || *iter == '\n') {
+ rv = manifest->HandleManifestLine(begin, iter);
+
+ if (NS_FAILED(rv)) {
+ LOG(("HandleManifestLine failed with 0x%08" PRIx32,
+ static_cast<uint32_t>(rv)));
+ *aBytesConsumed = 0; // Avoid assertion failure in stream tee
+ return NS_ERROR_ABORT;
+ }
+
+ begin = iter;
+ begin++;
+ }
+ }
+
+ // any leftovers are saved for next time
+ manifest->mReadBuf = Substring(begin, end);
+
+ return NS_OK;
+}
+
+nsresult nsOfflineManifestItem::AddNamespace(uint32_t namespaceType,
+ const nsCString& namespaceSpec,
+ const nsCString& data)
+
+{
+ nsresult rv;
+ if (!mNamespaces) {
+ mNamespaces = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIApplicationCacheNamespace> ns = new nsApplicationCacheNamespace();
+
+ rv = ns->Init(namespaceType, namespaceSpec, data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mNamespaces->AppendElement(ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+static nsresult GetURIDirectory(nsIURI* uri, nsAutoCString& directory) {
+ nsresult rv;
+
+ nsAutoCString path;
+ uri->GetFilePath(path);
+ if (path.Find("%2f") != kNotFound || path.Find("%2F") != kNotFound) {
+ return NS_ERROR_DOM_BAD_URI;
+ }
+
+ nsCOMPtr<nsIURL> url(do_QueryInterface(uri, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = url->GetDirectory(directory);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+static nsresult CheckFileContainedInPath(nsIURI* file,
+ nsACString const& masterDirectory) {
+ nsresult rv;
+
+ nsAutoCString directory;
+ rv = GetURIDirectory(file, directory);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ bool contains = StringBeginsWith(directory, masterDirectory);
+ if (!contains) {
+ return NS_ERROR_DOM_BAD_URI;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsOfflineManifestItem::HandleManifestLine(
+ const nsCString::const_iterator& aBegin,
+ const nsCString::const_iterator& aEnd) {
+ nsCString::const_iterator begin = aBegin;
+ nsCString::const_iterator end = aEnd;
+
+ // all lines ignore trailing spaces and tabs
+ nsCString::const_iterator last = end;
+ --last;
+ while (end != begin && (*last == ' ' || *last == '\t')) {
+ --end;
+ --last;
+ }
+
+ if (mParserState == PARSE_INIT) {
+ // Allow a UTF-8 BOM
+ if (begin != end && static_cast<unsigned char>(*begin) == 0xef) {
+ if (++begin == end || static_cast<unsigned char>(*begin) != 0xbb ||
+ ++begin == end || static_cast<unsigned char>(*begin) != 0xbf) {
+ mParserState = PARSE_ERROR;
+ LogToConsole("Offline cache manifest BOM error", this);
+ return NS_OK;
+ }
+ ++begin;
+ }
+
+ const nsACString& magic = Substring(begin, end);
+
+ if (!magic.EqualsLiteral("CACHE MANIFEST")) {
+ mParserState = PARSE_ERROR;
+ LogToConsole("Offline cache manifest magic incorrect", this);
+ return NS_OK;
+ }
+
+ mParserState = PARSE_CACHE_ENTRIES;
+ return NS_OK;
+ }
+
+ // lines other than the first ignore leading spaces and tabs
+ while (begin != end && (*begin == ' ' || *begin == '\t')) begin++;
+
+ // ignore blank lines and comments
+ if (begin == end || *begin == '#') return NS_OK;
+
+ const nsACString& line = Substring(begin, end);
+
+ if (line.EqualsLiteral("CACHE:")) {
+ mParserState = PARSE_CACHE_ENTRIES;
+ return NS_OK;
+ }
+
+ if (line.EqualsLiteral("FALLBACK:")) {
+ mParserState = PARSE_FALLBACK_ENTRIES;
+ return NS_OK;
+ }
+
+ if (line.EqualsLiteral("NETWORK:")) {
+ mParserState = PARSE_BYPASS_ENTRIES;
+ return NS_OK;
+ }
+
+ // Every other section type we don't know must be silently ignored.
+ nsCString::const_iterator lastChar = end;
+ if (*(--lastChar) == ':') {
+ mParserState = PARSE_UNKNOWN_SECTION;
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ switch (mParserState) {
+ case PARSE_INIT:
+ case PARSE_ERROR: {
+ // this should have been dealt with earlier
+ return NS_ERROR_FAILURE;
+ }
+
+ case PARSE_UNKNOWN_SECTION: {
+ // just jump over
+ return NS_OK;
+ }
+
+ case PARSE_CACHE_ENTRIES: {
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), line, nullptr, mURI);
+ if (NS_FAILED(rv)) break;
+ if (NS_FAILED(DropReferenceFromURL(uri))) break;
+
+ nsAutoCString scheme;
+ uri->GetScheme(scheme);
+
+ // Manifest URIs must have the same scheme as the manifest.
+ if (!mURI->SchemeIs(scheme.get())) {
+ break;
+ }
+
+ mExplicitURIs.AppendObject(uri);
+
+ if (!NS_SecurityCompareURIs(mURI, uri, mStrictFileOriginPolicy)) {
+ mAnonymousURIs.AppendObject(uri);
+ }
+
+ break;
+ }
+
+ case PARSE_FALLBACK_ENTRIES: {
+ int32_t separator = line.FindChar(' ');
+ if (separator == kNotFound) {
+ separator = line.FindChar('\t');
+ if (separator == kNotFound) break;
+ }
+
+ nsCString namespaceSpec(Substring(line, 0, separator));
+ nsCString fallbackSpec(Substring(line, separator + 1));
+ namespaceSpec.CompressWhitespace();
+ fallbackSpec.CompressWhitespace();
+
+ nsCOMPtr<nsIURI> namespaceURI;
+ rv =
+ NS_NewURI(getter_AddRefs(namespaceURI), namespaceSpec, nullptr, mURI);
+ if (NS_FAILED(rv)) break;
+ if (NS_FAILED(DropReferenceFromURL(namespaceURI))) break;
+ rv = namespaceURI->GetAsciiSpec(namespaceSpec);
+ if (NS_FAILED(rv)) break;
+
+ nsCOMPtr<nsIURI> fallbackURI;
+ rv = NS_NewURI(getter_AddRefs(fallbackURI), fallbackSpec, nullptr, mURI);
+ if (NS_FAILED(rv)) break;
+ if (NS_FAILED(DropReferenceFromURL(fallbackURI))) break;
+ rv = fallbackURI->GetAsciiSpec(fallbackSpec);
+ if (NS_FAILED(rv)) break;
+
+ // The following set of checks is preventing a website under
+ // a subdirectory to add fallback pages for the whole origin
+ // (or a parent directory) to prevent fallback attacks.
+ nsAutoCString manifestDirectory;
+ rv = GetURIDirectory(mURI, manifestDirectory);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ rv = CheckFileContainedInPath(namespaceURI, manifestDirectory);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ rv = CheckFileContainedInPath(fallbackURI, manifestDirectory);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+
+ // Manifest and namespace must be same origin
+ if (!NS_SecurityCompareURIs(mURI, namespaceURI, mStrictFileOriginPolicy))
+ break;
+
+ // Fallback and namespace must be same origin
+ if (!NS_SecurityCompareURIs(namespaceURI, fallbackURI,
+ mStrictFileOriginPolicy))
+ break;
+
+ mFallbackURIs.AppendObject(fallbackURI);
+
+ AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_FALLBACK,
+ namespaceSpec, fallbackSpec);
+ break;
+ }
+
+ case PARSE_BYPASS_ENTRIES: {
+ if (line[0] == '*' &&
+ (line.Length() == 1 || line[1] == ' ' || line[1] == '\t')) {
+ // '*' indicates to make the online whitelist wildcard flag open,
+ // i.e. do allow load of resources not present in the offline cache
+ // or not conforming any namespace.
+ // We achive that simply by adding an 'empty' - i.e. universal
+ // namespace of BYPASS type into the cache.
+ AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, ""_ns,
+ ""_ns);
+ break;
+ }
+
+ nsCOMPtr<nsIURI> bypassURI;
+ rv = NS_NewURI(getter_AddRefs(bypassURI), line, nullptr, mURI);
+ if (NS_FAILED(rv)) break;
+
+ nsAutoCString scheme;
+ bypassURI->GetScheme(scheme);
+ if (!mURI->SchemeIs(scheme.get())) {
+ break;
+ }
+ if (NS_FAILED(DropReferenceFromURL(bypassURI))) break;
+ nsCString spec;
+ if (NS_FAILED(bypassURI->GetAsciiSpec(spec))) break;
+
+ AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, spec, ""_ns);
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsOfflineManifestItem::GetOldManifestContentHash(
+ nsIRequest* aRequest) {
+ nsresult rv;
+
+ nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // load the main cache token that is actually the old offline cache token and
+ // read previous manifest content hash value
+ nsCOMPtr<nsISupports> cacheToken;
+ cachingChannel->GetCacheToken(getter_AddRefs(cacheToken));
+ if (cacheToken) {
+ nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cacheDescriptor->GetMetaDataElement(
+ "offline-manifest-hash", getter_Copies(mOldManifestHashValue));
+ if (NS_FAILED(rv)) mOldManifestHashValue.Truncate();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsOfflineManifestItem::CheckNewManifestContentHash(
+ nsIRequest* aRequest) {
+ nsresult rv;
+
+ if (!mManifestHash) {
+ // Nothing to compare against...
+ return NS_OK;
+ }
+
+ nsCString newManifestHashValue;
+ rv = mManifestHash->Finish(true, mManifestHashValue);
+ mManifestHash = nullptr;
+
+ if (NS_FAILED(rv)) {
+ LOG(("Could not finish manifest hash, rv=%08" PRIx32,
+ static_cast<uint32_t>(rv)));
+ // This is not critical error
+ return NS_OK;
+ }
+
+ if (!ParseSucceeded()) {
+ // Parsing failed, the hash is not valid
+ return NS_OK;
+ }
+
+ if (mOldManifestHashValue == mManifestHashValue) {
+ LOG(
+ ("Update not needed, downloaded manifest content is byte-for-byte "
+ "identical"));
+ mNeedsUpdate = false;
+ }
+
+ // Store the manifest content hash value to the new
+ // offline cache token
+ nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> cacheToken;
+ cachingChannel->GetOfflineCacheToken(getter_AddRefs(cacheToken));
+ if (cacheToken) {
+ nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash",
+ mManifestHashValue.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+void nsOfflineManifestItem::ReadStrictFileOriginPolicyPref() {
+ mStrictFileOriginPolicy =
+ Preferences::GetBool("security.fileuri.strict_origin_policy", true);
+}
+
+NS_IMETHODIMP
+nsOfflineManifestItem::OnStartRequest(nsIRequest* aRequest) {
+ nsresult rv;
+
+ nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool succeeded;
+ rv = channel->GetRequestSucceeded(&succeeded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!succeeded) {
+ LOG(("HTTP request failed"));
+ LogToConsole("Offline cache manifest HTTP request failed", this);
+ mParserState = PARSE_ERROR;
+ return NS_ERROR_ABORT;
+ }
+
+ rv = GetOldManifestContentHash(aRequest);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nsOfflineCacheUpdateItem::OnStartRequest(aRequest);
+}
+
+NS_IMETHODIMP
+nsOfflineManifestItem::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t bytesRead = 0;
+ aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead);
+ mBytesRead += bytesRead;
+
+ if (mParserState == PARSE_ERROR) {
+ LOG(("OnDataAvailable is canceling the request due a parse error\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ LOG(("loaded %u bytes into offline cache [offset=%" PRIu64 "]\n", bytesRead,
+ aOffset));
+
+ // All the parent method does is read and discard, don't bother
+ // chaining up.
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineManifestItem::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ if (mBytesRead == 0) {
+ // We didn't need to read (because LOAD_ONLY_IF_MODIFIED was
+ // specified).
+ mNeedsUpdate = false;
+ } else {
+ // Handle any leftover manifest data.
+ nsCString::const_iterator begin, end;
+ mReadBuf.BeginReading(begin);
+ mReadBuf.EndReading(end);
+ nsresult rv = HandleManifestLine(begin, end);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = CheckNewManifestContentHash(aRequest);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return nsOfflineCacheUpdateItem::OnStopRequest(aRequest, aStatus);
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdate::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsOfflineCacheUpdate, nsIOfflineCacheUpdateObserver,
+ nsIOfflineCacheUpdate, nsIRunnable)
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdate <public>
+//-----------------------------------------------------------------------------
+
+nsOfflineCacheUpdate::nsOfflineCacheUpdate()
+ : mState(STATE_UNINITIALIZED),
+ mAddedItems(false),
+ mPartialUpdate(false),
+ mOnlyCheckUpdate(false),
+ mSucceeded(true),
+ mObsolete(false),
+ mItemsInProgress(0),
+ mRescheduleCount(0),
+ mPinnedEntryRetriesCount(0),
+ mPinned(false),
+ mByteProgress(0) {}
+
+nsOfflineCacheUpdate::~nsOfflineCacheUpdate() {
+ LOG(("nsOfflineCacheUpdate::~nsOfflineCacheUpdate [%p]", this));
+}
+
+/* static */
+nsresult nsOfflineCacheUpdate::GetCacheKey(nsIURI* aURI, nsACString& aKey) {
+ aKey.Truncate();
+
+ nsCOMPtr<nsIURI> newURI;
+ nsresult rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(newURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = newURI->GetAsciiSpec(aKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::InitInternal(nsIURI* aManifestURI,
+ nsIPrincipal* aLoadingPrincipal) {
+ nsresult rv;
+
+ // Only http and https applications are supported.
+ if (!aManifestURI->SchemeIs("http") && !aManifestURI->SchemeIs("https")) {
+ return NS_ERROR_ABORT;
+ }
+
+ mManifestURI = aManifestURI;
+ mLoadingPrincipal = aLoadingPrincipal;
+
+ rv = mManifestURI->GetAsciiHost(mUpdateDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mPartialUpdate = false;
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::Init(nsIURI* aManifestURI, nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ dom::Document* aDocument,
+ nsIFile* aCustomProfileDir) {
+ nsresult rv;
+
+ // Make sure the service has been initialized
+ nsOfflineCacheUpdateService* service =
+ nsOfflineCacheUpdateService::EnsureService();
+ if (!service) return NS_ERROR_FAILURE;
+
+ LOG(("nsOfflineCacheUpdate::Init [%p]", this));
+
+ rv = InitInternal(aManifestURI, aLoadingPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIApplicationCacheService> cacheService =
+ do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString originSuffix;
+ rv = aLoadingPrincipal->GetOriginSuffix(originSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mDocumentURI = aDocumentURI;
+
+ if (aDocument) {
+ mCookieJarSettings = aDocument->CookieJarSettings();
+ }
+
+ if (aCustomProfileDir) {
+ rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix,
+ mGroupID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create only a new offline application cache in the custom profile
+ // This is a preload of a new cache.
+
+ // XXX Custom updates don't support "updating" of an existing cache
+ // in the custom profile at the moment. This support can be, though,
+ // simply added as well when needed.
+ mPreviousApplicationCache = nullptr;
+
+ rv = cacheService->CreateCustomApplicationCache(
+ mGroupID, aCustomProfileDir, kCustomProfileQuota,
+ getter_AddRefs(mApplicationCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCustomProfileDir = aCustomProfileDir;
+ } else {
+ rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix,
+ mGroupID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cacheService->GetActiveCache(
+ mGroupID, getter_AddRefs(mPreviousApplicationCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cacheService->CreateApplicationCache(
+ mGroupID, getter_AddRefs(mApplicationCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI,
+ &mPinned);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mState = STATE_INITIALIZED;
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::InitForUpdateCheck(
+ nsIURI* aManifestURI, nsIPrincipal* aLoadingPrincipal,
+ nsIObserver* aObserver) {
+ nsresult rv;
+
+ // Make sure the service has been initialized
+ nsOfflineCacheUpdateService* service =
+ nsOfflineCacheUpdateService::EnsureService();
+ if (!service) return NS_ERROR_FAILURE;
+
+ LOG(("nsOfflineCacheUpdate::InitForUpdateCheck [%p]", this));
+
+ rv = InitInternal(aManifestURI, aLoadingPrincipal);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIApplicationCacheService> cacheService =
+ do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString originSuffix;
+ rv = aLoadingPrincipal->GetOriginSuffix(originSuffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv =
+ cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, mGroupID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cacheService->GetActiveCache(mGroupID,
+ getter_AddRefs(mPreviousApplicationCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // To load the manifest properly using current app cache to satisfy and
+ // also to compare the cached content hash value we have to set 'some'
+ // app cache to write to on the channel. Otherwise the cached version will
+ // be used and no actual network request will be made. We use the same
+ // app cache here. OpenChannel prevents caching in this case using
+ // INHIBIT_CACHING load flag.
+ mApplicationCache = mPreviousApplicationCache;
+
+ rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aManifestURI,
+ &mPinned);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mUpdateAvailableObserver = aObserver;
+ mOnlyCheckUpdate = true;
+
+ mState = STATE_INITIALIZED;
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::InitPartial(
+ nsIURI* aManifestURI, const nsACString& clientID, nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal, nsICookieJarSettings* aCookieJarSettings) {
+ nsresult rv;
+
+ // Make sure the service has been initialized
+ nsOfflineCacheUpdateService* service =
+ nsOfflineCacheUpdateService::EnsureService();
+ if (!service) return NS_ERROR_FAILURE;
+
+ LOG(("nsOfflineCacheUpdate::InitPartial [%p]", this));
+
+ mPartialUpdate = true;
+ mDocumentURI = aDocumentURI;
+ mLoadingPrincipal = aLoadingPrincipal;
+
+ mManifestURI = aManifestURI;
+ rv = mManifestURI->GetAsciiHost(mUpdateDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIApplicationCacheService> cacheService =
+ do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cacheService->GetApplicationCache(clientID,
+ getter_AddRefs(mApplicationCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mApplicationCache) {
+ nsAutoCString manifestSpec;
+ rv = GetCacheKey(mManifestURI, manifestSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = cacheService->CreateApplicationCache(
+ manifestSpec, getter_AddRefs(mApplicationCache));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = mApplicationCache->GetManifestURI(getter_AddRefs(mManifestURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString groupID;
+ rv = mApplicationCache->GetGroupID(groupID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI,
+ &mPinned);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCookieJarSettings = aCookieJarSettings;
+
+ mState = STATE_INITIALIZED;
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::HandleManifest(bool* aDoUpdate) {
+ // Be pessimistic
+ *aDoUpdate = false;
+
+ bool succeeded;
+ nsresult rv = mManifestItem->GetRequestSucceeded(&succeeded);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!succeeded || !mManifestItem->ParseSucceeded()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mManifestItem->NeedsUpdate()) {
+ return NS_OK;
+ }
+
+ // Add items requested by the manifest.
+ const nsCOMArray<nsIURI>& manifestURIs = mManifestItem->GetExplicitURIs();
+ for (int32_t i = 0; i < manifestURIs.Count(); i++) {
+ rv = AddURI(manifestURIs[i], nsIApplicationCache::ITEM_EXPLICIT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ const nsCOMArray<nsIURI>& anonURIs = mManifestItem->GetAnonymousURIs();
+ for (int32_t i = 0; i < anonURIs.Count(); i++) {
+ rv = AddURI(anonURIs[i], nsIApplicationCache::ITEM_EXPLICIT,
+ nsIRequest::LOAD_ANONYMOUS);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ const nsCOMArray<nsIURI>& fallbackURIs = mManifestItem->GetFallbackURIs();
+ for (int32_t i = 0; i < fallbackURIs.Count(); i++) {
+ rv = AddURI(fallbackURIs[i], nsIApplicationCache::ITEM_FALLBACK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // The document that requested the manifest is implicitly included
+ // as part of that manifest update.
+ rv = AddURI(mDocumentURI, nsIApplicationCache::ITEM_IMPLICIT);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add items previously cached implicitly
+ rv = AddExistingItems(nsIApplicationCache::ITEM_IMPLICIT);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add items requested by the script API
+ rv = AddExistingItems(nsIApplicationCache::ITEM_DYNAMIC);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add opportunistically cached items conforming current opportunistic
+ // namespace list
+ rv = AddExistingItems(nsIApplicationCache::ITEM_OPPORTUNISTIC,
+ &mManifestItem->GetOpportunisticNamespaces());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aDoUpdate = true;
+
+ return NS_OK;
+}
+
+bool nsOfflineCacheUpdate::CheckUpdateAvailability() {
+ nsresult rv;
+
+ bool succeeded;
+ rv = mManifestItem->GetRequestSucceeded(&succeeded);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (!succeeded || !mManifestItem->ParseSucceeded()) {
+ return false;
+ }
+
+ if (!mPinned) {
+ uint16_t status;
+ rv = mManifestItem->GetStatus(&status);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Treat these as there would be an update available,
+ // since this is indication of demand to remove this
+ // offline cache.
+ if (status == 404 || status == 410) {
+ return true;
+ }
+ }
+
+ return mManifestItem->NeedsUpdate();
+}
+
+void nsOfflineCacheUpdate::LoadCompleted(nsOfflineCacheUpdateItem* aItem) {
+ nsresult rv;
+
+ LOG(("nsOfflineCacheUpdate::LoadCompleted [%p]", this));
+
+ if (mState == STATE_FINISHED) {
+ LOG((" after completion, ignoring"));
+ return;
+ }
+
+ // Keep the object alive through a Finish() call.
+ nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);
+
+ if (mState == STATE_CANCELLED) {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ Finish();
+ return;
+ }
+
+ if (mState == STATE_CHECKING) {
+ // Manifest load finished.
+
+ if (mOnlyCheckUpdate) {
+ Finish();
+ NotifyUpdateAvailability(CheckUpdateAvailability());
+ return;
+ }
+
+ NS_ASSERTION(mManifestItem, "Must have a manifest item in STATE_CHECKING.");
+ NS_ASSERTION(mManifestItem == aItem,
+ "Unexpected aItem in nsOfflineCacheUpdate::LoadCompleted");
+
+ // A 404 or 410 is interpreted as an intentional removal of
+ // the manifest file, rather than a transient server error.
+ // Obsolete this cache group if one of these is returned.
+ uint16_t status;
+ rv = mManifestItem->GetStatus(&status);
+ if (NS_FAILED(rv)) {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ Finish();
+ return;
+ }
+ if (status == 404 || status == 410) {
+ LogToConsole("Offline cache manifest removed, cache cleared",
+ mManifestItem);
+ mSucceeded = false;
+ if (mPreviousApplicationCache) {
+ if (mPinned) {
+ // Do not obsolete a pinned application.
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE);
+ } else {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_OBSOLETE);
+ mObsolete = true;
+ }
+ } else {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ mObsolete = true;
+ }
+ Finish();
+ return;
+ }
+
+ bool doUpdate;
+ if (NS_FAILED(HandleManifest(&doUpdate))) {
+ mSucceeded = false;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ Finish();
+ return;
+ }
+
+ if (!doUpdate) {
+ LogToConsole("Offline cache doesn't need to update", mManifestItem);
+
+ mSucceeded = false;
+
+ AssociateDocuments(mPreviousApplicationCache);
+
+ ScheduleImplicit();
+
+ // If we didn't need an implicit update, we can
+ // send noupdate and end the update now.
+ if (!mImplicitUpdate) {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE);
+ Finish();
+ }
+ return;
+ }
+
+ rv = mApplicationCache->MarkEntry(mManifestItem->mCacheKey,
+ mManifestItem->mItemType);
+ if (NS_FAILED(rv)) {
+ mSucceeded = false;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ Finish();
+ return;
+ }
+
+ mState = STATE_DOWNLOADING;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING);
+
+ // Start fetching resources.
+ ProcessNextURI();
+
+ return;
+ }
+
+ // Normal load finished.
+ if (mItemsInProgress) // Just to be safe here!
+ --mItemsInProgress;
+
+ bool succeeded;
+ rv = aItem->GetRequestSucceeded(&succeeded);
+
+ if (mPinned && NS_SUCCEEDED(rv) && succeeded) {
+ uint32_t dummy_cache_type;
+ rv = mApplicationCache->GetTypes(aItem->mCacheKey, &dummy_cache_type);
+ bool item_doomed = NS_FAILED(rv); // can not find it? -> doomed
+
+ if (item_doomed && mPinnedEntryRetriesCount < kPinnedEntryRetriesLimit &&
+ (aItem->mItemType & (nsIApplicationCache::ITEM_EXPLICIT |
+ nsIApplicationCache::ITEM_FALLBACK))) {
+ rv = EvictOneNonPinned();
+ if (NS_FAILED(rv)) {
+ mSucceeded = false;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ Finish();
+ return;
+ }
+
+ // This reverts the item state to UNINITIALIZED that makes it to
+ // be scheduled for download again.
+ rv = aItem->Cancel();
+ if (NS_FAILED(rv)) {
+ mSucceeded = false;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ Finish();
+ return;
+ }
+
+ mPinnedEntryRetriesCount++;
+
+ LogToConsole("An unpinned offline cache deleted");
+
+ // Retry this item.
+ ProcessNextURI();
+ return;
+ }
+ }
+
+ // According to parallelism this may imply more pinned retries count,
+ // but that is not critical, since at one moment the algorithm will
+ // stop anyway. Also, this code may soon be completely removed
+ // after we have a separate storage for pinned apps.
+ mPinnedEntryRetriesCount = 0;
+
+ // Check for failures. 3XX, 4XX and 5XX errors on items explicitly
+ // listed in the manifest will cause the update to fail.
+ if (NS_FAILED(rv) || !succeeded) {
+ if (aItem->mItemType & (nsIApplicationCache::ITEM_EXPLICIT |
+ nsIApplicationCache::ITEM_FALLBACK)) {
+ LogToConsole("Offline cache manifest item failed to load", aItem);
+ mSucceeded = false;
+ }
+ } else {
+ rv = mApplicationCache->MarkEntry(aItem->mCacheKey, aItem->mItemType);
+ if (NS_FAILED(rv)) {
+ mSucceeded = false;
+ }
+ }
+
+ if (!mSucceeded) {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ Finish();
+ return;
+ }
+
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMCOMPLETED);
+
+ ProcessNextURI();
+}
+
+void nsOfflineCacheUpdate::ManifestCheckCompleted(
+ nsresult aStatus, const nsCString& aManifestHash) {
+ // Keep the object alive through a Finish() call.
+ nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);
+
+ if (NS_SUCCEEDED(aStatus)) {
+ nsAutoCString firstManifestHash;
+ mManifestItem->GetManifestHash(firstManifestHash);
+ if (aManifestHash != firstManifestHash) {
+ LOG(("Manifest has changed during cache items download [%p]", this));
+ LogToConsole("Offline cache manifest changed during update",
+ mManifestItem);
+ aStatus = NS_ERROR_FAILURE;
+ }
+ }
+
+ if (NS_FAILED(aStatus)) {
+ mSucceeded = false;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ }
+
+ if (NS_FAILED(aStatus) && mRescheduleCount < kRescheduleLimit) {
+ // Do the final stuff but prevent notification of STATE_FINISHED.
+ // That would disconnect listeners that are responsible for document
+ // association after a successful update. Forwarding notifications
+ // from a new update through this dead update to them is absolutely
+ // correct.
+ FinishNoNotify();
+
+ RefPtr<nsOfflineCacheUpdate> newUpdate = new nsOfflineCacheUpdate();
+ // Leave aDocument argument null. Only glues and children keep
+ // document instances.
+ newUpdate->Init(mManifestURI, mDocumentURI, mLoadingPrincipal, nullptr,
+ mCustomProfileDir);
+
+ newUpdate->SetCookieJarSettings(mCookieJarSettings);
+
+ // In a rare case the manifest will not be modified on the next refetch
+ // transfer all master document URIs to the new update to ensure that
+ // all documents refering it will be properly cached.
+ for (int32_t i = 0; i < mDocumentURIs.Count(); i++) {
+ newUpdate->StickDocument(mDocumentURIs[i]);
+ }
+
+ newUpdate->mRescheduleCount = mRescheduleCount + 1;
+ newUpdate->AddObserver(this, false);
+ newUpdate->Schedule();
+ } else {
+ LogToConsole("Offline cache update done", mManifestItem);
+ Finish();
+ }
+}
+
+nsresult nsOfflineCacheUpdate::Begin() {
+ LOG(("nsOfflineCacheUpdate::Begin [%p]", this));
+
+ // Keep the object alive through a ProcessNextURI()/Finish() call.
+ nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);
+
+ mItemsInProgress = 0;
+
+ if (mState == STATE_CANCELLED) {
+ nsresult rv = NS_DispatchToMainThread(
+ NewRunnableMethod("nsOfflineCacheUpdate::AsyncFinishWithError", this,
+ &nsOfflineCacheUpdate::AsyncFinishWithError));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ if (mPartialUpdate) {
+ mState = STATE_DOWNLOADING;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING);
+ ProcessNextURI();
+ return NS_OK;
+ }
+
+ // Start checking the manifest.
+ mManifestItem =
+ new nsOfflineManifestItem(mManifestURI, mDocumentURI, mLoadingPrincipal,
+ mApplicationCache, mPreviousApplicationCache);
+ if (!mManifestItem) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ mState = STATE_CHECKING;
+ mByteProgress = 0;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_CHECKING);
+
+ nsresult rv = mManifestItem->OpenChannel(this);
+ if (NS_FAILED(rv)) {
+ LoadCompleted(mManifestItem);
+ }
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdate <private>
+//-----------------------------------------------------------------------------
+
+nsresult nsOfflineCacheUpdate::AddExistingItems(
+ uint32_t aType, nsTArray<nsCString>* namespaceFilter) {
+ if (!mPreviousApplicationCache) {
+ return NS_OK;
+ }
+
+ if (namespaceFilter && namespaceFilter->Length() == 0) {
+ // Don't bother to walk entries when there are no namespaces
+ // defined.
+ return NS_OK;
+ }
+
+ nsTArray<nsCString> keys;
+ nsresult rv = mPreviousApplicationCache->GatherEntries(aType, keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& key : keys) {
+ if (namespaceFilter) {
+ bool found = false;
+ for (uint32_t j = 0; j < namespaceFilter->Length() && !found; j++) {
+ found = StringBeginsWith(key, namespaceFilter->ElementAt(j));
+ }
+
+ if (!found) continue;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), key))) {
+ rv = AddURI(uri, aType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::ProcessNextURI() {
+ // Keep the object alive through a Finish() call.
+ nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);
+
+ LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p, inprogress=%d, numItems=%zu]",
+ this, mItemsInProgress, mItems.Length()));
+
+ if (mState != STATE_DOWNLOADING) {
+ LOG((" should only be called from the DOWNLOADING state, ignoring"));
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsOfflineCacheUpdateItem* runItem = nullptr;
+ uint32_t completedItems = 0;
+ for (uint32_t i = 0; i < mItems.Length(); ++i) {
+ nsOfflineCacheUpdateItem* item = mItems[i];
+
+ if (item->IsScheduled()) {
+ runItem = item;
+ break;
+ }
+
+ if (item->IsCompleted()) ++completedItems;
+ }
+
+ if (completedItems == mItems.Length()) {
+ LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p]: all items loaded", this));
+
+ if (mPartialUpdate) {
+ return Finish();
+ } else {
+ // Verify that the manifest wasn't changed during the
+ // update, to prevent capturing a cache while the server
+ // is being updated. The check will call
+ // ManifestCheckCompleted() when it's done.
+ RefPtr<nsManifestCheck> manifestCheck = new nsManifestCheck(
+ this, mManifestURI, mDocumentURI, mLoadingPrincipal);
+ if (NS_FAILED(manifestCheck->Begin())) {
+ mSucceeded = false;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ return Finish();
+ }
+
+ return NS_OK;
+ }
+ }
+
+ if (!runItem) {
+ LOG(
+ ("nsOfflineCacheUpdate::ProcessNextURI [%p]:"
+ " No more items to include in parallel load",
+ this));
+ return NS_OK;
+ }
+
+ if (LOG_ENABLED()) {
+ LOG(("%p: Opening channel for %s", this,
+ runItem->mURI->GetSpecOrDefault().get()));
+ }
+
+ ++mItemsInProgress;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMSTARTED);
+
+ nsresult rv = runItem->OpenChannel(this);
+ if (NS_FAILED(rv)) {
+ LoadCompleted(runItem);
+ return rv;
+ }
+
+ if (mItemsInProgress >= kParallelLoadLimit) {
+ LOG(
+ ("nsOfflineCacheUpdate::ProcessNextURI [%p]:"
+ " At parallel load limit",
+ this));
+ return NS_OK;
+ }
+
+ // This calls this method again via a post triggering
+ // a parallel item load
+ return NS_DispatchToCurrentThread(this);
+}
+
+void nsOfflineCacheUpdate::GatherObservers(
+ nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers) {
+ for (int32_t i = 0; i < mWeakObservers.Count(); i++) {
+ nsCOMPtr<nsIOfflineCacheUpdateObserver> observer =
+ do_QueryReferent(mWeakObservers[i]);
+ if (observer)
+ aObservers.AppendObject(observer);
+ else
+ mWeakObservers.RemoveObjectAt(i--);
+ }
+
+ for (int32_t i = 0; i < mObservers.Count(); i++) {
+ aObservers.AppendObject(mObservers[i]);
+ }
+}
+
+void nsOfflineCacheUpdate::NotifyState(uint32_t state) {
+ LOG(("nsOfflineCacheUpdate::NotifyState [%p, %d]", this, state));
+
+ if (state == STATE_ERROR) {
+ LogToConsole("Offline cache update error", mManifestItem);
+ }
+
+ nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+ GatherObservers(observers);
+
+ for (int32_t i = 0; i < observers.Count(); i++) {
+ observers[i]->UpdateStateChanged(this, state);
+ }
+}
+
+void nsOfflineCacheUpdate::NotifyUpdateAvailability(bool updateAvailable) {
+ if (!mUpdateAvailableObserver) return;
+
+ LOG(("nsOfflineCacheUpdate::NotifyUpdateAvailability [this=%p, avail=%d]",
+ this, updateAvailable));
+
+ const char* topic = updateAvailable ? "offline-cache-update-available"
+ : "offline-cache-update-unavailable";
+
+ nsCOMPtr<nsIObserver> observer;
+ observer.swap(mUpdateAvailableObserver);
+ observer->Observe(mManifestURI, topic, nullptr);
+}
+
+void nsOfflineCacheUpdate::AssociateDocuments(nsIApplicationCache* cache) {
+ if (!cache) {
+ LOG(
+ ("nsOfflineCacheUpdate::AssociateDocuments bypassed"
+ ", no cache provided [this=%p]",
+ this));
+ return;
+ }
+
+ nsCOMArray<nsIOfflineCacheUpdateObserver> observers;
+ GatherObservers(observers);
+
+ for (int32_t i = 0; i < observers.Count(); i++) {
+ observers[i]->ApplicationCacheAvailable(cache);
+ }
+}
+
+void nsOfflineCacheUpdate::StickDocument(nsIURI* aDocumentURI) {
+ if (!aDocumentURI) return;
+
+ mDocumentURIs.AppendObject(aDocumentURI);
+}
+
+void nsOfflineCacheUpdate::SetOwner(nsOfflineCacheUpdateOwner* aOwner) {
+ NS_ASSERTION(!mOwner, "Tried to set cache update owner twice.");
+ mOwner = aOwner;
+}
+
+bool nsOfflineCacheUpdate::IsForGroupID(const nsACString& groupID) {
+ return mGroupID == groupID;
+}
+
+bool nsOfflineCacheUpdate::IsForProfile(nsIFile* aCustomProfileDir) {
+ if (!mCustomProfileDir && !aCustomProfileDir) return true;
+ if (!mCustomProfileDir || !aCustomProfileDir) return false;
+
+ bool equals;
+ nsresult rv = mCustomProfileDir->Equals(aCustomProfileDir, &equals);
+
+ return NS_SUCCEEDED(rv) && equals;
+}
+
+nsresult nsOfflineCacheUpdate::UpdateFinished(nsOfflineCacheUpdate* aUpdate) {
+ // Keep the object alive through a Finish() call.
+ nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this);
+
+ mImplicitUpdate = nullptr;
+
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE);
+ Finish();
+
+ return NS_OK;
+}
+
+void nsOfflineCacheUpdate::OnByteProgress(uint64_t byteIncrement) {
+ mByteProgress += byteIncrement;
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMPROGRESS);
+}
+
+nsresult nsOfflineCacheUpdate::ScheduleImplicit() {
+ if (mDocumentURIs.Count() == 0) return NS_OK;
+
+ nsresult rv;
+
+ RefPtr<nsOfflineCacheUpdate> update = new nsOfflineCacheUpdate();
+ NS_ENSURE_TRUE(update, NS_ERROR_OUT_OF_MEMORY);
+
+ nsAutoCString clientID;
+ if (mPreviousApplicationCache) {
+ rv = mPreviousApplicationCache->GetClientID(clientID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (mApplicationCache) {
+ rv = mApplicationCache->GetClientID(clientID);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ NS_ERROR("Offline cache update not having set mApplicationCache?");
+ }
+
+ rv = update->InitPartial(mManifestURI, clientID, mDocumentURI,
+ mLoadingPrincipal, mCookieJarSettings);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (int32_t i = 0; i < mDocumentURIs.Count(); i++) {
+ rv = update->AddURI(mDocumentURIs[i], nsIApplicationCache::ITEM_IMPLICIT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ update->SetOwner(this);
+ rv = update->Begin();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mImplicitUpdate = update;
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::FinishNoNotify() {
+ LOG(("nsOfflineCacheUpdate::Finish [%p]", this));
+
+ mState = STATE_FINISHED;
+
+ if (!mPartialUpdate && !mOnlyCheckUpdate) {
+ if (mSucceeded) {
+ nsIArray* namespaces = mManifestItem->GetNamespaces();
+ nsresult rv = mApplicationCache->AddNamespaces(namespaces);
+ if (NS_FAILED(rv)) {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ mSucceeded = false;
+ }
+
+ rv = mApplicationCache->Activate();
+ if (NS_FAILED(rv)) {
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR);
+ mSucceeded = false;
+ }
+
+ AssociateDocuments(mApplicationCache);
+ }
+
+ if (mObsolete) {
+ nsCOMPtr<nsIApplicationCacheService> appCacheService =
+ do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID);
+ if (appCacheService) {
+ nsAutoCString groupID;
+ mApplicationCache->GetGroupID(groupID);
+ appCacheService->DeactivateGroup(groupID);
+ }
+ }
+
+ if (!mSucceeded) {
+ // Update was not merged, mark all the loads as failures
+ for (uint32_t i = 0; i < mItems.Length(); i++) {
+ mItems[i]->Cancel();
+ }
+
+ mApplicationCache->Discard();
+ }
+ }
+
+ nsresult rv = NS_OK;
+
+ if (mOwner) {
+ rv = mOwner->UpdateFinished(this);
+ // mozilla::WeakPtr is missing some key features, like setting it to
+ // null explicitly.
+ mOwner = mozilla::WeakPtr<nsOfflineCacheUpdateOwner>();
+ }
+
+ return rv;
+}
+
+nsresult nsOfflineCacheUpdate::Finish() {
+ nsresult rv = FinishNoNotify();
+
+ NotifyState(nsIOfflineCacheUpdateObserver::STATE_FINISHED);
+
+ return rv;
+}
+
+void nsOfflineCacheUpdate::AsyncFinishWithError() {
+ NotifyState(nsOfflineCacheUpdate::STATE_ERROR);
+ Finish();
+}
+
+static nsresult EvictOneOfCacheGroups(nsIApplicationCacheService* cacheService,
+ const nsTArray<nsCString>& groups) {
+ nsresult rv;
+
+ for (auto& group : groups) {
+ nsCOMPtr<nsIURI> uri;
+ rv = NS_NewURI(getter_AddRefs(uri), group);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIApplicationCache> cache;
+ rv = cacheService->GetActiveCache(group, getter_AddRefs(cache));
+ // Maybe someone in another thread or process have deleted it.
+ if (NS_FAILED(rv) || !cache) continue;
+
+ bool pinned;
+ rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(uri, &pinned);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!pinned) {
+ rv = cache->Discard();
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FILE_NOT_FOUND;
+}
+
+nsresult nsOfflineCacheUpdate::EvictOneNonPinned() {
+ nsresult rv;
+
+ nsCOMPtr<nsIApplicationCacheService> cacheService =
+ do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<nsCString> groups;
+ rv = cacheService->GetGroupsTimeOrdered(groups);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return EvictOneOfCacheGroups(cacheService, groups);
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdate::nsIOfflineCacheUpdate
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetUpdateDomain(nsACString& aUpdateDomain) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ aUpdateDomain = mUpdateDomain;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetStatus(uint16_t* aStatus) {
+ switch (mState) {
+ case STATE_CHECKING:
+ *aStatus = dom::OfflineResourceList_Binding::CHECKING;
+ return NS_OK;
+ case STATE_DOWNLOADING:
+ *aStatus = dom::OfflineResourceList_Binding::DOWNLOADING;
+ return NS_OK;
+ default:
+ *aStatus = dom::OfflineResourceList_Binding::IDLE;
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetPartial(bool* aPartial) {
+ *aPartial = mPartialUpdate || mOnlyCheckUpdate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetManifestURI(nsIURI** aManifestURI) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ NS_IF_ADDREF(*aManifestURI = mManifestURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetLoadingPrincipal(nsIPrincipal** aLoadingPrincipal) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ NS_IF_ADDREF(*aLoadingPrincipal = mLoadingPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetSucceeded(bool* aSucceeded) {
+ NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE);
+
+ *aSucceeded = mSucceeded;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetIsUpgrade(bool* aIsUpgrade) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ *aIsUpgrade = (mPreviousApplicationCache != nullptr);
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdate::AddURI(nsIURI* aURI, uint32_t aType,
+ uint32_t aLoadFlags) {
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ if (mState >= STATE_DOWNLOADING) return NS_ERROR_NOT_AVAILABLE;
+
+ // Resource URIs must have the same scheme as the manifest.
+ nsAutoCString scheme;
+ aURI->GetScheme(scheme);
+
+ if (!mManifestURI->SchemeIs(scheme.get())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Don't fetch the same URI twice.
+ for (uint32_t i = 0; i < mItems.Length(); i++) {
+ bool equals;
+ if (NS_SUCCEEDED(mItems[i]->mURI->Equals(aURI, &equals)) && equals &&
+ mItems[i]->mLoadFlags == aLoadFlags) {
+ // retain both types.
+ mItems[i]->mItemType |= aType;
+ return NS_OK;
+ }
+ }
+
+ RefPtr<nsOfflineCacheUpdateItem> item = new nsOfflineCacheUpdateItem(
+ aURI, mDocumentURI, mLoadingPrincipal, mApplicationCache,
+ mPreviousApplicationCache, aType, aLoadFlags);
+ if (!item) return NS_ERROR_OUT_OF_MEMORY;
+
+ mItems.AppendElement(item);
+ mAddedItems = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::AddDynamicURI(nsIURI* aURI) {
+ if (GeckoProcessType_Default != XRE_GetProcessType())
+ return NS_ERROR_NOT_IMPLEMENTED;
+
+ // If this is a partial update and the resource is already in the
+ // cache, we should only mark the entry, not fetch it again.
+ if (mPartialUpdate) {
+ nsAutoCString key;
+ GetCacheKey(aURI, key);
+
+ uint32_t types;
+ nsresult rv = mApplicationCache->GetTypes(key, &types);
+ if (NS_SUCCEEDED(rv)) {
+ if (!(types & nsIApplicationCache::ITEM_DYNAMIC)) {
+ mApplicationCache->MarkEntry(key, nsIApplicationCache::ITEM_DYNAMIC);
+ }
+ return NS_OK;
+ }
+ }
+
+ return AddURI(aURI, nsIApplicationCache::ITEM_DYNAMIC);
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::Cancel() {
+ LOG(("nsOfflineCacheUpdate::Cancel [%p]", this));
+
+ if ((mState == STATE_FINISHED) || (mState == STATE_CANCELLED)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mState = STATE_CANCELLED;
+ mSucceeded = false;
+
+ // Cancel all running downloads
+ for (uint32_t i = 0; i < mItems.Length(); ++i) {
+ nsOfflineCacheUpdateItem* item = mItems[i];
+
+ if (item->IsInProgress()) item->Cancel();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::AddObserver(nsIOfflineCacheUpdateObserver* aObserver,
+ bool aHoldWeak) {
+ LOG(("nsOfflineCacheUpdate::AddObserver [%p] to update [%p]", aObserver,
+ this));
+
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ if (aHoldWeak) {
+ nsWeakPtr weakRef = do_GetWeakReference(aObserver);
+ mWeakObservers.AppendObject(weakRef);
+ } else {
+ mObservers.AppendObject(aObserver);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::RemoveObserver(nsIOfflineCacheUpdateObserver* aObserver) {
+ LOG(("nsOfflineCacheUpdate::RemoveObserver [%p] from update [%p]", aObserver,
+ this));
+
+ NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED);
+
+ for (int32_t i = 0; i < mWeakObservers.Count(); i++) {
+ nsCOMPtr<nsIOfflineCacheUpdateObserver> observer =
+ do_QueryReferent(mWeakObservers[i]);
+ if (observer == aObserver) {
+ mWeakObservers.RemoveObjectAt(i);
+ return NS_OK;
+ }
+ }
+
+ for (int32_t i = 0; i < mObservers.Count(); i++) {
+ if (mObservers[i] == aObserver) {
+ mObservers.RemoveObjectAt(i);
+ return NS_OK;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::GetByteProgress(uint64_t* _result) {
+ NS_ENSURE_ARG(_result);
+
+ *_result = mByteProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::Schedule() {
+ LOG(("nsOfflineCacheUpdate::Schedule [%p]", this));
+
+ nsOfflineCacheUpdateService* service =
+ nsOfflineCacheUpdateService::EnsureService();
+
+ if (!service) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return service->ScheduleUpdate(this);
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::UpdateStateChanged(nsIOfflineCacheUpdate* aUpdate,
+ uint32_t aState) {
+ if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED) {
+ // Take the mSucceeded flag from the underlying update, we will be
+ // queried for it soon. mSucceeded of this update is false (manifest
+ // check failed) but the subsequent re-fetch update might succeed
+ bool succeeded;
+ aUpdate->GetSucceeded(&succeeded);
+ mSucceeded = succeeded;
+ }
+
+ NotifyState(aState);
+ if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED)
+ aUpdate->RemoveObserver(this);
+
+ return NS_OK;
+}
+
+void nsOfflineCacheUpdate::SetCookieJarSettings(
+ nsICookieJarSettings* aCookieJarSettings) {
+ mCookieJarSettings = aCookieJarSettings;
+}
+
+void nsOfflineCacheUpdate::SetCookieJarSettingsArgs(
+ const CookieJarSettingsArgs& aCookieJarSettingsArgs) {
+ MOZ_ASSERT(!mCookieJarSettings);
+
+ CookieJarSettings::Deserialize(aCookieJarSettingsArgs,
+ getter_AddRefs(mCookieJarSettings));
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::ApplicationCacheAvailable(
+ nsIApplicationCache* applicationCache) {
+ AssociateDocuments(applicationCache);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdate::nsIRunable
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCacheUpdate::Run() {
+ ProcessNextURI();
+ return NS_OK;
+}
diff --git a/uriloader/prefetch/nsOfflineCacheUpdate.h b/uriloader/prefetch/nsOfflineCacheUpdate.h
new file mode 100644
index 0000000000..bb7699c046
--- /dev/null
+++ b/uriloader/prefetch/nsOfflineCacheUpdate.h
@@ -0,0 +1,369 @@
+/* -*- 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 nsOfflineCacheUpdate_h__
+#define nsOfflineCacheUpdate_h__
+
+#include "nsIOfflineCacheUpdate.h"
+
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsIChannelEventSink.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIMutableArray.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIApplicationCache.h"
+#include "nsIRunnable.h"
+#include "nsIStreamListener.h"
+#include "nsIURI.h"
+#include "nsClassHashtable.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWeakReference.h"
+#include "nsICryptoHash.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/WeakPtr.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+
+namespace mozilla {
+
+namespace net {
+class CookieJarSettingsArgs;
+}
+
+} // namespace mozilla
+
+class nsOfflineCacheUpdate;
+
+class nsOfflineCacheUpdateItem : public nsIStreamListener,
+ public nsIRunnable,
+ public nsIInterfaceRequestor,
+ public nsIChannelEventSink {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+
+ nsOfflineCacheUpdateItem(nsIURI* aURI, nsIURI* aReferrerURI,
+ nsIPrincipal* aLoadingPrincipal,
+ nsIApplicationCache* aApplicationCache,
+ nsIApplicationCache* aPreviousApplicationCache,
+ uint32_t aType, uint32_t aLoadFlags);
+
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIURI> mReferrerURI;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+ nsCOMPtr<nsIApplicationCache> mApplicationCache;
+ nsCOMPtr<nsIApplicationCache> mPreviousApplicationCache;
+ nsCString mCacheKey;
+ uint32_t mItemType;
+ uint32_t mLoadFlags;
+
+ nsresult OpenChannel(nsOfflineCacheUpdate* aUpdate);
+ nsresult Cancel();
+ nsresult GetRequestSucceeded(bool* succeeded);
+
+ bool IsInProgress();
+ bool IsScheduled();
+ bool IsCompleted();
+
+ nsresult GetStatus(uint16_t* aStatus);
+
+ private:
+ enum LoadStatus : uint16_t {
+ UNINITIALIZED = 0U,
+ REQUESTED = 1U,
+ RECEIVING = 2U,
+ LOADED = 3U
+ };
+
+ RefPtr<nsOfflineCacheUpdate> mUpdate;
+ nsCOMPtr<nsIChannel> mChannel;
+ uint16_t mState;
+
+ protected:
+ virtual ~nsOfflineCacheUpdateItem();
+
+ int64_t mBytesRead;
+};
+
+class nsOfflineManifestItem : public nsOfflineCacheUpdateItem {
+ public:
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+
+ nsOfflineManifestItem(nsIURI* aURI, nsIURI* aReferrerURI,
+ nsIPrincipal* aLoadingPrincipal,
+ nsIApplicationCache* aApplicationCache,
+ nsIApplicationCache* aPreviousApplicationCache);
+ virtual ~nsOfflineManifestItem();
+
+ nsCOMArray<nsIURI>& GetExplicitURIs() { return mExplicitURIs; }
+ nsCOMArray<nsIURI>& GetAnonymousURIs() { return mAnonymousURIs; }
+ nsCOMArray<nsIURI>& GetFallbackURIs() { return mFallbackURIs; }
+
+ nsTArray<nsCString>& GetOpportunisticNamespaces() {
+ return mOpportunisticNamespaces;
+ }
+ nsIArray* GetNamespaces() { return mNamespaces.get(); }
+
+ bool ParseSucceeded() {
+ return (mParserState != PARSE_INIT && mParserState != PARSE_ERROR);
+ }
+ bool NeedsUpdate() { return mParserState != PARSE_INIT && mNeedsUpdate; }
+
+ void GetManifestHash(nsCString& aManifestHash) {
+ aManifestHash = mManifestHashValue;
+ }
+
+ private:
+ static nsresult ReadManifest(nsIInputStream* aInputStream, void* aClosure,
+ const char* aFromSegment, uint32_t aOffset,
+ uint32_t aCount, uint32_t* aBytesConsumed);
+
+ nsresult AddNamespace(uint32_t namespaceType, const nsCString& namespaceSpec,
+ const nsCString& data);
+
+ nsresult HandleManifestLine(const nsCString::const_iterator& aBegin,
+ const nsCString::const_iterator& aEnd);
+
+ /**
+ * Saves "offline-manifest-hash" meta data from the old offline cache
+ * token to mOldManifestHashValue member to be compared on
+ * successfull load.
+ */
+ nsresult GetOldManifestContentHash(nsIRequest* aRequest);
+ /**
+ * This method setups the mNeedsUpdate to false when hash value
+ * of the just downloaded manifest file is the same as stored in cache's
+ * "offline-manifest-hash" meta data. Otherwise stores the new value
+ * to this meta data.
+ */
+ nsresult CheckNewManifestContentHash(nsIRequest* aRequest);
+
+ void ReadStrictFileOriginPolicyPref();
+
+ enum {
+ PARSE_INIT,
+ PARSE_CACHE_ENTRIES,
+ PARSE_FALLBACK_ENTRIES,
+ PARSE_BYPASS_ENTRIES,
+ PARSE_UNKNOWN_SECTION,
+ PARSE_ERROR
+ } mParserState;
+
+ nsCString mReadBuf;
+
+ nsCOMArray<nsIURI> mExplicitURIs;
+ nsCOMArray<nsIURI> mAnonymousURIs;
+ nsCOMArray<nsIURI> mFallbackURIs;
+
+ // All opportunistic caching namespaces. Used to decide whether
+ // to include previously-opportunistically-cached entries.
+ nsTArray<nsCString> mOpportunisticNamespaces;
+
+ // Array of nsIApplicationCacheNamespace objects specified by the
+ // manifest.
+ nsCOMPtr<nsIMutableArray> mNamespaces;
+
+ bool mNeedsUpdate;
+ bool mStrictFileOriginPolicy;
+
+ // manifest hash data
+ nsCOMPtr<nsICryptoHash> mManifestHash;
+ bool mManifestHashInitialized;
+ nsCString mManifestHashValue;
+ nsCString mOldManifestHashValue;
+};
+
+class nsOfflineCacheUpdateOwner : public mozilla::SupportsWeakPtr {
+ public:
+ virtual ~nsOfflineCacheUpdateOwner() {}
+ virtual nsresult UpdateFinished(nsOfflineCacheUpdate* aUpdate) = 0;
+};
+
+class nsOfflineCacheUpdate final : public nsIOfflineCacheUpdate,
+ public nsIOfflineCacheUpdateObserver,
+ public nsIRunnable,
+ public nsOfflineCacheUpdateOwner {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOFFLINECACHEUPDATE
+ NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER
+ NS_DECL_NSIRUNNABLE
+
+ nsOfflineCacheUpdate();
+
+ static nsresult GetCacheKey(nsIURI* aURI, nsACString& aKey);
+
+ nsresult Init();
+
+ nsresult Begin();
+
+ void LoadCompleted(nsOfflineCacheUpdateItem* aItem);
+ void ManifestCheckCompleted(nsresult aStatus, const nsCString& aManifestHash);
+ void StickDocument(nsIURI* aDocumentURI);
+
+ void SetOwner(nsOfflineCacheUpdateOwner* aOwner);
+
+ bool IsForGroupID(const nsACString& groupID);
+ bool IsForProfile(nsIFile* aCustomProfileDir);
+
+ virtual nsresult UpdateFinished(nsOfflineCacheUpdate* aUpdate) override;
+
+ nsICookieJarSettings* CookieJarSettings() const { return mCookieJarSettings; }
+ void SetCookieJarSettings(nsICookieJarSettings* aCookieJarSettings);
+ void SetCookieJarSettingsArgs(
+ const mozilla::net::CookieJarSettingsArgs& aCookieJarSettingsArgs);
+
+ protected:
+ ~nsOfflineCacheUpdate();
+
+ friend class nsOfflineCacheUpdateItem;
+ void OnByteProgress(uint64_t byteIncrement);
+
+ private:
+ nsresult InitInternal(nsIURI* aManifestURI, nsIPrincipal* aPrincipal);
+ nsresult HandleManifest(bool* aDoUpdate);
+ nsresult AddURI(nsIURI* aURI, uint32_t aItemType, uint32_t aLoadFlags = 0);
+
+ nsresult ProcessNextURI();
+
+ // Adds items from the previous cache witha type matching aType.
+ // If namespaceFilter is non-null, only items matching the
+ // specified namespaces will be added.
+ nsresult AddExistingItems(uint32_t aType,
+ nsTArray<nsCString>* namespaceFilter = nullptr);
+ nsresult ScheduleImplicit();
+ void AssociateDocuments(nsIApplicationCache* cache);
+ bool CheckUpdateAvailability();
+ void NotifyUpdateAvailability(bool updateAvailable);
+
+ void GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers);
+ void NotifyState(uint32_t state);
+ nsresult Finish();
+ nsresult FinishNoNotify();
+
+ void AsyncFinishWithError();
+
+ // Find one non-pinned cache group and evict it.
+ nsresult EvictOneNonPinned();
+
+ enum {
+ STATE_UNINITIALIZED,
+ STATE_INITIALIZED,
+ STATE_CHECKING,
+ STATE_DOWNLOADING,
+ STATE_CANCELLED,
+ STATE_FINISHED
+ } mState;
+
+ mozilla::WeakPtr<nsOfflineCacheUpdateOwner> mOwner;
+
+ bool mAddedItems;
+ bool mPartialUpdate;
+ bool mOnlyCheckUpdate;
+ bool mSucceeded;
+ bool mObsolete;
+
+ nsCString mUpdateDomain;
+ nsCString mGroupID;
+ nsCOMPtr<nsIURI> mManifestURI;
+ nsCOMPtr<nsIURI> mDocumentURI;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+ nsCOMPtr<nsIFile> mCustomProfileDir;
+ nsCOMPtr<nsICookieJarSettings> mCookieJarSettings;
+
+ nsCOMPtr<nsIObserver> mUpdateAvailableObserver;
+
+ nsCOMPtr<nsIApplicationCache> mApplicationCache;
+ nsCOMPtr<nsIApplicationCache> mPreviousApplicationCache;
+
+ nsCOMPtr<nsIObserverService> mObserverService;
+
+ RefPtr<nsOfflineManifestItem> mManifestItem;
+
+ /* Items being updated */
+ uint32_t mItemsInProgress;
+ nsTArray<RefPtr<nsOfflineCacheUpdateItem> > mItems;
+
+ /* Clients watching this update for changes */
+ nsCOMArray<nsIWeakReference> mWeakObservers;
+ nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers;
+
+ /* Documents that requested this update */
+ nsCOMArray<nsIURI> mDocumentURIs;
+
+ /* Reschedule count. When an update is rescheduled due to
+ * mismatched manifests, the reschedule count will be increased. */
+ uint32_t mRescheduleCount;
+
+ /* Whena an entry for a pinned app is retried, retries count is
+ * increaded. */
+ uint32_t mPinnedEntryRetriesCount;
+
+ RefPtr<nsOfflineCacheUpdate> mImplicitUpdate;
+
+ bool mPinned;
+
+ uint64_t mByteProgress;
+};
+
+class nsOfflineCacheUpdateService final : public nsIOfflineCacheUpdateService,
+ public nsIObserver,
+ public nsOfflineCacheUpdateOwner,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOFFLINECACHEUPDATESERVICE
+ NS_DECL_NSIOBSERVER
+
+ nsOfflineCacheUpdateService();
+
+ nsresult Init();
+
+ nsresult ScheduleUpdate(nsOfflineCacheUpdate* aUpdate);
+ nsresult FindUpdate(nsIURI* aManifestURI, nsACString const& aOriginSuffix,
+ nsIFile* aCustomProfileDir,
+ nsOfflineCacheUpdate** aUpdate);
+
+ nsresult Schedule(nsIURI* aManifestURI, nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ mozilla::dom::Document* aDocument,
+ nsPIDOMWindowInner* aWindow, nsIFile* aCustomProfileDir,
+ nsIOfflineCacheUpdate** aUpdate);
+
+ virtual nsresult UpdateFinished(nsOfflineCacheUpdate* aUpdate) override;
+
+ /**
+ * Returns the singleton nsOfflineCacheUpdateService without an addref, or
+ * nullptr if the service couldn't be created.
+ */
+ static nsOfflineCacheUpdateService* EnsureService();
+
+ static already_AddRefed<nsOfflineCacheUpdateService> GetInstance();
+
+ static nsresult OfflineAppPinnedForURI(nsIURI* aDocumentURI, bool* aPinned);
+
+ static nsTHashtable<nsCStringHashKey>* AllowedDomains();
+
+ private:
+ ~nsOfflineCacheUpdateService();
+
+ nsresult ProcessNextUpdate();
+
+ nsTArray<RefPtr<nsOfflineCacheUpdate> > mUpdates;
+ static nsTHashtable<nsCStringHashKey>* mAllowedDomains;
+
+ bool mDisabled;
+ bool mUpdateRunning;
+};
+
+#endif
diff --git a/uriloader/prefetch/nsOfflineCacheUpdateService.cpp b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp
new file mode 100644
index 0000000000..3f43193e39
--- /dev/null
+++ b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp
@@ -0,0 +1,646 @@
+/* -*- 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 "OfflineCacheUpdateChild.h"
+#include "OfflineCacheUpdateParent.h"
+#include "nsXULAppAPI.h"
+#include "OfflineCacheUpdateGlue.h"
+#include "nsOfflineCacheUpdate.h"
+
+#include "nsCURILoader.h"
+#include "nsIApplicationCacheService.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsIObserverService.h"
+#include "nsIWebProgress.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrincipal.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStreamUtils.h"
+#include "nsThreadUtils.h"
+#include "nsProxyRelease.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Components.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/PermissionMessageUtils.h"
+#include "nsContentUtils.h"
+#include "mozilla/Unused.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static nsOfflineCacheUpdateService* gOfflineCacheUpdateService = nullptr;
+
+nsTHashtable<nsCStringHashKey>* nsOfflineCacheUpdateService::mAllowedDomains =
+ nullptr;
+
+nsTHashtable<nsCStringHashKey>* nsOfflineCacheUpdateService::AllowedDomains() {
+ if (!mAllowedDomains) mAllowedDomains = new nsTHashtable<nsCStringHashKey>();
+
+ return mAllowedDomains;
+}
+
+typedef mozilla::docshell::OfflineCacheUpdateParent OfflineCacheUpdateParent;
+typedef mozilla::docshell::OfflineCacheUpdateChild OfflineCacheUpdateChild;
+typedef mozilla::docshell::OfflineCacheUpdateGlue OfflineCacheUpdateGlue;
+
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=nsOfflineCacheUpdate:5
+// set MOZ_LOG_FILE=offlineupdate.log
+//
+// this enables LogLevel::Debug level information and places all output in
+// the file offlineupdate.log
+//
+LazyLogModule gOfflineCacheUpdateLog("nsOfflineCacheUpdate");
+
+#undef LOG
+#define LOG(args) \
+ MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args)
+
+#undef LOG_ENABLED
+#define LOG_ENABLED() \
+ MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug)
+
+//-----------------------------------------------------------------------------
+// nsOfflineCachePendingUpdate
+//-----------------------------------------------------------------------------
+
+class nsOfflineCachePendingUpdate final : public nsIWebProgressListener,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ nsOfflineCachePendingUpdate(nsOfflineCacheUpdateService* aService,
+ nsIURI* aManifestURI, nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ Document* aDocument)
+ : mService(aService),
+ mManifestURI(aManifestURI),
+ mDocumentURI(aDocumentURI),
+ mLoadingPrincipal(aLoadingPrincipal),
+ mDidReleaseThis(false) {
+ mDocument = do_GetWeakReference(aDocument);
+ }
+
+ private:
+ ~nsOfflineCachePendingUpdate() {}
+
+ RefPtr<nsOfflineCacheUpdateService> mService;
+ nsCOMPtr<nsIURI> mManifestURI;
+ nsCOMPtr<nsIURI> mDocumentURI;
+ nsCOMPtr<nsIPrincipal> mLoadingPrincipal;
+ nsWeakPtr mDocument;
+ bool mDidReleaseThis;
+};
+
+NS_IMPL_ISUPPORTS(nsOfflineCachePendingUpdate, nsIWebProgressListener,
+ nsISupportsWeakReference)
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateService::nsIWebProgressListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCachePendingUpdate::OnProgressChange(nsIWebProgress* aProgress,
+ nsIRequest* aRequest,
+ int32_t curSelfProgress,
+ int32_t maxSelfProgress,
+ int32_t curTotalProgress,
+ int32_t maxTotalProgress) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCachePendingUpdate::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t progressStateFlags,
+ nsresult aStatus) {
+ if (mDidReleaseThis) {
+ return NS_OK;
+ }
+ nsCOMPtr<Document> updateDoc = do_QueryReferent(mDocument);
+ if (!updateDoc) {
+ // The document that scheduled this update has gone away,
+ // we don't need to listen anymore.
+ aWebProgress->RemoveProgressListener(this);
+ MOZ_ASSERT(!mDidReleaseThis);
+ mDidReleaseThis = true;
+ NS_RELEASE_THIS();
+ return NS_OK;
+ }
+
+ if (!(progressStateFlags & STATE_STOP)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> windowProxy;
+ aWebProgress->GetDOMWindow(getter_AddRefs(windowProxy));
+ if (!windowProxy) return NS_OK;
+
+ auto* outerWindow = nsPIDOMWindowOuter::From(windowProxy);
+ nsPIDOMWindowInner* innerWindow = outerWindow->GetCurrentInnerWindow();
+
+ nsCOMPtr<Document> progressDoc = outerWindow->GetDoc();
+ if (!progressDoc || progressDoc != updateDoc) {
+ return NS_OK;
+ }
+
+ LOG(("nsOfflineCachePendingUpdate::OnStateChange [%p, doc=%p]", this,
+ progressDoc.get()));
+
+ // Only schedule the update if the document loaded successfully
+ if (NS_SUCCEEDED(aStatus)) {
+ nsCOMPtr<nsIOfflineCacheUpdate> update;
+ mService->Schedule(mManifestURI, mDocumentURI, mLoadingPrincipal, updateDoc,
+ innerWindow, nullptr, getter_AddRefs(update));
+ if (mDidReleaseThis) {
+ return NS_OK;
+ }
+ }
+
+ aWebProgress->RemoveProgressListener(this);
+ MOZ_ASSERT(!mDidReleaseThis);
+ mDidReleaseThis = true;
+ NS_RELEASE_THIS();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCachePendingUpdate::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI* location,
+ uint32_t aFlags) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCachePendingUpdate::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsresult aStatus,
+ const char16_t* aMessage) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCachePendingUpdate::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aState) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCachePendingUpdate::OnContentBlockingEvent(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aEvent) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateService::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateService, nsIOfflineCacheUpdateService,
+ nsIObserver, nsISupportsWeakReference)
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateService <public>
+//-----------------------------------------------------------------------------
+
+nsOfflineCacheUpdateService::nsOfflineCacheUpdateService()
+ : mDisabled(false), mUpdateRunning(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+nsOfflineCacheUpdateService::~nsOfflineCacheUpdateService() {
+ MOZ_ASSERT(gOfflineCacheUpdateService == this);
+ gOfflineCacheUpdateService = nullptr;
+
+ delete mAllowedDomains;
+ mAllowedDomains = nullptr;
+}
+
+nsresult nsOfflineCacheUpdateService::Init() {
+ // Observe xpcom-shutdown event
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return NS_ERROR_FAILURE;
+
+ nsresult rv =
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ gOfflineCacheUpdateService = this;
+
+ return NS_OK;
+}
+
+/* static */
+already_AddRefed<nsOfflineCacheUpdateService>
+nsOfflineCacheUpdateService::GetInstance() {
+ if (!gOfflineCacheUpdateService) {
+ auto serv = MakeRefPtr<nsOfflineCacheUpdateService>();
+ if (NS_FAILED(serv->Init())) serv = nullptr;
+ MOZ_ASSERT(gOfflineCacheUpdateService == serv.get());
+ return serv.forget();
+ }
+
+ return do_AddRef(gOfflineCacheUpdateService);
+}
+
+/* static */
+nsOfflineCacheUpdateService* nsOfflineCacheUpdateService::EnsureService() {
+ if (!gOfflineCacheUpdateService) {
+ // Make the service manager hold a long-lived reference to the service
+ nsCOMPtr<nsIOfflineCacheUpdateService> service =
+ components::OfflineCacheUpdate::Service();
+ Unused << service;
+ }
+
+ return gOfflineCacheUpdateService;
+}
+
+nsresult nsOfflineCacheUpdateService::ScheduleUpdate(
+ nsOfflineCacheUpdate* aUpdate) {
+ LOG(("nsOfflineCacheUpdateService::Schedule [%p, update=%p]", this, aUpdate));
+
+ aUpdate->SetOwner(this);
+
+ mUpdates.AppendElement(aUpdate);
+ ProcessNextUpdate();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::ScheduleOnDocumentStop(
+ nsIURI* aManifestURI, nsIURI* aDocumentURI, nsIPrincipal* aLoadingPrincipal,
+ Document* aDocument) {
+ LOG(
+ ("nsOfflineCacheUpdateService::ScheduleOnDocumentStop [%p, "
+ "manifestURI=%p, documentURI=%p doc=%p]",
+ this, aManifestURI, aDocumentURI, aDocument));
+
+ nsCOMPtr<nsIWebProgress> progress =
+ do_QueryInterface(aDocument->GetContainer());
+ NS_ENSURE_TRUE(progress, NS_ERROR_INVALID_ARG);
+
+ // Proceed with cache update
+ RefPtr<nsOfflineCachePendingUpdate> update = new nsOfflineCachePendingUpdate(
+ this, aManifestURI, aDocumentURI, aLoadingPrincipal, aDocument);
+ NS_ENSURE_TRUE(update, NS_ERROR_OUT_OF_MEMORY);
+
+ nsresult rv = progress->AddProgressListener(
+ update, nsIWebProgress::NOTIFY_STATE_DOCUMENT);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // The update will release when it has scheduled itself.
+ Unused << update.forget();
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdateService::UpdateFinished(
+ nsOfflineCacheUpdate* aUpdate) {
+ LOG(("nsOfflineCacheUpdateService::UpdateFinished [%p, update=%p]", this,
+ aUpdate));
+
+ NS_ASSERTION(mUpdates.Length() > 0 && mUpdates[0] == aUpdate,
+ "Unknown update completed");
+
+ // keep this item alive until we're done notifying observers
+ RefPtr<nsOfflineCacheUpdate> update = mUpdates[0];
+ Unused << update;
+ mUpdates.RemoveElementAt(0);
+ mUpdateRunning = false;
+
+ ProcessNextUpdate();
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateService <private>
+//-----------------------------------------------------------------------------
+
+nsresult nsOfflineCacheUpdateService::ProcessNextUpdate() {
+ LOG(("nsOfflineCacheUpdateService::ProcessNextUpdate [%p, num=%zu]", this,
+ mUpdates.Length()));
+
+ if (mDisabled) return NS_ERROR_ABORT;
+
+ if (mUpdateRunning) return NS_OK;
+
+ if (mUpdates.Length() > 0) {
+ mUpdateRunning = true;
+
+ return mUpdates[0]->Begin();
+ }
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateService::nsIOfflineCacheUpdateService
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::GetNumUpdates(uint32_t* aNumUpdates) {
+ LOG(("nsOfflineCacheUpdateService::GetNumUpdates [%p]", this));
+
+ *aNumUpdates = mUpdates.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::GetUpdate(uint32_t aIndex,
+ nsIOfflineCacheUpdate** aUpdate) {
+ LOG(("nsOfflineCacheUpdateService::GetUpdate [%p, %d]", this, aIndex));
+
+ if (aIndex < mUpdates.Length()) {
+ NS_ADDREF(*aUpdate = mUpdates[aIndex]);
+ } else {
+ *aUpdate = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsOfflineCacheUpdateService::FindUpdate(
+ nsIURI* aManifestURI, nsACString const& aOriginSuffix,
+ nsIFile* aCustomProfileDir, nsOfflineCacheUpdate** aUpdate) {
+ nsresult rv;
+
+ nsCOMPtr<nsIApplicationCacheService> cacheService =
+ do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString groupID;
+ rv =
+ cacheService->BuildGroupIDForSuffix(aManifestURI, aOriginSuffix, groupID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<nsOfflineCacheUpdate> update;
+ for (uint32_t i = 0; i < mUpdates.Length(); i++) {
+ update = mUpdates[i];
+
+ bool partial;
+ rv = update->GetPartial(&partial);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (partial) {
+ // Partial updates aren't considered
+ continue;
+ }
+
+ if (update->IsForGroupID(groupID) &&
+ update->IsForProfile(aCustomProfileDir)) {
+ update.swap(*aUpdate);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult nsOfflineCacheUpdateService::Schedule(
+ nsIURI* aManifestURI, nsIURI* aDocumentURI, nsIPrincipal* aLoadingPrincipal,
+ Document* aDocument, nsPIDOMWindowInner* aWindow,
+ nsIFile* aCustomProfileDir, nsIOfflineCacheUpdate** aUpdate) {
+ nsCOMPtr<nsIOfflineCacheUpdate> update;
+ if (GeckoProcessType_Default != XRE_GetProcessType()) {
+ update = new OfflineCacheUpdateChild(aWindow);
+ } else {
+ update = new OfflineCacheUpdateGlue();
+ }
+
+ nsresult rv;
+
+ if (aWindow) {
+ // Ensure there is window.applicationCache object that is
+ // responsible for association of the new applicationCache
+ // with the corresponding document. Just ignore the result.
+ aWindow->GetApplicationCache();
+ }
+
+ rv = update->Init(aManifestURI, aDocumentURI, aLoadingPrincipal, aDocument,
+ aCustomProfileDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = update->Schedule();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*aUpdate = update);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::ScheduleUpdate(nsIURI* aManifestURI,
+ nsIURI* aDocumentURI,
+ nsIPrincipal* aLoadingPrincipal,
+ mozIDOMWindow* aWindow,
+ nsIOfflineCacheUpdate** aUpdate) {
+ return Schedule(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr,
+ nsPIDOMWindowInner::From(aWindow), nullptr, aUpdate);
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::ScheduleAppUpdate(
+ nsIURI* aManifestURI, nsIURI* aDocumentURI, nsIPrincipal* aLoadingPrincipal,
+ nsIFile* aProfileDir, nsIOfflineCacheUpdate** aUpdate) {
+ return Schedule(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr,
+ nullptr, aProfileDir, aUpdate);
+}
+
+NS_IMETHODIMP nsOfflineCacheUpdateService::CheckForUpdate(
+ nsIURI* aManifestURI, nsIPrincipal* aLoadingPrincipal,
+ nsIObserver* aObserver) {
+ if (GeckoProcessType_Default != XRE_GetProcessType()) {
+ // Not intended to support this on child processes
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ nsCOMPtr<nsIOfflineCacheUpdate> update = new OfflineCacheUpdateGlue();
+
+ nsresult rv;
+
+ rv = update->InitForUpdateCheck(aManifestURI, aLoadingPrincipal, aObserver);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = update->Schedule();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateService::nsIObserver
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ if (mUpdates.Length() > 0) mUpdates[0]->Cancel();
+ mDisabled = true;
+ }
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsOfflineCacheUpdateService::nsIOfflineCacheUpdateService
+//-----------------------------------------------------------------------------
+
+static nsresult OfflineAppPermForPrincipal(nsIPrincipal* aPrincipal,
+ bool pinned, bool* aAllowed) {
+ *aAllowed = false;
+
+ if (!StaticPrefs::browser_cache_offline_enable()) {
+ return NS_OK;
+ }
+
+ if (!StaticPrefs::browser_cache_offline_storage_enable()) {
+ return NS_OK;
+ }
+
+ if (!aPrincipal) return NS_ERROR_INVALID_ARG;
+
+ nsCOMPtr<nsIURI> uri;
+ // Casting to BasePrincipal, as we can't get InnerMost URI otherwise
+ auto* basePrincipal = BasePrincipal::Cast(aPrincipal);
+ basePrincipal->GetURI(getter_AddRefs(uri));
+
+ if (!uri) return NS_OK;
+
+ nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri);
+ if (!innerURI) return NS_OK;
+
+ // only https applications can use offline APIs.
+ if (!innerURI->SchemeIs("https")) {
+ return NS_OK;
+ }
+
+ nsAutoCString domain;
+ nsresult rv = innerURI->GetAsciiHost(domain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (nsOfflineCacheUpdateService::AllowedDomains()->Contains(domain)) {
+ *aAllowed = true;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPermissionManager> permissionManager =
+ services::GetPermissionManager();
+ if (!permissionManager) {
+ return NS_OK;
+ }
+
+ uint32_t perm;
+ const nsLiteralCString permName = pinned ? "pin-app"_ns : "offline-app"_ns;
+ permissionManager->TestExactPermissionFromPrincipal(aPrincipal, permName,
+ &perm);
+
+ if (perm == nsIPermissionManager::ALLOW_ACTION ||
+ perm == nsIOfflineCacheUpdateService::ALLOW_NO_WARN) {
+ *aAllowed = true;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::OfflineAppAllowed(nsIPrincipal* aPrincipal,
+ bool* aAllowed) {
+ return OfflineAppPermForPrincipal(aPrincipal, false, aAllowed);
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::OfflineAppAllowedForURI(nsIURI* aURI,
+ bool* aAllowed) {
+ OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(aURI, attrs);
+ return OfflineAppPermForPrincipal(principal, false, aAllowed);
+}
+
+nsresult nsOfflineCacheUpdateService::OfflineAppPinnedForURI(
+ nsIURI* aDocumentURI, bool* aPinned) {
+ OriginAttributes attrs;
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(aDocumentURI, attrs);
+ return OfflineAppPermForPrincipal(principal, true, aPinned);
+}
+
+NS_IMETHODIMP
+nsOfflineCacheUpdateService::AllowOfflineApp(nsIPrincipal* aPrincipal) {
+ nsresult rv;
+
+ if (!StaticPrefs::browser_cache_offline_enable()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!StaticPrefs::browser_cache_offline_storage_enable()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ // Casting to BasePrincipal, as we can't get InnerMost URI otherwise
+ auto* basePrincipal = BasePrincipal::Cast(aPrincipal);
+ basePrincipal->GetURI(getter_AddRefs(uri));
+
+ if (!uri) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri);
+ if (!innerURI) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // if http then we should prevent this cache
+ if (innerURI->SchemeIs("http")) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (GeckoProcessType_Default != XRE_GetProcessType()) {
+ ContentChild* child = ContentChild::GetSingleton();
+
+ if (!child->SendSetOfflinePermission(IPC::Principal(aPrincipal))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString domain;
+ rv = aPrincipal->GetBaseDomain(domain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsOfflineCacheUpdateService::AllowedDomains()->PutEntry(domain);
+ } else {
+ nsCOMPtr<nsIPermissionManager> permissionManager =
+ services::GetPermissionManager();
+ if (!permissionManager) return NS_ERROR_NOT_AVAILABLE;
+
+ rv = permissionManager->AddFromPrincipal(
+ aPrincipal, "offline-app"_ns, nsIPermissionManager::ALLOW_ACTION,
+ nsIPermissionManager::EXPIRE_NEVER, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
diff --git a/uriloader/prefetch/nsPrefetchService.cpp b/uriloader/prefetch/nsPrefetchService.cpp
new file mode 100644
index 0000000000..94762702e6
--- /dev/null
+++ b/uriloader/prefetch/nsPrefetchService.cpp
@@ -0,0 +1,889 @@
+/* -*- 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 "nsPrefetchService.h"
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/CORSMode.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/dom/HTMLLinkElement.h"
+#include "mozilla/dom/ServiceWorkerDescriptor.h"
+#include "mozilla/Preferences.h"
+#include "ReferrerInfo.h"
+
+#include "nsIObserverService.h"
+#include "nsIWebProgress.h"
+#include "nsICacheInfoChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIURL.h"
+#include "nsISupportsPriority.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsStreamUtils.h"
+#include "prtime.h"
+#include "mozilla/Logging.h"
+#include "plstr.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsINode.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+
+using namespace mozilla;
+
+//
+// To enable logging (see mozilla/Logging.h for full details):
+//
+// set MOZ_LOG=nsPrefetch:5
+// set MOZ_LOG_FILE=prefetch.log
+//
+// this enables LogLevel::Debug level information and places all output in
+// the file prefetch.log
+//
+static LazyLogModule gPrefetchLog("nsPrefetch");
+
+#undef LOG
+#define LOG(args) MOZ_LOG(gPrefetchLog, mozilla::LogLevel::Debug, args)
+
+#undef LOG_ENABLED
+#define LOG_ENABLED() MOZ_LOG_TEST(gPrefetchLog, mozilla::LogLevel::Debug)
+
+#define PREFETCH_PREF "network.prefetch-next"
+#define PARALLELISM_PREF "network.prefetch-next.parallelism"
+#define AGGRESSIVE_PREF "network.prefetch-next.aggressive"
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode <public>
+//-----------------------------------------------------------------------------
+
+nsPrefetchNode::nsPrefetchNode(nsPrefetchService* aService, nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo, nsINode* aSource,
+ nsContentPolicyType aPolicyType, bool aPreload)
+ : mURI(aURI),
+ mReferrerInfo(aReferrerInfo),
+ mPolicyType(aPolicyType),
+ mPreload(aPreload),
+ mService(aService),
+ mChannel(nullptr),
+ mBytesRead(0),
+ mShouldFireLoadEvent(false) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ mSources.AppendElement(source);
+}
+
+nsresult nsPrefetchNode::OpenChannel() {
+ if (mSources.IsEmpty()) {
+ // Don't attempt to prefetch if we don't have a source node
+ // (which should never happen).
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsINode> source;
+ while (!mSources.IsEmpty() &&
+ !(source = do_QueryReferent(mSources.ElementAt(0)))) {
+ // If source is null remove it.
+ // (which should never happen).
+ mSources.RemoveElementAt(0);
+ }
+
+ if (!source) {
+ // Don't attempt to prefetch if we don't have a source node
+ // (which should never happen).
+
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsILoadGroup> loadGroup = source->OwnerDoc()->GetDocumentLoadGroup();
+ CORSMode corsMode = CORS_NONE;
+ if (auto* link = dom::HTMLLinkElement::FromNode(source)) {
+ corsMode = link->GetCORSMode();
+ }
+
+ uint32_t securityFlags;
+ if (corsMode == CORS_NONE) {
+ securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
+ } else {
+ securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
+ if (corsMode == CORS_USE_CREDENTIALS) {
+ securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+ }
+ }
+ nsresult rv = NS_NewChannelInternal(
+ getter_AddRefs(mChannel), mURI, source, source->NodePrincipal(),
+ nullptr, // aTriggeringPrincipal
+ Maybe<ClientInfo>(), Maybe<ServiceWorkerDescriptor>(), securityFlags,
+ mPolicyType, source->OwnerDoc()->CookieJarSettings(),
+ nullptr, // aPerformanceStorage
+ loadGroup, // aLoadGroup
+ this, // aCallbacks
+ nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // configure HTTP specific stuff
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(mReferrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(success));
+ success = httpChannel->SetRequestHeader("X-Moz"_ns, "prefetch"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(success));
+ }
+
+ // Reduce the priority of prefetch network requests.
+ nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(mChannel);
+ if (priorityChannel) {
+ priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST);
+ }
+
+ rv = mChannel->AsyncOpen(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // Drop the ref to the channel, because we don't want to end up with
+ // cycles through it.
+ mChannel = nullptr;
+ }
+ return rv;
+}
+
+nsresult nsPrefetchNode::CancelChannel(nsresult error) {
+ mChannel->Cancel(error);
+ mChannel = nullptr;
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsPrefetchNode, nsIRequestObserver, nsIStreamListener,
+ nsIInterfaceRequestor, nsIChannelEventSink,
+ nsIRedirectResultListener)
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIStreamListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::OnStartRequest(nsIRequest* aRequest) {
+ nsresult rv;
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // if the load is cross origin without CORS, or the CORS access is rejected,
+ // always fire load event to avoid leaking site information.
+ nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo();
+ mShouldFireLoadEvent =
+ loadInfo->GetTainting() == LoadTainting::Opaque ||
+ (loadInfo->GetTainting() == LoadTainting::CORS &&
+ (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv)));
+
+ // no need to prefetch http error page
+ bool requestSucceeded;
+ if (NS_FAILED(httpChannel->GetRequestSucceeded(&requestSucceeded)) ||
+ !requestSucceeded) {
+ return NS_BINDING_ABORTED;
+ }
+
+ nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel =
+ do_QueryInterface(aRequest, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // no need to prefetch a document that is already in the cache
+ bool fromCache;
+ if (NS_SUCCEEDED(cacheInfoChannel->IsFromCache(&fromCache)) && fromCache) {
+ LOG(("document is already in the cache; canceling prefetch\n"));
+ // although it's canceled we still want to fire load event
+ mShouldFireLoadEvent = true;
+ return NS_BINDING_ABORTED;
+ }
+
+ //
+ // no need to prefetch a document that must be requested fresh each
+ // and every time.
+ //
+ uint32_t expTime;
+ if (NS_SUCCEEDED(cacheInfoChannel->GetCacheTokenExpirationTime(&expTime))) {
+ if (NowInSeconds() >= expTime) {
+ LOG(
+ ("document cannot be reused from cache; "
+ "canceling prefetch\n"));
+ return NS_BINDING_ABORTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchNode::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t bytesRead = 0;
+ aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead);
+ mBytesRead += bytesRead;
+ LOG(("prefetched %u bytes [offset=%" PRIu64 "]\n", bytesRead, aOffset));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchNode::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ LOG(("done prefetching [status=%" PRIx32 "]\n",
+ static_cast<uint32_t>(aStatus)));
+
+ if (mBytesRead == 0 && aStatus == NS_OK && mChannel) {
+ // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was
+ // specified), but the object should report loadedSize as if it
+ // did.
+ mChannel->GetContentLength(&mBytesRead);
+ }
+
+ mService->NotifyLoadCompleted(this);
+ mService->DispatchEvent(this, mShouldFireLoadEvent || NS_SUCCEEDED(aStatus));
+ mService->RemoveNodeAndMaybeStartNextPrefetchURI(this);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIInterfaceRequestor
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::GetInterface(const nsIID& aIID, void** aResult) {
+ if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
+ NS_ADDREF_THIS();
+ *aResult = static_cast<nsIChannelEventSink*>(this);
+ return NS_OK;
+ }
+
+ if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
+ NS_ADDREF_THIS();
+ *aResult = static_cast<nsIRedirectResultListener*>(this);
+ return NS_OK;
+ }
+
+ return NS_ERROR_NO_INTERFACE;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIChannelEventSink
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* callback) {
+ nsCOMPtr<nsIURI> newURI;
+ nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI));
+ if (NS_FAILED(rv)) return rv;
+
+ if (!newURI->SchemeIs("http") && !newURI->SchemeIs("https")) {
+ LOG(("rejected: URL is not of type http/https\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ // HTTP request headers are not automatically forwarded to the new channel.
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);
+ NS_ENSURE_STATE(httpChannel);
+
+ rv = httpChannel->SetRequestHeader("X-Moz"_ns, "prefetch"_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Assign to mChannel after we get notification about success of the
+ // redirect in OnRedirectResult.
+ mRedirectChannel = aNewChannel;
+
+ callback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchNode::nsIRedirectResultListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchNode::OnRedirectResult(bool proceeding) {
+ if (proceeding && mRedirectChannel) mChannel = mRedirectChannel;
+
+ mRedirectChannel = nullptr;
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService <public>
+//-----------------------------------------------------------------------------
+
+nsPrefetchService::nsPrefetchService()
+ : mMaxParallelism(6),
+ mStopCount(0),
+ mHaveProcessed(false),
+ mPrefetchDisabled(true),
+ mAggressive(false) {}
+
+nsPrefetchService::~nsPrefetchService() {
+ Preferences::RemoveObserver(this, PREFETCH_PREF);
+ Preferences::RemoveObserver(this, PARALLELISM_PREF);
+ Preferences::RemoveObserver(this, AGGRESSIVE_PREF);
+ // cannot reach destructor if prefetch in progress (listener owns reference
+ // to this service)
+ EmptyPrefetchQueue();
+}
+
+nsresult nsPrefetchService::Init() {
+ nsresult rv;
+
+ // read prefs and hook up pref observer
+ mPrefetchDisabled = !Preferences::GetBool(PREFETCH_PREF, !mPrefetchDisabled);
+ Preferences::AddWeakObserver(this, PREFETCH_PREF);
+
+ mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism);
+ if (mMaxParallelism < 1) {
+ mMaxParallelism = 1;
+ }
+ Preferences::AddWeakObserver(this, PARALLELISM_PREF);
+
+ mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false);
+ Preferences::AddWeakObserver(this, AGGRESSIVE_PREF);
+
+ // Observe xpcom-shutdown event
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return NS_ERROR_FAILURE;
+
+ rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mPrefetchDisabled) {
+ AddProgressListener();
+ }
+
+ return NS_OK;
+}
+
+void nsPrefetchService::RemoveNodeAndMaybeStartNextPrefetchURI(
+ nsPrefetchNode* aFinished) {
+ if (aFinished) {
+ mCurrentNodes.RemoveElement(aFinished);
+ }
+
+ if ((!mStopCount && mHaveProcessed) || mAggressive) {
+ ProcessNextPrefetchURI();
+ }
+}
+
+void nsPrefetchService::ProcessNextPrefetchURI() {
+ if (mCurrentNodes.Length() >= static_cast<uint32_t>(mMaxParallelism)) {
+ // We already have enough prefetches going on, so hold off
+ // for now.
+ return;
+ }
+
+ nsresult rv;
+
+ do {
+ if (mPrefetchQueue.empty()) {
+ break;
+ }
+ RefPtr<nsPrefetchNode> node = std::move(mPrefetchQueue.front());
+ mPrefetchQueue.pop_front();
+
+ if (LOG_ENABLED()) {
+ LOG(("ProcessNextPrefetchURI [%s]\n",
+ node->mURI->GetSpecOrDefault().get()));
+ }
+
+ //
+ // if opening the channel fails (e.g. security check returns an error),
+ // send an error event and then just skip to the next uri
+ //
+ rv = node->OpenChannel();
+ if (NS_SUCCEEDED(rv)) {
+ mCurrentNodes.AppendElement(node);
+ } else {
+ DispatchEvent(node, false);
+ }
+ } while (NS_FAILED(rv));
+}
+
+void nsPrefetchService::NotifyLoadRequested(nsPrefetchNode* node) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return;
+
+ observerService->NotifyObservers(
+ static_cast<nsIStreamListener*>(node),
+ (node->mPreload) ? "preload-load-requested" : "prefetch-load-requested",
+ nullptr);
+}
+
+void nsPrefetchService::NotifyLoadCompleted(nsPrefetchNode* node) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (!observerService) return;
+
+ observerService->NotifyObservers(
+ static_cast<nsIStreamListener*>(node),
+ (node->mPreload) ? "preload-load-completed" : "prefetch-load-completed",
+ nullptr);
+}
+
+void nsPrefetchService::DispatchEvent(nsPrefetchNode* node, bool aSuccess) {
+ for (uint32_t i = 0; i < node->mSources.Length(); i++) {
+ nsCOMPtr<nsINode> domNode = do_QueryReferent(node->mSources.ElementAt(i));
+ if (domNode && domNode->IsInComposedDoc()) {
+ // We don't dispatch synchronously since |node| might be in a DocGroup
+ // that we're not allowed to touch. (Our network request happens in the
+ // DocGroup of one of the mSources nodes--not necessarily this one).
+ RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
+ domNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo);
+ dispatcher->RequireNodeInDocument();
+ dispatcher->PostDOMEvent();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService <private>
+//-----------------------------------------------------------------------------
+
+void nsPrefetchService::AddProgressListener() {
+ // Register as an observer for the document loader
+ nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
+ if (progress)
+ progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT);
+}
+
+void nsPrefetchService::RemoveProgressListener() {
+ // Register as an observer for the document loader
+ nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service();
+ if (progress) progress->RemoveProgressListener(this);
+}
+
+nsresult nsPrefetchService::EnqueueURI(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource,
+ nsPrefetchNode** aNode) {
+ RefPtr<nsPrefetchNode> node = new nsPrefetchNode(
+ this, aURI, aReferrerInfo, aSource, nsIContentPolicy::TYPE_OTHER, false);
+ mPrefetchQueue.push_back(node);
+ node.forget(aNode);
+ return NS_OK;
+}
+
+void nsPrefetchService::EmptyPrefetchQueue() {
+ while (!mPrefetchQueue.empty()) {
+ mPrefetchQueue.pop_back();
+ }
+}
+
+void nsPrefetchService::StartPrefetching() {
+ //
+ // at initialization time we might miss the first DOCUMENT START
+ // notification, so we have to be careful to avoid letting our
+ // stop count go negative.
+ //
+ if (mStopCount > 0) mStopCount--;
+
+ LOG(("StartPrefetching [stopcount=%d]\n", mStopCount));
+
+ // only start prefetching after we've received enough DOCUMENT
+ // STOP notifications. we do this inorder to defer prefetching
+ // until after all sub-frames have finished loading.
+ if (!mStopCount) {
+ mHaveProcessed = true;
+ while (!mPrefetchQueue.empty() &&
+ mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) {
+ ProcessNextPrefetchURI();
+ }
+ }
+}
+
+void nsPrefetchService::StopPrefetching() {
+ mStopCount++;
+
+ LOG(("StopPrefetching [stopcount=%d]\n", mStopCount));
+
+ // When we start a load, we need to stop all prefetches that has been
+ // added by the old load, therefore call StopAll only at the moment we
+ // switch to a new page load (i.e. mStopCount == 1).
+ // TODO: do not stop prefetches that are relevant for the new load.
+ if (mStopCount == 1) {
+ StopAll();
+ }
+}
+
+void nsPrefetchService::StopCurrentPrefetchsPreloads(bool aPreload) {
+ for (int32_t i = mCurrentNodes.Length() - 1; i >= 0; --i) {
+ if (mCurrentNodes[i]->mPreload == aPreload) {
+ mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED);
+ mCurrentNodes.RemoveElementAt(i);
+ }
+ }
+
+ if (!aPreload) {
+ EmptyPrefetchQueue();
+ }
+}
+
+void nsPrefetchService::StopAll() {
+ for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) {
+ mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED);
+ }
+ mCurrentNodes.Clear();
+ EmptyPrefetchQueue();
+}
+
+nsresult nsPrefetchService::CheckURIScheme(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo) {
+ //
+ // XXX we should really be asking the protocol handler if it supports
+ // caching, so we can determine if there is any value to prefetching.
+ // for now, we'll only prefetch http and https links since we know that's
+ // the most common case.
+ //
+ if (!aURI->SchemeIs("http") && !aURI->SchemeIs("https")) {
+ LOG(("rejected: URL is not of type http/https\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ //
+ // the referrer URI must be http:
+ //
+ nsCOMPtr<nsIURI> referrer = aReferrerInfo->GetOriginalReferrer();
+ if (!referrer) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (!referrer->SchemeIs("http") && !referrer->SchemeIs("https")) {
+ LOG(("rejected: referrer URL is neither http nor https\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsISupports
+//-----------------------------------------------------------------------------
+
+NS_IMPL_ISUPPORTS(nsPrefetchService, nsIPrefetchService, nsIWebProgressListener,
+ nsIObserver, nsISupportsWeakReference)
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsIPrefetchService
+//-----------------------------------------------------------------------------
+
+nsresult nsPrefetchService::Preload(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource,
+ nsContentPolicyType aPolicyType) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aReferrerInfo);
+ if (LOG_ENABLED()) {
+ LOG(("PreloadURI [%s]\n", aURI->GetSpecOrDefault().get()));
+ }
+
+ LOG(("rejected: preload service is deprecated\n"));
+ return NS_ERROR_ABORT;
+}
+
+nsresult nsPrefetchService::Prefetch(nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, bool aExplicit) {
+ NS_ENSURE_ARG_POINTER(aURI);
+ NS_ENSURE_ARG_POINTER(aReferrerInfo);
+
+ if (LOG_ENABLED()) {
+ LOG(("PrefetchURI [%s]\n", aURI->GetSpecOrDefault().get()));
+ }
+
+ if (mPrefetchDisabled) {
+ LOG(("rejected: prefetch service is disabled\n"));
+ return NS_ERROR_ABORT;
+ }
+
+ nsresult rv = CheckURIScheme(aURI, aReferrerInfo);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // XXX we might want to either leverage nsIProtocolHandler::protocolFlags
+ // or possibly nsIRequest::loadFlags to determine if this URI should be
+ // prefetched.
+ //
+
+ // skip URLs that contain query strings, except URLs for which prefetching
+ // has been explicitly requested.
+ if (!aExplicit) {
+ nsCOMPtr<nsIURL> url(do_QueryInterface(aURI, &rv));
+ if (NS_FAILED(rv)) return rv;
+ nsAutoCString query;
+ rv = url->GetQuery(query);
+ if (NS_FAILED(rv) || !query.IsEmpty()) {
+ LOG(("rejected: URL has a query string\n"));
+ return NS_ERROR_ABORT;
+ }
+ }
+
+ //
+ // Check whether it is being prefetched.
+ //
+ for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) {
+ bool equals;
+ if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (mCurrentNodes[i]->mSources.IndexOf(source) ==
+ mCurrentNodes[i]->mSources.NoIndex) {
+ LOG(
+ ("URL is already being prefetched, add a new reference "
+ "document\n"));
+ mCurrentNodes[i]->mSources.AppendElement(source);
+ return NS_OK;
+ } else {
+ LOG(("URL is already being prefetched by this document"));
+ return NS_ERROR_ABORT;
+ }
+ }
+ }
+
+ //
+ // Check whether it is on the prefetch queue.
+ //
+ for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt =
+ mPrefetchQueue.begin();
+ nodeIt != mPrefetchQueue.end(); nodeIt++) {
+ bool equals;
+ RefPtr<nsPrefetchNode> node = nodeIt->get();
+ if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (node->mSources.IndexOf(source) == node->mSources.NoIndex) {
+ LOG(
+ ("URL is already being prefetched, add a new reference "
+ "document\n"));
+ node->mSources.AppendElement(do_GetWeakReference(aSource));
+ return NS_OK;
+ } else {
+ LOG(("URL is already being prefetched by this document"));
+ return NS_ERROR_ABORT;
+ }
+ }
+ }
+
+ RefPtr<nsPrefetchNode> enqueuedNode;
+ rv = EnqueueURI(aURI, aReferrerInfo, aSource, getter_AddRefs(enqueuedNode));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NotifyLoadRequested(enqueuedNode);
+
+ // if there are no pages loading, kick off the request immediately
+ if ((!mStopCount && mHaveProcessed) || mAggressive) {
+ ProcessNextPrefetchURI();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::CancelPrefetchPreloadURI(nsIURI* aURI, nsINode* aSource) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ if (LOG_ENABLED()) {
+ LOG(("CancelPrefetchURI [%s]\n", aURI->GetSpecOrDefault().get()));
+ }
+
+ //
+ // look in current prefetches
+ //
+ for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) {
+ bool equals;
+ if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (mCurrentNodes[i]->mSources.IndexOf(source) !=
+ mCurrentNodes[i]->mSources.NoIndex) {
+ mCurrentNodes[i]->mSources.RemoveElement(source);
+ if (mCurrentNodes[i]->mSources.IsEmpty()) {
+ mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED);
+ mCurrentNodes.RemoveElementAt(i);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ //
+ // look into the prefetch queue
+ //
+ for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt =
+ mPrefetchQueue.begin();
+ nodeIt != mPrefetchQueue.end(); nodeIt++) {
+ bool equals;
+ RefPtr<nsPrefetchNode> node = nodeIt->get();
+ if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) {
+ nsWeakPtr source = do_GetWeakReference(aSource);
+ if (node->mSources.IndexOf(source) != node->mSources.NoIndex) {
+#ifdef DEBUG
+ int32_t inx = node->mSources.IndexOf(source);
+ nsCOMPtr<nsINode> domNode =
+ do_QueryReferent(node->mSources.ElementAt(inx));
+ MOZ_ASSERT(domNode);
+#endif
+
+ node->mSources.RemoveElement(source);
+ if (node->mSources.IsEmpty()) {
+ mPrefetchQueue.erase(nodeIt);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // not found!
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::PreloadURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource,
+ nsContentPolicyType aPolicyType) {
+ return Preload(aURI, aReferrerInfo, aSource, aPolicyType);
+}
+
+NS_IMETHODIMP
+nsPrefetchService::PrefetchURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, bool aExplicit) {
+ return Prefetch(aURI, aReferrerInfo, aSource, aExplicit);
+}
+
+NS_IMETHODIMP
+nsPrefetchService::HasMoreElements(bool* aHasMore) {
+ *aHasMore = (mCurrentNodes.Length() || !mPrefetchQueue.empty());
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsIWebProgressListener
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchService::OnProgressChange(nsIWebProgress* aProgress,
+ nsIRequest* aRequest,
+ int32_t curSelfProgress,
+ int32_t maxSelfProgress,
+ int32_t curTotalProgress,
+ int32_t maxTotalProgress) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t progressStateFlags,
+ nsresult aStatus) {
+ if (progressStateFlags & STATE_IS_DOCUMENT) {
+ if (progressStateFlags & STATE_STOP)
+ StartPrefetching();
+ else if (progressStateFlags & STATE_START)
+ StopPrefetching();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* location,
+ uint32_t aFlags) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsPrefetchService::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)");
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService::nsIObserver
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+nsPrefetchService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ LOG(("nsPrefetchService::Observe [topic=%s]\n", aTopic));
+
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ StopAll();
+ mPrefetchDisabled = true;
+ } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ const nsCString converted = NS_ConvertUTF16toUTF8(aData);
+ const char* pref = converted.get();
+ if (!strcmp(pref, PREFETCH_PREF)) {
+ if (Preferences::GetBool(PREFETCH_PREF, false)) {
+ if (mPrefetchDisabled) {
+ LOG(("enabling prefetching\n"));
+ mPrefetchDisabled = false;
+ AddProgressListener();
+ }
+ } else {
+ if (!mPrefetchDisabled) {
+ LOG(("disabling prefetching\n"));
+ StopCurrentPrefetchsPreloads(false);
+ mPrefetchDisabled = true;
+ RemoveProgressListener();
+ }
+ }
+ } else if (!strcmp(pref, PARALLELISM_PREF)) {
+ mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism);
+ if (mMaxParallelism < 1) {
+ mMaxParallelism = 1;
+ }
+ // If our parallelism has increased, go ahead and kick off enough
+ // prefetches to fill up our allowance. If we're now over our
+ // allowance, we'll just silently let some of them finish to get
+ // back below our limit.
+ while (((!mStopCount && mHaveProcessed) || mAggressive) &&
+ !mPrefetchQueue.empty() &&
+ mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) {
+ ProcessNextPrefetchURI();
+ }
+ } else if (!strcmp(pref, AGGRESSIVE_PREF)) {
+ mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false);
+ // in aggressive mode, start prefetching immediately
+ if (mAggressive) {
+ while (mStopCount && !mPrefetchQueue.empty() &&
+ mCurrentNodes.Length() <
+ static_cast<uint32_t>(mMaxParallelism)) {
+ ProcessNextPrefetchURI();
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// vim: ts=4 sw=2 expandtab
diff --git a/uriloader/prefetch/nsPrefetchService.h b/uriloader/prefetch/nsPrefetchService.h
new file mode 100644
index 0000000000..7b931ba52c
--- /dev/null
+++ b/uriloader/prefetch/nsPrefetchService.h
@@ -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/. */
+
+#ifndef nsPrefetchService_h__
+#define nsPrefetchService_h__
+
+#include "nsIObserver.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIChannelEventSink.h"
+#include "nsIPrefetchService.h"
+#include "nsIRedirectResultListener.h"
+#include "nsIWebProgressListener.h"
+#include "nsIStreamListener.h"
+#include "nsIChannel.h"
+#include "nsIURI.h"
+#include "nsWeakReference.h"
+#include "nsCOMPtr.h"
+#include "mozilla/Attributes.h"
+#include <deque>
+
+class nsPrefetchService;
+class nsPrefetchNode;
+class nsIReferrerInfo;
+
+//-----------------------------------------------------------------------------
+// nsPrefetchService
+//-----------------------------------------------------------------------------
+
+class nsPrefetchService final : public nsIPrefetchService,
+ public nsIWebProgressListener,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPREFETCHSERVICE
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIOBSERVER
+
+ nsPrefetchService();
+
+ nsresult Init();
+ void RemoveNodeAndMaybeStartNextPrefetchURI(nsPrefetchNode* aFinished);
+ void ProcessNextPrefetchURI();
+
+ void NotifyLoadRequested(nsPrefetchNode* node);
+ void NotifyLoadCompleted(nsPrefetchNode* node);
+ void DispatchEvent(nsPrefetchNode* node, bool aSuccess);
+
+ private:
+ ~nsPrefetchService();
+
+ nsresult Prefetch(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, bool aExplicit);
+
+ nsresult Preload(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, nsContentPolicyType aPolicyType);
+
+ void AddProgressListener();
+ void RemoveProgressListener();
+ nsresult EnqueueURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo,
+ nsINode* aSource, nsPrefetchNode** node);
+ void EmptyPrefetchQueue();
+
+ void StartPrefetching();
+ void StopPrefetching();
+ void StopCurrentPrefetchsPreloads(bool aPreload);
+ void StopAll();
+ nsresult CheckURIScheme(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo);
+
+ std::deque<RefPtr<nsPrefetchNode>> mPrefetchQueue;
+ nsTArray<RefPtr<nsPrefetchNode>> mCurrentNodes;
+ int32_t mMaxParallelism;
+ int32_t mStopCount;
+ bool mHaveProcessed;
+ bool mPrefetchDisabled;
+
+ // In usual case prefetch does not start until all normal loads are done.
+ // Aggressive mode ignores normal loads and just start prefetch ASAP.
+ // It's mainly for testing purpose and discoraged for normal use;
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=1281415 for details.
+ bool mAggressive;
+};
+
+//-----------------------------------------------------------------------------
+// nsPreFetchingNode
+//-----------------------------------------------------------------------------
+
+class nsPrefetchNode final : public nsIStreamListener,
+ public nsIInterfaceRequestor,
+ public nsIChannelEventSink,
+ public nsIRedirectResultListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIREDIRECTRESULTLISTENER
+
+ nsPrefetchNode(nsPrefetchService* aPrefetchService, nsIURI* aURI,
+ nsIReferrerInfo* aReferrerInfo, nsINode* aSource,
+ nsContentPolicyType aPolicyType, bool aPreload);
+
+ nsresult OpenChannel();
+ nsresult CancelChannel(nsresult error);
+
+ nsCOMPtr<nsIURI> mURI;
+ nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
+ nsTArray<nsWeakPtr> mSources;
+
+ // The policy type to be used for fetching the resource.
+ nsContentPolicyType mPolicyType;
+ // nsPrefetchNode is used for prefetching and preloading resource.
+ // mPreload is true if a resource is preloaded. Preloads and
+ // prefetches are fetched in different phases (during load and
+ // after a page load), therefore we need to distinguish them.
+ bool mPreload;
+
+ private:
+ ~nsPrefetchNode() {}
+
+ RefPtr<nsPrefetchService> mService;
+ nsCOMPtr<nsIChannel> mChannel;
+ nsCOMPtr<nsIChannel> mRedirectChannel;
+ int64_t mBytesRead;
+ bool mShouldFireLoadEvent;
+};
+
+#endif // !nsPrefetchService_h__
diff --git a/uriloader/preload/FetchPreloader.cpp b/uriloader/preload/FetchPreloader.cpp
new file mode 100644
index 0000000000..411ca266fc
--- /dev/null
+++ b/uriloader/preload/FetchPreloader.cpp
@@ -0,0 +1,339 @@
+/* -*- 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 "FetchPreloader.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Unused.h"
+#include "nsContentPolicyUtils.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsIClassOfService.h"
+#include "nsIHttpChannel.h"
+#include "nsITimedChannel.h"
+#include "nsNetUtil.h"
+#include "nsStringStream.h"
+#include "nsIDocShell.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver)
+
+FetchPreloader::FetchPreloader()
+ : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD) {}
+
+FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType)
+ : mContentPolicyType(aContentPolicyType) {}
+
+nsresult FetchPreloader::OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI,
+ const CORSMode aCORSMode,
+ const dom::ReferrerPolicy& aReferrerPolicy,
+ dom::Document* aDocument) {
+ nsresult rv;
+ nsCOMPtr<nsIChannel> channel;
+
+ auto notify = MakeScopeExit([&]() {
+ if (NS_FAILED(rv)) {
+ // Make sure we notify any <link preload> elements when opening fails
+ // because of various technical or security reasons.
+ NotifyStart(channel);
+ // Using the non-channel overload of this method to make it work even
+ // before NotifyOpen has been called on this preload. We are not
+ // switching between channels, so it's safe to do so.
+ NotifyStop(rv);
+ }
+ });
+
+ nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup();
+ nsCOMPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow();
+ nsCOMPtr<nsIInterfaceRequestor> prompter;
+ if (window) {
+ nsIDocShell* docshell = window->GetDocShell();
+ prompter = do_QueryInterface(docshell);
+ }
+
+ rv = CreateChannel(getter_AddRefs(channel), aURI, aCORSMode, aReferrerPolicy,
+ aDocument, loadGroup, prompter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Doing this now so that we have the channel and tainting set on it properly
+ // to notify the proper event (load or error) on the associated preload tags
+ // when the CSP check fails.
+ rv = CheckContentPolicy(aURI, aDocument);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ PrioritizeAsPreload(channel);
+ AddLoadBackgroundFlag(channel);
+
+ NotifyOpen(aKey, channel, aDocument, true);
+
+ return mAsyncConsumeResult = rv = channel->AsyncOpen(this);
+}
+
+nsresult FetchPreloader::CreateChannel(
+ nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode,
+ const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument,
+ nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks) {
+ nsresult rv;
+
+ nsSecurityFlags securityFlags =
+ aCORSMode == CORS_NONE
+ ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL
+ : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
+ if (aCORSMode == CORS_ANONYMOUS) {
+ securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN;
+ } else if (aCORSMode == CORS_USE_CREDENTIALS) {
+ securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+ }
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannelWithTriggeringPrincipal(
+ getter_AddRefs(channel), aURI, aDocument, aDocument->NodePrincipal(),
+ securityFlags, nsIContentPolicy::TYPE_FETCH, nullptr, aLoadGroup,
+ aCallbacks);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) {
+ nsCOMPtr<nsIReferrerInfo> referrerInfo = new dom::ReferrerInfo(
+ aDocument->GetDocumentURIAsReferrer(), aReferrerPolicy);
+ rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) {
+ timedChannel->SetInitiatorType(u"link"_ns);
+ }
+
+ channel.forget(aChannel);
+ return NS_OK;
+}
+
+nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI,
+ dom::Document* aDocument) {
+ if (!aDocument) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo(
+ aDocument->NodePrincipal(), aDocument->NodePrincipal(), aDocument,
+ nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, mContentPolicyType);
+
+ int16_t shouldLoad = nsIContentPolicy::ACCEPT;
+ nsresult rv =
+ NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo, ""_ns, &shouldLoad,
+ nsContentUtils::GetContentPolicy());
+ if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) {
+ return NS_OK;
+ }
+
+ return NS_ERROR_CONTENT_BLOCKED;
+}
+
+// PreloaderBase
+
+nsresult FetchPreloader::AsyncConsume(nsIStreamListener* aListener) {
+ if (NS_FAILED(mAsyncConsumeResult)) {
+ // Already being consumed or failed to open.
+ return mAsyncConsumeResult;
+ }
+
+ // Prevent duplicate calls.
+ mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE;
+
+ if (!mConsumeListener) {
+ // Called before we are getting response from the channel.
+ mConsumeListener = aListener;
+ } else {
+ // Channel already started, push cached calls to this listener.
+ // Can't be anything else than the `Cache`, hence a safe static_cast.
+ Cache* cache = static_cast<Cache*>(mConsumeListener.get());
+ cache->AsyncConsume(aListener);
+ }
+
+ return NS_OK;
+}
+
+// static
+void FetchPreloader::PrioritizeAsPreload(nsIChannel* aChannel) {
+ if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) {
+ cos->AddClassFlags(nsIClassOfService::Unblocked);
+ }
+}
+
+void FetchPreloader::PrioritizeAsPreload() { PrioritizeAsPreload(Channel()); }
+
+// nsIRequestObserver + nsIStreamListener
+
+NS_IMETHODIMP FetchPreloader::OnStartRequest(nsIRequest* request) {
+ NotifyStart(request);
+
+ if (!mConsumeListener) {
+ // AsyncConsume not called yet.
+ mConsumeListener = new Cache();
+ }
+
+ return mConsumeListener->OnStartRequest(request);
+}
+
+NS_IMETHODIMP FetchPreloader::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* input,
+ uint64_t offset, uint32_t count) {
+ return mConsumeListener->OnDataAvailable(request, input, offset, count);
+}
+
+NS_IMETHODIMP FetchPreloader::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ mConsumeListener->OnStopRequest(request, status);
+
+ // We want 404 or other types of server responses to be treated as 'error'.
+ if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request)) {
+ uint32_t responseStatus = 0;
+ Unused << httpChannel->GetResponseStatus(&responseStatus);
+ if (responseStatus / 100 != 2) {
+ status = NS_ERROR_FAILURE;
+ }
+ }
+
+ // Fetch preloader wants to keep the channel around so that consumers like XHR
+ // can access it even after the preload is done.
+ nsCOMPtr<nsIChannel> channel = mChannel;
+ NotifyStop(request, status);
+ mChannel.swap(channel);
+ return NS_OK;
+}
+
+// FetchPreloader::Cache
+
+NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver)
+
+NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) {
+ mRequest = request;
+
+ if (mFinalListener) {
+ return mFinalListener->OnStartRequest(mRequest);
+ }
+
+ mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}});
+ return NS_OK;
+}
+
+NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* input,
+ uint64_t offset,
+ uint32_t count) {
+ if (mFinalListener) {
+ return mFinalListener->OnDataAvailable(mRequest, input, offset, count);
+ }
+
+ DataAvailable data;
+ if (!data.mData.SetLength(count, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ uint32_t read;
+ nsresult rv = input->Read(data.mData.BeginWriting(), count, &read);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)});
+ return NS_OK;
+}
+
+NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ if (mFinalListener) {
+ return mFinalListener->OnStopRequest(mRequest, status);
+ }
+
+ mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}});
+ return NS_OK;
+}
+
+void FetchPreloader::Cache::AsyncConsume(nsIStreamListener* aListener) {
+ // Must dispatch for two reasons:
+ // 1. This is called directly from FetchLoader::AsyncConsume, which must
+ // behave the same way as nsIChannel::AsyncOpen.
+ // 2. In case there are already stream listener events scheduled on the main
+ // thread we preserve the order - those will still end up in Cache.
+
+ // * The `Cache` object is fully main thread only for now, doesn't support
+ // retargeting, but it can be improved to allow it.
+
+ nsCOMPtr<nsIStreamListener> listener(aListener);
+ NS_DispatchToMainThread(NewRunnableMethod<nsCOMPtr<nsIStreamListener>>(
+ "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume,
+ listener));
+}
+
+void FetchPreloader::Cache::Consume(nsCOMPtr<nsIStreamListener> aListener) {
+ MOZ_ASSERT(!mFinalListener, "Duplicate call");
+
+ mFinalListener = std::move(aListener);
+
+ // Status of the channel read after each call.
+ nsresult status = NS_OK;
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(mRequest));
+
+ RefPtr<Cache> self(this);
+ for (auto& call : mCalls) {
+ nsresult rv = call.match(
+ [&](const StartRequest& startRequest) mutable {
+ return self->mFinalListener->OnStartRequest(self->mRequest);
+ },
+ [&](const DataAvailable& dataAvailable) mutable {
+ if (NS_FAILED(status)) {
+ // Channel has been cancelled during this mCalls loop.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIInputStream> input;
+ rv = NS_NewCStringInputStream(getter_AddRefs(input),
+ dataAvailable.mData);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return self->mFinalListener->OnDataAvailable(
+ self->mRequest, input, 0, dataAvailable.mData.Length());
+ },
+ [&](const StopRequest& stopRequest) {
+ // First cancellation overrides mStatus in nsHttpChannel.
+ nsresult stopStatus =
+ NS_FAILED(status) ? status : stopRequest.mStatus;
+ self->mFinalListener->OnStopRequest(self->mRequest, stopStatus);
+ self->mFinalListener = nullptr;
+ self->mRequest = nullptr;
+ return NS_OK;
+ });
+
+ if (!mRequest) {
+ // We are done!
+ break;
+ }
+
+ bool isCancelled = false;
+ Unused << channel->GetCanceled(&isCancelled);
+ if (isCancelled) {
+ mRequest->GetStatus(&status);
+ } else if (NS_FAILED(rv)) {
+ status = rv;
+ mRequest->Cancel(status);
+ }
+ }
+
+ mCalls.Clear();
+}
+
+} // namespace mozilla
diff --git a/uriloader/preload/FetchPreloader.h b/uriloader/preload/FetchPreloader.h
new file mode 100644
index 0000000000..f501a79c97
--- /dev/null
+++ b/uriloader/preload/FetchPreloader.h
@@ -0,0 +1,99 @@
+/* -*- 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 FetchPreloader_h_
+#define FetchPreloader_h_
+
+#include "mozilla/PreloaderBase.h"
+#include "mozilla/Variant.h"
+#include "nsCOMPtr.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIContentPolicy.h"
+#include "nsIStreamListener.h"
+
+class nsIChannel;
+class nsILoadGroup;
+class nsIInterfaceRequestor;
+
+namespace mozilla {
+
+namespace dom {
+enum class ReferrerPolicy : uint8_t;
+}
+
+class FetchPreloader : public PreloaderBase, public nsIStreamListener {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ FetchPreloader();
+ nsresult OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI,
+ const CORSMode aCORSMode,
+ const dom::ReferrerPolicy& aReferrerPolicy,
+ dom::Document* aDocument);
+
+ // PreloaderBase
+ nsresult AsyncConsume(nsIStreamListener* aListener) override;
+ static void PrioritizeAsPreload(nsIChannel* aChannel);
+ void PrioritizeAsPreload() override;
+
+ protected:
+ explicit FetchPreloader(nsContentPolicyType aContentPolicyType);
+ virtual ~FetchPreloader() = default;
+
+ // Create and setup the channel with necessary security properties. This is
+ // overridable by subclasses to allow different initial conditions.
+ virtual nsresult CreateChannel(nsIChannel** aChannel, nsIURI* aURI,
+ const CORSMode aCORSMode,
+ const dom::ReferrerPolicy& aReferrerPolicy,
+ dom::Document* aDocument,
+ nsILoadGroup* aLoadGroup,
+ nsIInterfaceRequestor* aCallbacks);
+
+ private:
+ nsresult CheckContentPolicy(nsIURI* aURI, dom::Document* aDocument);
+
+ class Cache final : public nsIStreamListener {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ void AsyncConsume(nsIStreamListener* aListener);
+ void Consume(nsCOMPtr<nsIStreamListener> aListener);
+
+ private:
+ virtual ~Cache() = default;
+
+ struct StartRequest {};
+ struct DataAvailable {
+ nsCString mData;
+ };
+ struct StopRequest {
+ nsresult mStatus;
+ };
+
+ typedef Variant<StartRequest, DataAvailable, StopRequest> Call;
+ nsCOMPtr<nsIRequest> mRequest;
+ nsCOMPtr<nsIStreamListener> mFinalListener;
+ nsTArray<Call> mCalls;
+ };
+
+ // The listener passed to AsyncConsume in case we haven't started getting the
+ // data from the channel yet.
+ nsCOMPtr<nsIStreamListener> mConsumeListener;
+
+ // Returned by AsyncConsume when a failure. This remembers the result of
+ // opening the channel and prevents duplicate consumation.
+ nsresult mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE;
+
+ // The CP type to check against. Derived classes have to override call to CSP
+ // constructor of this class.
+ nsContentPolicyType mContentPolicyType;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/preload/PreloadHashKey.cpp b/uriloader/preload/PreloadHashKey.cpp
new file mode 100644
index 0000000000..2096fedf0f
--- /dev/null
+++ b/uriloader/preload/PreloadHashKey.cpp
@@ -0,0 +1,213 @@
+/* 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 "PreloadHashKey.h"
+
+#include "mozilla/dom/Element.h" // StringToCORSMode
+#include "mozilla/css/SheetLoadData.h"
+#include "mozilla/dom/ReferrerPolicyBinding.h"
+#include "nsIPrincipal.h"
+#include "nsIReferrerInfo.h"
+
+namespace mozilla {
+
+PreloadHashKey::PreloadHashKey(const nsIURI* aKey, ResourceType aAs)
+ : nsURIHashKey(aKey), mAs(aAs) {}
+
+PreloadHashKey::PreloadHashKey(const PreloadHashKey* aKey)
+ : nsURIHashKey(aKey->mKey) {
+ *this = *aKey;
+}
+
+PreloadHashKey::PreloadHashKey(PreloadHashKey&& aToMove)
+ : nsURIHashKey(std::move(aToMove)) {
+ mAs = std::move(aToMove.mAs);
+ mCORSMode = std::move(aToMove.mCORSMode);
+ mPrincipal = std::move(aToMove.mPrincipal);
+
+ switch (mAs) {
+ case ResourceType::SCRIPT:
+ mScript = std::move(aToMove.mScript);
+ break;
+ case ResourceType::STYLE:
+ mStyle = std::move(aToMove.mStyle);
+ break;
+ case ResourceType::IMAGE:
+ break;
+ case ResourceType::FONT:
+ break;
+ case ResourceType::FETCH:
+ break;
+ case ResourceType::NONE:
+ break;
+ }
+}
+
+PreloadHashKey& PreloadHashKey::operator=(const PreloadHashKey& aOther) {
+ MOZ_ASSERT(mAs == ResourceType::NONE || aOther.mAs == ResourceType::NONE,
+ "Assigning more than once, only reset is allowed");
+
+ nsURIHashKey::operator=(aOther);
+
+ mAs = aOther.mAs;
+ mCORSMode = aOther.mCORSMode;
+ mPrincipal = aOther.mPrincipal;
+
+ switch (mAs) {
+ case ResourceType::SCRIPT:
+ mScript = aOther.mScript;
+ break;
+ case ResourceType::STYLE:
+ mStyle = aOther.mStyle;
+ break;
+ case ResourceType::IMAGE:
+ break;
+ case ResourceType::FONT:
+ break;
+ case ResourceType::FETCH:
+ break;
+ case ResourceType::NONE:
+ break;
+ }
+
+ return *this;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsScript(nsIURI* aURI, CORSMode aCORSMode,
+ dom::ScriptKind aScriptKind) {
+ PreloadHashKey key(aURI, ResourceType::SCRIPT);
+ key.mCORSMode = aCORSMode;
+
+ key.mScript.mScriptKind = aScriptKind;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsScript(nsIURI* aURI,
+ const nsAString& aCrossOrigin,
+ const nsAString& aType) {
+ dom::ScriptKind scriptKind = dom::ScriptKind::eClassic;
+ if (aType.LowerCaseEqualsASCII("module")) {
+ scriptKind = dom::ScriptKind::eModule;
+ }
+ CORSMode crossOrigin = dom::Element::StringToCORSMode(aCrossOrigin);
+ return CreateAsScript(aURI, crossOrigin, scriptKind);
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsStyle(
+ nsIURI* aURI, nsIPrincipal* aPrincipal, CORSMode aCORSMode,
+ css::SheetParsingMode aParsingMode) {
+ PreloadHashKey key(aURI, ResourceType::STYLE);
+ key.mCORSMode = aCORSMode;
+ key.mPrincipal = aPrincipal;
+
+ key.mStyle.mParsingMode = aParsingMode;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsStyle(
+ css::SheetLoadData& aSheetLoadData) {
+ return CreateAsStyle(aSheetLoadData.mURI, aSheetLoadData.mTriggeringPrincipal,
+ aSheetLoadData.mSheet->GetCORSMode(),
+ aSheetLoadData.mSheet->ParsingMode());
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsImage(nsIURI* aURI,
+ nsIPrincipal* aPrincipal,
+ CORSMode aCORSMode) {
+ PreloadHashKey key(aURI, ResourceType::IMAGE);
+ key.mCORSMode = aCORSMode;
+ key.mPrincipal = aPrincipal;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode) {
+ PreloadHashKey key(aURI, ResourceType::FETCH);
+ key.mCORSMode = aCORSMode;
+
+ return key;
+}
+
+// static
+PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI,
+ const nsAString& aCrossOrigin) {
+ return CreateAsFetch(aURI, dom::Element::StringToCORSMode(aCrossOrigin));
+}
+
+PreloadHashKey PreloadHashKey::CreateAsFont(nsIURI* aURI, CORSMode aCORSMode) {
+ PreloadHashKey key(aURI, ResourceType::FONT);
+ key.mCORSMode = aCORSMode;
+
+ return key;
+}
+
+bool PreloadHashKey::KeyEquals(KeyTypePointer aOther) const {
+ if (mAs != aOther->mAs || mCORSMode != aOther->mCORSMode) {
+ return false;
+ }
+
+ if (!mPrincipal != !aOther->mPrincipal) {
+ // One or the other has a principal, but not both... not equal
+ return false;
+ }
+
+ if (mPrincipal && !mPrincipal->Equals(aOther->mPrincipal)) {
+ return false;
+ }
+
+ if (!nsURIHashKey::KeyEquals(
+ static_cast<const nsURIHashKey*>(aOther)->GetKey())) {
+ return false;
+ }
+
+ switch (mAs) {
+ case ResourceType::SCRIPT:
+ if (mScript.mScriptKind != aOther->mScript.mScriptKind) {
+ return false;
+ }
+ break;
+ case ResourceType::STYLE: {
+ if (mStyle.mParsingMode != aOther->mStyle.mParsingMode) {
+ return false;
+ }
+ break;
+ }
+ case ResourceType::IMAGE:
+ // No more checks needed. The image cache key consists of the document
+ // (which we are scoped into), origin attributes (which we compare as part
+ // of the principal check) and the URL. imgLoader::ValidateEntry compares
+ // CORS, referrer info and principal, which we do by default.
+ break;
+ case ResourceType::FONT:
+ break;
+ case ResourceType::FETCH:
+ // No more checks needed.
+ break;
+ case ResourceType::NONE:
+ break;
+ }
+
+ return true;
+}
+
+// static
+PLDHashNumber PreloadHashKey::HashKey(KeyTypePointer aKey) {
+ PLDHashNumber hash = nsURIHashKey::HashKey(aKey->mKey);
+
+ // Enough to use the common attributes
+ hash = AddToHash(hash, static_cast<uint32_t>(aKey->mAs));
+ hash = AddToHash(hash, static_cast<uint32_t>(aKey->mCORSMode));
+
+ return hash;
+}
+
+} // namespace mozilla
diff --git a/uriloader/preload/PreloadHashKey.h b/uriloader/preload/PreloadHashKey.h
new file mode 100644
index 0000000000..2ab7af6013
--- /dev/null
+++ b/uriloader/preload/PreloadHashKey.h
@@ -0,0 +1,105 @@
+/* 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 PreloadHashKey_h__
+#define PreloadHashKey_h__
+
+#include "mozilla/CORSMode.h"
+#include "mozilla/css/SheetParsingMode.h"
+#include "mozilla/dom/ScriptKind.h"
+#include "nsURIHashKey.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+
+namespace css {
+class SheetLoadData;
+}
+
+/**
+ * This key is used for coalescing and lookup of preloading or regular
+ * speculative loads. It consists of:
+ * - the resource type, which is the value of the 'as' attribute
+ * - the URI of the resource
+ * - set of attributes that is common to all resource types
+ * - resource-type-specific attributes that we use to distinguish loads that has
+ * to be treated separately, some of these attributes may remain at their
+ * default values
+ */
+class PreloadHashKey : public nsURIHashKey {
+ public:
+ enum class ResourceType : uint8_t { NONE, SCRIPT, STYLE, IMAGE, FONT, FETCH };
+
+ using KeyType = const PreloadHashKey&;
+ using KeyTypePointer = const PreloadHashKey*;
+
+ PreloadHashKey() = default;
+ PreloadHashKey(const nsIURI* aKey, ResourceType aAs);
+ explicit PreloadHashKey(const PreloadHashKey* aKey);
+ PreloadHashKey(PreloadHashKey&& aToMove);
+
+ PreloadHashKey& operator=(const PreloadHashKey& aOther);
+
+ // Construct key for "script"
+ static PreloadHashKey CreateAsScript(nsIURI* aURI, CORSMode aCORSMode,
+ dom::ScriptKind aScriptKind);
+ static PreloadHashKey CreateAsScript(nsIURI* aURI,
+ const nsAString& aCrossOrigin,
+ const nsAString& aType);
+
+ // Construct key for "style"
+ static PreloadHashKey CreateAsStyle(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ CORSMode aCORSMode,
+ css::SheetParsingMode aParsingMode);
+ static PreloadHashKey CreateAsStyle(css::SheetLoadData&);
+
+ // Construct key for "image"
+ static PreloadHashKey CreateAsImage(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ CORSMode aCORSMode);
+
+ // Construct key for "fetch"
+ static PreloadHashKey CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode);
+ static PreloadHashKey CreateAsFetch(nsIURI* aURI,
+ const nsAString& aCrossOrigin);
+
+ // Construct key for "font"
+ static PreloadHashKey CreateAsFont(nsIURI* aURI, CORSMode aCORSMode);
+
+ KeyType GetKey() const { return *this; }
+ KeyTypePointer GetKeyPointer() const { return this; }
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+
+ bool KeyEquals(KeyTypePointer aOther) const;
+ static PLDHashNumber HashKey(KeyTypePointer aKey);
+ ResourceType As() const { return mAs; }
+
+#ifdef MOZILLA_INTERNAL_API
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ // Bug 1627752
+ return 0;
+ }
+#endif
+
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ // Attributes common to all resource types
+ ResourceType mAs = ResourceType::NONE;
+
+ CORSMode mCORSMode = CORS_NONE;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+
+ struct {
+ dom::ScriptKind mScriptKind = dom::ScriptKind::eClassic;
+ } mScript;
+
+ struct {
+ css::SheetParsingMode mParsingMode = css::eAuthorSheetFeatures;
+ } mStyle;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/preload/PreloadService.cpp b/uriloader/preload/PreloadService.cpp
new file mode 100644
index 0000000000..5707c3d109
--- /dev/null
+++ b/uriloader/preload/PreloadService.cpp
@@ -0,0 +1,290 @@
+/* -*- 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 "PreloadService.h"
+
+#include "FetchPreloader.h"
+#include "PreloaderBase.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/dom/HTMLLinkElement.h"
+#include "mozilla/dom/ScriptLoader.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/FontPreloader.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+
+PreloadService::PreloadService(dom::Document* aDoc) : mDocument(aDoc) {}
+PreloadService::~PreloadService() = default;
+
+bool PreloadService::RegisterPreload(const PreloadHashKey& aKey,
+ PreloaderBase* aPreload) {
+ if (PreloadExists(aKey)) {
+ return false;
+ }
+
+ mPreloads.Put(aKey, RefPtr{aPreload});
+ return true;
+}
+
+void PreloadService::DeregisterPreload(const PreloadHashKey& aKey) {
+ mPreloads.Remove(aKey);
+}
+
+void PreloadService::ClearAllPreloads() { mPreloads.Clear(); }
+
+bool PreloadService::PreloadExists(const PreloadHashKey& aKey) {
+ bool found;
+ mPreloads.GetWeak(aKey, &found);
+ return found;
+}
+
+already_AddRefed<PreloaderBase> PreloadService::LookupPreload(
+ const PreloadHashKey& aKey) const {
+ return mPreloads.Get(aKey);
+}
+
+already_AddRefed<nsIURI> PreloadService::GetPreloadURI(const nsAString& aURL) {
+ nsIURI* base = BaseURIForPreload();
+ auto encoding = mDocument->GetDocumentCharacterSet();
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base);
+ if (NS_FAILED(rv)) {
+ return nullptr;
+ }
+
+ return uri.forget();
+}
+
+already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement(
+ dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) {
+ // Even if the pref is disabled, we still want to collect telemetry about
+ // attempted preloads.
+ const bool preloadEnabled = StaticPrefs::network_preload();
+ if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
+ MOZ_ASSERT_UNREACHABLE("Caller should check");
+ return nullptr;
+ }
+
+ if (auto* wgc = mDocument->GetWindowGlobalChild()) {
+ wgc->MaybeSendUpdateDocumentWouldPreloadResources();
+ }
+
+ if (!preloadEnabled) {
+ return nullptr;
+ }
+
+ nsAutoString as, charset, crossOrigin, integrity, referrerPolicy, srcset,
+ sizes, type, url;
+
+ nsCOMPtr<nsIURI> uri = aLinkElement->GetURI();
+ aLinkElement->GetAs(as);
+ aLinkElement->GetCharset(charset);
+ aLinkElement->GetImageSrcset(srcset);
+ aLinkElement->GetImageSizes(sizes);
+ aLinkElement->GetHref(url);
+ aLinkElement->GetCrossOrigin(crossOrigin);
+ aLinkElement->GetIntegrity(integrity);
+ aLinkElement->GetReferrerPolicy(referrerPolicy);
+ aLinkElement->GetType(type);
+
+ auto result =
+ PreloadOrCoalesce(uri, url, aPolicyType, as, type, charset, srcset, sizes,
+ integrity, crossOrigin, referrerPolicy);
+
+ if (!result.mPreloader) {
+ NotifyNodeEvent(aLinkElement, result.mAlreadyComplete);
+ return nullptr;
+ }
+
+ result.mPreloader->AddLinkPreloadNode(aLinkElement);
+ return result.mPreloader.forget();
+}
+
+void PreloadService::PreloadLinkHeader(
+ nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
+ const nsAString& aAs, const nsAString& aType, const nsAString& aIntegrity,
+ const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aCORS,
+ const nsAString& aReferrerPolicy) {
+ // Even if the pref is disabled, we still want to collect telemetry about
+ // attempted preloads.
+ const bool preloadEnabled = StaticPrefs::network_preload();
+
+ if (aPolicyType == nsIContentPolicy::TYPE_INVALID) {
+ MOZ_ASSERT_UNREACHABLE("Caller should check");
+ return;
+ }
+
+ if (auto* wgc = mDocument->GetWindowGlobalChild()) {
+ wgc->MaybeSendUpdateDocumentWouldPreloadResources();
+ }
+
+ if (!preloadEnabled) {
+ return;
+ }
+
+ PreloadOrCoalesce(aURI, aURL, aPolicyType, aAs, aType, u""_ns, aSrcset,
+ aSizes, aIntegrity, aCORS, aReferrerPolicy);
+}
+
+PreloadService::PreloadOrCoalesceResult PreloadService::PreloadOrCoalesce(
+ nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
+ const nsAString& aAs, const nsAString& aType, const nsAString& aCharset,
+ const nsAString& aSrcset, const nsAString& aSizes,
+ const nsAString& aIntegrity, const nsAString& aCORS,
+ const nsAString& aReferrerPolicy) {
+ if (!aURI) {
+ MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI");
+ return {nullptr, false};
+ }
+
+ bool isImgSet = false;
+ PreloadHashKey preloadKey;
+ nsCOMPtr<nsIURI> uri = aURI;
+
+ if (aAs.LowerCaseEqualsASCII("script")) {
+ preloadKey = PreloadHashKey::CreateAsScript(uri, aCORS, aType);
+ } else if (aAs.LowerCaseEqualsASCII("style")) {
+ preloadKey = PreloadHashKey::CreateAsStyle(
+ uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS),
+ css::eAuthorSheetFeatures /* see Loader::LoadSheet */);
+ } else if (aAs.LowerCaseEqualsASCII("image")) {
+ uri = mDocument->ResolvePreloadImage(BaseURIForPreload(), aURL, aSrcset,
+ aSizes, &isImgSet);
+ if (!uri) {
+ return {nullptr, false};
+ }
+
+ preloadKey = PreloadHashKey::CreateAsImage(
+ uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS));
+ } else if (aAs.LowerCaseEqualsASCII("font")) {
+ preloadKey = PreloadHashKey::CreateAsFont(
+ uri, dom::Element::StringToCORSMode(aCORS));
+ } else if (aAs.LowerCaseEqualsASCII("fetch")) {
+ preloadKey = PreloadHashKey::CreateAsFetch(
+ uri, dom::Element::StringToCORSMode(aCORS));
+ } else {
+ return {nullptr, false};
+ }
+
+ if (RefPtr<PreloaderBase> preload = LookupPreload(preloadKey)) {
+ return {std::move(preload), false};
+ }
+
+ if (aAs.LowerCaseEqualsASCII("script")) {
+ PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aIntegrity,
+ true /* isInHead - TODO */);
+ } else if (aAs.LowerCaseEqualsASCII("style")) {
+ switch (PreloadStyle(uri, aCharset, aCORS, aReferrerPolicy, aIntegrity)) {
+ case dom::SheetPreloadStatus::AlreadyComplete:
+ return {nullptr, /* already_complete = */ true};
+ case dom::SheetPreloadStatus::Errored:
+ case dom::SheetPreloadStatus::InProgress:
+ break;
+ }
+ } else if (aAs.LowerCaseEqualsASCII("image")) {
+ PreloadImage(uri, aCORS, aReferrerPolicy, isImgSet);
+ } else if (aAs.LowerCaseEqualsASCII("font")) {
+ PreloadFont(uri, aCORS, aReferrerPolicy);
+ } else if (aAs.LowerCaseEqualsASCII("fetch")) {
+ PreloadFetch(uri, aCORS, aReferrerPolicy);
+ }
+
+ return {LookupPreload(preloadKey), false};
+}
+
+void PreloadService::PreloadScript(nsIURI* aURI, const nsAString& aType,
+ const nsAString& aCharset,
+ const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ const nsAString& aIntegrity,
+ bool aScriptFromHead) {
+ mDocument->ScriptLoader()->PreloadURI(
+ aURI, aCharset, aType, aCrossOrigin, aIntegrity, aScriptFromHead, false,
+ false, false, true, PreloadReferrerPolicy(aReferrerPolicy));
+}
+
+dom::SheetPreloadStatus PreloadService::PreloadStyle(
+ nsIURI* aURI, const nsAString& aCharset, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy, const nsAString& aIntegrity) {
+ return mDocument->PreloadStyle(
+ aURI, Encoding::ForLabel(aCharset), aCrossOrigin,
+ PreloadReferrerPolicy(aReferrerPolicy), aIntegrity, true);
+}
+
+void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aImageReferrerPolicy,
+ bool aIsImgSet) {
+ mDocument->PreLoadImage(aURI, aCrossOrigin,
+ PreloadReferrerPolicy(aImageReferrerPolicy),
+ aIsImgSet, true);
+}
+
+void PreloadService::PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy) {
+ CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
+ auto key = PreloadHashKey::CreateAsFont(aURI, cors);
+
+ // * Bug 1618549: Depending on where we decide to do the deduplication, we may
+ // want to check if the font is already being preloaded here.
+
+ RefPtr<FontPreloader> preloader = new FontPreloader();
+ dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
+ preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument);
+}
+
+void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy) {
+ CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin);
+ auto key = PreloadHashKey::CreateAsFetch(aURI, cors);
+
+ // * Bug 1618549: Depending on where we decide to do the deduplication, we may
+ // want to check if a fetch is already being preloaded here.
+
+ RefPtr<FetchPreloader> preloader = new FetchPreloader();
+ dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy);
+ preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument);
+}
+
+// static
+void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) {
+ if (!aNode->IsInComposedDoc()) {
+ return;
+ }
+
+ // We don't dispatch synchronously since |node| might be in a DocGroup
+ // that we're not allowed to touch. (Our network request happens in the
+ // DocGroup of one of the mSources nodes--not necessarily this one).
+
+ RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher(
+ aNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo);
+
+ dispatcher->RequireNodeInDocument();
+ dispatcher->PostDOMEvent();
+}
+
+dom::ReferrerPolicy PreloadService::PreloadReferrerPolicy(
+ const nsAString& aReferrerPolicy) {
+ dom::ReferrerPolicy referrerPolicy =
+ dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy);
+ if (referrerPolicy == dom::ReferrerPolicy::_empty) {
+ referrerPolicy = mDocument->GetPreloadReferrerInfo()->ReferrerPolicy();
+ }
+
+ return referrerPolicy;
+}
+
+nsIURI* PreloadService::BaseURIForPreload() {
+ nsIURI* documentURI = mDocument->GetDocumentURI();
+ nsIURI* documentBaseURI = mDocument->GetDocBaseURI();
+ return (documentURI == documentBaseURI)
+ ? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI)
+ : documentBaseURI;
+}
+
+} // namespace mozilla
diff --git a/uriloader/preload/PreloadService.h b/uriloader/preload/PreloadService.h
new file mode 100644
index 0000000000..44077e819a
--- /dev/null
+++ b/uriloader/preload/PreloadService.h
@@ -0,0 +1,126 @@
+/* -*- 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 PreloadService_h__
+#define PreloadService_h__
+
+#include "nsIContentPolicy.h"
+#include "nsIURI.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/PreloadHashKey.h"
+
+class nsINode;
+
+namespace mozilla {
+
+class PreloaderBase;
+
+namespace dom {
+
+class HTMLLinkElement;
+class Document;
+enum class ReferrerPolicy : uint8_t;
+enum class SheetPreloadStatus : uint8_t;
+
+} // namespace dom
+
+/**
+ * Intended to scope preloads and speculative loads under one roof. This class
+ * is intended to be a member of dom::Document. Provides registration of
+ * speculative loads via a `key` which is defined to consist of the URL,
+ * resource type, and resource-specific attributes that are further
+ * distinguishing loads with otherwise same type and url.
+ */
+class PreloadService {
+ public:
+ explicit PreloadService(dom::Document*);
+ ~PreloadService();
+
+ // Called by resource loaders to register a running resource load. This is
+ // called for a speculative load when it's started the first time, being it
+ // either regular speculative load or a preload.
+ //
+ // Returns false and does nothing if a preload is already registered under
+ // this key, true otherwise.
+ bool RegisterPreload(const PreloadHashKey& aKey, PreloaderBase* aPreload);
+
+ // Called when the load is about to be cancelled. Exact behavior is to be
+ // determined yet.
+ void DeregisterPreload(const PreloadHashKey& aKey);
+
+ // Called when the scope is to go away.
+ void ClearAllPreloads();
+
+ // True when there is a preload registered under the key.
+ bool PreloadExists(const PreloadHashKey& aKey);
+
+ // Returns an existing preload under the key or null, when there is none
+ // registered under the key.
+ already_AddRefed<PreloaderBase> LookupPreload(
+ const PreloadHashKey& aKey) const;
+
+ void SetSpeculationBase(nsIURI* aURI) { mSpeculationBaseURI = aURI; }
+ already_AddRefed<nsIURI> GetPreloadURI(const nsAString& aURL);
+
+ already_AddRefed<PreloaderBase> PreloadLinkElement(
+ dom::HTMLLinkElement* aLink, nsContentPolicyType aPolicyType);
+
+ void PreloadLinkHeader(nsIURI* aURI, const nsAString& aURL,
+ nsContentPolicyType aPolicyType, const nsAString& aAs,
+ const nsAString& aType, const nsAString& aIntegrity,
+ const nsAString& aSrcset, const nsAString& aSizes,
+ const nsAString& aCORS,
+ const nsAString& aReferrerPolicy);
+
+ void PreloadScript(nsIURI* aURI, const nsAString& aType,
+ const nsAString& aCharset, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ const nsAString& aIntegrity, bool aScriptFromHead);
+
+ dom::SheetPreloadStatus PreloadStyle(nsIURI* aURI, const nsAString& aCharset,
+ const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy,
+ const nsAString& aIntegrity);
+
+ void PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aImageReferrerPolicy, bool aIsImgSet);
+
+ void PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy);
+
+ void PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin,
+ const nsAString& aReferrerPolicy);
+
+ static void NotifyNodeEvent(nsINode* aNode, bool aSuccess);
+
+ private:
+ dom::ReferrerPolicy PreloadReferrerPolicy(const nsAString& aReferrerPolicy);
+ nsIURI* BaseURIForPreload();
+
+ struct PreloadOrCoalesceResult {
+ RefPtr<PreloaderBase> mPreloader;
+ bool mAlreadyComplete;
+ };
+
+ PreloadOrCoalesceResult PreloadOrCoalesce(
+ nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType,
+ const nsAString& aAs, const nsAString& aType, const nsAString& aCharset,
+ const nsAString& aSrcset, const nsAString& aSizes,
+ const nsAString& aIntegrity, const nsAString& aCORS,
+ const nsAString& aReferrerPolicy);
+
+ private:
+ nsRefPtrHashtable<PreloadHashKey, PreloaderBase> mPreloads;
+
+ // Raw pointer only, we are intended to be a direct member of dom::Document
+ dom::Document* mDocument;
+
+ // Set by `nsHtml5TreeOpExecutor::SetSpeculationBase`.
+ nsCOMPtr<nsIURI> mSpeculationBaseURI;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/uriloader/preload/PreloaderBase.cpp b/uriloader/preload/PreloaderBase.cpp
new file mode 100644
index 0000000000..56f8c7e7bf
--- /dev/null
+++ b/uriloader/preload/PreloaderBase.cpp
@@ -0,0 +1,380 @@
+/* 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 "PreloaderBase.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/Telemetry.h"
+#include "nsContentUtils.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsIChannel.h"
+#include "nsILoadGroup.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIRedirectResultListener.h"
+
+// Change this if we want to cancel and remove the associated preload on removal
+// of all <link rel=preload> tags from the tree.
+constexpr static bool kCancelAndRemovePreloadOnZeroReferences = false;
+
+namespace mozilla {
+
+PreloaderBase::UsageTimer::UsageTimer(PreloaderBase* aPreload,
+ dom::Document* aDocument)
+ : mDocument(aDocument), mPreload(aPreload) {}
+
+class PreloaderBase::RedirectSink final : public nsIInterfaceRequestor,
+ public nsIChannelEventSink,
+ public nsIRedirectResultListener {
+ RedirectSink() = delete;
+ virtual ~RedirectSink();
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIREDIRECTRESULTLISTENER
+
+ RedirectSink(PreloaderBase* aPreloader, nsIInterfaceRequestor* aCallbacks);
+
+ private:
+ MainThreadWeakPtr<PreloaderBase> mPreloader;
+ nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
+ nsCOMPtr<nsIChannel> mRedirectChannel;
+};
+
+PreloaderBase::RedirectSink::RedirectSink(PreloaderBase* aPreloader,
+ nsIInterfaceRequestor* aCallbacks)
+ : mPreloader(aPreloader), mCallbacks(aCallbacks) {}
+
+PreloaderBase::RedirectSink::~RedirectSink() = default;
+
+NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor,
+ nsIChannelEventSink, nsIRedirectResultListener)
+
+NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect(
+ nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* aCallback) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ mRedirectChannel = aNewChannel;
+
+ // Deliberately adding this before confirmation.
+ nsCOMPtr<nsIURI> uri;
+ aNewChannel->GetOriginalURI(getter_AddRefs(uri));
+ if (mPreloader) {
+ mPreloader->mRedirectRecords.AppendElement(
+ RedirectRecord(aFlags, uri.forget()));
+ }
+
+ if (mCallbacks) {
+ nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks));
+ if (sink) {
+ return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags,
+ aCallback);
+ }
+ }
+
+ aCallback->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP PreloaderBase::RedirectSink::OnRedirectResult(bool proceeding) {
+ if (proceeding && mRedirectChannel) {
+ mPreloader->mChannel = std::move(mRedirectChannel);
+ } else {
+ mRedirectChannel = nullptr;
+ }
+
+ if (mCallbacks) {
+ nsCOMPtr<nsIRedirectResultListener> sink(do_GetInterface(mCallbacks));
+ if (sink) {
+ return sink->OnRedirectResult(proceeding);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP PreloaderBase::RedirectSink::GetInterface(const nsIID& aIID,
+ void** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (aIID.Equals(NS_GET_IID(nsIChannelEventSink)) ||
+ aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) {
+ return QueryInterface(aIID, aResult);
+ }
+
+ if (mCallbacks) {
+ return mCallbacks->GetInterface(aIID, aResult);
+ }
+
+ *aResult = nullptr;
+ return NS_ERROR_NO_INTERFACE;
+}
+
+PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); }
+
+// static
+void PreloaderBase::AddLoadBackgroundFlag(nsIChannel* aChannel) {
+ nsLoadFlags loadFlags;
+ aChannel->GetLoadFlags(&loadFlags);
+ aChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND);
+}
+
+void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey,
+ dom::Document* aDocument, bool aIsPreload) {
+ if (aDocument && !aDocument->Preloads().RegisterPreload(aKey, this)) {
+ // This means there is already a preload registered under this key in this
+ // document. We only allow replacement when this is a regular load.
+ // Otherwise, this should never happen and is a suspected misuse of the API.
+ MOZ_ASSERT(!aIsPreload);
+ aDocument->Preloads().DeregisterPreload(aKey);
+ aDocument->Preloads().RegisterPreload(aKey, this);
+ }
+
+ mKey = aKey;
+ mIsUsed = !aIsPreload;
+
+ if (!mIsUsed && !mUsageTimer) {
+ auto callback = MakeRefPtr<UsageTimer>(this, aDocument);
+ NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer), callback, 10000,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+
+ ReportUsageTelemetry();
+}
+
+void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel,
+ dom::Document* aDocument, bool aIsPreload) {
+ NotifyOpen(aKey, aDocument, aIsPreload);
+ mChannel = aChannel;
+
+ nsCOMPtr<nsIInterfaceRequestor> callbacks;
+ mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks));
+ RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks));
+ mChannel->SetNotificationCallbacks(sink);
+}
+
+void PreloaderBase::NotifyUsage(LoadBackground aLoadBackground) {
+ if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) {
+ nsLoadFlags loadFlags;
+ mChannel->GetLoadFlags(&loadFlags);
+
+ // Preloads are initially set the LOAD_BACKGROUND flag. When becoming
+ // regular loads by hitting its consuming tag, we need to drop that flag,
+ // which also means to re-add the request from/to it's loadgroup to reflect
+ // that flag change.
+ if (loadFlags & nsIRequest::LOAD_BACKGROUND) {
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ mChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+
+ if (loadGroup) {
+ nsresult status;
+ mChannel->GetStatus(&status);
+
+ nsresult rv = loadGroup->RemoveRequest(mChannel, nullptr, status);
+ mChannel->SetLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND);
+ if (NS_SUCCEEDED(rv)) {
+ loadGroup->AddRequest(mChannel, nullptr);
+ }
+ }
+ }
+ }
+
+ mIsUsed = true;
+ ReportUsageTelemetry();
+ CancelUsageTimer();
+}
+
+void PreloaderBase::RemoveSelf(dom::Document* aDocument) {
+ if (aDocument) {
+ aDocument->Preloads().DeregisterPreload(mKey);
+ }
+}
+
+void PreloaderBase::NotifyRestart(dom::Document* aDocument,
+ PreloaderBase* aNewPreloader) {
+ RemoveSelf(aDocument);
+ mKey = PreloadHashKey();
+
+ CancelUsageTimer();
+
+ if (aNewPreloader) {
+ aNewPreloader->mNodes = std::move(mNodes);
+ }
+}
+
+void PreloaderBase::NotifyStart(nsIRequest* aRequest) {
+ // If there is no channel assigned on this preloader, we are not between
+ // channel switching, so we can freely update the mShouldFireLoadEvent using
+ // the given channel.
+ if (mChannel && !SameCOMIdentity(aRequest, mChannel)) {
+ return;
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
+ if (!httpChannel) {
+ return;
+ }
+
+ // if the load is cross origin without CORS, or the CORS access is rejected,
+ // always fire load event to avoid leaking site information.
+ nsresult rv;
+ nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo();
+ mShouldFireLoadEvent =
+ loadInfo->GetTainting() == LoadTainting::Opaque ||
+ (loadInfo->GetTainting() == LoadTainting::CORS &&
+ (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv)));
+}
+
+void PreloaderBase::NotifyStop(nsIRequest* aRequest, nsresult aStatus) {
+ // Filter out notifications that may be arriving from the old channel before
+ // restarting this request.
+ if (!SameCOMIdentity(aRequest, mChannel)) {
+ return;
+ }
+
+ NotifyStop(aStatus);
+}
+
+void PreloaderBase::NotifyStop(nsresult aStatus) {
+ mOnStopStatus.emplace(aStatus);
+
+ nsTArray<nsWeakPtr> nodes = std::move(mNodes);
+
+ for (nsWeakPtr& weak : nodes) {
+ nsCOMPtr<nsINode> node = do_QueryReferent(weak);
+ if (node) {
+ NotifyNodeEvent(node);
+ }
+ }
+
+ mChannel = nullptr;
+}
+
+void PreloaderBase::NotifyValidating() { mOnStopStatus.reset(); }
+
+void PreloaderBase::NotifyValidated(nsresult aStatus) {
+ NotifyStop(nullptr, aStatus);
+}
+
+void PreloaderBase::AddLinkPreloadNode(nsINode* aNode) {
+ if (mOnStopStatus) {
+ return NotifyNodeEvent(aNode);
+ }
+
+ mNodes.AppendElement(do_GetWeakReference(aNode));
+}
+
+void PreloaderBase::RemoveLinkPreloadNode(nsINode* aNode) {
+ // Note that do_GetWeakReference returns the internal weak proxy, which is
+ // always the same, so we can use it to search the array using default
+ // comparator.
+ nsWeakPtr node = do_GetWeakReference(aNode);
+ mNodes.RemoveElement(node);
+
+ if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 &&
+ !mIsUsed) {
+ // Keep a reference, because the following call may release us. The caller
+ // may use a WeakPtr to access this.
+ RefPtr<PreloaderBase> self(this);
+ RemoveSelf(aNode->OwnerDoc());
+
+ if (mChannel) {
+ mChannel->Cancel(NS_BINDING_ABORTED);
+ }
+ }
+}
+
+void PreloaderBase::NotifyNodeEvent(nsINode* aNode) {
+ PreloadService::NotifyNodeEvent(
+ aNode, mShouldFireLoadEvent || NS_SUCCEEDED(*mOnStopStatus));
+}
+
+void PreloaderBase::CancelUsageTimer() {
+ if (mUsageTimer) {
+ mUsageTimer->Cancel();
+ mUsageTimer = nullptr;
+ }
+}
+
+void PreloaderBase::ReportUsageTelemetry() {
+ if (mUsageTelementryReported) {
+ return;
+ }
+ mUsageTelementryReported = true;
+
+ if (mKey.As() == PreloadHashKey::ResourceType::NONE) {
+ return;
+ }
+
+ // The labels are structured as type1-used, type1-unused, type2-used, ...
+ // The first "as" resource type is NONE with value 0.
+ auto index = (static_cast<uint32_t>(mKey.As()) - 1) * 2;
+ if (!mIsUsed) {
+ ++index;
+ }
+
+ auto label = static_cast<Telemetry::LABELS_REL_PRELOAD_MISS_RATIO>(index);
+ Telemetry::AccumulateCategorical(label);
+}
+
+nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) {
+ // We want to return an error so that consumers can't ever use a preload to
+ // consume data unless it's properly implemented.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// PreloaderBase::RedirectRecord
+
+nsCString PreloaderBase::RedirectRecord::Spec() const {
+ nsCOMPtr<nsIURI> noFragment;
+ NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment));
+ MOZ_ASSERT(noFragment);
+ return noFragment->GetSpecOrDefault();
+}
+
+nsCString PreloaderBase::RedirectRecord::Fragment() const {
+ nsCString fragment;
+ mURI->GetRef(fragment);
+ return fragment;
+}
+
+// PreloaderBase::UsageTimer
+
+NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer, nsITimerCallback)
+
+NS_IMETHODIMP PreloaderBase::UsageTimer::Notify(nsITimer* aTimer) {
+ if (!mPreload || !mDocument) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aTimer == mPreload->mUsageTimer);
+ mPreload->mUsageTimer = nullptr;
+
+ if (mPreload->IsUsed()) {
+ // Left in the hashtable, but marked as used. This is a valid case, and we
+ // don't want to emit a warning for this preload then.
+ return NS_OK;
+ }
+
+ mPreload->ReportUsageTelemetry();
+
+ // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get
+ // the URI.
+ nsIURI* uri = static_cast<nsURIHashKey*>(&mPreload->mKey)->GetKey();
+ if (!uri) {
+ return NS_OK;
+ }
+
+ nsString spec;
+ NS_GetSanitizedURIStringFromURI(uri, spec);
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns,
+ mDocument, nsContentUtils::eDOM_PROPERTIES,
+ "UnusedLinkPreloadPending",
+ nsTArray<nsString>({std::move(spec)}));
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/uriloader/preload/PreloaderBase.h b/uriloader/preload/PreloaderBase.h
new file mode 100644
index 0000000000..dcd3738217
--- /dev/null
+++ b/uriloader/preload/PreloaderBase.h
@@ -0,0 +1,200 @@
+/* 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 PreloaderBase_h__
+#define PreloaderBase_h__
+
+#include "mozilla/Maybe.h"
+#include "mozilla/PreloadHashKey.h"
+#include "mozilla/WeakPtr.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsITimer.h"
+#include "nsIURI.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsTArray.h"
+
+class nsIChannel;
+class nsINode;
+class nsIRequest;
+class nsIStreamListener;
+
+namespace mozilla {
+
+namespace dom {
+
+class Document;
+
+} // namespace dom
+
+/**
+ * A half-abstract base class that resource loaders' respective
+ * channel-listening classes should derive from. Provides a unified API to
+ * register the load or preload in a document scoped service, links <link
+ * rel="preload"> DOM nodes with the load progress and provides API to possibly
+ * consume the data by later, dynamically discovered consumers.
+ *
+ * This class is designed to be used only on the main thread.
+ */
+class PreloaderBase : public SupportsWeakPtr, public nsISupports {
+ public:
+ PreloaderBase() = default;
+
+ // Called by resource loaders to register this preload in the document's
+ // preload service to provide coalescing, and access to the preload when it
+ // should be used for an actual load.
+ void NotifyOpen(const PreloadHashKey& aKey, dom::Document* aDocument,
+ bool aIsPreload);
+ void NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel,
+ dom::Document* aDocument, bool aIsPreload);
+
+ // Called when the load is about to be started all over again and thus this
+ // PreloaderBase will be registered again with the same key. This method
+ // taks care to deregister this preload prior to that.
+ // @param aNewPreloader: If there is a new request and loader created for the
+ // restarted load, we will pass any necessary information into it.
+ void NotifyRestart(dom::Document* aDocument,
+ PreloaderBase* aNewPreloader = nullptr);
+
+ // Called by the loading object when the channel started to load
+ // (OnStartRequest or equal) and when it finished (OnStopRequest or equal)
+ void NotifyStart(nsIRequest* aRequest);
+ void NotifyStop(nsIRequest* aRequest, nsresult aStatus);
+ // Use this variant only in complement to NotifyOpen without providing a
+ // channel.
+ void NotifyStop(nsresult aStatus);
+
+ // Called when this currently existing load has to be asynchronously
+ // revalidated before it can be used. This prevents link preload DOM nodes
+ // being notified until the validation is resolved.
+ void NotifyValidating();
+ // Called when the validation process has been done. This will notify
+ // associated link DOM nodes.
+ void NotifyValidated(nsresult aStatus);
+
+ // Called by resource loaders or any suitable component to notify the preload
+ // has been used for an actual load. This is intended to stop any usage
+ // timers.
+ // @param aDropLoadBackground: If `Keep` then the loading channel, if still in
+ // progress, will not be removed the LOAD_BACKGROUND flag, for instance XHR is
+ // the user here.
+ enum class LoadBackground { Keep, Drop };
+ void NotifyUsage(LoadBackground aLoadBackground = LoadBackground::Drop);
+ // Whether this preloader has been used for a regular/actual load or not.
+ bool IsUsed() const { return mIsUsed; }
+
+ // Removes itself from the document's preloads hashtable
+ void RemoveSelf(dom::Document* aDocument);
+
+ // When a loader starting an actual load finds a preload, the data can be
+ // delivered using this method. It will deliver stream listener notifications
+ // as if it were coming from the resource loading channel. The |request|
+ // argument will be the channel that loaded/loads the resource.
+ // This method must keep to the nsIChannel.AsyncOpen contract. A loader is
+ // not obligated to re-implement this method when not necessarily needed.
+ virtual nsresult AsyncConsume(nsIStreamListener* aListener);
+
+ // Accessor to the resource loading channel.
+ nsIChannel* Channel() const { return mChannel; }
+
+ // May change priority of the resource loading channel so that it's treated as
+ // preload when this was initially representing a normal speculative load but
+ // later <link rel="preload"> was found for this resource.
+ virtual void PrioritizeAsPreload() = 0;
+
+ // Helper function to set the LOAD_BACKGROUND flag on channel initiated by
+ // <link rel=preload>. This MUST be used before the channel is AsyncOpen'ed.
+ static void AddLoadBackgroundFlag(nsIChannel* aChannel);
+
+ // These are linking this preload to <link rel="preload"> DOM nodes. If we
+ // are already loaded, immediately notify events on the node, otherwise wait
+ // for NotifyStop() call.
+ void AddLinkPreloadNode(nsINode* aNode);
+ void RemoveLinkPreloadNode(nsINode* aNode);
+
+ // A collection of redirects, the main consumer is fetch.
+ class RedirectRecord {
+ public:
+ RedirectRecord(uint32_t aFlags, already_AddRefed<nsIURI> aURI)
+ : mFlags(aFlags), mURI(aURI) {}
+
+ uint32_t Flags() const { return mFlags; }
+ nsCString Spec() const;
+ nsCString Fragment() const;
+
+ private:
+ uint32_t mFlags;
+ nsCOMPtr<nsIURI> mURI;
+ };
+
+ const nsTArray<RedirectRecord>& Redirects() { return mRedirectRecords; }
+
+ protected:
+ virtual ~PreloaderBase();
+
+ // The loading channel. This will update when a redirect occurs.
+ nsCOMPtr<nsIChannel> mChannel;
+
+ private:
+ void NotifyNodeEvent(nsINode* aNode);
+ void CancelUsageTimer();
+
+ void ReportUsageTelemetry();
+
+ // A helper class that will update the PreloaderBase.mChannel member when a
+ // redirect happens, so that we can reprioritize or cancel when needed.
+ // Having a separate class instead of implementing this on PreloaderBase
+ // directly is to keep PreloaderBase as simple as possible so that derived
+ // classes don't have to deal with calling super when implementing these
+ // interfaces from some reason as well.
+ class RedirectSink;
+
+ // A timer callback to trigger the unuse warning for this preload
+ class UsageTimer final : public nsITimerCallback {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+
+ UsageTimer(PreloaderBase* aPreload, dom::Document* aDocument);
+
+ private:
+ ~UsageTimer() = default;
+
+ WeakPtr<dom::Document> mDocument;
+ WeakPtr<PreloaderBase> mPreload;
+ };
+
+ private:
+ // Reference to HTMLLinkElement DOM nodes to deliver onload and onerror
+ // notifications to.
+ nsTArray<nsWeakPtr> mNodes;
+
+ // History of redirects.
+ nsTArray<RedirectRecord> mRedirectRecords;
+
+ // Usage timer, emits warning when the preload is not used in time. Started
+ // in NotifyOpen and stopped in NotifyUsage.
+ nsCOMPtr<nsITimer> mUsageTimer;
+
+ // The key this preload has been registered under. We want to remember it to
+ // be able to deregister itself from the document's preloads.
+ PreloadHashKey mKey;
+
+ // This overrides the final event we send to DOM nodes to be always 'load'.
+ // Modified in NotifyStart based on LoadInfo data of the loading channel.
+ bool mShouldFireLoadEvent = false;
+
+ // True after call to NotifyUsage.
+ bool mIsUsed = false;
+
+ // True after we have reported the usage telemetry. Prevent duplicates.
+ bool mUsageTelementryReported = false;
+
+ // Emplaced when the data delivery has finished, in NotifyStop, holds the
+ // result of the load.
+ Maybe<nsresult> mOnStopStatus;
+};
+
+} // namespace mozilla
+
+#endif // !PreloaderBase_h__
diff --git a/uriloader/preload/gtest/TestFetchPreloader.cpp b/uriloader/preload/gtest/TestFetchPreloader.cpp
new file mode 100644
index 0000000000..4d4d2b5103
--- /dev/null
+++ b/uriloader/preload/gtest/TestFetchPreloader.cpp
@@ -0,0 +1,937 @@
+#include "gtest/gtest.h"
+
+#include "mozilla/CORSMode.h"
+#include "mozilla/dom/XMLDocument.h"
+#include "mozilla/dom/ReferrerPolicyBinding.h"
+#include "mozilla/FetchPreloader.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/PreloadHashKey.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "nsNetUtil.h"
+#include "nsIChannel.h"
+#include "nsIStreamListener.h"
+#include "nsThreadUtils.h"
+#include "nsStringStream.h"
+
+namespace {
+
+auto const ERROR_CANCEL = NS_ERROR_ABORT;
+auto const ERROR_ONSTOP = NS_ERROR_NET_INTERRUPT;
+auto const ERROR_THROW = NS_ERROR_FAILURE;
+
+class FakeChannel : public nsIChannel {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICHANNEL
+ NS_DECL_NSIREQUEST
+
+ nsresult Start() { return mListener->OnStartRequest(this); }
+ nsresult Data(const nsACString& aData) {
+ if (NS_FAILED(mStatus)) {
+ return mStatus;
+ }
+ nsCOMPtr<nsIInputStream> is;
+ NS_NewCStringInputStream(getter_AddRefs(is), aData);
+ return mListener->OnDataAvailable(this, is, 0, aData.Length());
+ }
+ nsresult Stop(nsresult status) {
+ if (NS_SUCCEEDED(mStatus)) {
+ mStatus = status;
+ }
+ mListener->OnStopRequest(this, mStatus);
+ mListener = nullptr;
+ return mStatus;
+ }
+
+ private:
+ virtual ~FakeChannel() = default;
+ bool mIsPending = false;
+ bool mCanceled = false;
+ nsresult mStatus = NS_OK;
+ nsCOMPtr<nsIStreamListener> mListener;
+};
+
+NS_IMPL_ISUPPORTS(FakeChannel, nsIChannel, nsIRequest)
+
+NS_IMETHODIMP FakeChannel::GetName(nsACString& result) { return NS_OK; }
+NS_IMETHODIMP FakeChannel::IsPending(bool* result) {
+ *result = mIsPending;
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetStatus(nsresult* status) {
+ *status = mStatus;
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::Cancel(nsresult status) {
+ if (!mCanceled) {
+ mCanceled = true;
+ mStatus = status;
+ }
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::Suspend() { return NS_OK; }
+NS_IMETHODIMP FakeChannel::Resume() { return NS_OK; }
+NS_IMETHODIMP FakeChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = 0;
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+NS_IMETHODIMP FakeChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+NS_IMETHODIMP FakeChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetOriginalURI(nsIURI** aURI) { return NS_OK; }
+NS_IMETHODIMP FakeChannel::SetOriginalURI(nsIURI* aURI) { return NS_OK; }
+NS_IMETHODIMP FakeChannel::GetURI(nsIURI** aURI) { return NS_OK; }
+NS_IMETHODIMP FakeChannel::GetOwner(nsISupports** aOwner) { return NS_OK; }
+NS_IMETHODIMP FakeChannel::SetOwner(nsISupports* aOwner) { return NS_OK; }
+NS_IMETHODIMP FakeChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { return NS_OK; }
+NS_IMETHODIMP FakeChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetIsDocument(bool* aIsDocument) {
+ *aIsDocument = false;
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aCallbacks) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aCallbacks) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetSecurityInfo(nsISupports** aSecurityInfo) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetContentType(nsACString& aContentType) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetContentType(const nsACString& aContentType) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetContentCharset(nsACString& aContentCharset) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetContentCharset(
+ const nsACString& aContentCharset) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetContentDisposition(
+ uint32_t* aContentDisposition) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetContentDisposition(uint32_t aContentDisposition) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetContentDispositionFilename(
+ nsAString& aContentDispositionFilename) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetContentDispositionFilename(
+ const nsAString& aContentDispositionFilename) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetContentDispositionHeader(
+ nsACString& aContentDispositionHeader) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+NS_IMETHODIMP FakeChannel::GetContentLength(int64_t* aContentLength) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::SetContentLength(int64_t aContentLength) {
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::GetCanceled(bool* aCanceled) {
+ *aCanceled = mCanceled;
+ return NS_OK;
+}
+NS_IMETHODIMP FakeChannel::Open(nsIInputStream** aStream) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+NS_IMETHODIMP
+FakeChannel::AsyncOpen(nsIStreamListener* aListener) {
+ mIsPending = true;
+ mListener = aListener;
+ return NS_OK;
+}
+
+class FakePreloader : public mozilla::FetchPreloader {
+ public:
+ explicit FakePreloader(FakeChannel* aChannel) : mDrivingChannel(aChannel) {}
+
+ private:
+ RefPtr<FakeChannel> mDrivingChannel;
+
+ virtual nsresult CreateChannel(
+ nsIChannel** aChannel, nsIURI* aURI, const mozilla::CORSMode aCORSMode,
+ const mozilla::dom::ReferrerPolicy& aReferrerPolicy,
+ mozilla::dom::Document* aDocument, nsILoadGroup* aLoadGroup,
+ nsIInterfaceRequestor* aCallbacks) override {
+ mDrivingChannel.forget(aChannel);
+ return NS_OK;
+ }
+};
+
+class FakeListener : public nsIStreamListener {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ enum { Never, OnStart, OnData, OnStop } mCancelIn = Never;
+
+ nsresult mOnStartResult = NS_OK;
+ nsresult mOnDataResult = NS_OK;
+ nsresult mOnStopResult = NS_OK;
+
+ bool mOnStart = false;
+ nsCString mOnData;
+ Maybe<nsresult> mOnStop;
+
+ private:
+ virtual ~FakeListener() = default;
+};
+
+NS_IMPL_ISUPPORTS(FakeListener, nsIStreamListener, nsIRequestObserver)
+
+NS_IMETHODIMP FakeListener::OnStartRequest(nsIRequest* request) {
+ EXPECT_FALSE(mOnStart);
+ mOnStart = true;
+
+ if (mCancelIn == OnStart) {
+ request->Cancel(ERROR_CANCEL);
+ }
+
+ return mOnStartResult;
+}
+NS_IMETHODIMP FakeListener::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* input,
+ uint64_t offset, uint32_t count) {
+ nsAutoCString data;
+ data.SetLength(count);
+
+ uint32_t read;
+ input->Read(data.BeginWriting(), count, &read);
+ mOnData += data;
+
+ if (mCancelIn == OnData) {
+ request->Cancel(ERROR_CANCEL);
+ }
+
+ return mOnDataResult;
+}
+NS_IMETHODIMP FakeListener::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ EXPECT_FALSE(mOnStop);
+ mOnStop.emplace(status);
+
+ if (mCancelIn == OnStop) {
+ request->Cancel(ERROR_CANCEL);
+ }
+
+ return mOnStopResult;
+}
+
+bool eventInProgress = true;
+
+void Await() {
+ MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil([&]() {
+ bool yield = !eventInProgress;
+ eventInProgress = true; // Just for convenience
+ return yield;
+ }));
+}
+
+void Yield() { eventInProgress = false; }
+
+} // namespace
+
+// ****************************************************************************
+// Test body
+// ****************************************************************************
+
+// Caching with all good results (data + NS_OK)
+TEST(TestFetchPreloader, CacheNoneBeforeConsume)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CacheStartBeforeConsume)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CachePartOfDataBeforeConsume)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CacheAllDataBeforeConsume)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+
+ // Request consumation of the preload...
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CacheAllBeforeConsume)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+// Get data before the channel fails
+TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelError)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_FAILED(channel->Stop(ERROR_ONSTOP)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_ONSTOP);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+// Cancel the channel between caching and consuming
+TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelCancel)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ channel->Cancel(ERROR_CANCEL);
+ EXPECT_TRUE(NS_FAILED(channel->Stop(ERROR_CANCEL)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ // XXX - This is hard to solve; the data is there but we won't deliver it.
+ // This is a bit different case than e.g. a network error. We want to
+ // deliver some data in that case. Cancellation probably happens because of
+ // navigation or a demand to not consume the channel anyway.
+ EXPECT_TRUE(listener->mOnData.IsEmpty());
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+// Let the listener throw while data is already cached
+TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStartRequest)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnStartResult = ERROR_THROW;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.IsEmpty());
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnDataAvailable)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnDataResult = ERROR_THROW;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStopRequest)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnStopResult = ERROR_THROW;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree"));
+ // Throwing from OnStopRequest is generally ignored.
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+// Cancel the channel in various callbacks
+TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnStartRequest)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnStart;
+ // check that throwing from OnStartRequest doesn't affect the cancellation
+ // status.
+ listener->mOnStartResult = ERROR_THROW;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.IsEmpty());
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnDataAvailable)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnData;
+ // check that throwing from OnStartRequest doesn't affect the cancellation
+ // status.
+ listener->mOnDataResult = ERROR_THROW;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+// Corner cases
+TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailable)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnData;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(NS_FAILED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_FAILED(channel->Stop(NS_OK)));
+
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnStartRequestAndRace)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+
+ // This has to simulate a possibiilty when stream listener notifications from
+ // the channel are already pending in the queue while AsyncConsume is called.
+ // At this moment the listener has not been notified yet.
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+ }));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnStart;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ // Check listener's been fed properly. Expected is to NOT get any data and
+ // propagate the cancellation code and not being called duplicated
+ // OnStopRequest.
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.IsEmpty());
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailableAndRace)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+
+ // This has to simulate a possibiilty when stream listener notifications from
+ // the channel are already pending in the queue while AsyncConsume is called.
+ // At this moment the listener has not been notified yet.
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+ }));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mCancelIn = FakeListener::OnData;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ // Check listener's been fed properly. Expected is to NOT get anything after
+ // the first OnData and propagate the cancellation code and not being called
+ // duplicated OnStopRequest.
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.EqualsLiteral("one"));
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
+
+TEST(TestFetchPreloader, CachePartlyBeforeConsumeThrowFromOnStartRequestAndRace)
+{
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns);
+ auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE);
+
+ RefPtr<FakeChannel> channel = new FakeChannel();
+ RefPtr<FakePreloader> preloader = new FakePreloader(channel);
+ RefPtr<mozilla::dom::Document> doc;
+ NS_NewXMLDocument(getter_AddRefs(doc));
+
+ EXPECT_TRUE(NS_SUCCEEDED(
+ preloader->OpenChannel(key, uri, mozilla::CORS_NONE,
+ mozilla::dom::ReferrerPolicy::_empty, doc)));
+
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Start()));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns)));
+
+ // This has to simulate a possibiilty when stream listener notifications from
+ // the channel are already pending in the queue while AsyncConsume is called.
+ // At this moment the listener has not been notified yet.
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns)));
+ EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK)));
+ }));
+
+ RefPtr<FakeListener> listener = new FakeListener();
+ listener->mOnStartResult = ERROR_THROW;
+
+ EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+
+ // Check listener's been fed properly. Expected is to NOT get any data and
+ // propagate the throwing code and not being called duplicated OnStopRequest.
+ NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() {
+ EXPECT_TRUE(listener->mOnStart);
+ EXPECT_TRUE(listener->mOnData.IsEmpty());
+ EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW);
+
+ Yield();
+ }));
+
+ Await();
+
+ EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener)));
+}
diff --git a/uriloader/preload/gtest/moz.build b/uriloader/preload/gtest/moz.build
new file mode 100644
index 0000000000..72c2afa539
--- /dev/null
+++ b/uriloader/preload/gtest/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestFetchPreloader.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/netwerk/base",
+ "/xpcom/tests/gtest",
+]
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += ["!/xpcom", "/xpcom/components"]
diff --git a/uriloader/preload/moz.build b/uriloader/preload/moz.build
new file mode 100644
index 0000000000..5ce2f28d89
--- /dev/null
+++ b/uriloader/preload/moz.build
@@ -0,0 +1,28 @@
+# -*- 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Networking")
+
+TEST_DIRS += ["gtest"]
+
+EXPORTS.mozilla += [
+ "FetchPreloader.h",
+ "PreloaderBase.h",
+ "PreloadHashKey.h",
+ "PreloadService.h",
+]
+
+UNIFIED_SOURCES += [
+ "FetchPreloader.cpp",
+ "PreloaderBase.cpp",
+ "PreloadHashKey.cpp",
+ "PreloadService.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+include("/ipc/chromium/chromium-config.mozbuild")