From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- uriloader/base/moz.build | 33 + uriloader/base/nsCURILoader.idl | 49 + uriloader/base/nsDocLoader.cpp | 1516 ++++++++++ uriloader/base/nsDocLoader.h | 335 +++ uriloader/base/nsIContentHandler.idl | 35 + uriloader/base/nsIDocumentLoader.idl | 36 + uriloader/base/nsITransfer.idl | 106 + uriloader/base/nsIURIContentListener.idl | 135 + uriloader/base/nsIURILoader.idl | 140 + uriloader/base/nsIWebProgress.idl | 153 + uriloader/base/nsIWebProgressListener.idl | 425 +++ uriloader/base/nsIWebProgressListener2.idl | 69 + uriloader/base/nsURILoader.cpp | 966 +++++++ uriloader/base/nsURILoader.h | 59 + uriloader/exthandler/ContentHandlerService.cpp | 169 ++ uriloader/exthandler/ContentHandlerService.h | 52 + uriloader/exthandler/ExternalHelperAppChild.cpp | 127 + uriloader/exthandler/ExternalHelperAppChild.h | 45 + uriloader/exthandler/ExternalHelperAppParent.cpp | 512 ++++ uriloader/exthandler/ExternalHelperAppParent.h | 119 + uriloader/exthandler/HandlerServiceChild.h | 15 + uriloader/exthandler/HandlerServiceParent.cpp | 270 ++ uriloader/exthandler/HandlerServiceParent.h | 31 + uriloader/exthandler/PExternalHelperApp.ipdl | 29 + uriloader/exthandler/PHandlerService.ipdl | 42 + .../exthandler/android/nsAndroidHandlerApp.cpp | 91 + uriloader/exthandler/android/nsAndroidHandlerApp.h | 33 + .../android/nsExternalSharingAppService.cpp | 61 + .../android/nsExternalSharingAppService.h | 28 + .../android/nsExternalURLHandlerService.cpp | 27 + .../android/nsExternalURLHandlerService.h | 27 + uriloader/exthandler/android/nsMIMEInfoAndroid.cpp | 427 +++ uriloader/exthandler/android/nsMIMEInfoAndroid.h | 60 + .../exthandler/android/nsOSHelperAppService.cpp | 67 + .../exthandler/android/nsOSHelperAppService.h | 40 + uriloader/exthandler/gonk/nsOSHelperAppService.cpp | 56 + uriloader/exthandler/gonk/nsOSHelperAppService.h | 39 + uriloader/exthandler/mac/nsDecodeAppleFile.cpp | 389 +++ uriloader/exthandler/mac/nsDecodeAppleFile.h | 118 + uriloader/exthandler/mac/nsLocalHandlerAppMac.h | 26 + uriloader/exthandler/mac/nsLocalHandlerAppMac.mm | 84 + uriloader/exthandler/mac/nsMIMEInfoMac.h | 34 + uriloader/exthandler/mac/nsMIMEInfoMac.mm | 114 + uriloader/exthandler/mac/nsOSHelperAppService.h | 48 + uriloader/exthandler/mac/nsOSHelperAppService.mm | 569 ++++ uriloader/exthandler/moz.build | 139 + uriloader/exthandler/nsCExternalHandlerService.idl | 58 + uriloader/exthandler/nsContentHandlerApp.cpp | 79 + uriloader/exthandler/nsContentHandlerApp.h | 30 + uriloader/exthandler/nsDBusHandlerApp.cpp | 177 ++ uriloader/exthandler/nsDBusHandlerApp.h | 33 + .../exthandler/nsExternalHelperAppService.cpp | 2926 ++++++++++++++++++++ uriloader/exthandler/nsExternalHelperAppService.h | 498 ++++ uriloader/exthandler/nsExternalProtocolHandler.cpp | 561 ++++ uriloader/exthandler/nsExternalProtocolHandler.h | 38 + uriloader/exthandler/nsHandlerService.js | 1429 ++++++++++ uriloader/exthandler/nsHandlerService.manifest | 2 + uriloader/exthandler/nsIContentDispatchChooser.idl | 45 + .../exthandler/nsIExternalHelperAppService.idl | 154 ++ .../exthandler/nsIExternalProtocolService.idl | 135 + .../exthandler/nsIExternalSharingAppService.idl | 28 + .../exthandler/nsIExternalURLHandlerService.idl | 26 + uriloader/exthandler/nsIHandlerService.idl | 117 + .../exthandler/nsIHelperAppLauncherDialog.idl | 89 + uriloader/exthandler/nsLocalHandlerApp.cpp | 179 ++ uriloader/exthandler/nsLocalHandlerApp.h | 60 + uriloader/exthandler/nsMIMEInfoImpl.cpp | 435 +++ uriloader/exthandler/nsMIMEInfoImpl.h | 196 ++ uriloader/exthandler/nsWebHandlerApp.js | 172 ++ uriloader/exthandler/nsWebHandlerApp.manifest | 2 + uriloader/exthandler/tests/Makefile.in | 11 + uriloader/exthandler/tests/WriteArgument.cpp | 24 + uriloader/exthandler/tests/mochitest/browser.ini | 8 + .../browser_download_always_ask_preferred_app.js | 19 + .../mochitest/browser_remember_download_option.js | 49 + .../mochitest/browser_web_protocol_handlers.js | 76 + .../exthandler/tests/mochitest/handlerApp.xhtml | 30 + .../exthandler/tests/mochitest/handlerApps.js | 110 + uriloader/exthandler/tests/mochitest/head.js | 105 + uriloader/exthandler/tests/mochitest/mochitest.ini | 10 + .../tests/mochitest/protocolHandler.html | 16 + .../tests/mochitest/test_handlerApps.xhtml | 12 + .../tests/mochitest/test_unsafeBidiChars.xhtml | 77 + .../tests/mochitest/unsafeBidiFileName.sjs | 14 + .../tests/mochitest/unsafeBidi_chromeScript.js | 28 + uriloader/exthandler/tests/moz.build | 24 + .../exthandler/tests/unit/head_handlerService.js | 163 ++ uriloader/exthandler/tests/unit/mailcap | 2 + .../exthandler/tests/unit/tail_handlerService.js | 5 + .../exthandler/tests/unit/test_badMIMEType.js | 26 + ...est_getTypeFromExtension_ext_to_type_mapping.js | 53 + ...getTypeFromExtension_with_empty_Content_Type.js | 186 ++ .../exthandler/tests/unit/test_handlerService.js | 470 ++++ .../exthandler/tests/unit/test_punycodeURIs.js | 126 + uriloader/exthandler/tests/unit/xpcshell.ini | 15 + .../exthandler/tests/unit_ipc/test_encoding.js | 231 ++ uriloader/exthandler/tests/unit_ipc/xpcshell.ini | 8 + .../exthandler/uikit/nsLocalHandlerAppUIKit.h | 32 + .../exthandler/uikit/nsLocalHandlerAppUIKit.mm | 17 + uriloader/exthandler/uikit/nsMIMEInfoUIKit.h | 37 + uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm | 20 + uriloader/exthandler/uikit/nsOSHelperAppService.h | 52 + uriloader/exthandler/uikit/nsOSHelperAppService.mm | 64 + .../exthandler/unix/nsExternalSharingAppService.h | 31 + uriloader/exthandler/unix/nsGNOMERegistry.cpp | 109 + uriloader/exthandler/unix/nsGNOMERegistry.h | 28 + uriloader/exthandler/unix/nsMIMEInfoUnix.cpp | 136 + uriloader/exthandler/unix/nsMIMEInfoUnix.h | 32 + uriloader/exthandler/unix/nsOSHelperAppService.cpp | 1537 ++++++++++ uriloader/exthandler/unix/nsOSHelperAppService.h | 128 + uriloader/exthandler/win/nsMIMEInfoWin.cpp | 883 ++++++ uriloader/exthandler/win/nsMIMEInfoWin.h | 71 + uriloader/exthandler/win/nsOSHelperAppService.cpp | 626 +++++ uriloader/exthandler/win/nsOSHelperAppService.h | 65 + uriloader/moz.build | 11 + uriloader/prefetch/OfflineCacheUpdateChild.cpp | 528 ++++ uriloader/prefetch/OfflineCacheUpdateChild.h | 94 + uriloader/prefetch/OfflineCacheUpdateGlue.cpp | 228 ++ uriloader/prefetch/OfflineCacheUpdateGlue.h | 80 + uriloader/prefetch/OfflineCacheUpdateParent.cpp | 294 ++ uriloader/prefetch/OfflineCacheUpdateParent.h | 65 + uriloader/prefetch/POfflineCacheUpdate.ipdl | 28 + uriloader/prefetch/moz.build | 45 + uriloader/prefetch/nsCPrefetchService.h | 52 + uriloader/prefetch/nsIOfflineCacheUpdate.idl | 292 ++ uriloader/prefetch/nsIPrefetchService.idl | 37 + uriloader/prefetch/nsOfflineCacheUpdate.cpp | 2471 +++++++++++++++++ uriloader/prefetch/nsOfflineCacheUpdate.h | 381 +++ uriloader/prefetch/nsOfflineCacheUpdateService.cpp | 736 +++++ uriloader/prefetch/nsPrefetchService.cpp | 931 +++++++ uriloader/prefetch/nsPrefetchService.h | 121 + 131 files changed, 27574 insertions(+) create mode 100644 uriloader/base/moz.build create mode 100644 uriloader/base/nsCURILoader.idl create mode 100644 uriloader/base/nsDocLoader.cpp create mode 100644 uriloader/base/nsDocLoader.h create mode 100644 uriloader/base/nsIContentHandler.idl create mode 100644 uriloader/base/nsIDocumentLoader.idl create mode 100644 uriloader/base/nsITransfer.idl create mode 100644 uriloader/base/nsIURIContentListener.idl create mode 100644 uriloader/base/nsIURILoader.idl create mode 100644 uriloader/base/nsIWebProgress.idl create mode 100644 uriloader/base/nsIWebProgressListener.idl create mode 100644 uriloader/base/nsIWebProgressListener2.idl create mode 100644 uriloader/base/nsURILoader.cpp create mode 100644 uriloader/base/nsURILoader.h create mode 100644 uriloader/exthandler/ContentHandlerService.cpp create mode 100644 uriloader/exthandler/ContentHandlerService.h create mode 100644 uriloader/exthandler/ExternalHelperAppChild.cpp create mode 100644 uriloader/exthandler/ExternalHelperAppChild.h create mode 100644 uriloader/exthandler/ExternalHelperAppParent.cpp create mode 100644 uriloader/exthandler/ExternalHelperAppParent.h create mode 100644 uriloader/exthandler/HandlerServiceChild.h create mode 100644 uriloader/exthandler/HandlerServiceParent.cpp create mode 100644 uriloader/exthandler/HandlerServiceParent.h create mode 100644 uriloader/exthandler/PExternalHelperApp.ipdl create mode 100644 uriloader/exthandler/PHandlerService.ipdl create mode 100644 uriloader/exthandler/android/nsAndroidHandlerApp.cpp create mode 100644 uriloader/exthandler/android/nsAndroidHandlerApp.h create mode 100644 uriloader/exthandler/android/nsExternalSharingAppService.cpp create mode 100644 uriloader/exthandler/android/nsExternalSharingAppService.h create mode 100644 uriloader/exthandler/android/nsExternalURLHandlerService.cpp create mode 100644 uriloader/exthandler/android/nsExternalURLHandlerService.h create mode 100644 uriloader/exthandler/android/nsMIMEInfoAndroid.cpp create mode 100644 uriloader/exthandler/android/nsMIMEInfoAndroid.h create mode 100644 uriloader/exthandler/android/nsOSHelperAppService.cpp create mode 100644 uriloader/exthandler/android/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/gonk/nsOSHelperAppService.cpp create mode 100644 uriloader/exthandler/gonk/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/mac/nsDecodeAppleFile.cpp create mode 100644 uriloader/exthandler/mac/nsDecodeAppleFile.h create mode 100644 uriloader/exthandler/mac/nsLocalHandlerAppMac.h create mode 100644 uriloader/exthandler/mac/nsLocalHandlerAppMac.mm create mode 100644 uriloader/exthandler/mac/nsMIMEInfoMac.h create mode 100644 uriloader/exthandler/mac/nsMIMEInfoMac.mm create mode 100644 uriloader/exthandler/mac/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/mac/nsOSHelperAppService.mm create mode 100644 uriloader/exthandler/moz.build create mode 100644 uriloader/exthandler/nsCExternalHandlerService.idl create mode 100644 uriloader/exthandler/nsContentHandlerApp.cpp create mode 100644 uriloader/exthandler/nsContentHandlerApp.h create mode 100644 uriloader/exthandler/nsDBusHandlerApp.cpp create mode 100644 uriloader/exthandler/nsDBusHandlerApp.h create mode 100644 uriloader/exthandler/nsExternalHelperAppService.cpp create mode 100644 uriloader/exthandler/nsExternalHelperAppService.h create mode 100644 uriloader/exthandler/nsExternalProtocolHandler.cpp create mode 100644 uriloader/exthandler/nsExternalProtocolHandler.h create mode 100644 uriloader/exthandler/nsHandlerService.js create mode 100644 uriloader/exthandler/nsHandlerService.manifest create mode 100644 uriloader/exthandler/nsIContentDispatchChooser.idl create mode 100644 uriloader/exthandler/nsIExternalHelperAppService.idl create mode 100644 uriloader/exthandler/nsIExternalProtocolService.idl create mode 100644 uriloader/exthandler/nsIExternalSharingAppService.idl create mode 100644 uriloader/exthandler/nsIExternalURLHandlerService.idl create mode 100644 uriloader/exthandler/nsIHandlerService.idl create mode 100644 uriloader/exthandler/nsIHelperAppLauncherDialog.idl create mode 100644 uriloader/exthandler/nsLocalHandlerApp.cpp create mode 100644 uriloader/exthandler/nsLocalHandlerApp.h create mode 100644 uriloader/exthandler/nsMIMEInfoImpl.cpp create mode 100644 uriloader/exthandler/nsMIMEInfoImpl.h create mode 100644 uriloader/exthandler/nsWebHandlerApp.js create mode 100644 uriloader/exthandler/nsWebHandlerApp.manifest create mode 100644 uriloader/exthandler/tests/Makefile.in create mode 100644 uriloader/exthandler/tests/WriteArgument.cpp create mode 100644 uriloader/exthandler/tests/mochitest/browser.ini create mode 100644 uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_remember_download_option.js create mode 100644 uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js create mode 100644 uriloader/exthandler/tests/mochitest/handlerApp.xhtml create mode 100644 uriloader/exthandler/tests/mochitest/handlerApps.js create mode 100644 uriloader/exthandler/tests/mochitest/head.js create mode 100644 uriloader/exthandler/tests/mochitest/mochitest.ini create mode 100644 uriloader/exthandler/tests/mochitest/protocolHandler.html create mode 100644 uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml create mode 100644 uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml create mode 100644 uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs create mode 100644 uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js create mode 100644 uriloader/exthandler/tests/moz.build create mode 100644 uriloader/exthandler/tests/unit/head_handlerService.js create mode 100644 uriloader/exthandler/tests/unit/mailcap create mode 100644 uriloader/exthandler/tests/unit/tail_handlerService.js create mode 100644 uriloader/exthandler/tests/unit/test_badMIMEType.js create mode 100644 uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js create mode 100644 uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js create mode 100644 uriloader/exthandler/tests/unit/test_handlerService.js create mode 100644 uriloader/exthandler/tests/unit/test_punycodeURIs.js create mode 100644 uriloader/exthandler/tests/unit/xpcshell.ini create mode 100644 uriloader/exthandler/tests/unit_ipc/test_encoding.js create mode 100644 uriloader/exthandler/tests/unit_ipc/xpcshell.ini create mode 100644 uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h create mode 100644 uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm create mode 100644 uriloader/exthandler/uikit/nsMIMEInfoUIKit.h create mode 100644 uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm create mode 100644 uriloader/exthandler/uikit/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/uikit/nsOSHelperAppService.mm create mode 100644 uriloader/exthandler/unix/nsExternalSharingAppService.h create mode 100644 uriloader/exthandler/unix/nsGNOMERegistry.cpp create mode 100644 uriloader/exthandler/unix/nsGNOMERegistry.h create mode 100644 uriloader/exthandler/unix/nsMIMEInfoUnix.cpp create mode 100644 uriloader/exthandler/unix/nsMIMEInfoUnix.h create mode 100644 uriloader/exthandler/unix/nsOSHelperAppService.cpp create mode 100644 uriloader/exthandler/unix/nsOSHelperAppService.h create mode 100644 uriloader/exthandler/win/nsMIMEInfoWin.cpp create mode 100644 uriloader/exthandler/win/nsMIMEInfoWin.h create mode 100644 uriloader/exthandler/win/nsOSHelperAppService.cpp create mode 100644 uriloader/exthandler/win/nsOSHelperAppService.h create mode 100644 uriloader/moz.build create mode 100644 uriloader/prefetch/OfflineCacheUpdateChild.cpp create mode 100644 uriloader/prefetch/OfflineCacheUpdateChild.h create mode 100644 uriloader/prefetch/OfflineCacheUpdateGlue.cpp create mode 100644 uriloader/prefetch/OfflineCacheUpdateGlue.h create mode 100644 uriloader/prefetch/OfflineCacheUpdateParent.cpp create mode 100644 uriloader/prefetch/OfflineCacheUpdateParent.h create mode 100644 uriloader/prefetch/POfflineCacheUpdate.ipdl create mode 100644 uriloader/prefetch/moz.build create mode 100644 uriloader/prefetch/nsCPrefetchService.h create mode 100644 uriloader/prefetch/nsIOfflineCacheUpdate.idl create mode 100644 uriloader/prefetch/nsIPrefetchService.idl create mode 100644 uriloader/prefetch/nsOfflineCacheUpdate.cpp create mode 100644 uriloader/prefetch/nsOfflineCacheUpdate.h create mode 100644 uriloader/prefetch/nsOfflineCacheUpdateService.cpp create mode 100644 uriloader/prefetch/nsPrefetchService.cpp create mode 100644 uriloader/prefetch/nsPrefetchService.h (limited to 'uriloader') diff --git a/uriloader/base/moz.build b/uriloader/base/moz.build new file mode 100644 index 0000000000..0d0f9a1c67 --- /dev/null +++ b/uriloader/base/moz.build @@ -0,0 +1,33 @@ +# -*- 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', +] + +FINAL_LIBRARY = 'xul' diff --git a/uriloader/base/nsCURILoader.idl b/uriloader/base/nsCURILoader.idl new file mode 100644 index 0000000000..5d9d8f0131 --- /dev/null +++ b/uriloader/base/nsCURILoader.idl @@ -0,0 +1,49 @@ +/* -*- 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++ +// {9F6D5D40-90E7-11d3-AF93-00A024FFC08C} - +#define NS_URI_LOADER_CID \ +{ 0x9f6d5d40, 0x90e7, 0x11d3, { 0xaf, 0x80, 0x00, 0xa0, 0x24, 0xff, 0xc0, 0x8c } } +#define NS_URI_LOADER_CONTRACTID \ +"@mozilla.org/uriloader;1" + +/* 057b04d0-0ccf-11d2-beba-00805f8a66dc */ +#define NS_DOCUMENTLOADER_SERVICE_CID \ + { 0x057b04d0, 0x0ccf, 0x11d2,{0xbe, 0xba, 0x00, 0x80, 0x5f, 0x8a, 0x66, 0xdc}} + +#define NS_DOCUMENTLOADER_SERVICE_CONTRACTID \ +"@mozilla.org/docloaderservice;1" + +#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..69885b93f8 --- /dev/null +++ b/uriloader/base/nsDocLoader.cpp @@ -0,0 +1,1516 @@ +/* -*- 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/Logging.h" + +#include "nsDocLoader.h" +#include "nsCURILoader.h" +#include "nsNetUtil.h" +#include "nsIHttpChannel.h" +#include "nsIWebProgressListener2.h" + +#include "nsIServiceManager.h" +#include "nsXPIDLString.h" + +#include "nsIURL.h" +#include "nsCOMPtr.h" +#include "nscore.h" +#include "nsWeakPtr.h" +#include "nsAutoPtr.h" +#include "nsQueryObject.h" + +#include "nsIDOMWindow.h" + +#include "nsIStringBundle.h" +#include "nsIScriptSecurityManager.h" + +#include "nsITransport.h" +#include "nsISocketTransport.h" +#include "nsIDocShell.h" +#include "nsIDOMDocument.h" +#include "nsIDocument.h" +#include "nsPresContext.h" +#include "nsIAsyncVerifyRedirectCallback.h" + +using mozilla::DebugOnly; +using mozilla::LogLevel; + +static NS_DEFINE_CID(kThisImplCID, NS_THIS_DOCLOADER_IMPL_CID); + +// +// 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(entry); + info->~nsRequestInfo(); +} + +// this is used for mListenerInfoList.Contains() +template <> +class nsDefaultComparator { + public: + bool Equals(const nsDocLoader::nsListenerInfo& aInfo, + nsIWebProgressListener* const& aListener) const { + nsCOMPtr 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), + mCurrentSelfProgress(0), + mMaxSelfProgress(0), + mCurrentTotalProgress(0), + mMaxTotalProgress(0), + mRequestInfoHash(&sRequestInfoHashOps, sizeof(nsRequestInfo)), + mCompletedTotalProgress(0), + mIsLoadingDocument(false), + mIsRestoringDocument(false), + mDontFlushLayout(false), + mIsFlushingLayout(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 %x.\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_ADDREF(nsDocLoader) +NS_IMPL_RELEASE(nsDocLoader) + +NS_INTERFACE_MAP_BEGIN(nsDocLoader) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRequestObserver) + 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(nsISecurityEventSink) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + if (aIID.Equals(kThisImplCID)) + foundInterface = static_cast(this); + else +NS_INTERFACE_MAP_END + + +/* + * 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::GetAsDocLoader(nsISupports* aSupports) +{ + RefPtr ret = do_QueryObject(aSupports); + return ret.forget(); +} + +/* static */ +nsresult +nsDocLoader::AddDocLoaderAsChildOfRoot(nsDocLoader* aDocLoader) +{ + nsresult rv; + nsCOMPtr docLoaderService = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr 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, nsDocLoader, 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 want to make sure to fire our + // onload at this point, and there's no issue with mChildrenInOnload + // after this, since mDocumentRequest will be null after the + // DocLoaderIsEmpty() call. + mChildrenInOnload.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"); + DocLoaderIsEmpty(false); + + return rv; +} + + +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.Count() || mIsFlushingLayout) { + return true; + } + + /* Is this document loader busy? */ + if (!mIsLoadingDocument) { + return false; + } + + 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); + + // This is a safe cast, because we only put nsDocLoader objects into the + // array + if (loader && static_cast(loader)->IsBusy()) + return true; + } + + return false; +} + +NS_IMETHODIMP +nsDocLoader::GetContainer(nsISupports** aResult) +{ + NS_ADDREF(*aResult = static_cast(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 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 rv = + static_cast(loader)->SetDocLoaderParent(nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetDocLoaderParent failed"); + } + } + mChildList.Clear(); +} + +NS_IMETHODIMP +nsDocLoader::OnStartRequest(nsIRequest *request, nsISupports *aCtxt) +{ + // 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; + 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!"); + + doStartURLLoad(request); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::OnStopRequest(nsIRequest *aRequest, + nsISupports *aCtxt, + 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=%x mIsLoadingDocument=%s, %u active URLs", + this, aRequest, name.get(), + aStatus, (mIsLoadingDocument ? "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 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 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 the document loader has initiated a + // load. This will handle removing the request from our hashtable as needed. + // + if (mIsLoadingDocument) { + nsCOMPtr ds = do_QueryInterface(static_cast(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) +{ + nsresult rv = mChildList.AppendElement(aChild) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + if (NS_SUCCEEDED(rv)) { + rv = aChild->SetDocLoaderParent(this); + } + return rv; +} + +NS_IMETHODIMP nsDocLoader::GetDocumentChannel(nsIChannel ** aChannel) +{ + if (!mDocumentRequest) { + *aChannel = nullptr; + return NS_OK; + } + + return CallQueryInterface(mDocumentRequest, aChannel); +} + + +void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) +{ + if (mIsLoadingDocument) { + /* 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 kungFuDeathGrip(this); + + // Don't flush layout if we're still busy. + if (IsBusy()) { + return; + } + + NS_ASSERTION(!mIsFlushingLayout, "Someone screwed up"); + NS_ASSERTION(mDocumentRequest, "No Document Request!"); + + // The load group for this DocumentLoader is idle. Flush if we need to. + if (aFlushLayout && !mDontFlushLayout) { + nsCOMPtr domDoc = do_GetInterface(GetAsSupports(this)); + nsCOMPtr doc = do_QueryInterface(domDoc); + 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. + mozFlushType flushType = Flush_Style; + nsIPresShell* shell = doc->GetShell(); + if (shell) { + // Be safe in case this presshell is in teardown now + nsPresContext* presContext = shell->GetPresContext(); + if (presContext && presContext->GetUserFontSet()) { + flushType = Flush_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 if the flushing above re-entered this + // method. + if (!IsBusy() && 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 docRequest = mDocumentRequest; + + mDocumentRequest = nullptr; + mIsLoadingDocument = false; + + // Update the progress status state - the document is done + mProgressStateFlags = nsIWebProgressListener::STATE_STOP; + + + nsresult loadGroupStatus = NS_OK; + 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 DocLoaderIsEmpty() on + // it even if our onload handler removes us from the docloader tree. + RefPtr 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); + + if (parent) { + parent->ChildDoneWithOnload(this); + } + } + } + } +} + +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) +{ +#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, + 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=%x\n", + this, buffer.get(), 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=%x\n", + this, buffer.get(), 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; + } + + return mListenerInfoList.AppendElement(nsListenerInfo(listener, aNotifyMask)) ? + NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +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::GetDOMWindowID(uint64_t *aResult) +{ + *aResult = 0; + + nsCOMPtr window; + nsresult rv = GetDOMWindow(getter_AddRefs(window)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr piwindow = nsPIDOMWindowOuter::From(window); + NS_ENSURE_STATE(piwindow); + + MOZ_ASSERT(piwindow->IsOuterWindow()); + *aResult = piwindow->WindowID(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetIsTopLevel(bool *aResult) +{ + *aResult = false; + + nsCOMPtr window; + GetDOMWindow(getter_AddRefs(window)); + if (window) { + nsCOMPtr piwindow = nsPIDOMWindowOuter::From(window); + NS_ENSURE_STATE(piwindow); + + nsCOMPtr topWindow = piwindow->GetTop(); + *aResult = piwindow == topWindow; + } + + 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; +} + +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, nsISupports* ctxt, + 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, nsISupports* ctxt, + 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 sbs = + mozilla::services::GetStringBundleService(); + if (!sbs) + return NS_ERROR_FAILURE; + nsXPIDLString msg; + nsresult rv = sbs->FormatStatusMessage(aStatus, aStatusArg, + getter_Copies(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 = new 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); + } + FireOnStatusChange(this, aRequest, aStatus, msg); + } + 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 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: %d maxSelf: %d curTotal: %d maxTotal %d\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", this, listener.get())); + 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 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) +{ + return static_cast(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(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) + nsCOMPtr request(do_QueryInterface(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; +} + +/* + * Implementation of nsISecurityEventSink method... + */ + +NS_IMETHODIMP nsDocLoader::OnSecurityChange(nsISupports * aContext, + uint32_t aState) +{ + // + // Fire progress notifications out to any registered nsIWebProgressListeners. + // + + nsCOMPtr request = do_QueryInterface(aContext); + nsIWebProgress* webProgress = static_cast(this); + + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_SECURITY, + listener->OnSecurityChange(webProgress, request, aState); + ); + + // Pass the notification up to the parent... + if (mParent) { + mParent->OnSecurityChange(aContext, aState); + } + return NS_OK; +} + +/* + * 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 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 p = do_QueryInterface(mLoadGroup); + if (p) + p->SetPriority(aPriority); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, nsDocLoader, + 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 p = do_QueryInterface(mLoadGroup); + if (p) + p->AdjustPriority(aDelta); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, nsDocLoader, + 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; imURI) { + 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..481b1397bc --- /dev/null +++ b/uriloader/base/nsDocLoader.h @@ -0,0 +1,335 @@ +/* -*- 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/. */ + +/* +*/ + +#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 "nsISecurityEventSink.h" +#include "nsISupportsPriority.h" +#include "nsCOMPtr.h" +#include "PLDHashTable.h" +#include "nsAutoPtr.h" + +#include "mozilla/LinkedList.h" + +/**************************************************************************** + * 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 nsISecurityEventSink, + public nsISupportsPriority +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_THIS_DOCLOADER_IMPL_CID) + + nsDocLoader(); + + virtual MOZ_MUST_USE nsresult Init(); + + static already_AddRefed GetAsDocLoader(nsISupports* aSupports); + // Needed to deal with ambiguous inheritance from nsISupports... + static nsISupports* GetAsSupports(nsDocLoader* aDocLoader) { + return static_cast(aDocLoader); + } + + // Add aDocLoader as a child to the docloader service. + static MOZ_MUST_USE nsresult AddDocLoaderAsChildOfRoot(nsDocLoader* aDocLoader); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOCUMENTLOADER + + // nsIProgressEventSink + NS_DECL_NSIPROGRESSEVENTSINK + + NS_DECL_NSISECURITYEVENTSINK + + // nsIRequestObserver methods: (for observing the load group) + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIWEBPROGRESS + + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSISUPPORTSPRIORITY + + // Implementation specific methods... + + // Remove aChild from our childlist. This nulls out the child's mParent + // pointer. + MOZ_MUST_USE nsresult RemoveChildLoader(nsDocLoader *aChild); + // Add aChild to our child list. This will set aChild's mParent pointer to + // |this|. + MOZ_MUST_USE 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; + }; + +protected: + virtual ~nsDocLoader(); + + virtual MOZ_MUST_USE nsresult SetDocLoaderParent(nsDocLoader * aLoader); + + bool IsBusy(); + + 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, 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); + + MOZ_MUST_USE 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); + void doStopURLLoad(nsIRequest *request, nsresult aStatus); + void doStopDocumentLoad(nsIRequest *request, nsresult aStatus); + + // Inform a parent docloader that aChild is about to call its onload + // handler. + MOZ_MUST_USE 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); + } + +protected: + struct nsStatusInfo : public mozilla::LinkedListElement + { + 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) : + mRequest(aRequest) + { + MOZ_COUNT_CTOR(nsStatusInfo); + } + ~nsStatusInfo() + { + MOZ_COUNT_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); + } + + ~nsRequestInfo() + { + MOZ_COUNT_DTOR(nsRequestInfo); + } + + nsIRequest* Request() { + return static_cast(const_cast(mKey)); + } + + const void* mKey; // Must be first for the PLDHashTable stubs to work + int64_t mCurrentProgress; + int64_t mMaxProgress; + bool mUploading; + + nsAutoPtr 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 mDocumentRequest; // [OWNER] ???compare with document + + nsDocLoader* mParent; // [WEAK] + + typedef nsAutoTObserverArray ListenerArray; + ListenerArray mListenerInfoList; + + nsCOMPtr mLoadGroup; + // We hold weak refs to all our kids + nsTObserverArray 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 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; + +private: + 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 mChildrenInOnload; + + // 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. + void DocLoaderIsEmpty(bool aFlushLayout); + + int64_t GetMaxTotalProgress(); + + nsresult AddRequestInfo(nsIRequest* aRequest); + void RemoveRequestInfo(nsIRequest* aRequest); + nsRequestInfo *GetRequestInfo(nsIRequest* aRequest); + void ClearRequestInfoHash(); + int64_t CalculateMaxProgress(); +/// void DumpChannelInfo(void); + + // used to clear our internal progress state between loads... + void ClearInternalProgress(); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsDocLoader, NS_THIS_DOCLOADER_IMPL_CID) + +#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 + * ( + + + 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/mochitest/unsafeBidi_chromeScript.js b/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js new file mode 100644 index 0000000000..16c82d818e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js @@ -0,0 +1,28 @@ +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.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(); + +function HelperAppLauncherDialog() {} +HelperAppLauncherDialog.prototype = { + show: function(aLauncher, aWindowContext, aReason) { + sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]) +}; + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory(FAKE_CID, "", HELPERAPP_DIALOG_CONTRACT, + XPCOMUtils._getFactory(HelperAppLauncherDialog)); + +addMessageListener("unregister", function() { + registrar.registerFactory(HELPERAPP_DIALOG_CID, "", + HELPERAPP_DIALOG_CONTRACT, null); + sendAsyncMessage("unregistered"); +}); diff --git a/uriloader/exthandler/tests/moz.build b/uriloader/exthandler/tests/moz.build new file mode 100644 index 0000000000..6aadfdc527 --- /dev/null +++ b/uriloader/exthandler/tests/moz.build @@ -0,0 +1,24 @@ +# -*- 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'] + +# The encoding test is already implemented in the Downloads API by a set of +# test cases with the string "content_encoding" in their names. +if not CONFIG['MOZ_JSDOWNLOADS']: + XPCSHELL_TESTS_MANIFESTS += ['unit_ipc/xpcshell.ini'] + +GeckoSimplePrograms([ + 'WriteArgument', +], linkage=None) + +USE_LIBS += [ + 'nspr', +] diff --git a/uriloader/exthandler/tests/unit/head_handlerService.js b/uriloader/exthandler/tests/unit/head_handlerService.js new file mode 100644 index 0000000000..8b6803d241 --- /dev/null +++ b/uriloader/exthandler/tests/unit/head_handlerService.js @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Inspired by the Places infrastructure in head_bookmarks.js + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; +var Cu = Components.utils; + +var HandlerServiceTest = { + //**************************************************************************// + // Convenience Getters + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + QueryInterface(Ci.nsIDirectoryService); + return this.__dirSvc; + }, + + __consoleSvc: null, + get _consoleSvc() { + if (!this.__consoleSvc) + this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__consoleSvc; + }, + + + //**************************************************************************// + // nsISupports + + interfaces: [Ci.nsIDirectoryServiceProvider, Ci.nsISupports], + + QueryInterface: function HandlerServiceTest_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + + + //**************************************************************************// + // Initialization & Destruction + + init: function HandlerServiceTest_init() { + // Register ourselves as a directory provider for the datasource file + // if there isn't one registered already. + try { + this._dirSvc.get("UMimTyp", Ci.nsIFile); + } catch (ex) { + this._dirSvc.registerProvider(this); + this._providerRegistered = true; + } + + // Delete the existing datasource file, if any, so we start from scratch. + // We also do this after finishing the tests, so there shouldn't be an old + // file lying around, but just in case we delete it here as well. + this._deleteDatasourceFile(); + + // Turn on logging so we can troubleshoot problems with the tests. + var prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefBranch.setBoolPref("browser.contentHandling.log", true); + }, + + destroy: function HandlerServiceTest_destroy() { + // Delete the existing datasource file, if any, so we don't leave test files + // lying around and we start from scratch the next time. + this._deleteDatasourceFile(); + // Unregister the directory service provider + if (this._providerRegistered) + this._dirSvc.unregisterProvider(this); + }, + + + //**************************************************************************// + // nsIDirectoryServiceProvider + + getFile: function HandlerServiceTest_getFile(property, persistent) { + this.log("getFile: requesting " + property); + + persistent.value = true; + + if (property == "UMimTyp") { + var datasourceFile = this._dirSvc.get("CurProcD", Ci.nsIFile); + datasourceFile.append("mimeTypes.rdf"); + return datasourceFile; + } + + // This causes extraneous errors to show up in the log when the directory + // service asks us first for CurProcD and MozBinD. I wish there was a way + // to suppress those errors. + this.log("the following NS_ERROR_FAILURE exception in " + + "nsIDirectoryServiceProvider::getFile is expected, " + + "as we don't provide the '" + property + "' file"); + throw Cr.NS_ERROR_FAILURE; + }, + + + //**************************************************************************// + // Utilities + + /** + * Delete the datasource file. + */ + _deleteDatasourceFile: function HandlerServiceTest__deleteDatasourceFile() { + var file = this._dirSvc.get("UMimTyp", Ci.nsIFile); + if (file.exists()) + file.remove(false); + }, + + /** + * Get the contents of the datasource as a serialized string. Useful for + * debugging problems with test failures, i.e.: + * + * HandlerServiceTest.log(HandlerServiceTest.getDatasourceContents()); + * + * @returns {string} the serialized datasource + */ + getDatasourceContents: function HandlerServiceTest_getDatasourceContents() { + var rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); + + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var fileHandler = ioService.getProtocolHandler("file"). + QueryInterface(Ci.nsIFileProtocolHandler); + var fileURL = fileHandler.getURLSpecFromFile(this.getDatasourceFile()); + var ds = rdf.GetDataSourceBlocking(fileURL); + + var outputStream = { + data: "", + close: function() {}, + flush: function() {}, + write: function (buffer,count) { + this.data += buffer; + return count; + }, + writeFrom: function (stream,count) {}, + isNonBlocking: false + }; + + ds.QueryInterface(Components.interfaces.nsIRDFXMLSource); + ds.Serialize(outputStream); + + return outputStream.data; + }, + + /** + * Log a message to the console and the test log. + */ + log: function HandlerServiceTest_log(message) { + message = "*** HandlerServiceTest: " + message; + this._consoleSvc.logStringMessage(message); + print(message); + } + +}; + +HandlerServiceTest.init(); 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/tail_handlerService.js b/uriloader/exthandler/tests/unit/tail_handlerService.js new file mode 100644 index 0000000000..1d1989127e --- /dev/null +++ b/uriloader/exthandler/tests/unit/tail_handlerService.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +HandlerServiceTest.destroy(); diff --git a/uriloader/exthandler/tests/unit/test_badMIMEType.js b/uriloader/exthandler/tests/unit/test_badMIMEType.js new file mode 100644 index 0000000000..df1202a0f2 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_badMIMEType.js @@ -0,0 +1,26 @@ +/* -*- 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ÿ"; + do_check_eq(badMimeType.length, 11); + + try { + var type = 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)) { + // This is an expected exception, thrown if the type can't be determined + } finally { + } + // Not crashing is good enough + do_check_eq(true, true); +} 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..d83c486bb4 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js @@ -0,0 +1,53 @@ +/* -*- 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 : + * 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 = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + + // --- 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)) { + // 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); + do_check_eq(type, kTestMimeType); + + // Check that the mapping is recognized even if the extension has mixed case. + type = mimeService.getTypeFromExtension(kTestExtensionMixedCase); + do_check_eq(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..1ae2a6fcfa --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js @@ -0,0 +1,186 @@ +/* -*- 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 : + * nsIMIMEService.getTypeFromExtension may fail unexpectedly on Windows when + * "Content Type" is empty in the registry. + */ +function run_test() { + // --- Preliminary platform check --- + + // If this test is not running on the Windows platform, stop now, before + // calling XPCOMUtils.generateQI during the MockWindowsRegKey declaration. + if (mozinfo.os != "win") + return; + + // --- Modified nsIWindowsRegKey implementation --- + + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + + /** + * 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 ".txt" 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: XPCOMUtils.generateQI([Ci.nsIWindowsRegKey]), + + // --- Overridden nsIWindowsRegKey interface functions --- + + open: function(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: function(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: function(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: function(aName) { + // If this is the key under test, return a fake value + if (this._rootKey == Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT && + this._relPath.toLowerCase() == ".txt" && + aName.toLowerCase() == "content type") { + return ""; + } + + // Return the real value in the registry + return this._wrappedObject.readStringValue(aName); + } + }; + + // --- Mock nsIWindowsRegKey factory --- + + var componentRegistrar = Components.manager. + QueryInterface(Ci.nsIComponentRegistrar); + + var originalWindowsRegKeyCID; + var mockWindowsRegKeyFactory; + + const kMockCID = Components.ID("{9b23dfe9-296b-4740-ba1c-d39c9a16e55e}"); + const kWindowsRegKeyContractID = "@mozilla.org/windows-registry-key;1"; + const kWindowsRegKeyClassName = "nsWindowsRegKey"; + + function registerMockWindowsRegKeyFactory() { + mockWindowsRegKeyFactory = { + createInstance: function(aOuter, aIid) { + if (aOuter != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + + var innerKey = originalWindowsRegKeyFactory.createInstance(null, aIid); + var key = new MockWindowsRegKey(innerKey); + + return key.QueryInterface(aIid); + } + }; + + // Preserve the original factory + originalWindowsRegKeyCID = Cc[kWindowsRegKeyContractID].number; + + // Register the mock factory + componentRegistrar.registerFactory( + kMockCID, + "Mock Windows Registry Key Implementation", + kWindowsRegKeyContractID, + mockWindowsRegKeyFactory + ); + } + + function unregisterMockWindowsRegKeyFactory() { + // Free references to the mock factory + componentRegistrar.unregisterFactory( + kMockCID, + mockWindowsRegKeyFactory + ); + + // Restore the original factory + componentRegistrar.registerFactory( + Components.ID(originalWindowsRegKeyCID), + "", + kWindowsRegKeyContractID, + null + ); + } + + // --- Test procedure --- + + // Activate the override of the ".txt" file association data in the registry + registerMockWindowsRegKeyFactory(); + try { + // Try and get the MIME type associated with the extension. If this + // operation does not throw an unexpected exception, the test succeeds. + var type = Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService). + getTypeFromExtension(".txt"); + } catch (e if (e instanceof Ci.nsIException && + e.result == Cr.NS_ERROR_NOT_AVAILABLE)) { + // This is an expected exception, thrown if the type can't be determined + } finally { + // Ensure we restore the original factory when the test is finished + unregisterMockWindowsRegKeyFactory(); + } +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService.js b/uriloader/exthandler/tests/unit/test_handlerService.js new file mode 100644 index 0000000000..3facc63aeb --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService.js @@ -0,0 +1,470 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + + const ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + const env = Cc["@mozilla.org/process/environment;1"]. + getService(Components.interfaces.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 = HandlerServiceTest._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 = { + name: "Local Handler", + executable: executable, + interfaces: [Ci.nsIHandlerApp, Ci.nsILocalHandlerApp, Ci.nsISupports], + QueryInterface: function(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + } + }; + + 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. + do_check_true(handlerInfo instanceof Ci.nsIHandlerInfo); + + do_check_eq(handlerInfo.type, "nonexistent/type"); + + // Deprecated property, but we should still make sure it's set correctly. + do_check_eq(handlerInfo.MIMEType, "nonexistent/type"); + + // These properties are the ones the handler service knows how to store. + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk); + do_check_eq(handlerInfo.preferredApplicationHandler, null); + do_check_eq(handlerInfo.possibleApplicationHandlers.length, 0); + do_check_true(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. + do_check_eq(handlerInfo.description, ""); + do_check_eq(handlerInfo.hasDefaultHandler, false); + do_check_eq(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"); + do_check_eq(protoInfo.preferredAction, protoInfo.alwaysAsk); + do_check_true(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"); + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_false(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. + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_true(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: false + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + else + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + + // Win7+ or Linux's GIO might not have a default mailto: handler + if (noMailto) + do_check_true(protoInfo.alwaysAskBeforeHandling); + else + do_check_false(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", true); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) { + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + // Win7+ or Linux's GIO may have no default mailto: handler. Otherwise + // alwaysAskBeforeHandling is expected to be false here, because although + // the pref is true, the value in RDF is false. The injected mailto handler + // carried over the default pref value, and so when we set the pref above + // to true it's ignored. + if (noMailto) + do_check_true(protoInfo.alwaysAskBeforeHandling); + else + do_check_false(protoInfo.alwaysAskBeforeHandling); + + } else { + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_true(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"); + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + do_check_true(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); + + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + + do_check_neq(handlerInfo.preferredApplicationHandler, null); + var preferredHandler = handlerInfo.preferredApplicationHandler; + do_check_eq(typeof preferredHandler, "object"); + do_check_eq(preferredHandler.name, "Local Handler"); + do_check_true(preferredHandler instanceof Ci.nsILocalHandlerApp); + preferredHandler.QueryInterface(Ci.nsILocalHandlerApp); + do_check_eq(preferredHandler.executable.path, localHandler.executable.path); + + do_check_false(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("webcal"); + handlerTypes.push("mailto"); + handlerTypes.push("irc"); + handlerTypes.push("ircs"); + } + var handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) { + var handler = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo); + do_check_neq(handlerTypes.indexOf(handler.type), -1); + handlerTypes.splice(handlerTypes.indexOf(handler.type), 1); + } + do_check_eq(handlerTypes.length, 0); + + // Make sure the handler service's remove method removes a handler record. + handlerSvc.remove(handlerInfo2); + handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) + do_check_neq(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); + do_check_eq(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); + do_check_eq(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); + do_check_eq(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); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Add two handlers, store the object, re-retrieve it, and make sure it has + // two handlers. + possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler, + false); + possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler, + false); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(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. + do_check_eq(localPossibleHandler.name, localHandler.name); + do_check_true(localPossibleHandler.equals(localHandler)); + do_check_eq(webPossibleHandler.name, webHandler.name); + do_check_true(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); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 1); + + // Make sure the handler is the one we didn't remove. + webPossibleHandler = possibleHandlersInfo.possibleApplicationHandlers. + queryElementAt(0, Ci.nsIWebHandlerApp); + do_check_eq(webPossibleHandler.name, webHandler.name); + do_check_true(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); + + do_check_true(handlerApp.equals(localApp)); + + localApp.executable = executable; + + do_check_eq(0, localApp.parameterCount); + localApp.appendParameter("-test1"); + do_check_eq(1, localApp.parameterCount); + localApp.appendParameter("-test2"); + do_check_eq(2, localApp.parameterCount); + do_check_true(localApp.parameterExists("-test1")); + do_check_true(localApp.parameterExists("-test2")); + do_check_false(localApp.parameterExists("-false")); + localApp.clearParameters(); + do_check_eq(0, localApp.parameterCount); + + var localApp2 = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + + localApp2.executable = executable; + + localApp.clearParameters(); + do_check_true(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"); + do_check_true(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"); + do_check_false(localApp2.equals(localApp)); + + var str; + str = localApp.getParameter(0) + do_check_eq(str, "-test1"); + str = localApp.getParameter(1) + do_check_eq(str, "-test2"); + str = localApp.getParameter(2) + do_check_eq(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"); + do_check_eq(lolType, ""); + + + // add a handler for the extension + var lolHandler = mimeSvc.getFromTypeAndExtension("application/lolcat", null); + + do_check_false(lolHandler.extensionExists("lolcat")); + lolHandler.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + lolHandler.preferredApplicationHandler = localHandler; + lolHandler.alwaysAskBeforeHandling = false; + + // store the handler + do_check_false(handlerSvc.exists(lolHandler)); + handlerSvc.store(lolHandler); + do_check_true(handlerSvc.exists(lolHandler)); + + // Get a file:// string pointing to mimeTypes.rdf + var rdfFile = HandlerServiceTest._dirSvc.get("UMimTyp", Ci.nsIFile); + var fileHandler = ioService.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler); + var rdfFileURI = fileHandler.getURLSpecFromFile(rdfFile); + + // Assign a file extenstion to the handler. handlerSvc.store() doesn't + // actually store any file extensions added with setFileExtensions(), you + // have to wade into RDF muck to do so. + + // Based on toolkit/mozapps/downloads/content/helperApps.js :: addExtension() + var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); + var mimeSource = gRDF.GetUnicodeResource("urn:mimetype:application/lolcat"); + var valueProperty = gRDF.GetUnicodeResource("http://home.netscape.com/NC-rdf#fileExtensions"); + var mimeLiteral = gRDF.GetLiteral("lolcat"); + + var DS = gRDF.GetDataSourceBlocking(rdfFileURI); + DS.Assert(mimeSource, valueProperty, mimeLiteral, true); + + + // test now-existent extension + lolType = handlerSvc.getTypeFromExtension("lolcat"); + do_check_eq(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); + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useSystemDefault); + do_check_eq(handlerInfo.defaultDescription, "sed"); + } +} diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js new file mode 100644 index 0000000000..38622c840a --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Encoded test URI to work on all platforms/independent of file encoding +const kTestURI = "http://\u65e5\u672c\u8a93.jp/"; +const kExpectedURI = "http://xn--wgv71a309e.jp/"; +const kOutputFile = "result.txt"; + +// Try several times in case the box we're running on is slow. +const kMaxCheckExistAttempts = 30; // seconds +var gCheckExistsAttempts = 0; + +const tempDir = do_get_tempdir(); + +function checkFile() { + // This is where we expect the output + var tempFile = tempDir.clone(); + tempFile.append(kOutputFile); + + if (!tempFile.exists()) { + if (gCheckExistsAttempts >= kMaxCheckExistAttempts) { + do_throw("Expected File " + tempFile.path + " does not exist after " + + kMaxCheckExistAttempts + " seconds"); + } + else { + ++gCheckExistsAttempts; + // Wait a bit longer then try again + do_timeout(1000, checkFile); + return; + } + } + + // Now read it + var fstream = + Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + var sstream = + Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.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_") + do_check_eq(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 = + Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + // set up the local handler object + var localHandler = + Components.classes["@mozilla.org/uriloader/local-handler-app;1"] + .createInstance(Components.interfaces.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 = + Components.classes["@mozilla.org/process/environment;1"] + .getService(Components.interfaces.nsIEnvironment); + + // The Write Argument file needs to know where its libraries are, so + // just force the path variable + // For mac + var greDir = HandlerServiceTest._dirSvc.get("GreD", Components.interfaces.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, null, null); + + // Just check we've got these matching, if we haven't there's a problem + // with ascii spec or our test case. + do_check_eq(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..e268ff9c37 --- /dev/null +++ b/uriloader/exthandler/tests/unit/xpcshell.ini @@ -0,0 +1,15 @@ +[DEFAULT] +head = head_handlerService.js +tail = tail_handlerService.js +run-sequentially = Bug 912235 - Intermittent failures + +[test_getTypeFromExtension_ext_to_type_mapping.js] +[test_getTypeFromExtension_with_empty_Content_Type.js] +[test_badMIMEType.js] +[test_handlerService.js] +support-files = mailcap +# Bug 676997: test consistently fails on Android +fail-if = os == "android" +[test_punycodeURIs.js] +# Bug 676997: test consistently fails on Android +fail-if = os == "android" diff --git a/uriloader/exthandler/tests/unit_ipc/test_encoding.js b/uriloader/exthandler/tests/unit_ipc/test_encoding.js new file mode 100644 index 0000000000..a2a00937a1 --- /dev/null +++ b/uriloader/exthandler/tests/unit_ipc/test_encoding.js @@ -0,0 +1,231 @@ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://testing-common/MockRegistrar.js"); + +do_get_profile(); + +var DownloadListener = { + init: function () { + let obs = Services.obs; + obs.addObserver(this, "dl-done", true); + }, + + observe: function (subject, topic, data) { + this.onFinished(subject, topic, data); + }, + + QueryInterface: function (iid) { + if (iid.equals(Ci.nsIObserver) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +} +DownloadListener.init(); + +function HelperAppDlg() { } +HelperAppDlg.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), + show: function (launcher, ctx, reason, usePrivateUI) { + launcher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToFile; + launcher.launchWithApplication(null, false); + } +} + +// Override the download-manager-ui to prevent anyone from trying to open +// a window. +function DownloadMgrUI() { } +DownloadMgrUI.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]), + show: function (ir, aID, reason) { }, + + visible: false, + + getAttention: function () { } +} + +function AlertsSVC() { } +AlertsSVC.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService]), + showAlertNotification: function (url, title, text, clickable, cookie, listener, name) { }, +} + +MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", + HelperAppDlg); +MockRegistrar.register("@mozilla.org/download-manager-ui;1", + DownloadMgrUI); +MockRegistrar.register("@mozilla.org/alerts-service;1", + AlertsSVC); + +function initChildTestEnv() +{ + sendCommand(' \ + const Cc = Components.classes; \ + const Ci = Components.interfaces; \ + const Cr = Components.results; \ + const Cu = Components.utils; \ + Cu.import("resource://gre/modules/Services.jsm"); \ + function WindowContext() { } \ + \ + WindowContext.prototype = { \ + getInterface: function (iid) { \ + if (iid.equals(Ci.nsIInterfaceRequestor) || \ + iid.equals(Ci.nsIURIContentListener) || \ + iid.equals(Ci.nsILoadGroup) || \ + iid.equals(Ci.nsIDocumentLoader) || \ + iid.equals(Ci.nsIDOMWindow)) \ + return this; \ + \ + throw Cr.NS_ERROR_NO_INTERFACE; \ + }, \ + \ + /* nsIURIContentListener */ \ + onStartURIOpen: function (uri) { }, \ + isPreferred: function (type, desiredtype) { return false; }, \ + \ + /* nsILoadGroup */ \ + addRequest: function (request, context) { }, \ + removeRequest: function (request, context, status) { } \ + }; \ + \ + var ioservice = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);\ + var uriloader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);\ + '); +} + +function testFinisher(endFunc) { + let ef = endFunc; + return function (file) { + ef(file); + runNextTest(); + } +} + +function runChildTestSet(set) +{ + DownloadListener.onFinished = testFinisher(set[2]); + sendCommand('\ + let uri = ioservice.newURI("http://localhost:4444' + set[0] + '", null, null); \ + let channel = NetUtil.newChannel({uri: uri, loadUsingSystemPrincipal: true}); \ + uriloader.openURI(channel, Ci.nsIURILoader.IS_CONTENT_PREFERRED, new WindowContext()); \ + '); +} + +var httpserver = null; +var currentTest = 0; +function runNextTest() +{ + if (currentTest == tests.length) { + httpserver.stop(do_test_finished); + return; + } + + let set = tests[currentTest++]; + runChildTestSet(set); +} + +const responseBody = [0x1f, 0x8b, 0x08, 0x00, 0x16, 0x5a, 0x8a, 0x48, 0x02, + 0x03, 0x2b, 0x49, 0x2d, 0x2e, 0xe1, 0x02, 0x00, 0xc6, + 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00]; + +/* + * First test: a file with Content-Type application/x-gzip and Content-Encoding gzip + * should not be decoded in a round-trip + */ +function testResponse1(metadata, response) { + response.setHeader("Content-Type", "application/x-gzip", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest1(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test1.gz") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + do_check_matches(str, responseBody); +} + +/* + * Second test: a file with Content-Type text/html and Content-Encoding gzip + * should not be decoded in a round-trip, if its filename ends in ".gz". + * We specify a Content-disposition header to force it to be saved as a file. + */ +function testResponse2(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest2(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test2.gz") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + do_check_matches(str, responseBody); +} + +function testResponse3(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest3(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test3.txt") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + let decodedBody = [ 116, 101, 115, 116, 10 ]; // 't','e','s','t','\n' + do_check_matches(str, decodedBody); +} + +var tests = [ + [ "/test1.gz", testResponse1, finishTest1 ], + [ "/test2.gz", testResponse2, finishTest2 ], + [ "/test3.txt", testResponse3, finishTest3 ], +]; + +function run_test() { +// do_load_child_test_harness(); + httpserver = new HttpServer(); + httpserver.start(4444); + do_test_pending(); + + initChildTestEnv(); + + for (let set of tests) + httpserver.registerPathHandler(set[0], set[1]); + + runNextTest(); +} diff --git a/uriloader/exthandler/tests/unit_ipc/xpcshell.ini b/uriloader/exthandler/tests/unit_ipc/xpcshell.ini new file mode 100644 index 0000000000..962b93decd --- /dev/null +++ b/uriloader/exthandler/tests/unit_ipc/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = +tail = + +[test_encoding.js] +# Bug 676995: test hangs consistently on Android +# Bug 907732: thunderbird still uses legacy downloads manager. +skip-if = (os == "android" || buildapp == '../mail') diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h new file mode 100644 index 0000000000..9d5ef31da1 --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h @@ -0,0 +1,32 @@ +/* -*- 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, nsIInterfaceRequestor* aWindowContext); +}; + +#endif /* nslocalhandlerappuikit_h_ */ diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm new file mode 100644 index 0000000000..afa600721e --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm @@ -0,0 +1,17 @@ +/* -*- 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 + +#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..3f6bc8b42f --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h @@ -0,0 +1,37 @@ +/* -*- 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); + +protected: + virtual nsresult LoadUriInternal(nsIURI* aURI); +#ifdef DEBUG + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) + { + NS_NOTREACHED("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif + static nsresult OpenApplicationWithURI(nsIFile* aApplication, + const nsCString& aURI); +}; + +#endif diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm new file mode 100644 index 0000000000..4b950dffc4 --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm @@ -0,0 +1,20 @@ +/* -*- 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" +#include "nsIFileURL.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..1f33be0fba --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.h @@ -0,0 +1,52 @@ +/* -*- 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); + + // method overrides --> used to hook the mime service into internet config.... + NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo); + already_AddRefed GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound); + 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); +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.mm b/uriloader/exthandler/uikit/nsOSHelperAppService.mm new file mode 100644 index 0000000000..dfee24b2db --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm @@ -0,0 +1,64 @@ +/* -*- 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; +} + +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); +} + +already_AddRefed +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound) +{ + *aFound = false; + return nullptr; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) +{ + *found = false; + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsExternalSharingAppService.h b/uriloader/exthandler/unix/nsExternalSharingAppService.h new file mode 100644 index 0000000000..3b1794b5e8 --- /dev/null +++ b/uriloader/exthandler/unix/nsExternalSharingAppService.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 NS_EXTERNAL_SHARING_APP_SERVICE_H +#define NS_EXTERNAL_SHARING_APP_SERVICE_H + +#include "nsIExternalSharingAppService.h" +#include "nsAutoPtr.h" + +class ShareUiInterface; + +#define NS_EXTERNALSHARINGAPPSERVICE_CID \ + {0xea782c90, 0xc6ec, 0x11df, \ + {0xbd, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66, 0x74}} + +class nsExternalSharingAppService : public nsIExternalSharingAppService +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALSHARINGAPPSERVICE + + nsExternalSharingAppService(); + +private: + ~nsExternalSharingAppService(); + +protected: + nsAutoPtr mShareUi; +}; +#endif diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.cpp b/uriloader/exthandler/unix/nsGNOMERegistry.cpp new file mode 100644 index 0000000000..bf71ae913c --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.cpp @@ -0,0 +1,109 @@ +/* -*- 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 "nsIComponentManager.h" +#include "nsMIMEInfoUnix.h" +#include "nsAutoPtr.h" +#include "nsIGIOService.h" + +/* static */ bool +nsGNOMERegistry::HandlerExists(const char *aProtocolScheme) +{ + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return false; + } + + nsCOMPtr app; + return NS_SUCCEEDED(giovfs->GetAppForURIScheme(nsDependentCString(aProtocolScheme), + getter_AddRefs(app))); +} + +// XXX Check HandlerExists() before calling LoadURL. + +/* static */ nsresult +nsGNOMERegistry::LoadURL(nsIURI *aURL) +{ + nsCOMPtr 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 giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) + return; + + nsAutoCString name; + nsCOMPtr app; + if (NS_FAILED(giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(app)))) + return; + + app->GetName(name); + + CopyUTF8toUTF16(name, aDesc); +} + + +/* static */ already_AddRefed +nsGNOMERegistry::GetFromExtension(const nsACString& aFileExt) +{ + nsAutoCString mimeType; + nsCOMPtr 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 mi = GetFromType(mimeType); + if (mi) { + mi->AppendExtension(aFileExt); + } + + return mi.forget(); +} + +/* static */ already_AddRefed +nsGNOMERegistry::GetFromType(const nsACString& aMIMEType) +{ + RefPtr mimeInfo = new nsMIMEInfoUnix(aMIMEType); + NS_ENSURE_TRUE(mimeInfo, nullptr); + + nsAutoCString name; + nsAutoCString description; + + nsCOMPtr giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + nsCOMPtr gioHandlerApp; + if (NS_FAILED(giovfs->GetAppForMimeType(aMIMEType, getter_AddRefs(gioHandlerApp))) || + !gioHandlerApp) { + return nullptr; + } + gioHandlerApp->GetName(name); + giovfs->GetDescriptionForMimeType(aMIMEType, description); + + mimeInfo->SetDefaultDescription(NS_ConvertUTF8toUTF16(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..fe42ee622f --- /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 GetFromExtension(const nsACString& aFileExt); + + static already_AddRefed 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..e516608873 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp @@ -0,0 +1,136 @@ +/* -*- 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" +#include "nsAutoPtr.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 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; + +#if defined(MOZ_ENABLE_CONTENTACTION) + ContentAction::Action action = + ContentAction::Action::defaultActionForFile(QUrl(), QString(mSchemeOrType.get())); + if (action.isValid()) { + *_retval = true; + return NS_OK; + } +#endif + + 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); + +#if defined(MOZ_ENABLE_CONTENTACTION) + QUrl uri = QUrl::fromLocalFile(QString::fromUtf8(nativePath.get())); + ContentAction::Action action = + ContentAction::Action::defaultActionForFile(uri, QString(mSchemeOrType.get())); + if (action.isValid()) { + action.trigger(); + return NS_OK; + } + return NS_ERROR_FAILURE; +#endif + + nsCOMPtr 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 ioservice = do_GetService(NS_IOSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr uri; + rv = ioservice->NewFileURI(aFile, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString uriSpec; + uri->GetSpec(uriSpec); + + nsCOMPtr app; + if (NS_FAILED(giovfs->GetAppForMimeType(mSchemeOrType, getter_AddRefs(app))) || !app) { + return NS_ERROR_FILE_NOT_FOUND; + } + + return app->Launch(uriSpec); +} + +#if defined(MOZ_ENABLE_CONTENTACTION) +NS_IMETHODIMP +nsMIMEInfoUnix::GetPossibleApplicationHandlers(nsIMutableArray ** aPossibleAppHandlers) +{ + if (!mPossibleApplications) { + mPossibleApplications = do_CreateInstance(NS_ARRAY_CONTRACTID); + + if (!mPossibleApplications) + return NS_ERROR_OUT_OF_MEMORY; + + QList actions = + ContentAction::Action::actionsForFile(QUrl(), QString(mSchemeOrType.get())); + + for (int i = 0; i < actions.size(); ++i) { + nsContentHandlerApp* app = + new nsContentHandlerApp(nsString((char16_t*)actions[i].name().data()), + mSchemeOrType, actions[i]); + mPossibleApplications->AppendElement(app, false); + } + } + + *aPossibleAppHandlers = mPossibleApplications; + NS_ADDREF(*aPossibleAppHandlers); + return NS_OK; +} +#endif + diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.h b/uriloader/exthandler/unix/nsMIMEInfoUnix.h new file mode 100644 index 0000000000..65f6d996d4 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.h @@ -0,0 +1,32 @@ +/* -*- 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); + + virtual nsresult LoadUriInternal(nsIURI *aURI); + + virtual nsresult LaunchDefaultWithFile(nsIFile *aFile); +#if defined(MOZ_ENABLE_CONTENTACTION) + NS_IMETHOD GetPossibleApplicationHandlers(nsIMutableArray * *aPossibleAppHandlers); +#endif +}; + +#endif // nsMIMEInfoUnix_h_ diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.cpp b/uriloader/exthandler/unix/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..84e1cf5b04 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp @@ -0,0 +1,1537 @@ +/* -*- 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 +#include + +#if defined(MOZ_ENABLE_CONTENTACTION) +#include +#include +#endif + +#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 "nsXPIDLString.h" +#include "nsIURL.h" +#include "nsIFileStreams.h" +#include "nsILineInputStream.h" +#include "nsIFile.h" +#include "nsIProcess.h" +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsISupportsPrimitives.h" +#include "nsCRT.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "prenv.h" // for PR_GetEnv() +#include "nsAutoPtr.h" +#include "mozilla/Preferences.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() : nsExternalHelperAppService() +{ + mode_t mask = umask(0777); + umask(mask); + mPermissions = 0666 & ~mask; +} + +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)); + NS_PRECONDITION(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 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())); + nsresult rv = NS_OK; + nsAutoString mimeFileName; + + const char* filenamePref = aUserData ? + "helpers.private_mime_types_file" : "helpers.global_mime_types_file"; + + 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, NS_LITERAL_CSTRING("#--Netscape Communications Corporation MIME Information")) || + StringBeginsWith(aBuffer, NS_LITERAL_CSTRING("#--MCOM MIME Information")); +} + +/* + * 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 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 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 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())); + nsresult rv = NS_OK; + nsCOMPtr mimeFile; + nsCOMPtr mimeTypes; + bool netscapeFormat; + nsAutoString buf; + nsAutoCString cBuf; + bool more = false; + rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), getter_AddRefs(mimeTypes), + cBuf, &netscapeFormat, &more); + + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsString entry; + entry.SetCapacity(100); + 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())); + nsresult rv = NS_OK; + nsAutoString mimeFileName; + + 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())); + + nsresult rv = NS_OK; + nsCOMPtr mimeFile; + nsCOMPtr mimeTypes; + bool netscapeFormat; + nsAutoCString cBuf; + nsAutoString buf; + bool more = false; + rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), getter_AddRefs(mimeTypes), + cBuf, &netscapeFormat, &more); + + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsString entry; + entry.SetCapacity(100); + 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; + } else 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(NS_LITERAL_STRING("type="), 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(NS_LITERAL_STRING("exts="), 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(NS_LITERAL_STRING("desc=\""), 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(NS_LITERAL_STRING("desc=\""), match_start, match_end)) { + aDescriptionStart = match_end; + match_start = aDescriptionStart; + match_end = end_iter; + if (FindInReadable(NS_LITERAL_STRING("exts="), 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)); + + if (aDescriptionStart != aDescriptionStart && *aDescriptionEnd == '"') { + --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, + NS_LITERAL_STRING("*"), + aHandler, + aDescription, + aMozillaFlags, + true); + } + + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, + NS_LITERAL_STRING("*"), + 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())); + nsresult rv = NS_OK; + nsAutoString mailcapFileName; + + const char * filenamePref = aUserData ? + "helpers.private_mailcap_file" : "helpers.global_mailcap_file"; + const char * filenameEnvVar = aUserData ? + "PERSONAL_MAILCAP" : "MAILCAP"; + + 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 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 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 mailcap (do_QueryInterface(mailcapFile, &rv)); + + if (NS_FAILED(rv)) { + LOG(("Interface trouble in stream land!")); + return rv; + } + + nsString entry, buffer; + nsAutoCString cBuffer; + entry.SetCapacity(128); + cBuffer.SetCapacity(80); + 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 process = do_CreateInstance(NS_PROCESS_CONTRACTID, &rv); + if (NS_FAILED(rv)) + continue; + nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) + continue; + rv = file->InitWithNativePath(NS_LITERAL_CSTRING("/bin/sh")); + 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; + } else { // 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) +{ + LOG(("-- nsOSHelperAppService::OSProtocolHandlerExists for '%s'\n", + aProtocolScheme)); + *aHandlerExists = false; + +#if defined(MOZ_ENABLE_CONTENTACTION) + // libcontentaction requires character ':' after scheme + ContentAction::Action action = + ContentAction::Action::defaultActionForScheme(QString(aProtocolScheme) + ':'); + + if (action.isValid()) + *aHandlerExists = true; +#endif + +#ifdef MOZ_WIDGET_GTK + // Check the GNOME registry for a protocol handler + *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme); +#endif + + return NS_OK; +} + +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 +} + +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 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 +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 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_LITERAL_CSTRING("/") + asciiMinorType); + RefPtr 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 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 +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 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, + NS_LITERAL_STRING("*"), + handler, + mailcap_description, + mozillaFlags, + true); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, + NS_LITERAL_STRING("*"), + 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 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 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(); +} + + +already_AddRefed +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType, + const nsACString& aFileExt, + bool *aFound) { + *aFound = true; + RefPtr retval = GetFromType(PromiseFlatCString(aType)); + bool hasDefault = false; + if (retval) + retval->GetHasDefaultHandler(&hasDefault); + if (!retval || !hasDefault) { + RefPtr miByExt = GetFromExtension(PromiseFlatCString(aFileExt)); + // If we had no extension match, but a type match, use that + if (!miByExt && retval) + return retval.forget(); + // 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); + + return retval.forget(); + } + // If we got nothing, make a new mimeinfo + if (!retval) { + *aFound = false; + retval = new nsMIMEInfoUnix(aType); + if (retval) { + if (!aFileExt.IsEmpty()) + retval->AppendExtension(aFileExt); + } + + return retval.forget(); + } + + // 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); + } + return retval.forget(); +} + +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..33eb934ed7 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.h @@ -0,0 +1,128 @@ +/* -*- 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 "nsMIMEInfoImpl.h" +#include "nsCOMPtr.h" + +class nsILineInputStream; + +class nsOSHelperAppService : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // method overrides for mime.types and mime.info look up steps + already_AddRefed GetMIMEInfoFromOS(const nsACString& aMimeType, + const nsACString& aFileExt, + bool *aFound); + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **_retval); + + // override nsIExternalProtocolService methods + nsresult OSProtocolHandlerExists(const char * aProtocolScheme, bool * aHandlerExists); + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, nsAString& _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); + +protected: + already_AddRefed GetFromType(const nsCString& aMimeType); + already_AddRefed GetFromExtension(const nsCString& aFileExt); + +private: + uint32_t mPermissions; + + // 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..2c7171c877 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp @@ -0,0 +1,883 @@ +/* -*- 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 +#include +#include "nsAutoPtr.h" +#include "nsIMutableArray.h" +#include "nsTArray.h" +#include "shlobj.h" +#include "windows.h" +#include "nsIWindowsRegKey.h" +#include "nsIProcess.h" +#include "nsUnicharUtils.h" +#include "nsITextToSubURI.h" +#include "nsVariant.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(); +} + +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) { + 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 localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString path; + aFile->GetPath(path); + + // 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; + } + } + return LaunchWithIProcess(executable, path); + } + + 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 properties; + + nsCOMPtr variant; + GetProperty(NS_LITERAL_STRING("defaultApplicationIconURL"), getter_AddRefs(variant)); + if (variant) + properties.AppendObject(variant); + + GetProperty(NS_LITERAL_STRING("customApplicationIconURL"), getter_AddRefs(variant)); + if (variant) + properties.AppendObject(variant); + + return NS_NewArrayEnumerator(_retval, properties); +} + +static nsresult GetIconURLVariant(nsIFile* aApplication, nsIVariant* *_retval) +{ + nsAutoCString fileURLSpec; + NS_GetURLSpecFromFile(aApplication, fileURLSpec); + nsAutoCString iconURLSpec; iconURLSpec.AssignLiteral("moz-icon://"); + iconURLSpec += fileURLSpec; + RefPtr 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 localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 + nsAutoCString urlCharset; + nsAutoString utf16Spec; + rv = aURL->GetOriginCharset(urlCharset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr textToSubURI = do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_FAILED(textToSubURI->UnEscapeNonAsciiURI(urlCharset, urlSpec, utf16Spec))) { + CopyASCIItoUTF16(urlSpec, utf16Spec); + } + + static const wchar_t cmdVerb[] = L"open"; + SHELLEXECUTEINFOW sinfo; + memset(&sinfo, 0, sizeof(sinfo)); + sinfo.cbSize = sizeof(sinfo); + sinfo.fMask = SEE_MASK_FLAG_DDEWAIT | + SEE_MASK_FLAG_NO_UI; + sinfo.hwnd = nullptr; + sinfo.lpVerb = (LPWSTR)&cmdVerb; + sinfo.nShow = SW_SHOWNORMAL; + + LPITEMIDLIST pidl = nullptr; + SFGAOF sfgao; + + // Bug 394974 + if (SUCCEEDED(SHParseDisplayName(utf16Spec.get(), nullptr, + &pidl, 0, &sfgao))) { + sinfo.lpIDList = pidl; + sinfo.fMask |= SEE_MASK_INVOKEIDLIST; + } else { + // SHParseDisplayName failed. Bailing out as work around for + // Microsoft Security Bulletin MS07-061 + rv = NS_ERROR_FAILURE; + } + if (NS_SUCCEEDED(rv)) { + BOOL result = ShellExecuteExW(&sinfo); + if (!result || ((LONG_PTR)sinfo.hInstApp) < 32) + rv = NS_ERROR_FAILURE; + } + if (pidl) + CoTaskMemFree(pidl); + } + + return rv; +} + +// Given a path to a local file, return its nsILocalHandlerApp instance. +bool nsMIMEInfoWin::GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr& aApp) +{ + nsCOMPtr 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 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( + NS_LITERAL_STRING("NoOpenWith"), &value)) && + value == 1) + return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue( + NS_LITERAL_STRING("NoOpenWith"), 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(EmptyString(), + 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 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(NS_LITERAL_STRING("NoOpenWith"), &value); + if (NS_SUCCEEDED(rv) && value == 1) + return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(NS_LITERAL_STRING("NoOpenWith"), + 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(EmptyString(), + appFilesystemCommand))) { + // Replace embedded environment variables. + uint32_t bufLength = + ::ExpandEnvironmentStringsW(appFilesystemCommand.get(), + L"", 0); + if (bufLength == 0) // Error + return false; + + auto destination = mozilla::MakeUniqueFallible(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; + NS_NAMED_LITERAL_STRING(rundllSegment, "rundll32.exe "); + 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 + NS_NAMED_LITERAL_STRING(percentOneParam, "%1"); + 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 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(EmptyString(), 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& appList, + nsTArray& 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) { + uint32_t index = lower.Find(exe); + if (index != -1) + return; + } + + nsCOMPtr aApp; + if (!GetLocalHandlerApp(appFilesystemCommand, aApp)) + return; + + // Save in our main tracking arrays + appList->AppendElement(aApp, false); + trackList.AppendElement(lower); +} + +// Helper routine that handles a compare between a path +// and an array of paths. +static bool IsPathInList(nsAString& appPath, + nsTArray& 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 appList = + do_CreateInstance("@mozilla.org/array;1"); + + if (!appList) + return NS_ERROR_FAILURE; + + nsTArray trackList; + + nsAutoCString fileExt; + GetPrimaryExtension(fileExt); + + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) + return NS_ERROR_FAILURE; + nsCOMPtr 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(EmptyString(), mimeFileExt))) { + CopyUTF16toUTF8(mimeFileExt, fileExt); + extKnown = false; + } + } + } + } + + nsAutoString fileExtToUse; + if (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(EmptyString(), appProgId))) { + // Bug 358297 - ignore the embedded internet explorer handler + if (appProgId != NS_LITERAL_STRING("XPSViewer.Document")) { + 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 = + NS_LITERAL_STRING("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 = + NS_LITERAL_STRING("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(NS_LITERAL_STRING("PerceivedType"), + perceivedType); + if (NS_SUCCEEDED(rv)) { + nsAutoString openWithListPath(NS_LITERAL_STRING("SystemFileAssociations\\")); + 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 = NS_LITERAL_STRING("*\\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(); + } + + + // 8) General application's list - not file extension specific on windows + workingRegistryPath = NS_LITERAL_STRING("Applications"); + + 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; +} diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.h b/uriloader/exthandler/win/nsMIMEInfoWin.h new file mode 100644 index 0000000000..0a5b678e8a --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.h @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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: + nsMIMEInfoWin(const char* aType = "") : nsMIMEInfoBase(aType) {} + nsMIMEInfoWin(const nsACString& aMIMEType) : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoWin(const nsACString& aType, HandlerClass aClass) : + nsMIMEInfoBase(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile); + NS_IMETHOD GetHasDefaultHandler(bool * _retval); + NS_IMETHOD GetPossibleLocalHandlers(nsIArray **_retval); + + 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 mDefaultApplication; + + // Given a path to a local handler, return its + // nsILocalHandlerApp instance. + bool GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr& 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& appList, + nsTArray& trackList, + const nsAString& appFilesystemCommand); + +}; + +#endif diff --git a/uriloader/exthandler/win/nsOSHelperAppService.cpp b/uriloader/exthandler/win/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..f01f3b49b5 --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp @@ -0,0 +1,626 @@ +/* -*- 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 "nsOSHelperAppService.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsXPIDLString.h" +#include "nsIURL.h" +#include "nsIMIMEInfo.h" +#include "nsMIMEInfoWin.h" +#include "nsMimeTypes.h" +#include "nsIProcess.h" +#include "plstr.h" +#include "nsAutoPtr.h" +#include "nsNativeCharsetUtils.h" +#include "nsLocalFile.h" +#include "nsIWindowsRegKey.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WindowsVersion.h" + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include +#include + +#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args) + +// helper methods: forward declarations... +static nsresult GetExtensionFrom4xRegistryInfo(const nsACString& aMimeType, + nsString& aFileExtension); +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 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(NS_LITERAL_STRING("Extension"), aFileExtension); + + return NS_OK; +} + +// We have a serious problem!! I have this content type and the windows registry only gives me +// helper apps based on extension. Right now, we really don't have a good place to go for +// trying to figure out the extension for a particular mime type....One short term hack is to look +// this information in 4.x (it's stored in the windows regsitry). +static nsresult GetExtensionFrom4xRegistryInfo(const nsACString& aMimeType, + nsString& aFileExtension) +{ + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) + return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = regKey-> + Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + NS_LITERAL_STRING("Software\\Netscape\\Netscape Navigator\\Suffixes"), + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) + return NS_ERROR_NOT_AVAILABLE; + + rv = regKey->ReadStringValue(NS_ConvertASCIItoUTF16(aMimeType), + aFileExtension); + if (NS_FAILED(rv)) + return NS_OK; + + aFileExtension.Insert(char16_t('.'), 0); + + // this may be a comma separated list of extensions...just take the + // first one for now... + + int32_t pos = aFileExtension.FindChar(char16_t(',')); + if (pos > 0) { + // we have a comma separated list of types... + // truncate everything after the first comma (including the comma) + aFileExtension.Truncate(pos); + } + + 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) + { + // Vista: use new application association interface + if (mAppAssoc) { + 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); + *aHandlerExists = true; + } + return NS_OK; + } + + HKEY hKey; + LONG err = ::RegOpenKeyExW(HKEY_CLASSES_ROOT, + NS_ConvertASCIItoUTF16(aProtocolScheme).get(), + 0, + KEY_QUERY_VALUE, + &hKey); + if (err == ERROR_SUCCESS) + { + err = ::RegQueryValueExW(hKey, L"URL Protocol", + nullptr, nullptr, nullptr, nullptr); + *aHandlerExists = (err == ERROR_SUCCESS); + // close the key + ::RegCloseKey(hKey); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) +{ + nsCOMPtr 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; + } + } + + if (mAppAssoc) { + // Vista: use new application association interface + 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 app; + nsAutoString appInfo(pResult); + CoTaskMemFree(pResult); + if (NS_SUCCEEDED(GetDefaultAppInfo(appInfo, _retval, getter_AddRefs(app)))) + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr app; + GetDefaultAppInfo(buf, _retval, getter_AddRefs(app)); + + if (!_retval.Equals(buf)) + return NS_OK; + + // Fall back to full path + buf.AppendLiteral("\\shell\\open\\command"); + nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + buf, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) + return NS_ERROR_NOT_AVAILABLE; + + rv = regKey->ReadStringValue(EmptyString(), _retval); + + return NS_SUCCEEDED(rv) ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +// 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 nsAFlatString& fileType, nsIMIMEInfo *pInfo) +{ + nsresult rv = NS_OK; + + NS_ENSURE_ARG(pInfo); + nsCOMPtr 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(EmptyString(), 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 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(NS_LITERAL_STRING("Content Type"), type); + if (NS_SUCCEEDED(rv)) + eq = type.EqualsASCII(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 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 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(EmptyString(), handlerCommand); + if (NS_FAILED(rv)) { + + // Check if there is a DelegateExecute string + nsAutoString delegateExecute; + rv = regKey->ReadStringValue(NS_LITERAL_STRING("DelegateExecute"), 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(EmptyString(), 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(EmptyString(), 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 lf = new nsLocalFile(); + rv = lf->InitWithCommandLine(handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + + // The "FileDescription" field contains the actual name of the application. + lf->GetVersionInfoField("FileDescription", aDefaultDescription); + lf.forget(aDefaultApplication); + + return NS_OK; +} + +already_AddRefed nsOSHelperAppService::GetByExtension(const nsAFlatString& 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 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. + mimeInfo->AppendExtension(NS_ConvertUTF16toUTF8(Substring(fileExtToUse, 1))); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + + nsAutoString appInfo; + bool found; + + // Retrieve the default application for this extension + if (mAppAssoc) { + // Vista: use the new application association COM interfaces + // for resolving helpers. + 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; + } + } + else + { + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) + return nullptr; + nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + found = NS_SUCCEEDED(regKey->ReadStringValue(EmptyString(), + appInfo)); + } + } + + // 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 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(); +} + +already_AddRefed nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, const nsACString& aFileExt, bool *aFound) +{ + *aFound = true; + + const nsCString& flatType = PromiseFlatCString(aMIMEType); + const nsCString& flatExt = PromiseFlatCString(aFileExt); + + nsAutoString fileExtension; + /* XXX The Equals 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.... + * We'll do extension-based lookup for this type later in this function. + */ + if (!aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM)) { + // (1) try to use the windows mime database to see if there is a mapping to a file extension + // (2) try to see if we have some left over 4.x registry info we can peek at... + GetExtensionFromWindowsMimeDatabase(aMIMEType, fileExtension); + LOG(("Windows mime database: extension '%s'\n", fileExtension.get())); + if (fileExtension.IsEmpty()) { + GetExtensionFrom4xRegistryInfo(aMIMEType, fileExtension); + LOG(("4.x Registry: extension '%s'\n", fileExtension.get())); + } + } + // If we found an extension for the type, do the lookup + RefPtr mi; + if (!fileExtension.IsEmpty()) + mi = GetByExtension(fileExtension, flatType.get()); + LOG(("Extension lookup on '%s' found: 0x%p\n", fileExtension.get(), mi.get())); + + bool hasDefault = false; + if (mi) { + mi->GetHasDefaultHandler(&hasDefault); + // OK. We might have the case that |aFileExt| is a valid extension for the + // mimetype we were given. In that case, we do want to append aFileExt + // to the mimeinfo that we have. (E.g.: We are asked for video/mpeg and + // .mpg, but the primary extension for video/mpeg is .mpeg. But because + // .mpg is an extension for video/mpeg content, we want to append it) + if (!aFileExt.IsEmpty() && typeFromExtEquals(NS_ConvertUTF8toUTF16(flatExt).get(), flatType.get())) { + LOG(("Appending extension '%s' to mimeinfo, because its mimetype is '%s'\n", + flatExt.get(), flatType.get())); + bool extExist = false; + mi->ExtensionExists(aFileExt, &extExist); + if (!extExist) + mi->AppendExtension(aFileExt); + } + } + if (!mi || !hasDefault) { + RefPtr miByExt = + GetByExtension(NS_ConvertUTF8toUTF16(aFileExt), flatType.get()); + LOG(("Ext. lookup for '%s' found 0x%p\n", flatExt.get(), miByExt.get())); + if (!miByExt && mi) + return mi.forget(); + if (miByExt && !mi) { + return miByExt.forget(); + } + if (!miByExt && !mi) { + *aFound = false; + mi = new nsMIMEInfoWin(flatType); + if (!aFileExt.IsEmpty()) { + mi->AppendExtension(aFileExt); + } + + return mi.forget(); + } + + // if we get here, mi has no default app. copy from extension lookup. + nsCOMPtr defaultApp; + nsAutoString desc; + miByExt->GetDefaultDescription(desc); + + mi->SetDefaultDescription(desc); + } + return mi.forget(); +} + +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 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(NS_LITERAL_STRING("Content Type"), + 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..b00529433c --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.h @@ -0,0 +1,65 @@ +/* -*- 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 "nsMIMEInfoImpl.h" +#include "nsCOMPtr.h" +#include + +#ifdef _WIN32_WINNT +#undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 +#include + +class nsMIMEInfoWin; + +class nsOSHelperAppService : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + nsresult OSProtocolHandlerExists(const char * aProtocolScheme, bool * aHandlerExists); + nsresult LoadUriInternal(nsIURI * aURL); + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, nsAString& _retval); + + // method overrides for windows registry look up steps.... + already_AddRefed GetMIMEInfoFromOS(const nsACString& aMIMEType, const nsACString& aFileExt, bool *aFound); + 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 GetByExtension(const nsAFlatString& aFileExt, const char *aTypeHint = nullptr); + nsresult FindOSMimeInfoForType(const char * aMimeContentType, nsIURI * aURI, char ** aFileExtension, nsIMIMEInfo ** aMIMEInfo); + + static nsresult GetMIMEInfoFromRegistry(const nsAFlatString& 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..840c9cc05e --- /dev/null +++ b/uriloader/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'base', + 'exthandler', + 'prefetch', +] diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.cpp b/uriloader/prefetch/OfflineCacheUpdateChild.cpp new file mode 100644 index 0000000000..555508c373 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateChild.cpp @@ -0,0 +1,528 @@ +/* -*- 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 "BackgroundUtils.h" +#include "OfflineCacheUpdateChild.h" +#include "nsOfflineCacheUpdate.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/TabChild.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoCommon.h" + +#include "nsIApplicationCacheContainer.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheService.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "nsPIDOMWindow.h" +#include "nsIDOMOfflineResourceList.h" +#include "nsIDocument.h" +#include "nsIObserverService.h" +#include "nsIURL.h" +#include "nsITabChild.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" + +using namespace mozilla::ipc; +using namespace mozilla::net; +using mozilla::dom::TabChild; +using mozilla::dom::ContentChild; + +// +// 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 +//----------------------------------------------------------------------------- + +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 &aObservers) +{ + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr 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(nsIDOMDocument *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). + nsCOMPtr document = do_QueryInterface(aDocument); + if (!document) + return; + + nsIChannel* channel = document->GetChannel(); + nsCOMPtr appCacheChannel = + do_QueryInterface(channel); + if (!appCacheChannel) + return; + + bool loadedFromAppCache; + appCacheChannel->GetLoadedFromApplicationCache(&loadedFromAppCache); + if (loadedFromAppCache) + return; + + mDocument = aDocument; +} + +nsresult +OfflineCacheUpdateChild::AssociateDocument(nsIDOMDocument *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 container = + do_QueryInterface(aDocument); + if (!container) + return NS_OK; + + nsCOMPtr existingCache; + nsresult rv = container->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 = container->SetApplicationCache(aApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateChild::nsIOfflineCacheUpdate +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +OfflineCacheUpdateChild::Init(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal *aLoadingPrincipal, + nsIDOMDocument *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. + bool match; + rv = aManifestURI->SchemeIs("http", &match); + NS_ENSURE_SUCCESS(rv, rv); + + if (!match) { + rv = aManifestURI->SchemeIs("https", &match); + NS_ENSURE_SUCCESS(rv, rv); + if (!match) + 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) +{ + NS_NOTREACHED("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) +{ + NS_NOTREACHED("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 = nsIDOMOfflineResourceList::CHECKING; + return NS_OK; + case STATE_DOWNLOADING : + *aStatus = nsIDOMOfflineResourceList::DOWNLOADING; + return NS_OK; + default : + *aStatus = nsIDOMOfflineResourceList::IDLE; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetPartial(bool *aPartial) +{ + *aPartial = false; + 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) { + nsCOMPtr 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 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 window = mWindow.forget(); + nsCOMPtrdocshell = window->GetDocShell(); + if (!docshell) { + NS_WARNING("doc shell tree item is null"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr tabchild = docshell->GetTabChild(); + // because owner implements nsITabChild, we can assume that it is + // the one and only TabChild. + TabChild* child = tabchild ? static_cast(tabchild.get()) : nullptr; + + if (MissingRequiredTabChild(child, "offlinecacheupdate")) { + return NS_ERROR_FAILURE; + } + + URIParams manifestURI, documentURI; + SerializeURI(mManifestURI, manifestURI); + SerializeURI(mDocumentURI, documentURI); + + nsresult rv = NS_OK; + PrincipalInfo loadingPrincipalInfo; + rv = PrincipalToPrincipalInfo(mLoadingPrincipal, + &loadingPrincipalInfo); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-added")); + observerService->NotifyObservers(static_cast(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; + + // Need to addref ourself here, because the IPC stack doesn't hold + // a reference to us. Will be released in RecvFinish() that identifies + // the work has been done. + ContentChild::GetSingleton()->SendPOfflineCacheUpdateConstructor( + this, manifestURI, documentURI, loadingPrincipalInfo, + stickDocument); + + // ContentChild::DeallocPOfflineCacheUpdate will release this. + NS_ADDREF_THIS(); + + return NS_OK; +} + +bool +OfflineCacheUpdateChild::RecvAssociateDocuments(const nsCString &cacheGroupId, + const nsCString &cacheClientId) +{ + LOG(("OfflineCacheUpdateChild::RecvAssociateDocuments [%p, cache=%s]", this, cacheClientId.get())); + + nsresult rv; + + nsCOMPtr cache = + do_CreateInstance(NS_APPLICATIONCACHE_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return true; + + cache->InitAsHandle(cacheGroupId, cacheClientId); + + if (mDocument) { + AssociateDocument(mDocument, cache); + } + + nsCOMArray observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) + observers[i]->ApplicationCacheAvailable(cache); + + return true; +} + +bool +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 observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) + observers[i]->UpdateStateChanged(this, event); + + return true; +} + +bool +OfflineCacheUpdateChild::RecvFinish(const bool &succeeded, + const bool &isUpgrade) +{ + LOG(("OfflineCacheUpdateChild::RecvFinish [%p]", this)); + + RefPtr kungFuDeathGrip(this); + + mState = STATE_FINISHED; + mSucceeded = succeeded; + mIsUpgrade = isUpgrade; + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-completed")); + observerService->NotifyObservers(static_cast(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(). + // TabChild::DeallocPOfflineCacheUpdate will call Release. + OfflineCacheUpdateChild::Send__delete__(this); + + return true; +} + +} // namespace docshell +} // namespace mozilla diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.h b/uriloader/prefetch/OfflineCacheUpdateChild.h new file mode 100644 index 0000000000..89d1e6f1f8 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateChild.h @@ -0,0 +1,94 @@ +/* -*- 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/. */ + +#ifndef nsOfflineCacheUpdateChild_h +#define nsOfflineCacheUpdateChild_h + +#include "mozilla/docshell/POfflineCacheUpdateChild.h" +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIDOMDocument.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "nsString.h" +#include "nsWeakReference.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace docshell { + +class OfflineCacheUpdateChild : public nsIOfflineCacheUpdate + , public POfflineCacheUpdateChild +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATE + + virtual bool + RecvNotifyStateEvent(const uint32_t& stateEvent, + const uint64_t& byteProgress) override; + + virtual bool + RecvAssociateDocuments( + const nsCString& cacheGroupId, + const nsCString& cacheClientId) override; + + virtual bool + RecvFinish(const bool& succeeded, + const bool& isUpgrade) override; + + explicit OfflineCacheUpdateChild(nsPIDOMWindowInner* aWindow); + + void SetDocument(nsIDOMDocument *aDocument); + +private: + ~OfflineCacheUpdateChild(); + + nsresult AssociateDocument(nsIDOMDocument *aDocument, + nsIApplicationCache *aApplicationCache); + void GatherObservers(nsCOMArray &aObservers); + nsresult Finish(); + + enum { + STATE_UNINITIALIZED, + STATE_INITIALIZED, + STATE_CHECKING, + STATE_DOWNLOADING, + STATE_CANCELLED, + STATE_FINISHED + } mState; + + bool mIsUpgrade; + bool mSucceeded; + + nsCString mUpdateDomain; + nsCOMPtr mManifestURI; + nsCOMPtr mDocumentURI; + nsCOMPtr mLoadingPrincipal; + + nsCOMPtr mObserverService; + + /* Clients watching this update for changes */ + nsCOMArray mWeakObservers; + nsCOMArray mObservers; + + /* Document that requested this update */ + nsCOMPtr mDocument; + + /* Keep reference to the window that owns this update to call the + parent offline cache update construcor */ + nsCOMPtr 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..71ca986ffb --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateGlue.cpp @@ -0,0 +1,228 @@ +/* -*- 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 "OfflineCacheUpdateGlue.h" +#include "nsOfflineCacheUpdate.h" +#include "mozilla/Services.h" + +#include "nsIApplicationCache.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheContainer.h" +#include "nsIChannel.h" +#include "nsIDocument.h" +#include "mozilla/Logging.h" + +// +// 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 +//----------------------------------------------------------------------------- + +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())); + } + + return mUpdate; +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::Schedule() +{ + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-added")); + observerService->NotifyObservers(static_cast(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, + nsIDOMDocument *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; + } + + return mUpdate->Init(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr, + aCustomProfileDir); +} + +void +OfflineCacheUpdateGlue::SetDocument(nsIDOMDocument *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). + nsCOMPtr document = do_QueryInterface(aDocument); + if (!document) + return; + + nsIChannel* channel = document->GetChannel(); + nsCOMPtr 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 observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-completed")); + observerService->NotifyObservers(static_cast(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. + nsCOMPtr container = + do_QueryInterface(mDocument); + if (!container) + return NS_OK; + + nsCOMPtr existingCache; + nsresult rv = container->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 = container->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..92201ec82b --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateGlue.h @@ -0,0 +1,80 @@ +/* -*- 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/. */ + +#ifndef nsOfflineCacheUpdateGlue_h +#define nsOfflineCacheUpdateGlue_h + +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsOfflineCacheUpdate; + +namespace mozilla { +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 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) override { return !_to ? NS_ERROR_NULL_POINTER : _to->InitPartial(aManifestURI, aClientID, aDocumentURI, aLoadingPrincipal); } \ + 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, + nsIDOMDocument *aDocument, + nsIFile *aCustomProfileDir) override; + + NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER + + OfflineCacheUpdateGlue(); + + void SetDocument(nsIDOMDocument *aDocument); + +private: + ~OfflineCacheUpdateGlue(); + + RefPtr mUpdate; + bool mCoalesced; + + /* Document that requested this update */ + nsCOMPtr mDocument; + nsCOMPtr mDocumentURI; + nsCOMPtr mLoadingPrincipal; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/OfflineCacheUpdateParent.cpp b/uriloader/prefetch/OfflineCacheUpdateParent.cpp new file mode 100644 index 0000000000..0381ec3f62 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateParent.cpp @@ -0,0 +1,294 @@ +/* -*- 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 "OfflineCacheUpdateParent.h" + +#include "BackgroundUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/TabParent.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" +#include "nsOfflineCacheUpdate.h" +#include "nsIApplicationCache.h" +#include "nsIScriptSecurityManager.h" +#include "nsNetUtil.h" + +using namespace mozilla::ipc; +using mozilla::BasePrincipal; +using mozilla::DocShellOriginAttributes; +using mozilla::PrincipalOriginAttributes; +using mozilla::dom::TabParent; + +// +// 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 +//----------------------------------------------------------------------------- + + +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(const URIParams& aManifestURI, + const URIParams& aDocumentURI, + const PrincipalInfo& aLoadingPrincipalInfo, + const bool& stickDocument) +{ + LOG(("OfflineCacheUpdateParent::RecvSchedule [%p]", this)); + + nsresult rv; + + RefPtr update; + nsCOMPtr manifestURI = DeserializeURI(aManifestURI); + if (!manifestURI) + return NS_ERROR_FAILURE; + + mLoadingPrincipal = PrincipalInfoToPrincipal(aLoadingPrincipalInfo, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) + return NS_ERROR_FAILURE; + + bool offlinePermissionAllowed = false; + + rv = service->OfflineAppAllowed( + mLoadingPrincipal, nullptr, &offlinePermissionAllowed); + NS_ENSURE_SUCCESS(rv, rv); + + if (!offlinePermissionAllowed) + return NS_ERROR_DOM_SECURITY_ERR; + + nsCOMPtr documentURI = DeserializeURI(aDocumentURI); + if (!documentURI) + return NS_ERROR_FAILURE; + + if (!NS_SecurityCompareURIs(manifestURI, documentURI, false)) + return NS_ERROR_DOM_SECURITY_ERR; + + nsAutoCString originSuffix; + rv = mLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + service->FindUpdate(manifestURI, + originSuffix, + nullptr, + getter_AddRefs(update)); + if (!update) { + update = new nsOfflineCacheUpdate(); + + // Leave aDocument argument null. Only glues and children keep + // document instances. + rv = update->Init(manifestURI, documentURI, mLoadingPrincipal, nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + // 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) { + nsCOMPtr stickURI; + documentURI->Clone(getter_AddRefs(stickURI)); + update->StickDocument(stickURI); + } + + 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(nsIDOMElement** aElement) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetNestedFrameId(uint64_t* aId) +{ + 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::GetIsInIsolatedMozBrowserElement(bool *aIsInIsolatedMozBrowserElement) +{ + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + return mLoadingPrincipal->GetIsInIsolatedMozBrowserElement(aIsInIsolatedMozBrowserElement); +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetAppId(uint32_t *aAppId) +{ + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + return mLoadingPrincipal->GetAppId(aAppId); +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetOriginAttributes(JS::MutableHandleValue aAttrs) +{ + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + MOZ_ASSERT(cx); + + nsresult rv = mLoadingPrincipal->GetOriginAttributes(cx, aAttrs); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::IsTrackingProtectionOn(bool* aIsTrackingProtectionOn) +{ + 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..f6dbc1cb2a --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateParent.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#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 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(const URIParams& manifestURI, + const URIParams& documentURI, + const PrincipalInfo& loadingPrincipalInfo, + const bool& stickDocument); + + void + StopSendingMessagesToChild() + { + mIPCClosed = true; + } + + explicit OfflineCacheUpdateParent(); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; +private: + ~OfflineCacheUpdateParent(); + + bool mIPCClosed; + + nsCOMPtr mLoadingPrincipal; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/POfflineCacheUpdate.ipdl b/uriloader/prefetch/POfflineCacheUpdate.ipdl new file mode 100644 index 0000000000..e624752522 --- /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 { + +//------------------------------------------------------------------- +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..348d57a92c --- /dev/null +++ b/uriloader/prefetch/moz.build @@ -0,0 +1,45 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsIOfflineCacheUpdate.idl', + 'nsIPrefetchService.idl', +] + +XPIDL_MODULE = 'prefetch' + +EXPORTS += [ + 'nsCPrefetchService.h', +] + +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['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/uriloader/prefetch/nsCPrefetchService.h b/uriloader/prefetch/nsCPrefetchService.h new file mode 100644 index 0000000000..d74d89fe7b --- /dev/null +++ b/uriloader/prefetch/nsCPrefetchService.h @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsCPrefetchService_h__ +#define nsCPrefetchService_h__ + +#include "nsIPrefetchService.h" + +/** + * nsPrefetchService : nsIPrefetchService + */ +#define NS_PREFETCHSERVICE_CONTRACTID \ + "@mozilla.org/prefetch-service;1" +#define NS_PREFETCHSERVICE_CID \ +{ /* 6b8bdffc-3394-417d-be83-a81b7c0f63bf */ \ + 0x6b8bdffc, \ + 0x3394, \ + 0x417d, \ + {0xbe, 0x83, 0xa8, 0x1b, 0x7c, 0x0f, 0x63, 0xbf} \ +} + +/** + * nsOfflineCacheUpdateService : nsIOfflineCacheUpdateService + */ + +#define NS_OFFLINECACHEUPDATESERVICE_CONTRACTID \ + "@mozilla.org/offlinecacheupdate-service;1" +#define NS_OFFLINECACHEUPDATESERVICE_CID \ +{ /* ec06f3fc-70db-4ecd-94e0-a6e91ca44d8a */ \ + 0xec06f3fc, \ + 0x70db, \ + 0x4ecd , \ + {0x94, 0xe0, 0xa6, 0xe9, 0x1c, 0xa4, 0x4d, 0x8a} \ +} + +/** + * nsOfflineCacheUpdate : nsIOfflineCacheUpdate + */ + +#define NS_OFFLINECACHEUPDATE_CONTRACTID \ + "@mozilla.org/offlinecacheupdate;1" +#define NS_OFFLINECACHEUPDATE_CID \ +{ /* e56f5e01-c7cc-4675-a9d7-b8f1e4127295 */ \ + 0xe56f5e01, \ + 0xc7cc, \ + 0x4675, \ + {0xa9, 0xd7, 0xb8, 0xf1, 0xe4, 0x12, 0x72, 0x95} \ +} + + +#endif // !nsCPrefetchService_h__ diff --git a/uriloader/prefetch/nsIOfflineCacheUpdate.idl b/uriloader/prefetch/nsIOfflineCacheUpdate.idl new file mode 100644 index 0000000000..1308a8de27 --- /dev/null +++ b/uriloader/prefetch/nsIOfflineCacheUpdate.idl @@ -0,0 +1,292 @@ +/* -*- 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 nsIDOMNode; +interface nsIDOMDocument; +interface nsIOfflineCacheUpdate; +interface nsIPrincipal; +interface nsIPrefBranch; +interface nsIApplicationCache; +interface nsIFile; +interface nsIObserver; + +[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 nsIDOMOfflineResourceList. + */ + 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; + + /** + * 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 nsIDOMDocument 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. + */ + void initPartial(in nsIURI aManifestURI, in ACString aClientID, + in nsIURI aDocumentURI, in nsIPrincipal aPrincipal); + + /** + * 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, + 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+inIsolatedMozBrowser 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 nsIDOMDocument 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. + * @param aPrefBranch + * The pref branch to use to check the + * offline-apps.allow_by_default pref. If not specified, + * the pref service will be used. + */ + boolean offlineAppAllowed(in nsIPrincipal aPrincipal, + in nsIPrefBranch aPrefBranch); + + /** + * Checks whether a document at the given URI should have access + * to the offline cache. + * @param aURI + * The URI to check + * @param aPrefBranch + * The pref branch to use to check the + * offline-apps.allow_by_default pref. If not specified, + * the pref service will be used. + */ + boolean offlineAppAllowedForURI(in nsIURI aURI, + in nsIPrefBranch aPrefBranch); + + /** + * 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..198320dd2d --- /dev/null +++ b/uriloader/prefetch/nsIPrefetchService.idl @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsIDOMNode; +interface nsISimpleEnumerator; + +[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 aReferrerURI the URI of the referring page + * @param aSource the DOM node (such as a 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 nsIURI aReferrerURI, + in nsIDOMNode aSource, + in boolean aExplicit); + + /** + * Find out if there are any prefetches running or queued + */ + boolean hasMoreElements(); + + /** + * Cancel prefetch + */ + void cancelPrefetchURI(in nsIURI aURI, in nsIDOMNode aSource); +}; diff --git a/uriloader/prefetch/nsOfflineCacheUpdate.cpp b/uriloader/prefetch/nsOfflineCacheUpdate.cpp new file mode 100644 index 0000000000..4b6cd4d0cf --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdate.cpp @@ -0,0 +1,2471 @@ +/* -*- 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 "nsOfflineCacheUpdate.h" + +#include "nsCPrefetchService.h" +#include "nsCURILoader.h" +#include "nsIApplicationCacheContainer.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheService.h" +#include "nsICachingChannel.h" +#include "nsIContent.h" +#include "mozilla/dom/Element.h" +#include "nsIDocumentLoader.h" +#include "nsIDOMElement.h" +#include "nsIDOMWindow.h" +#include "nsIDOMOfflineResourceList.h" +#include "nsIDocument.h" +#include "nsIObserverService.h" +#include "nsIURL.h" +#include "nsIWebProgress.h" +#include "nsICryptoHash.h" +#include "nsICacheEntry.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 "nsIConsoleService.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "nsContentUtils.h" +#include "nsIPrincipal.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) + +class AutoFreeArray { +public: + AutoFreeArray(uint32_t count, char **values) + : mCount(count), mValues(values) {}; + ~AutoFreeArray() { NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(mCount, mValues); } +private: + uint32_t mCount; + char **mValues; +}; + +namespace { + +nsresult +DropReferenceFromURL(nsIURI * aURI) +{ + // XXXdholbert If this SetRef fails, callers of this method probably + // want to call aURI->CloneIgnoringRef() and use the result of that. + return aURI->SetRef(EmptyCString()); +} + +void +LogToConsole(const char * message, nsOfflineCacheUpdateItem * item = nullptr) +{ + nsCOMPtr 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 mUpdate; + nsCOMPtr mURI; + nsCOMPtr mReferrerURI; + nsCOMPtr mLoadingPrincipal; + nsCOMPtr mManifestHash; + nsCOMPtr mChannel; +}; + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsISupports +//----------------------------------------------------------------------------- +NS_IMPL_ISUPPORTS(nsManifestCheck, + nsIRequestObserver, + nsIStreamListener, + nsIChannelEventSink, + nsIInterfaceRequestor) + +//----------------------------------------------------------------------------- +// nsManifestCheck +//----------------------------------------------------------------------------- + +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, + nullptr, // loadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_BYPASS_CACHE); + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr httpChannel = + do_QueryInterface(mChannel); + if (httpChannel) { + httpChannel->SetReferrer(mReferrerURI); + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("offline-resource"), + false); + } + + return mChannel->AsyncOpen2(this); +} + +//----------------------------------------------------------------------------- +// nsManifestCheck +//----------------------------------------------------------------------------- + +/* static */ nsresult +nsManifestCheck::ReadManifest(nsIInputStream *aInputStream, + void *aClosure, + const char *aFromSegment, + uint32_t aOffset, + uint32_t aCount, + uint32_t *aBytesConsumed) +{ + nsManifestCheck *manifestCheck = + static_cast(aClosure); + + nsresult rv; + *aBytesConsumed = aCount; + + rv = manifestCheck->mManifestHash->Update( + reinterpret_cast(aFromSegment), aCount); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsManifestCheck::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsManifestCheck::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + 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, + nsISupports *aContext, + 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(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 +//----------------------------------------------------------------------------- + +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_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // aLoadGroup + this, // aCallbacks + flags); + + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 httpChannel = + do_QueryInterface(mChannel); + if (httpChannel) { + httpChannel->SetReferrer(mReferrerURI); + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("offline-resource"), + false); + } + + rv = mChannel->AsyncOpen2(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, + nsISupports *aContext) +{ + mState = LoadStatus::RECEIVING; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + 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=%llu]\n", + bytesRead, aOffset)); + + mUpdate->OnByteProgress(bytesRead); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatus) +{ + if (LOG_ENABLED()) { + LOG(("%p: Done fetching offline item %s [status=%x]\n", + this, mURI->GetSpecOrDefault().get(), 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 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 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(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 newURI; + nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr appCacheChannel = + do_QueryInterface(aNewChannel); + if (appCacheChannel) { + rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoCString oldScheme; + mURI->GetScheme(oldScheme); + + bool match; + if (NS_FAILED(newURI->SchemeIs(oldScheme.get(), &match)) || !match) { + LOG(("rejected: redirected to a different scheme\n")); + return NS_ERROR_ABORT; + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("offline-resource"), + false); + + mChannel = aNewChannel; + + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +nsresult +nsOfflineCacheUpdateItem::GetRequestSucceeded(bool * succeeded) +{ + *succeeded = false; + + if (!mChannel) + return NS_OK; + + nsresult rv; + nsCOMPtr 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%08x", 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 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 +//----------------------------------------------------------------------------- + +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 +//----------------------------------------------------------------------------- + +/* static */ +nsresult +nsOfflineManifestItem::ReadManifest(nsIInputStream *aInputStream, + void *aClosure, + const char *aFromSegment, + uint32_t aOffset, + uint32_t aCount, + uint32_t *aBytesConsumed) +{ + nsOfflineManifestItem *manifest = + static_cast(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=%08x", rv)); + } + } + } + + if (manifest->mManifestHash) { + rv = manifest->mManifestHash->Update(reinterpret_cast(aFromSegment), aCount); + if (NS_FAILED(rv)) { + manifest->mManifestHash = nullptr; + LOG(("Could not update manifest hash, rv=%08x", 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%08x", 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 ns = + do_CreateInstance(NS_APPLICATIONCACHENAMESPACE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ns->Init(namespaceType, namespaceSpec, data); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mNamespaces->AppendElement(ns, false); + NS_ENSURE_SUCCESS(rv, rv); + + 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(*begin) == 0xef) { + if (++begin == end || static_cast(*begin) != 0xbb || + ++begin == end || static_cast(*begin) != 0xbf) { + mParserState = PARSE_ERROR; + LogToConsole("Offline cache manifest BOM error", this); + return NS_OK; + } + ++begin; + } + + const nsCSubstring &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 nsCSubstring &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 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. + bool match; + if (NS_FAILED(mURI->SchemeIs(scheme.get(), &match)) || !match) + 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 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 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; + + // 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, + EmptyCString(), EmptyCString()); + break; + } + + nsCOMPtr bypassURI; + rv = NS_NewURI(getter_AddRefs(bypassURI), line, nullptr, mURI); + if (NS_FAILED(rv)) + break; + + nsAutoCString scheme; + bypassURI->GetScheme(scheme); + bool equals; + if (NS_FAILED(mURI->SchemeIs(scheme.get(), &equals)) || !equals) + break; + if (NS_FAILED(DropReferenceFromURL(bypassURI))) + break; + nsCString spec; + if (NS_FAILED(bypassURI->GetAsciiSpec(spec))) + break; + + AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, + spec, EmptyCString()); + break; + } + } + + return NS_OK; +} + +nsresult +nsOfflineManifestItem::GetOldManifestContentHash(nsIRequest *aRequest) +{ + nsresult rv; + + nsCOMPtr 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 cacheToken; + cachingChannel->GetCacheToken(getter_AddRefs(cacheToken)); + if (cacheToken) { + nsCOMPtr 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=%08x", 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 cachingChannel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr cacheToken; + cachingChannel->GetOfflineCacheToken(getter_AddRefs(cacheToken)); + if (cacheToken) { + nsCOMPtr 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, + nsISupports *aContext) +{ + nsresult rv; + + nsCOMPtr 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, aContext); +} + +NS_IMETHODIMP +nsOfflineManifestItem::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + 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=%u]\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, + nsISupports *aContext, + 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, aContext, aStatus); +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdate, + nsIOfflineCacheUpdateObserver, + nsIOfflineCacheUpdate, + nsIRunnable) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate +//----------------------------------------------------------------------------- + +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 newURI; + nsresult rv = aURI->CloneIgnoringRef(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. + bool match; + rv = aManifestURI->SchemeIs("http", &match); + NS_ENSURE_SUCCESS(rv, rv); + + if (!match) { + rv = aManifestURI->SchemeIs("https", &match); + NS_ENSURE_SUCCESS(rv, rv); + if (!match) + 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, + nsIDOMDocument *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 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 (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, + nullptr, + &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 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, + nullptr, + &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) +{ + 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 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, + nullptr, + &mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + 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 &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 &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 &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 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 (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 algoritm 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 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 newUpdate = + new nsOfflineCacheUpdate(); + // Leave aDocument argument null. Only glues and children keep + // document instances. + newUpdate->Init(mManifestURI, mDocumentURI, mLoadingPrincipal, nullptr, + mCustomProfileDir); + + // 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 kungFuDeathGrip(this); + + mItemsInProgress = 0; + + if (mState == STATE_CANCELLED) { + nsresult rv = NS_DispatchToMainThread(NewRunnableMethod(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 +//----------------------------------------------------------------------------- + +nsresult +nsOfflineCacheUpdate::AddExistingItems(uint32_t aType, + nsTArray* 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; + } + + uint32_t count = 0; + char **keys = nullptr; + nsresult rv = mPreviousApplicationCache->GatherEntries(aType, + &count, &keys); + NS_ENSURE_SUCCESS(rv, rv); + + AutoFreeArray autoFree(count, keys); + + for (uint32_t i = 0; i < count; i++) { + if (namespaceFilter) { + bool found = false; + for (uint32_t j = 0; j < namespaceFilter->Length() && !found; j++) { + found = StringBeginsWith(nsDependentCString(keys[i]), + namespaceFilter->ElementAt(j)); + } + + if (!found) + continue; + } + + nsCOMPtr uri; + if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), keys[i]))) { + rv = AddURI(uri, aType); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::ProcessNextURI() +{ + // Keep the object alive through a Finish() call. + nsCOMPtr kungFuDeathGrip(this); + + LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p, inprogress=%d, numItems=%d]", + 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 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 &aObservers) +{ + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr 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 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 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 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 nsCSubstring &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 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 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); + 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 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(); + } + + 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, + uint32_t count, const char * const *groups) +{ + nsresult rv; + unsigned int i; + + for (i = 0; i < count; i++) { + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), groups[i]); + NS_ENSURE_SUCCESS(rv, rv); + + nsDependentCString group_name(groups[i]); + nsCOMPtr cache; + rv = cacheService->GetActiveCache(group_name, 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, + nullptr, + &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 cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t count; + char **groups; + rv = cacheService->GetGroupsTimeOrdered(&count, &groups); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EvictOneOfCacheGroups(cacheService, count, groups); + + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, groups); + return rv; +} + +//----------------------------------------------------------------------------- +// 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 = nsIDOMOfflineResourceList::CHECKING; + return NS_OK; + case STATE_DOWNLOADING : + *aStatus = nsIDOMOfflineResourceList::DOWNLOADING; + return NS_OK; + default : + *aStatus = nsIDOMOfflineResourceList::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::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); + + bool match; + if (NS_FAILED(mManifestURI->SchemeIs(scheme.get(), &match)) || !match) + 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 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) { + nsCOMPtr 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 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; +} + +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..4ccba41359 --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdate.h @@ -0,0 +1,381 @@ +/* -*- 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/. */ + +#ifndef nsOfflineCacheUpdate_h__ +#define nsOfflineCacheUpdate_h__ + +#include "nsIOfflineCacheUpdate.h" + +#include "nsAutoPtr.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIChannelEventSink.h" +#include "nsIDOMDocument.h" +#include "nsIDOMNode.h" +#include "nsIInterfaceRequestor.h" +#include "nsIMutableArray.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIApplicationCache.h" +#include "nsIRequestObserver.h" +#include "nsIRunnable.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsIWebProgressListener.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" + +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 mURI; + nsCOMPtr mReferrerURI; + nsCOMPtr mLoadingPrincipal; + nsCOMPtr mApplicationCache; + nsCOMPtr 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 mUpdate; + nsCOMPtr 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 &GetExplicitURIs() { return mExplicitURIs; } + nsCOMArray &GetAnonymousURIs() { return mAnonymousURIs; } + nsCOMArray &GetFallbackURIs() { return mFallbackURIs; } + + nsTArray &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 mExplicitURIs; + nsCOMArray mAnonymousURIs; + nsCOMArray mFallbackURIs; + + // All opportunistic caching namespaces. Used to decide whether + // to include previously-opportunistically-cached entries. + nsTArray mOpportunisticNamespaces; + + // Array of nsIApplicationCacheNamespace objects specified by the + // manifest. + nsCOMPtr mNamespaces; + + bool mNeedsUpdate; + bool mStrictFileOriginPolicy; + + // manifest hash data + nsCOMPtr mManifestHash; + bool mManifestHashInitialized; + nsCString mManifestHashValue; + nsCString mOldManifestHashValue; +}; + +class nsOfflineCacheUpdateOwner + : public mozilla::SupportsWeakPtr +{ +public: + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(nsOfflineCacheUpdateOwner) + 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 nsCSubstring &groupID); + bool IsForProfile(nsIFile* aCustomProfileDir); + + virtual nsresult UpdateFinished(nsOfflineCacheUpdate *aUpdate) override; + +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* namespaceFilter = nullptr); + nsresult ScheduleImplicit(); + void AssociateDocuments(nsIApplicationCache* cache); + bool CheckUpdateAvailability(); + void NotifyUpdateAvailability(bool updateAvailable); + + void GatherObservers(nsCOMArray &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 mOwner; + + bool mAddedItems; + bool mPartialUpdate; + bool mOnlyCheckUpdate; + bool mSucceeded; + bool mObsolete; + + nsCString mUpdateDomain; + nsCString mGroupID; + nsCOMPtr mManifestURI; + nsCOMPtr mDocumentURI; + nsCOMPtr mLoadingPrincipal; + nsCOMPtr mCustomProfileDir; + + nsCOMPtr mUpdateAvailableObserver; + + nsCOMPtr mApplicationCache; + nsCOMPtr mPreviousApplicationCache; + + nsCOMPtr mObserverService; + + RefPtr mManifestItem; + + /* Items being updated */ + uint32_t mItemsInProgress; + nsTArray > mItems; + + /* Clients watching this update for changes */ + nsCOMArray mWeakObservers; + nsCOMArray mObservers; + + /* Documents that requested this update */ + nsCOMArray 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 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, + nsIDOMDocument *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(); + + /** Addrefs and returns the singleton nsOfflineCacheUpdateService. */ + static nsOfflineCacheUpdateService *GetInstance(); + + static nsresult OfflineAppPinnedForURI(nsIURI *aDocumentURI, + nsIPrefBranch *aPrefBranch, + bool *aPinned); + + static nsTHashtable* AllowedDomains(); + +private: + ~nsOfflineCacheUpdateService(); + + nsresult ProcessNextUpdate(); + + nsTArray > mUpdates; + static nsTHashtable* mAllowedDomains; + + bool mDisabled; + bool mUpdateRunning; + bool mLowFreeSpace; +}; + +#endif diff --git a/uriloader/prefetch/nsOfflineCacheUpdateService.cpp b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp new file mode 100644 index 0000000000..adb3fd5160 --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp @@ -0,0 +1,736 @@ +/* -*- 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 "OfflineCacheUpdateChild.h" +#include "OfflineCacheUpdateParent.h" +#include "nsXULAppAPI.h" +#include "OfflineCacheUpdateGlue.h" +#include "nsOfflineCacheUpdate.h" + +#include "nsCPrefetchService.h" +#include "nsCURILoader.h" +#include "nsIApplicationCacheContainer.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheService.h" +#include "nsICachingChannel.h" +#include "nsIContent.h" +#include "nsIDocShell.h" +#include "nsIDocumentLoader.h" +#include "nsIDOMElement.h" +#include "nsIDOMWindow.h" +#include "nsIDOMOfflineResourceList.h" +#include "nsIDocument.h" +#include "nsIObserverService.h" +#include "nsIURL.h" +#include "nsIWebProgress.h" +#include "nsIWebNavigation.h" +#include "nsICryptoHash.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "mozilla/Unused.h" +#include "nsIDiskSpaceWatcher.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.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; +static bool sAllowOfflineCache = true; + +nsTHashtable* nsOfflineCacheUpdateService::mAllowedDomains = nullptr; + +nsTHashtable* nsOfflineCacheUpdateService::AllowedDomains() +{ + if (!mAllowedDomains) + mAllowedDomains = new nsTHashtable(); + + 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, + nsIDOMDocument *aDocument) + : mService(aService) + , mManifestURI(aManifestURI) + , mDocumentURI(aDocumentURI) + , mLoadingPrincipal(aLoadingPrincipal) + , mDidReleaseThis(false) + { + mDocument = do_GetWeakReference(aDocument); + } + +private: + ~nsOfflineCachePendingUpdate() {} + + RefPtr mService; + nsCOMPtr mManifestURI; + nsCOMPtr mDocumentURI; + nsCOMPtr mLoadingPrincipal; + nsCOMPtr 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) +{ + NS_NOTREACHED("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 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 windowProxy; + aWebProgress->GetDOMWindow(getter_AddRefs(windowProxy)); + if (!windowProxy) return NS_OK; + + auto* outerWindow = nsPIDOMWindowOuter::From(windowProxy); + nsPIDOMWindowInner* innerWindow = outerWindow->GetCurrentInnerWindow(); + + nsCOMPtr progressDoc = outerWindow->GetDoc(); + if (!progressDoc) return NS_OK; + + if (!SameCOMIdentity(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 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) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + uint32_t state) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateService, + nsIOfflineCacheUpdateService, + nsIObserver, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService +//----------------------------------------------------------------------------- + +nsOfflineCacheUpdateService::nsOfflineCacheUpdateService() + : mDisabled(false) + , mUpdateRunning(false) + , mLowFreeSpace(false) +{ + MOZ_ASSERT(NS_IsMainThread()); + Preferences::AddBoolVarCache(&sAllowOfflineCache, + "browser.cache.offline.enable", + true); +} + +nsOfflineCacheUpdateService::~nsOfflineCacheUpdateService() +{ + gOfflineCacheUpdateService = nullptr; +} + +nsresult +nsOfflineCacheUpdateService::Init() +{ + // Observe xpcom-shutdown event + nsCOMPtr 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); + + // Get the current status of the disk in terms of free space and observe + // low device storage notifications. + nsCOMPtr diskSpaceWatcherService = + do_GetService("@mozilla.org/toolkit/disk-space-watcher;1"); + if (diskSpaceWatcherService) { + diskSpaceWatcherService->GetIsDiskFull(&mLowFreeSpace); + } else { + NS_WARNING("Could not get disk status from nsIDiskSpaceWatcher"); + } + + rv = observerService->AddObserver(this, "disk-space-watcher", false); + NS_ENSURE_SUCCESS(rv, rv); + + gOfflineCacheUpdateService = this; + + return NS_OK; +} + +/* static */ +nsOfflineCacheUpdateService * +nsOfflineCacheUpdateService::GetInstance() +{ + if (!gOfflineCacheUpdateService) { + gOfflineCacheUpdateService = new nsOfflineCacheUpdateService(); + if (!gOfflineCacheUpdateService) + return nullptr; + NS_ADDREF(gOfflineCacheUpdateService); + nsresult rv = gOfflineCacheUpdateService->Init(); + if (NS_FAILED(rv)) { + NS_RELEASE(gOfflineCacheUpdateService); + return nullptr; + } + return gOfflineCacheUpdateService; + } + + NS_ADDREF(gOfflineCacheUpdateService); + + return gOfflineCacheUpdateService; +} + +/* static */ +nsOfflineCacheUpdateService * +nsOfflineCacheUpdateService::EnsureService() +{ + if (!gOfflineCacheUpdateService) { + // Make the service manager hold a long-lived reference to the service + nsCOMPtr service = + do_GetService(NS_OFFLINECACHEUPDATESERVICE_CONTRACTID); + } + + 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, + nsIDOMDocument *aDocument) +{ + LOG(("nsOfflineCacheUpdateService::ScheduleOnDocumentStop [%p, manifestURI=%p, documentURI=%p doc=%p]", + this, aManifestURI, aDocumentURI, aDocument)); + + nsCOMPtr doc = do_QueryInterface(aDocument); + nsCOMPtr progress = do_QueryInterface(doc->GetContainer()); + NS_ENSURE_TRUE(progress, NS_ERROR_INVALID_ARG); + + // Proceed with cache update + RefPtr 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 update = mUpdates[0]; + Unused << update; + mUpdates.RemoveElementAt(0); + mUpdateRunning = false; + + ProcessNextUpdate(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService +//----------------------------------------------------------------------------- + +nsresult +nsOfflineCacheUpdateService::ProcessNextUpdate() +{ + LOG(("nsOfflineCacheUpdateService::ProcessNextUpdate [%p, num=%d]", + this, mUpdates.Length())); + + if (mDisabled) + return NS_ERROR_ABORT; + + if (mUpdateRunning) + return NS_OK; + + if (mUpdates.Length() > 0) { + mUpdateRunning = true; + // Canceling the update before Begin() call will make the update + // asynchronously finish with an error. + if (mLowFreeSpace) { + mUpdates[0]->Cancel(); + } + 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 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 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, + nsIDOMDocument *aDocument, + nsPIDOMWindowInner* aWindow, + nsIFile* aCustomProfileDir, + nsIOfflineCacheUpdate **aUpdate) +{ + nsCOMPtr 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. + nsCOMPtr appCacheWindowObject = + 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 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; + } + + if (!strcmp(aTopic, "disk-space-watcher")) { + if (NS_LITERAL_STRING("full").Equals(aData)) { + mLowFreeSpace = true; + for (uint32_t i = 0; i < mUpdates.Length(); i++) { + mUpdates[i]->Cancel(); + } + } else if (NS_LITERAL_STRING("free").Equals(aData)) { + mLowFreeSpace = false; + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIOfflineCacheUpdateService +//----------------------------------------------------------------------------- + +static nsresult +OfflineAppPermForPrincipal(nsIPrincipal *aPrincipal, + nsIPrefBranch *aPrefBranch, + bool pinned, + bool *aAllowed) +{ + *aAllowed = false; + + if (!sAllowOfflineCache) { + return NS_OK; + } + + if (!aPrincipal) + return NS_ERROR_INVALID_ARG; + + nsCOMPtr uri; + aPrincipal->GetURI(getter_AddRefs(uri)); + + if (!uri) + return NS_OK; + + nsCOMPtr innerURI = NS_GetInnermostURI(uri); + if (!innerURI) + return NS_OK; + + // only http and https applications can use offline APIs. + bool match; + nsresult rv = innerURI->SchemeIs("http", &match); + NS_ENSURE_SUCCESS(rv, rv); + + if (!match) { + rv = innerURI->SchemeIs("https", &match); + NS_ENSURE_SUCCESS(rv, rv); + if (!match) { + return NS_OK; + } + } + + nsAutoCString domain; + rv = innerURI->GetAsciiHost(domain); + NS_ENSURE_SUCCESS(rv, rv); + + if (nsOfflineCacheUpdateService::AllowedDomains()->Contains(domain)) { + *aAllowed = true; + return NS_OK; + } + + nsCOMPtr permissionManager = + services::GetPermissionManager(); + if (!permissionManager) { + return NS_OK; + } + + uint32_t perm; + const char *permName = pinned ? "pin-app" : "offline-app"; + permissionManager->TestExactPermissionFromPrincipal(aPrincipal, permName, &perm); + + if (perm == nsIPermissionManager::ALLOW_ACTION || + perm == nsIOfflineCacheUpdateService::ALLOW_NO_WARN) { + *aAllowed = true; + } + + // offline-apps.allow_by_default is now effective at the cache selection + // algorithm code (nsContentSink). + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::OfflineAppAllowed(nsIPrincipal *aPrincipal, + nsIPrefBranch *aPrefBranch, + bool *aAllowed) +{ + return OfflineAppPermForPrincipal(aPrincipal, aPrefBranch, false, aAllowed); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::OfflineAppAllowedForURI(nsIURI *aURI, + nsIPrefBranch *aPrefBranch, + bool *aAllowed) +{ + PrincipalOriginAttributes attrs; + nsCOMPtr principal = + BasePrincipal::CreateCodebasePrincipal(aURI, attrs); + return OfflineAppPermForPrincipal(principal, aPrefBranch, false, aAllowed); +} + +nsresult +nsOfflineCacheUpdateService::OfflineAppPinnedForURI(nsIURI *aDocumentURI, + nsIPrefBranch *aPrefBranch, + bool *aPinned) +{ + PrincipalOriginAttributes attrs; + nsCOMPtr principal = + BasePrincipal::CreateCodebasePrincipal(aDocumentURI, attrs); + return OfflineAppPermForPrincipal(principal, aPrefBranch, true, aPinned); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::AllowOfflineApp(nsIPrincipal *aPrincipal) +{ + nsresult rv; + + if (!sAllowOfflineCache) { + 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 permissionManager = + services::GetPermissionManager(); + if (!permissionManager) + return NS_ERROR_NOT_AVAILABLE; + + rv = permissionManager->AddFromPrincipal( + aPrincipal, "offline-app", 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..bd2b10d304 --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.cpp @@ -0,0 +1,931 @@ +/* -*- 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 "nsICacheEntry.h" +#include "nsIServiceManager.h" +#include "nsICategoryManager.h" +#include "nsIObserverService.h" +#include "nsIWebProgress.h" +#include "nsCURILoader.h" +#include "nsICacheInfoChannel.h" +#include "nsIHttpChannel.h" +#include "nsIURL.h" +#include "nsISimpleEnumerator.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsXPIDLString.h" +#include "nsReadableUtils.h" +#include "nsStreamUtils.h" +#include "nsAutoPtr.h" +#include "prtime.h" +#include "mozilla/Logging.h" +#include "plstr.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "nsIDOMNode.h" +#include "nsINode.h" +#include "nsIDocument.h" +#include "nsContentUtils.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" + +//----------------------------------------------------------------------------- +// helpers +//----------------------------------------------------------------------------- + +static inline uint32_t +PRTimeToSeconds(PRTime t_usec) +{ + PRTime usec_per_sec = PR_USEC_PER_SEC; + return uint32_t(t_usec /= usec_per_sec); +} + +#define NowInSeconds() PRTimeToSeconds(PR_Now()) + +//----------------------------------------------------------------------------- +// nsPrefetchNode +//----------------------------------------------------------------------------- + +nsPrefetchNode::nsPrefetchNode(nsPrefetchService *aService, + nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource) + : mURI(aURI) + , mReferrerURI(aReferrerURI) + , mService(aService) + , mChannel(nullptr) + , mBytesRead(0) + , mShouldFireLoadEvent(false) +{ + nsCOMPtr 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 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 loadGroup = source->OwnerDoc()->GetDocumentLoadGroup(); + CORSMode corsMode = CORS_NONE; + net::ReferrerPolicy referrerPolicy = net::RP_Unset; + if (source->IsHTMLElement(nsGkAtoms::link)) { + dom::HTMLLinkElement* link = static_cast(source.get()); + corsMode = link->GetCORSMode(); + referrerPolicy = link->GetLinkReferrerPolicy(); + } + + if (referrerPolicy == net::RP_Unset) { + referrerPolicy = source->OwnerDoc()->GetReferrerPolicy(); + } + + uint32_t securityFlags; + if (corsMode == CORS_NONE) { + securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; + } else { + securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS; + if (corsMode == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + } + nsresult rv = NS_NewChannelInternal(getter_AddRefs(mChannel), + mURI, + source, + source->NodePrincipal(), + nullptr, //aTriggeringPrincipal + securityFlags, + nsIContentPolicy::TYPE_OTHER, + loadGroup, // aLoadGroup + this, // aCallbacks + nsIRequest::LOAD_BACKGROUND | + nsICachingChannel::LOAD_ONLY_IF_MODIFIED); + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr httpChannel = + do_QueryInterface(mChannel); + if (httpChannel) { + httpChannel->SetReferrerWithPolicy(mReferrerURI, referrerPolicy); + httpChannel->SetRequestHeader( + NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("prefetch"), + false); + } + + rv = mChannel->AsyncOpen2(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, + nsISupports *aContext) +{ + nsresult rv; + + nsCOMPtr 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 loadInfo = httpChannel->GetLoadInfo(); + 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 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, + nsISupports *aContext, + 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=%llu]\n", bytesRead, aOffset)); + return NS_OK; +} + + +NS_IMETHODIMP +nsPrefetchNode::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatus) +{ + LOG(("done prefetching [status=%x]\n", 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->ProcessNextURI(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(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + NS_ADDREF_THIS(); + *aResult = static_cast(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 newURI; + nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) + return rv; + + bool match; + rv = newURI->SchemeIs("http", &match); + if (NS_FAILED(rv) || !match) { + rv = newURI->SchemeIs("https", &match); + if (NS_FAILED(rv) || !match) { + 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 httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("prefetch"), + false); + + // 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 +//----------------------------------------------------------------------------- + +nsPrefetchService::nsPrefetchService() + : mMaxParallelism(6) + , mStopCount(0) + , mHaveProcessed(false) + , mDisabled(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) + EmptyQueue(); +} + +nsresult +nsPrefetchService::Init() +{ + nsresult rv; + + // read prefs and hook up pref observer + mDisabled = !Preferences::GetBool(PREFETCH_PREF, !mDisabled); + 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 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 (!mDisabled) + AddProgressListener(); + + return NS_OK; +} + +void +nsPrefetchService::ProcessNextURI(nsPrefetchNode *aFinished) +{ + nsresult rv; + + if (aFinished) { + mCurrentNodes.RemoveElement(aFinished); + } + + if (mCurrentNodes.Length() >= static_cast(mMaxParallelism)) { + // We already have enough prefetches going on, so hold off + // for now. + return; + } + + do { + if (mQueue.empty()) { + break; + } + RefPtr node = mQueue.front().forget(); + mQueue.pop_front(); + + if (LOG_ENABLED()) { + LOG(("ProcessNextURI [%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 observerService = + mozilla::services::GetObserverService(); + if (!observerService) + return; + + observerService->NotifyObservers(static_cast(node), + "prefetch-load-requested", nullptr); +} + +void +nsPrefetchService::NotifyLoadCompleted(nsPrefetchNode *node) +{ + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (!observerService) + return; + + observerService->NotifyObservers(static_cast(node), + "prefetch-load-completed", nullptr); +} + +void +nsPrefetchService::DispatchEvent(nsPrefetchNode *node, bool aSuccess) +{ + for (uint32_t i = 0; i < node->mSources.Length(); i++) { + nsCOMPtr domNode = do_QueryReferent(node->mSources.ElementAt(i)); + if (domNode && domNode->IsInComposedDoc()) { + nsContentUtils::DispatchTrustedEvent(domNode->OwnerDoc(), + domNode, + aSuccess ? + NS_LITERAL_STRING("load") : + NS_LITERAL_STRING("error"), + /* aCanBubble = */ false, + /* aCancelable = */ false); + } + } +} + +//----------------------------------------------------------------------------- +// nsPrefetchService +//----------------------------------------------------------------------------- + +void +nsPrefetchService::AddProgressListener() +{ + // Register as an observer for the document loader + nsCOMPtr progress = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); + if (progress) + progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); +} + +void +nsPrefetchService::RemoveProgressListener() +{ + // Register as an observer for the document loader + nsCOMPtr progress = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); + if (progress) + progress->RemoveProgressListener(this); +} + +nsresult +nsPrefetchService::EnqueueURI(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + nsPrefetchNode **aNode) +{ + RefPtr node = new nsPrefetchNode(this, aURI, aReferrerURI, + aSource); + mQueue.push_back(node); + node.forget(aNode); + return NS_OK; +} + +void +nsPrefetchService::EmptyQueue() +{ + while (!mQueue.empty()) { + mQueue.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 (!mQueue.empty() && mCurrentNodes.Length() < static_cast(mMaxParallelism)) { + ProcessNextURI(nullptr); + } + } +} + +void +nsPrefetchService::StopPrefetching() +{ + mStopCount++; + + LOG(("StopPrefetching [stopcount=%d]\n", mStopCount)); + + // only kill the prefetch queue if we are actively prefetching right now + if (mCurrentNodes.IsEmpty()) { + return; + } + + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + } + mCurrentNodes.Clear(); + EmptyQueue(); +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsPrefetchService, + nsIPrefetchService, + nsIWebProgressListener, + nsIObserver, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIPrefetchService +//----------------------------------------------------------------------------- + +nsresult +nsPrefetchService::Prefetch(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + bool aExplicit) +{ + nsresult rv; + + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aReferrerURI); + + if (LOG_ENABLED()) { + LOG(("PrefetchURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + if (mDisabled) { + LOG(("rejected: prefetch service is disabled\n")); + return NS_ERROR_ABORT; + } + + // + // 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 links since we know that's the + // most common case. ignore https links since https content only goes + // into the memory cache. + // + // XXX we might want to either leverage nsIProtocolHandler::protocolFlags + // or possibly nsIRequest::loadFlags to determine if this URI should be + // prefetched. + // + bool match; + rv = aURI->SchemeIs("http", &match); + if (NS_FAILED(rv) || !match) { + rv = aURI->SchemeIs("https", &match); + if (NS_FAILED(rv) || !match) { + LOG(("rejected: URL is not of type http/https\n")); + return NS_ERROR_ABORT; + } + } + + // + // the referrer URI must be http: + // + rv = aReferrerURI->SchemeIs("http", &match); + if (NS_FAILED(rv) || !match) { + rv = aReferrerURI->SchemeIs("https", &match); + if (NS_FAILED(rv) || !match) { + LOG(("rejected: referrer URL is neither http nor https\n")); + return NS_ERROR_ABORT; + } + } + + // skip URLs that contain query strings, except URLs for which prefetching + // has been explicitly requested. + if (!aExplicit) { + nsCOMPtr 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) { + nsCOMPtr 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>::iterator nodeIt = mQueue.begin(); + nodeIt != mQueue.end(); nodeIt++) { + bool equals; + RefPtr node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsCOMPtr 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 enqueuedNode; + rv = EnqueueURI(aURI, aReferrerURI, aSource, + getter_AddRefs(enqueuedNode)); + NS_ENSURE_SUCCESS(rv, rv); + + NotifyLoadRequested(enqueuedNode); + + // if there are no pages loading, kick off the request immediately + if (mStopCount == 0 && mHaveProcessed) { + ProcessNextURI(nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::CancelPrefetchURI(nsIURI* aURI, + nsIDOMNode* 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) { + nsCOMPtr 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>::iterator nodeIt = mQueue.begin(); + nodeIt != mQueue.end(); nodeIt++) { + bool equals; + RefPtr node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsCOMPtr source = do_GetWeakReference(aSource); + if (node->mSources.IndexOf(source) != + node->mSources.NoIndex) { + +#ifdef DEBUG + int32_t inx = node->mSources.IndexOf(source); + nsCOMPtr domNode = + do_QueryReferent(node->mSources.ElementAt(inx)); + MOZ_ASSERT(domNode); +#endif + + node->mSources.RemoveElement(source); + if (node->mSources.IsEmpty()) { + mQueue.erase(nodeIt); + } + return NS_OK; + } + return NS_ERROR_FAILURE; + } + } + + // not found! + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsPrefetchService::PrefetchURI(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + bool aExplicit) +{ + return Prefetch(aURI, aReferrerURI, aSource, aExplicit); +} + +NS_IMETHODIMP +nsPrefetchService::HasMoreElements(bool *aHasMore) +{ + *aHasMore = (mCurrentNodes.Length() || !mQueue.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) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest *aRequest, + uint32_t progressStateFlags, + nsresult aStatus) +{ + if (mAggressive) { + LOG(("Document load state is ignored in aggressive mode")); + return NS_OK; + } + + 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) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + uint32_t state) +{ + NS_NOTREACHED("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)) { + StopPrefetching(); + EmptyQueue(); + mDisabled = 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 (mDisabled) { + LOG(("enabling prefetching\n")); + mDisabled = false; + AddProgressListener(); + } + } else { + if (!mDisabled) { + LOG(("disabling prefetching\n")); + StopPrefetching(); + EmptyQueue(); + mDisabled = 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 (!mQueue.empty() && mCurrentNodes.Length() < static_cast(mMaxParallelism)) { + ProcessNextURI(nullptr); + } + } else if (!strcmp(pref, AGGRESSIVE_PREF)) { + mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false); + // in aggressive mode, clear stop count and start prefetching immediately + if (mAggressive) { + mStopCount = 0; + StartPrefetching(); + } + } + } + + return NS_OK; +} + +// vim: ts=4 sw=4 expandtab diff --git a/uriloader/prefetch/nsPrefetchService.h b/uriloader/prefetch/nsPrefetchService.h new file mode 100644 index 0000000000..883449e689 --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.h @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "nsCPrefetchService.h" +#include "nsIObserver.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" +#include "nsIRedirectResultListener.h" +#include "nsIWebProgressListener.h" +#include "nsIStreamListener.h" +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsWeakReference.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" +#include "mozilla/Attributes.h" +#include + +class nsPrefetchService; +class nsPrefetchNode; + +//----------------------------------------------------------------------------- +// 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 ProcessNextURI(nsPrefetchNode *aFinished); + + void NotifyLoadRequested(nsPrefetchNode *node); + void NotifyLoadCompleted(nsPrefetchNode *node); + void DispatchEvent(nsPrefetchNode *node, bool aSuccess); + +private: + ~nsPrefetchService(); + + nsresult Prefetch(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + bool aExplicit); + + void AddProgressListener(); + void RemoveProgressListener(); + nsresult EnqueueURI(nsIURI *aURI, nsIURI *aReferrerURI, + nsIDOMNode *aSource, nsPrefetchNode **node); + void EmptyQueue(); + + void StartPrefetching(); + void StopPrefetching(); + + std::deque> mQueue; + nsTArray> mCurrentNodes; + int32_t mMaxParallelism; + int32_t mStopCount; + // true if pending document loads have ever reached zero. + int32_t mHaveProcessed; + bool mDisabled; + + // 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; +}; + +//----------------------------------------------------------------------------- +// nsPrefetchNode +//----------------------------------------------------------------------------- + +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, + nsIURI *aReferrerURI, + nsIDOMNode *aSource); + + nsresult OpenChannel(); + nsresult CancelChannel(nsresult error); + + nsCOMPtr mURI; + nsCOMPtr mReferrerURI; + nsTArray> mSources; + +private: + ~nsPrefetchNode() {} + + RefPtr mService; + nsCOMPtr mChannel; + nsCOMPtr mRedirectChannel; + int64_t mBytesRead; + bool mShouldFireLoadEvent; +}; + +#endif // !nsPrefetchService_h__ -- cgit v1.2.3