diff options
Diffstat (limited to 'uriloader/exthandler')
140 files changed, 20954 insertions, 0 deletions
diff --git a/uriloader/exthandler/ContentHandlerService.cpp b/uriloader/exthandler/ContentHandlerService.cpp new file mode 100644 index 0000000000..96eb6a0cbb --- /dev/null +++ b/uriloader/exthandler/ContentHandlerService.cpp @@ -0,0 +1,248 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ContentHandlerService.h" +#include "HandlerServiceChild.h" +#include "ContentChild.h" +#include "nsIMutableArray.h" +#include "nsIMIMEInfo.h" +#include "nsIStringEnumerator.h" +#include "nsReadableUtils.h" + +using mozilla::dom::ContentChild; +using mozilla::dom::HandlerInfo; +using mozilla::dom::PHandlerServiceChild; + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(ContentHandlerService, nsIHandlerService) + +ContentHandlerService::ContentHandlerService() {} + +nsresult ContentHandlerService::Init() { + if (!XRE_IsContentProcess()) { + return NS_ERROR_FAILURE; + } + ContentChild* cpc = ContentChild::GetSingleton(); + + mHandlerServiceChild = new HandlerServiceChild(); + if (!cpc->SendPHandlerServiceConstructor(mHandlerServiceChild)) { + mHandlerServiceChild = nullptr; + } + return NS_OK; +} + +void ContentHandlerService::nsIHandlerInfoToHandlerInfo( + nsIHandlerInfo* aInfo, HandlerInfo* aHandlerInfo) { + nsCString type; + aInfo->GetType(type); + nsCOMPtr<nsIMIMEInfo> mimeInfo = do_QueryInterface(aInfo); + bool isMIMEInfo = !!mimeInfo; + nsString description; + aInfo->GetDescription(description); + bool alwaysAskBeforeHandling; + aInfo->GetAlwaysAskBeforeHandling(&alwaysAskBeforeHandling); + nsCOMPtr<nsIHandlerApp> app; + aInfo->GetPreferredApplicationHandler(getter_AddRefs(app)); + nsString name; + nsString detailedDescription; + if (app) { + app->GetName(name); + app->GetDetailedDescription(detailedDescription); + } + HandlerApp happ(name, detailedDescription); + nsTArray<HandlerApp> happs; + nsCOMPtr<nsIMutableArray> apps; + aInfo->GetPossibleApplicationHandlers(getter_AddRefs(apps)); + if (apps) { + unsigned int length; + apps->GetLength(&length); + for (unsigned int i = 0; i < length; i++) { + apps->QueryElementAt(i, NS_GET_IID(nsIHandlerApp), getter_AddRefs(app)); + app->GetName(name); + app->GetDetailedDescription(detailedDescription); + happs.AppendElement(HandlerApp(name, detailedDescription)); + } + } + + nsTArray<nsCString> extensions; + + if (isMIMEInfo) { + nsCOMPtr<nsIUTF8StringEnumerator> extensionsIter; + mimeInfo->GetFileExtensions(getter_AddRefs(extensionsIter)); + if (extensionsIter) { + bool hasMore = false; + while (NS_SUCCEEDED(extensionsIter->HasMore(&hasMore)) && hasMore) { + nsAutoCString extension; + if (NS_SUCCEEDED(extensionsIter->GetNext(extension))) { + extensions.AppendElement(std::move(extension)); + } + } + } + } + + nsHandlerInfoAction action; + aInfo->GetPreferredAction(&action); + HandlerInfo info(type, isMIMEInfo, description, alwaysAskBeforeHandling, + std::move(extensions), happ, happs, action); + *aHandlerInfo = info; +} + +NS_IMETHODIMP RemoteHandlerApp::GetName(nsAString& aName) { + aName.Assign(mAppChild.name()); + return NS_OK; +} + +NS_IMETHODIMP RemoteHandlerApp::SetName(const nsAString& aName) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP RemoteHandlerApp::GetDetailedDescription( + nsAString& aDetailedDescription) { + aDetailedDescription.Assign(mAppChild.detailedDescription()); + return NS_OK; +} + +NS_IMETHODIMP RemoteHandlerApp::SetDetailedDescription( + const nsAString& aDetailedDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP RemoteHandlerApp::Equals(nsIHandlerApp* aHandlerApp, + bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP RemoteHandlerApp::LaunchWithURI( + nsIURI* aURI, BrowsingContext* aBrowsingContext) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMPL_ISUPPORTS(RemoteHandlerApp, nsIHandlerApp) + +static inline void CopyHanderInfoTonsIHandlerInfo( + const HandlerInfo& info, nsIHandlerInfo* aHandlerInfo) { + HandlerApp preferredApplicationHandler = info.preferredApplicationHandler(); + nsCOMPtr<nsIHandlerApp> preferredApp( + new RemoteHandlerApp(preferredApplicationHandler)); + aHandlerInfo->SetPreferredApplicationHandler(preferredApp); + nsCOMPtr<nsIMutableArray> possibleHandlers; + aHandlerInfo->GetPossibleApplicationHandlers( + getter_AddRefs(possibleHandlers)); + possibleHandlers->AppendElement(preferredApp); + + if (info.isMIMEInfo()) { + nsCOMPtr<nsIMIMEInfo> mimeInfo(do_QueryInterface(aHandlerInfo)); + MOZ_ASSERT(mimeInfo, + "parent and child don't agree on whether this is a MIME info"); + mimeInfo->SetFileExtensions(StringJoin(","_ns, info.extensions())); + } +} + +ContentHandlerService::~ContentHandlerService() {} + +NS_IMETHODIMP ContentHandlerService::AsyncInit() { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ContentHandlerService::Enumerate(nsISimpleEnumerator** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ContentHandlerService::FillHandlerInfo( + nsIHandlerInfo* aHandlerInfo, const nsACString& aOverrideType) { + HandlerInfo info, returnedInfo; + nsIHandlerInfoToHandlerInfo(aHandlerInfo, &info); + mHandlerServiceChild->SendFillHandlerInfo(info, nsCString(aOverrideType), + &returnedInfo); + CopyHanderInfoTonsIHandlerInfo(returnedInfo, aHandlerInfo); + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::GetMIMEInfoFromOS( + nsIHandlerInfo* aHandlerInfo, const nsACString& aMIMEType, + const nsACString& aExtension, bool* aFound) { + nsresult rv = NS_ERROR_FAILURE; + HandlerInfo returnedInfo; + if (!mHandlerServiceChild->SendGetMIMEInfoFromOS(nsCString(aMIMEType), + nsCString(aExtension), &rv, + &returnedInfo, aFound)) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + CopyHanderInfoTonsIHandlerInfo(returnedInfo, aHandlerInfo); + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::Store(nsIHandlerInfo* aHandlerInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ContentHandlerService::Exists(nsIHandlerInfo* aHandlerInfo, + bool* _retval) { + HandlerInfo info; + nsIHandlerInfoToHandlerInfo(aHandlerInfo, &info); + mHandlerServiceChild->SendExists(info, _retval); + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::Remove(nsIHandlerInfo* aHandlerInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ContentHandlerService::ExistsForProtocolOS(const nsACString& aProtocolScheme, + bool* aRetval) { + if (!mHandlerServiceChild->SendExistsForProtocolOS(nsCString(aProtocolScheme), + aRetval)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +ContentHandlerService::ExistsForProtocol(const nsACString& aProtocolScheme, + bool* aRetval) { + if (!mHandlerServiceChild->SendExistsForProtocol(nsCString(aProtocolScheme), + aRetval)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::GetTypeFromExtension( + const nsACString& aFileExtension, nsACString& _retval) { + nsCString* cachedType = nullptr; + if (!!mExtToTypeMap.Get(aFileExtension, &cachedType) && !!cachedType) { + _retval.Assign(*cachedType); + return NS_OK; + } + nsCString type; + mHandlerServiceChild->SendGetTypeFromExtension(nsCString(aFileExtension), + &type); + _retval.Assign(type); + mExtToTypeMap.Put(nsCString(aFileExtension), new nsCString(type)); + + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::GetApplicationDescription( + const nsACString& aProtocolScheme, nsAString& aRetVal) { + nsresult rv = NS_ERROR_FAILURE; + nsAutoCString scheme(aProtocolScheme); + nsAutoString desc; + mHandlerServiceChild->SendGetApplicationDescription(scheme, &rv, &desc); + aRetVal.Assign(desc); + return rv; +} + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/ContentHandlerService.h b/uriloader/exthandler/ContentHandlerService.h new file mode 100644 index 0000000000..81d0c44148 --- /dev/null +++ b/uriloader/exthandler/ContentHandlerService.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ContentHandlerService_h +#define ContentHandlerService_h + +#include "mozilla/dom/PHandlerService.h" +#include "nsIHandlerService.h" +#include "nsClassHashtable.h" +#include "nsIMIMEInfo.h" + +namespace mozilla { + +class HandlerServiceChild; + +namespace dom { + +class PHandlerServiceChild; + +class ContentHandlerService : public nsIHandlerService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERSERVICE + + ContentHandlerService(); + [[nodiscard]] nsresult Init(); + static void nsIHandlerInfoToHandlerInfo(nsIHandlerInfo* aInfo, + HandlerInfo* aHandlerInfo); + + private: + virtual ~ContentHandlerService(); + RefPtr<HandlerServiceChild> mHandlerServiceChild; + nsClassHashtable<nsCStringHashKey, nsCString> mExtToTypeMap; +}; + +class RemoteHandlerApp : public nsIHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + + explicit RemoteHandlerApp(HandlerApp aAppChild) : mAppChild(aAppChild) {} + + private: + virtual ~RemoteHandlerApp() {} + HandlerApp mAppChild; +}; + +} // namespace dom +} // namespace mozilla +#endif diff --git a/uriloader/exthandler/DBusHelpers.h b/uriloader/exthandler/DBusHelpers.h new file mode 100644 index 0000000000..4f4f64309d --- /dev/null +++ b/uriloader/exthandler/DBusHelpers.h @@ -0,0 +1,84 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_DBusHelpers_h +#define mozilla_DBusHelpers_h + +#include <dbus/dbus.h> +#include "mozilla/UniquePtr.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { + +template <> +struct RefPtrTraits<DBusMessage> { + static void AddRef(DBusMessage* aMessage) { + MOZ_ASSERT(aMessage); + dbus_message_ref(aMessage); + } + static void Release(DBusMessage* aMessage) { + MOZ_ASSERT(aMessage); + dbus_message_unref(aMessage); + } +}; + +template <> +struct RefPtrTraits<DBusPendingCall> { + static void AddRef(DBusPendingCall* aPendingCall) { + MOZ_ASSERT(aPendingCall); + dbus_pending_call_ref(aPendingCall); + } + static void Release(DBusPendingCall* aPendingCall) { + MOZ_ASSERT(aPendingCall); + dbus_pending_call_unref(aPendingCall); + } +}; + +/* + * |RefPtrTraits<DBusConnection>| specializes |RefPtrTraits<>| + * for managing |DBusConnection| with |RefPtr|. + * + * |RefPtrTraits<DBusConnection>| will _not_ close the DBus + * connection upon the final unref. The caller is responsible + * for closing the connection. + */ +template <> +struct RefPtrTraits<DBusConnection> { + static void AddRef(DBusConnection* aConnection) { + MOZ_ASSERT(aConnection); + dbus_connection_ref(aConnection); + } + static void Release(DBusConnection* aConnection) { + MOZ_ASSERT(aConnection); + dbus_connection_unref(aConnection); + } +}; + +/* + * |DBusConnectionDelete| is a deleter for managing instances + * of |DBusConnection| in |UniquePtr|. Upon destruction, it + * will close an open connection before unref'ing the data + * structure. + * + * Do not use |UniquePtr| with shared DBus connections. For + * shared connections, use |RefPtr|. + */ +class DBusConnectionDelete { + public: + constexpr DBusConnectionDelete() {} + + void operator()(DBusConnection* aConnection) const { + MOZ_ASSERT(aConnection); + if (dbus_connection_get_is_connected(aConnection)) { + dbus_connection_close(aConnection); + } + dbus_connection_unref(aConnection); + } +}; + +} // namespace mozilla + +#endif // mozilla_DBusHelpers_h diff --git a/uriloader/exthandler/ExternalHelperAppChild.cpp b/uriloader/exthandler/ExternalHelperAppChild.cpp new file mode 100644 index 0000000000..569d42e7a4 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppChild.cpp @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ExternalHelperAppChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "nsIInputStream.h" +#include "nsIRequest.h" +#include "nsIResumableChannel.h" +#include "nsIPropertyBag2.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(ExternalHelperAppChild, nsIStreamListener, nsIRequestObserver) + +ExternalHelperAppChild::ExternalHelperAppChild() : mStatus(NS_OK) {} + +ExternalHelperAppChild::~ExternalHelperAppChild() {} + +//----------------------------------------------------------------------------- +// nsIStreamListener +//----------------------------------------------------------------------------- +NS_IMETHODIMP +ExternalHelperAppChild::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, uint64_t offset, + uint32_t count) { + if (NS_FAILED(mStatus)) return mStatus; + + static uint32_t const kCopyChunkSize = 128 * 1024; + uint32_t toRead = std::min<uint32_t>(count, kCopyChunkSize); + + nsCString data; + + while (count) { + nsresult rv = NS_ReadInputStreamToString(input, data, toRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!SendOnDataAvailable(data, offset, toRead))) { + return NS_ERROR_UNEXPECTED; + } + + count -= toRead; + offset += toRead; + toRead = std::min<uint32_t>(count, kCopyChunkSize); + } + + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////// +// nsIRequestObserver +////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +ExternalHelperAppChild::OnStartRequest(nsIRequest* request) { + nsresult rv = mHandler->OnStartRequest(request); + NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); + + nsCString entityID; + nsCOMPtr<nsIResumableChannel> resumable(do_QueryInterface(request)); + if (resumable) { + resumable->GetEntityID(entityID); + } + SendOnStartRequest(entityID); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppChild::OnStopRequest(nsIRequest* request, nsresult status) { + // mHandler can be null if we diverted the request to the parent + if (mHandler) { + nsresult rv = mHandler->OnStopRequest(request, status); + SendOnStopRequest(status); + NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); + } + + return NS_OK; +} + +mozilla::ipc::IPCResult ExternalHelperAppChild::RecvCancel( + const nsresult& aStatus) { + mStatus = aStatus; + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/ExternalHelperAppChild.h b/uriloader/exthandler/ExternalHelperAppChild.h new file mode 100644 index 0000000000..228d7e45b3 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppChild.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ExternalHelperAppChild_h +#define mozilla_dom_ExternalHelperAppChild_h + +#include "mozilla/dom/PExternalHelperAppChild.h" +#include "nsExternalHelperAppService.h" +#include "nsIStreamListener.h" + +class nsIDivertableChannel; + +namespace mozilla { +namespace dom { + +class BrowserChild; + +class ExternalHelperAppChild : public PExternalHelperAppChild, + public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + ExternalHelperAppChild(); + + // Give the listener a real nsExternalAppHandler to complete processing on + // the child. + void SetHandler(nsExternalAppHandler* handler) { mHandler = handler; } + + mozilla::ipc::IPCResult RecvCancel(const nsresult& aStatus); + + private: + virtual ~ExternalHelperAppChild(); + + RefPtr<nsExternalAppHandler> mHandler; + nsresult mStatus; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ExternalHelperAppChild_h diff --git a/uriloader/exthandler/ExternalHelperAppParent.cpp b/uriloader/exthandler/ExternalHelperAppParent.cpp new file mode 100644 index 0000000000..1a88e54df7 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppParent.cpp @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/DebugOnly.h" + +#include "ExternalHelperAppParent.h" +#include "nsIContent.h" +#include "nsCExternalHandlerService.h" +#include "nsIExternalHelperAppService.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/BrowserParent.h" +#include "nsStringStream.h" +#include "mozilla/ipc/URIUtils.h" +#include "nsNetUtil.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "nsQueryObject.h" + +#include "mozilla/Unused.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS_INHERITED(ExternalHelperAppParent, nsHashPropertyBag, + nsIRequest, nsIChannel, nsIMultiPartChannel, + nsIPrivateBrowsingChannel, nsIResumableChannel, + nsIStreamListener, nsIExternalHelperAppParent) + +ExternalHelperAppParent::ExternalHelperAppParent( + nsIURI* uri, const int64_t& aContentLength, const bool& aWasFileChannel, + const nsCString& aContentDispositionHeader, + const uint32_t& aContentDispositionHint, + const nsString& aContentDispositionFilename) + : mURI(uri), + mPending(false), + mIPCClosed(false), + mLoadFlags(0), + mStatus(NS_OK), + mCanceled(false), + mContentLength(aContentLength), + mWasFileChannel(aWasFileChannel) { + mContentDispositionHeader = aContentDispositionHeader; + if (!mContentDispositionHeader.IsEmpty()) { + NS_GetFilenameFromDisposition(mContentDispositionFilename, + mContentDispositionHeader); + mContentDisposition = + NS_GetContentDispositionFromHeader(mContentDispositionHeader, this); + } else { + mContentDisposition = aContentDispositionHint; + mContentDispositionFilename = aContentDispositionFilename; + } +} + +void ExternalHelperAppParent::Init( + const Maybe<mozilla::net::LoadInfoArgs>& aLoadInfoArgs, + const nsCString& aMimeContentType, const bool& aForceSave, + nsIURI* aReferrer, BrowsingContext* aContext, + const bool& aShouldCloseWindow) { + mozilla::ipc::LoadInfoArgsToLoadInfo(aLoadInfoArgs, + getter_AddRefs(mLoadInfo)); + + nsCOMPtr<nsIExternalHelperAppService> helperAppService = + do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID); + NS_ASSERTION(helperAppService, "No Helper App Service!"); + + if (aReferrer) { + SetPropertyAsInterface(u"docshell.internalReferrer"_ns, aReferrer); + } + + if (aContext) { + WindowGlobalParent* parent = + aContext->Canonical()->GetCurrentWindowGlobal(); + if (parent) { + RefPtr<BrowserParent> browser = parent->GetBrowserParent(); + if (browser) { + bool isPrivate = false; + nsCOMPtr<nsILoadContext> loadContext = browser->GetLoadContext(); + loadContext->GetUsePrivateBrowsing(&isPrivate); + SetPrivate(isPrivate); + } + } + } + + helperAppService->CreateListener(aMimeContentType, this, aContext, aForceSave, + nullptr, getter_AddRefs(mListener)); + if (aShouldCloseWindow) { + RefPtr<nsExternalAppHandler> handler = do_QueryObject(mListener); + if (handler) { + handler->SetShouldCloseWindow(); + } + } +} + +void ExternalHelperAppParent::ActorDestroy(ActorDestroyReason why) { + mIPCClosed = true; +} + +void ExternalHelperAppParent::Delete() { + if (!mIPCClosed) { + Unused << Send__delete__(this); + } +} + +mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnStartRequest( + const nsCString& entityID) { + mEntityID = entityID; + mPending = true; + mStatus = mListener->OnStartRequest(this); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnDataAvailable( + const nsCString& data, const uint64_t& offset, const uint32_t& count) { + if (NS_FAILED(mStatus)) { + return IPC_OK(); + } + + MOZ_ASSERT(mPending, "must be pending!"); + + nsCOMPtr<nsIInputStream> stringStream; + DebugOnly<nsresult> rv = NS_NewByteInputStream( + getter_AddRefs(stringStream), Span(data).To(count), NS_ASSIGNMENT_DEPEND); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create dependent string!"); + mStatus = mListener->OnDataAvailable(this, stringStream, offset, count); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnStopRequest( + const nsresult& code) { + mPending = false; + mListener->OnStopRequest( + this, (NS_SUCCEEDED(code) && NS_FAILED(mStatus)) ? mStatus : code); + Delete(); + return IPC_OK(); +} + +// +// nsIStreamListener +// + +NS_IMETHODIMP +ExternalHelperAppParent::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, uint64_t offset, + uint32_t count) { + return mListener->OnDataAvailable(request, input, offset, count); +} + +NS_IMETHODIMP +ExternalHelperAppParent::OnStartRequest(nsIRequest* request) { + return mListener->OnStartRequest(request); +} + +NS_IMETHODIMP +ExternalHelperAppParent::OnStopRequest(nsIRequest* request, nsresult status) { + nsresult rv = mListener->OnStopRequest(request, status); + Delete(); + return rv; +} + +ExternalHelperAppParent::~ExternalHelperAppParent() {} + +// +// nsIRequest implementation... +// + +NS_IMETHODIMP +ExternalHelperAppParent::GetName(nsACString& aResult) { + if (!mURI) { + aResult.Truncate(); + return NS_ERROR_NOT_AVAILABLE; + } + mURI->GetAsciiSpec(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::IsPending(bool* aResult) { + *aResult = mPending; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetStatus(nsresult* aResult) { + *aResult = mStatus; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::Cancel(nsresult aStatus) { + mCanceled = true; + mStatus = aStatus; + Unused << SendCancel(aStatus); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::Suspend() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +ExternalHelperAppParent::Resume() { return NS_ERROR_NOT_IMPLEMENTED; } + +// +// nsIChannel implementation +// + +NS_IMETHODIMP +ExternalHelperAppParent::GetOriginalURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetOriginalURI(nsIURI* aURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::Open(nsIInputStream** aResult) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::AsyncOpen(nsIStreamListener* aListener) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetLoadGroup(nsILoadGroup** aLoadGroup) { + *aLoadGroup = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetOwner(nsISupports** aOwner) { + *aOwner = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetOwner(nsISupports* aOwner) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetLoadInfo(nsILoadInfo* aLoadInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + *aCallbacks = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetSecurityInfo(nsISupports** aSecurityInfo) { + *aSecurityInfo = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentType(nsACString& aContentType) { + aContentType.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentType(const nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentCharset(nsACString& aContentCharset) { + aContentCharset.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentCharset(const nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentDisposition(uint32_t* aContentDisposition) { + // NB: mContentDisposition may or may not be set to a non UINT32_MAX value in + // nsExternalHelperAppService::DoContentContentProcessHelper + if (mContentDispositionHeader.IsEmpty() && mContentDisposition == UINT32_MAX) + return NS_ERROR_NOT_AVAILABLE; + + *aContentDisposition = mContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentDisposition(uint32_t aContentDisposition) { + mContentDisposition = aContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + if (mContentDispositionFilename.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + aContentDispositionFilename = mContentDispositionFilename; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + mContentDispositionFilename = aContentDispositionFilename; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + if (mContentDispositionHeader.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + aContentDispositionHeader = mContentDispositionHeader; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentLength(int64_t* aContentLength) { + if (mContentLength < 0) { + *aContentLength = -1; + } else { + *aContentLength = mContentLength; + } + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentLength(int64_t aContentLength) { + mContentLength = aContentLength; + return NS_OK; +} + +// +// nsIResumableChannel implementation +// + +NS_IMETHODIMP +ExternalHelperAppParent::ResumeAt(uint64_t startPos, + const nsACString& entityID) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetEntityID(nsACString& aEntityID) { + aEntityID = mEntityID; + return NS_OK; +} + +// +// nsIMultiPartChannel implementation +// + +NS_IMETHODIMP +ExternalHelperAppParent::GetBaseChannel(nsIChannel** aChannel) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetPartID(uint32_t* aPartID) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetIsLastPart(bool* aIsLastPart) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/ExternalHelperAppParent.h b/uriloader/exthandler/ExternalHelperAppParent.h new file mode 100644 index 0000000000..71ac04cee8 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppParent.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/PExternalHelperAppParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "nsIChannel.h" +#include "nsIMultiPartChannel.h" +#include "nsIResumableChannel.h" +#include "nsIStreamListener.h" +#include "nsHashPropertyBag.h" +#include "mozilla/net/PrivateBrowsingChannel.h" + +namespace IPC { +class URI; +} // namespace IPC + +class nsExternalAppHandler; + +namespace mozilla { + +namespace net { +class PChannelDiverterParent; +} // namespace net + +namespace dom { + +#define NS_IEXTERNALHELPERAPPPARENT_IID \ + { \ + 0x127a01bc, 0x2a49, 0x46a8, { \ + 0x8c, 0x63, 0x4b, 0x5d, 0x3c, 0xa4, 0x07, 0x9c \ + } \ + } + +class nsIExternalHelperAppParent : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IEXTERNALHELPERAPPPARENT_IID) + + /** + * Returns true if this fake channel represented a file channel in the child. + */ + virtual bool WasFileChannel() = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIExternalHelperAppParent, + NS_IEXTERNALHELPERAPPPARENT_IID) + +class ContentParent; +class PBrowserParent; + +class ExternalHelperAppParent + : public PExternalHelperAppParent, + public nsHashPropertyBag, + public nsIChannel, + public nsIMultiPartChannel, + public nsIResumableChannel, + public nsIStreamListener, + public net::PrivateBrowsingChannel<ExternalHelperAppParent>, + public nsIExternalHelperAppParent { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIMULTIPARTCHANNEL + NS_DECL_NSIRESUMABLECHANNEL + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + mozilla::ipc::IPCResult RecvOnStartRequest( + const nsCString& entityID) override; + mozilla::ipc::IPCResult RecvOnDataAvailable(const nsCString& data, + const uint64_t& offset, + const uint32_t& count) override; + mozilla::ipc::IPCResult RecvOnStopRequest(const nsresult& code) override; + + bool WasFileChannel() override { return mWasFileChannel; } + + ExternalHelperAppParent(nsIURI* uri, const int64_t& contentLength, + const bool& wasFileChannel, + const nsCString& aContentDispositionHeader, + const uint32_t& aContentDispositionHint, + const nsString& aContentDispositionFilename); + void Init(const Maybe<mozilla::net::LoadInfoArgs>& aLoadInfoArgs, + const nsCString& aMimeContentType, const bool& aForceSave, + nsIURI* aReferrer, BrowsingContext* aContext, + const bool& aShouldCloseWindow); + + protected: + virtual ~ExternalHelperAppParent(); + + virtual void ActorDestroy(ActorDestroyReason why) override; + void Delete(); + + private: + RefPtr<nsIStreamListener> mListener; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsILoadInfo> mLoadInfo; + bool mPending; + bool mIPCClosed; + nsLoadFlags mLoadFlags; + nsresult mStatus; + bool mCanceled; + int64_t mContentLength; + bool mWasFileChannel; + uint32_t mContentDisposition; + nsString mContentDispositionFilename; + nsCString mContentDispositionHeader; + nsCString mEntityID; +}; + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/HandlerService.js b/uriloader/exthandler/HandlerService.js new file mode 100644 index 0000000000..085ddab31c --- /dev/null +++ b/uriloader/exthandler/HandlerService.js @@ -0,0 +1,675 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged"; + +ChromeUtils.defineModuleGetter( + this, + "FileUtils", + "resource://gre/modules/FileUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "JSONFile", + "resource://gre/modules/JSONFile.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); + +function HandlerService() { + // Observe handlersvc-json-replace so we can switch to the datasource + Services.obs.addObserver(this, "handlersvc-json-replace", true); +} + +HandlerService.prototype = { + classID: Components.ID("{220cc253-b60f-41f6-b9cf-fdcb325f970f}"), + QueryInterface: ChromeUtils.generateQI([ + "nsISupportsWeakReference", + "nsIHandlerService", + "nsIObserver", + ]), + + __store: null, + get _store() { + if (!this.__store) { + this.__store = new JSONFile({ + path: PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "handlers.json" + ), + dataPostProcessor: this._dataPostProcessor.bind(this), + }); + } + + // Always call this even if this.__store was set, since it may have been + // set by asyncInit, which might not have completed yet. + this._ensureStoreInitialized(); + return this.__store; + }, + + __storeInitialized: false, + _ensureStoreInitialized() { + if (!this.__storeInitialized) { + this.__storeInitialized = true; + this.__store.ensureDataReady(); + + this._injectDefaultProtocolHandlersIfNeeded(); + this._migrateProtocolHandlersIfNeeded(); + + Services.obs.notifyObservers(null, "handlersvc-store-initialized"); + } + }, + + _dataPostProcessor(data) { + return data.defaultHandlersVersion + ? data + : { + defaultHandlersVersion: {}, + mimeTypes: {}, + schemes: {}, + }; + }, + + /** + * Injects new default protocol handlers if the version in the preferences is + * newer than the one in the data store. + */ + _injectDefaultProtocolHandlersIfNeeded() { + let prefsDefaultHandlersVersion; + try { + prefsDefaultHandlersVersion = Services.prefs.getComplexValue( + "gecko.handlerService.defaultHandlersVersion", + Ci.nsIPrefLocalizedString + ); + } catch (ex) { + if ( + ex instanceof Components.Exception && + ex.result == Cr.NS_ERROR_UNEXPECTED + ) { + // This platform does not have any default protocol handlers configured. + return; + } + throw ex; + } + + try { + prefsDefaultHandlersVersion = Number(prefsDefaultHandlersVersion.data); + let locale = Services.locale.appLocaleAsBCP47; + + let defaultHandlersVersion = + this._store.data.defaultHandlersVersion[locale] || 0; + if (defaultHandlersVersion < prefsDefaultHandlersVersion) { + this._injectDefaultProtocolHandlers(); + this._store.data.defaultHandlersVersion[ + locale + ] = prefsDefaultHandlersVersion; + // Now save the result: + this._store.saveSoon(); + } + } catch (ex) { + Cu.reportError(ex); + } + }, + + _injectDefaultProtocolHandlers() { + let schemesPrefBranch = Services.prefs.getBranch( + "gecko.handlerService.schemes." + ); + let schemePrefList = schemesPrefBranch.getChildList(""); + + let schemes = {}; + + // read all the scheme prefs into a hash + for (let schemePrefName of schemePrefList) { + let [scheme, handlerNumber, attribute] = schemePrefName.split("."); + + try { + let attrData = schemesPrefBranch.getComplexValue( + schemePrefName, + Ci.nsIPrefLocalizedString + ).data; + if (!(scheme in schemes)) { + schemes[scheme] = {}; + } + + if (!(handlerNumber in schemes[scheme])) { + schemes[scheme][handlerNumber] = {}; + } + + schemes[scheme][handlerNumber][attribute] = attrData; + } catch (ex) {} + } + + // Now drop any entries without a uriTemplate, or with a broken one. + // The Array.from calls ensure we can safely delete things without + // affecting the iterator. + for (let [scheme, handlerObject] of Array.from(Object.entries(schemes))) { + let handlers = Array.from(Object.entries(handlerObject)); + let validHandlers = 0; + for (let [key, obj] of handlers) { + if ( + !obj.uriTemplate || + !obj.uriTemplate.startsWith("https://") || + !obj.uriTemplate.toLowerCase().includes("%s") + ) { + delete handlerObject[key]; + } else { + validHandlers++; + } + } + if (!validHandlers) { + delete schemes[scheme]; + } + } + + // Now, we're going to cheat. Terribly. The idiologically correct way + // of implementing the following bit of code would be to fetch the + // handler info objects from the protocol service, manipulate those, + // and then store each of them. + // However, that's expensive. It causes us to talk to the OS about + // default apps, which causes the OS to go hit the disk. + // All we're trying to do is insert some web apps into the list. We + // don't care what's already in the file, we just want to do the + // equivalent of appending into the database. So let's just go do that: + for (let scheme of Object.keys(schemes)) { + let existingSchemeInfo = this._store.data.schemes[scheme]; + if (!existingSchemeInfo) { + // Haven't seen this scheme before. Default to asking which app the + // user wants to use: + existingSchemeInfo = { + // Signal to future readers that we didn't ask the OS anything. + // When the entry is first used, get the info from the OS. + stubEntry: true, + // The first item in the list is the preferred handler, and + // there isn't one, so we fill in null: + handlers: [null], + }; + this._store.data.schemes[scheme] = existingSchemeInfo; + } + let { handlers } = existingSchemeInfo; + for (let handlerNumber of Object.keys(schemes[scheme])) { + let newHandler = schemes[scheme][handlerNumber]; + // If there is already a handler registered with the same template + // URL, ignore the new one: + let matchingTemplate = handler => + handler && handler.uriTemplate == newHandler.uriTemplate; + if (!handlers.some(matchingTemplate)) { + handlers.push(newHandler); + } + } + } + }, + + /** + * Execute any migrations. Migrations are defined here for any changes or removals for + * existing handlers. Additions are still handled via the localized prefs infrastructure. + * + * This depends on the browser.handlers.migrations pref being set by migrateUI in + * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products. + * This is a comma-separated list of identifiers of migrations that need running. + * This avoids both re-running older migrations and keeping an additional + * pref around permanently. + */ + _migrateProtocolHandlersIfNeeded() { + const kMigrations = { + "30boxes": () => { + const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i; + let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo( + "webcal" + ); + if (this.exists(webcalHandler)) { + this.fillHandlerInfo(webcalHandler, ""); + let shouldStore = false; + // First remove 30boxes from possible handlers. + let handlers = webcalHandler.possibleApplicationHandlers; + for (let i = handlers.length - 1; i >= 0; i--) { + let app = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if ( + app instanceof Ci.nsIWebHandlerApp && + k30BoxesRegex.test(app.uriTemplate) + ) { + shouldStore = true; + handlers.removeElementAt(i); + } + } + // Then remove as a preferred handler. + if (webcalHandler.preferredApplicationHandler) { + let app = webcalHandler.preferredApplicationHandler; + if ( + app instanceof Ci.nsIWebHandlerApp && + k30BoxesRegex.test(app.uriTemplate) + ) { + webcalHandler.preferredApplicationHandler = null; + shouldStore = true; + } + } + // Then store, if we changed anything. + if (shouldStore) { + this.store(webcalHandler); + } + } + }, + }; + let migrationsToRun = Services.prefs.getCharPref( + "browser.handlers.migrations", + "" + ); + migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : []; + for (let migration of migrationsToRun) { + migration.trim(); + try { + kMigrations[migration](); + } catch (ex) { + Cu.reportError(ex); + } + } + + if (migrationsToRun.length) { + Services.prefs.clearUserPref("browser.handlers.migrations"); + } + }, + + _onDBChange() { + return (async () => { + if (this.__store) { + await this.__store.finalize(); + } + this.__store = null; + this.__storeInitialized = false; + })().catch(Cu.reportError); + }, + + // nsIObserver + observe(subject, topic, data) { + if (topic != "handlersvc-json-replace") { + return; + } + let promise = this._onDBChange(); + promise.then(() => { + Services.obs.notifyObservers(null, "handlersvc-json-replace-complete"); + }); + }, + + // nsIHandlerService + asyncInit() { + if (!this.__store) { + this.__store = new JSONFile({ + path: PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "handlers.json" + ), + dataPostProcessor: this._dataPostProcessor.bind(this), + }); + this.__store + .load() + .then(() => { + // __store can be null if we called _onDBChange in the mean time. + if (this.__store) { + this._ensureStoreInitialized(); + } + }) + .catch(Cu.reportError); + } + }, + + // nsIHandlerService + enumerate() { + let handlers = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + for (let type of Object.keys(this._store.data.mimeTypes)) { + let handler = gMIMEService.getFromTypeAndExtension(type, null); + handlers.appendElement(handler); + } + for (let type of Object.keys(this._store.data.schemes)) { + // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive + // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo + // until one of its properties is accessed. + // + // Note: our caller still needs to yield periodically when iterating + // the enumerator and accessing handler properties to avoid monopolizing + // the main thread. + // + let handler = new Proxy( + { + QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]), + type, + get _handlerInfo() { + delete this._handlerInfo; + return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo( + type + )); + }, + }, + { + get(target, name) { + return target[name] || target._handlerInfo[name]; + }, + set(target, name, value) { + target._handlerInfo[name] = value; + }, + } + ); + handlers.appendElement(handler); + } + return handlers.enumerate(Ci.nsIHandlerInfo); + }, + + // nsIHandlerService + store(handlerInfo) { + let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo); + + // Retrieve an existing entry if present, instead of creating a new one, so + // that we preserve unknown properties for forward compatibility. + let storedHandlerInfo = handlerList[handlerInfo.type]; + if (!storedHandlerInfo) { + storedHandlerInfo = {}; + handlerList[handlerInfo.type] = storedHandlerInfo; + } + + // Only a limited number of preferredAction values is allowed. + if ( + handlerInfo.preferredAction == Ci.nsIHandlerInfo.saveToDisk || + handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault || + handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally + ) { + storedHandlerInfo.action = handlerInfo.preferredAction; + } else { + storedHandlerInfo.action = Ci.nsIHandlerInfo.useHelperApp; + } + + if (handlerInfo.alwaysAskBeforeHandling) { + storedHandlerInfo.ask = true; + } else { + delete storedHandlerInfo.ask; + } + + // Build a list of unique nsIHandlerInfo instances to process later. + let handlers = []; + if (handlerInfo.preferredApplicationHandler) { + handlers.push(handlerInfo.preferredApplicationHandler); + } + for (let handler of handlerInfo.possibleApplicationHandlers.enumerate( + Ci.nsIHandlerApp + )) { + // If the caller stored duplicate handlers, we save them only once. + if (!handlers.some(h => h.equals(handler))) { + handlers.push(handler); + } + } + + // If any of the nsIHandlerInfo instances cannot be serialized, it is not + // included in the final list. The first element is always the preferred + // handler, or null if there is none. + let serializableHandlers = handlers + .map(h => this.handlerAppToSerializable(h)) + .filter(h => h); + if (serializableHandlers.length) { + if (!handlerInfo.preferredApplicationHandler) { + serializableHandlers.unshift(null); + } + storedHandlerInfo.handlers = serializableHandlers; + } else { + delete storedHandlerInfo.handlers; + } + + if (this._isMIMEInfo(handlerInfo)) { + let extensions = storedHandlerInfo.extensions || []; + for (let extension of handlerInfo.getFileExtensions()) { + extension = extension.toLowerCase(); + // If the caller stored duplicate extensions, we save them only once. + if (!extensions.includes(extension)) { + extensions.push(extension); + } + } + if (extensions.length) { + storedHandlerInfo.extensions = extensions; + } else { + delete storedHandlerInfo.extensions; + } + } + + // If we're saving *anything*, it stops being a stub: + delete storedHandlerInfo.stubEntry; + + this._store.saveSoon(); + + // Now notify PDF.js. This is hacky, but a lot better than expecting all + // the consumers to do it... + if (handlerInfo.type == "application/pdf") { + Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED); + } + }, + + // nsIHandlerService + fillHandlerInfo(handlerInfo, overrideType) { + let type = overrideType || handlerInfo.type; + let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[ + type + ]; + if (!storedHandlerInfo) { + throw new Components.Exception( + "handlerSvc fillHandlerInfo: don't know this type", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + let isStub = !!storedHandlerInfo.stubEntry; + // In the normal case, this is not a stub, so we can just read stored info + // and write to the handlerInfo object we were passed. + if (!isStub) { + handlerInfo.preferredAction = storedHandlerInfo.action; + handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask; + } else { + // If we've got a stub, ensure the defaults are still set: + gExternalProtocolService.setProtocolHandlerDefaults( + handlerInfo, + handlerInfo.hasDefaultHandler + ); + if ( + handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk && + handlerInfo.alwaysAskBeforeHandling + ) { + // `store` will default to `useHelperApp` because `alwaysAsk` is + // not one of the 3 recognized options; for compatibility, do + // the same here. + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + } + } + // If it *is* a stub, don't override alwaysAskBeforeHandling or the + // preferred actions. Instead, just append the stored handlers, without + // overriding the preferred app, and then schedule a task to store proper + // info for this handler. + this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub); + + if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) { + for (let extension of storedHandlerInfo.extensions) { + handlerInfo.appendExtension(extension); + } + } + }, + + /** + * Private method to inject stored handler information into an nsIHandlerInfo + * instance. + * @param handlerInfo the nsIHandlerInfo instance to write to + * @param storedHandlers the stored handlers + * @param keepPreferredApp whether to keep the handlerInfo's + * preferredApplicationHandler or override it + * (default: false, ie override it) + */ + _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) { + // If the first item is not null, it is also the preferred handler. Since + // we cannot modify the stored array, use a boolean to keep track of this. + let isFirstItem = true; + for (let handler of storedHandlers || [null]) { + let handlerApp = this.handlerAppFromSerializable(handler || {}); + if (isFirstItem) { + isFirstItem = false; + // Do not overwrite the preferred app unless that's allowed + if (!keepPreferredApp) { + handlerInfo.preferredApplicationHandler = handlerApp; + } + } + if (handlerApp) { + handlerInfo.possibleApplicationHandlers.appendElement(handlerApp); + } + } + }, + + /** + * @param handler + * A nsIHandlerApp handler app + * @returns Serializable representation of a handler app object. + */ + handlerAppToSerializable(handler) { + if (handler instanceof Ci.nsILocalHandlerApp) { + return { + name: handler.name, + path: handler.executable.path, + }; + } else if (handler instanceof Ci.nsIWebHandlerApp) { + return { + name: handler.name, + uriTemplate: handler.uriTemplate, + }; + } else if (handler instanceof Ci.nsIDBusHandlerApp) { + return { + name: handler.name, + service: handler.service, + method: handler.method, + objectPath: handler.objectPath, + dBusInterface: handler.dBusInterface, + }; + } else if (handler instanceof Ci.nsIGIOMimeApp) { + return { + name: handler.name, + command: handler.command, + }; + } + // If the handler is an unknown handler type, return null. + // Android default application handler is the case. + return null; + }, + + /** + * @param handlerObj + * Serializable representation of a handler object. + * @returns {nsIHandlerApp} the handler app, if any; otherwise null + */ + handlerAppFromSerializable(handlerObj) { + let handlerApp; + if ("path" in handlerObj) { + try { + let file = new FileUtils.File(handlerObj.path); + if (!file.exists()) { + return null; + } + handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = file; + } catch (ex) { + return null; + } + } else if ("uriTemplate" in handlerObj) { + handlerApp = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + handlerApp.uriTemplate = handlerObj.uriTemplate; + } else if ("service" in handlerObj) { + handlerApp = Cc[ + "@mozilla.org/uriloader/dbus-handler-app;1" + ].createInstance(Ci.nsIDBusHandlerApp); + handlerApp.service = handlerObj.service; + handlerApp.method = handlerObj.method; + handlerApp.objectPath = handlerObj.objectPath; + handlerApp.dBusInterface = handlerObj.dBusInterface; + } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) { + try { + handlerApp = Cc["@mozilla.org/gio-service;1"] + .getService(Ci.nsIGIOService) + .createAppFromCommand(handlerObj.command, handlerObj.name); + } catch (ex) { + return null; + } + } else { + return null; + } + + handlerApp.name = handlerObj.name; + return handlerApp; + }, + + /** + * The function returns a reference to the "mimeTypes" or "schemes" object + * based on which type of handlerInfo is provided. + */ + _getHandlerListByHandlerInfoType(handlerInfo) { + return this._isMIMEInfo(handlerInfo) + ? this._store.data.mimeTypes + : this._store.data.schemes; + }, + + /** + * Determines whether an nsIHandlerInfo instance represents a MIME type. + */ + _isMIMEInfo(handlerInfo) { + // We cannot rely only on the instanceof check because on Android both MIME + // types and protocols are instances of nsIMIMEInfo. We still do the check + // so that properties of nsIMIMEInfo become available to the callers. + return ( + handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/") + ); + }, + + // nsIHandlerService + exists(handlerInfo) { + return ( + handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo) + ); + }, + + // nsIHandlerService + remove(handlerInfo) { + delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type]; + this._store.saveSoon(); + }, + + // nsIHandlerService + getTypeFromExtension(fileExtension) { + let extension = fileExtension.toLowerCase(); + let mimeTypes = this._store.data.mimeTypes; + for (let type of Object.keys(mimeTypes)) { + if ( + mimeTypes[type].extensions && + mimeTypes[type].extensions.includes(extension) + ) { + return type; + } + } + return ""; + }, +}; + +this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]); diff --git a/uriloader/exthandler/HandlerService.manifest b/uriloader/exthandler/HandlerService.manifest new file mode 100644 index 0000000000..854d13adab --- /dev/null +++ b/uriloader/exthandler/HandlerService.manifest @@ -0,0 +1,2 @@ +component {220cc253-b60f-41f6-b9cf-fdcb325f970f} HandlerService.js +contract @mozilla.org/uriloader/handler-service;1 {220cc253-b60f-41f6-b9cf-fdcb325f970f} process=main diff --git a/uriloader/exthandler/HandlerServiceChild.h b/uriloader/exthandler/HandlerServiceChild.h new file mode 100644 index 0000000000..5c58a3c230 --- /dev/null +++ b/uriloader/exthandler/HandlerServiceChild.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef handler_service_child_h +#define handler_service_child_h + +#include "mozilla/dom/PHandlerServiceChild.h" + +namespace mozilla { + +class HandlerServiceChild final : public mozilla::dom::PHandlerServiceChild { + public: + NS_INLINE_DECL_REFCOUNTING(HandlerServiceChild, final) + HandlerServiceChild() {} + + private: + virtual ~HandlerServiceChild() {} +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/exthandler/HandlerServiceParent.cpp b/uriloader/exthandler/HandlerServiceParent.cpp new file mode 100644 index 0000000000..8f73c7cb8c --- /dev/null +++ b/uriloader/exthandler/HandlerServiceParent.cpp @@ -0,0 +1,379 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/Logging.h" +#include "HandlerServiceParent.h" +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "ContentHandlerService.h" +#include "nsStringEnumerator.h" +#ifdef MOZ_WIDGET_GTK +# include "unix/nsGNOMERegistry.h" +#endif + +using mozilla::dom::ContentHandlerService; +using mozilla::dom::HandlerApp; +using mozilla::dom::HandlerInfo; +using mozilla::dom::RemoteHandlerApp; + +namespace { + +class ProxyHandlerInfo final : public nsIHandlerInfo { + public: + explicit ProxyHandlerInfo(const HandlerInfo& aHandlerInfo); + NS_DECL_ISUPPORTS; + NS_DECL_NSIHANDLERINFO; + + nsTArray<nsCString>& Extensions() { return mHandlerInfo.extensions(); } + + protected: + ~ProxyHandlerInfo() {} + HandlerInfo mHandlerInfo; + nsHandlerInfoAction mPrefAction; + nsCOMPtr<nsIMutableArray> mPossibleApps; +}; + +NS_IMPL_ISUPPORTS(ProxyHandlerInfo, nsIHandlerInfo) + +ProxyHandlerInfo::ProxyHandlerInfo(const HandlerInfo& aHandlerInfo) + : mHandlerInfo(aHandlerInfo), + mPrefAction(nsIHandlerInfo::alwaysAsk), + mPossibleApps(do_CreateInstance(NS_ARRAY_CONTRACTID)) { + for (auto& happ : aHandlerInfo.possibleApplicationHandlers()) { + mPossibleApps->AppendElement(new RemoteHandlerApp(happ)); + } +} + +/* readonly attribute ACString type; */ +NS_IMETHODIMP ProxyHandlerInfo::GetType(nsACString& aType) { + aType.Assign(mHandlerInfo.type()); + return NS_OK; +} + +/* attribute AString description; */ +NS_IMETHODIMP ProxyHandlerInfo::GetDescription(nsAString& aDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP ProxyHandlerInfo::SetDescription(const nsAString& aDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* attribute nsIHandlerApp preferredApplicationHandler; */ +NS_IMETHODIMP ProxyHandlerInfo::GetPreferredApplicationHandler( + nsIHandlerApp** aPreferredApplicationHandler) { + *aPreferredApplicationHandler = + new RemoteHandlerApp(mHandlerInfo.preferredApplicationHandler()); + NS_IF_ADDREF(*aPreferredApplicationHandler); + return NS_OK; +} + +NS_IMETHODIMP ProxyHandlerInfo::SetPreferredApplicationHandler( + nsIHandlerApp* aApp) { + nsString name; + nsString detailedDescription; + if (aApp) { + aApp->GetName(name); + aApp->GetDetailedDescription(detailedDescription); + } + + mHandlerInfo.preferredApplicationHandler() = + HandlerApp(name, detailedDescription); + return NS_OK; +} + +/* readonly attribute nsIMutableArray possibleApplicationHandlers; */ +NS_IMETHODIMP ProxyHandlerInfo::GetPossibleApplicationHandlers( + nsIMutableArray** aPossibleApplicationHandlers) { + *aPossibleApplicationHandlers = mPossibleApps; + NS_IF_ADDREF(*aPossibleApplicationHandlers); + return NS_OK; +} + +/* readonly attribute boolean hasDefaultHandler; */ +NS_IMETHODIMP ProxyHandlerInfo::GetHasDefaultHandler(bool* aHasDefaultHandler) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* readonly attribute AString defaultDescription; */ +NS_IMETHODIMP ProxyHandlerInfo::GetDefaultDescription( + nsAString& aDefaultDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void launchWithURI (in nsIURI aURI, + [optional] in BrowsingContext aBrowsingContext); */ +NS_IMETHODIMP ProxyHandlerInfo::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* attribute ProxyHandlerInfoAction preferredAction; */ +NS_IMETHODIMP ProxyHandlerInfo::GetPreferredAction( + nsHandlerInfoAction* aPreferredAction) { + *aPreferredAction = mPrefAction; + return NS_OK; +} +NS_IMETHODIMP ProxyHandlerInfo::SetPreferredAction( + nsHandlerInfoAction aPreferredAction) { + mHandlerInfo.preferredAction() = aPreferredAction; + mPrefAction = aPreferredAction; + return NS_OK; +} + +/* attribute boolean alwaysAskBeforeHandling; */ +NS_IMETHODIMP ProxyHandlerInfo::GetAlwaysAskBeforeHandling( + bool* aAlwaysAskBeforeHandling) { + *aAlwaysAskBeforeHandling = mHandlerInfo.alwaysAskBeforeHandling(); + return NS_OK; +} +NS_IMETHODIMP ProxyHandlerInfo::SetAlwaysAskBeforeHandling( + bool aAlwaysAskBeforeHandling) { + mHandlerInfo.alwaysAskBeforeHandling() = aAlwaysAskBeforeHandling; + return NS_OK; +} + +class ProxyMIMEInfo : public nsIMIMEInfo { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMIMEINFO + NS_FORWARD_NSIHANDLERINFO(mProxyHandlerInfo->); + + explicit ProxyMIMEInfo(const HandlerInfo& aHandlerInfo) + : mProxyHandlerInfo(new ProxyHandlerInfo(aHandlerInfo)) {} + + private: + virtual ~ProxyMIMEInfo() {} + RefPtr<ProxyHandlerInfo> mProxyHandlerInfo; + + protected: + /* additional members */ +}; + +NS_IMPL_ISUPPORTS(ProxyMIMEInfo, nsIMIMEInfo, nsIHandlerInfo) + +/* nsIUTF8StringEnumerator getFileExtensions (); */ +NS_IMETHODIMP ProxyMIMEInfo::GetFileExtensions( + nsIUTF8StringEnumerator** _retval) { + return NS_NewUTF8StringEnumerator(_retval, &mProxyHandlerInfo->Extensions(), + this); +} + +/* void setFileExtensions (in AUTF8String aExtensions); */ +NS_IMETHODIMP ProxyMIMEInfo::SetFileExtensions(const nsACString& aExtensions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean extensionExists (in AUTF8String aExtension); */ +NS_IMETHODIMP ProxyMIMEInfo::ExtensionExists(const nsACString& aExtension, + bool* _retval) { + *_retval = mProxyHandlerInfo->Extensions().Contains( + aExtension, nsCaseInsensitiveCStringArrayComparator()); + return NS_OK; +} + +/* void appendExtension (in AUTF8String aExtension); */ +NS_IMETHODIMP ProxyMIMEInfo::AppendExtension(const nsACString& aExtension) { + if (!aExtension.IsEmpty() && + !mProxyHandlerInfo->Extensions().Contains( + aExtension, nsCaseInsensitiveCStringArrayComparator())) { + mProxyHandlerInfo->Extensions().AppendElement(aExtension); + } + return NS_OK; +} + +/* attribute AUTF8String primaryExtension; */ +NS_IMETHODIMP ProxyMIMEInfo::GetPrimaryExtension( + nsACString& aPrimaryExtension) { + const auto& extensions = mProxyHandlerInfo->Extensions(); + if (extensions.IsEmpty()) { + aPrimaryExtension.Truncate(); + return NS_ERROR_FAILURE; + } + aPrimaryExtension = extensions[0]; + return NS_OK; +} + +NS_IMETHODIMP ProxyMIMEInfo::SetPrimaryExtension( + const nsACString& aPrimaryExtension) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* readonly attribute ACString MIMEType; */ +NS_IMETHODIMP ProxyMIMEInfo::GetMIMEType(nsACString& aMIMEType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean equals (in nsIMIMEInfo aMIMEInfo); */ +NS_IMETHODIMP ProxyMIMEInfo::Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* readonly attribute nsIArray possibleLocalHandlers; */ +NS_IMETHODIMP ProxyMIMEInfo::GetPossibleLocalHandlers( + nsIArray** aPossibleLocalHandlers) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void launchWithFile (in nsIFile aFile); */ +NS_IMETHODIMP ProxyMIMEInfo::LaunchWithFile(nsIFile* aFile) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean isCurrentAppOSDefault(); */ +NS_IMETHODIMP ProxyMIMEInfo::IsCurrentAppOSDefault(bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +static already_AddRefed<nsIHandlerInfo> WrapHandlerInfo( + const HandlerInfo& aHandlerInfo) { + nsCOMPtr<nsIHandlerInfo> info; + if (aHandlerInfo.isMIMEInfo()) { + info = new ProxyMIMEInfo(aHandlerInfo); + } else { + info = new ProxyHandlerInfo(aHandlerInfo); + } + return info.forget(); +} + +} // anonymous namespace + +HandlerServiceParent::HandlerServiceParent() {} + +HandlerServiceParent::~HandlerServiceParent() {} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvFillHandlerInfo( + const HandlerInfo& aHandlerInfoData, const nsCString& aOverrideType, + HandlerInfo* handlerInfoData) { + nsCOMPtr<nsIHandlerInfo> info(WrapHandlerInfo(aHandlerInfoData)); + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + handlerSvc->FillHandlerInfo(info, aOverrideType); + ContentHandlerService::nsIHandlerInfoToHandlerInfo(info, handlerInfoData); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvGetMIMEInfoFromOS( + const nsCString& aMIMEType, const nsCString& aExtension, nsresult* aRv, + HandlerInfo* aHandlerInfoData, bool* aFound) { + *aFound = false; + if (aMIMEType.Length() > MAX_MIMETYPE_LENGTH || + aExtension.Length() > MAX_EXT_LENGTH) { + *aRv = NS_OK; + return IPC_OK(); + } + + nsCOMPtr<nsIMIMEService> mimeService = + do_GetService(NS_MIMESERVICE_CONTRACTID, aRv); + if (NS_WARN_IF(NS_FAILED(*aRv))) { + return IPC_OK(); + } + + nsCOMPtr<nsIMIMEInfo> mimeInfo; + *aRv = mimeService->GetMIMEInfoFromOS(aMIMEType, aExtension, aFound, + getter_AddRefs(mimeInfo)); + if (NS_WARN_IF(NS_FAILED(*aRv))) { + return IPC_OK(); + } + + if (mimeInfo) { + ContentHandlerService::nsIHandlerInfoToHandlerInfo(mimeInfo, + aHandlerInfoData); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvExists( + const HandlerInfo& aHandlerInfo, bool* exists) { + nsCOMPtr<nsIHandlerInfo> info(WrapHandlerInfo(aHandlerInfo)); + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + handlerSvc->Exists(info, exists); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvExistsForProtocolOS( + const nsCString& aProtocolScheme, bool* aHandlerExists) { + if (aProtocolScheme.Length() > MAX_SCHEME_LENGTH) { + *aHandlerExists = false; + return IPC_OK(); + } +#ifdef MOZ_WIDGET_GTK + // Check the GNOME registry for a protocol handler + *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme.get()); +#else + *aHandlerExists = false; +#endif + return IPC_OK(); +} + +/* + * Check if a handler exists for the provided protocol. Check the datastore + * first and then fallback to checking the OS for a handler. + */ +mozilla::ipc::IPCResult HandlerServiceParent::RecvExistsForProtocol( + const nsCString& aProtocolScheme, bool* aHandlerExists) { + if (aProtocolScheme.Length() > MAX_SCHEME_LENGTH) { + *aHandlerExists = false; + return IPC_OK(); + } +#if defined(XP_MACOSX) + // Check the datastore and fallback to an OS check. + // ExternalProcotolHandlerExists() does the fallback. + nsresult rv; + nsCOMPtr<nsIExternalProtocolService> protoSvc = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aHandlerExists = false; + return IPC_OK(); + } + rv = protoSvc->ExternalProtocolHandlerExists(aProtocolScheme.get(), + aHandlerExists); + + if (NS_WARN_IF(NS_FAILED(rv))) { + *aHandlerExists = false; + } +#else + MOZ_RELEASE_ASSERT(false, "No implementation on this platform."); + *aHandlerExists = false; +#endif + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvGetTypeFromExtension( + const nsCString& aFileExtension, nsCString* type) { + if (aFileExtension.Length() > MAX_EXT_LENGTH) { + return IPC_OK(); + } + + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_OK(); + } + + rv = handlerSvc->GetTypeFromExtension(aFileExtension, *type); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvGetApplicationDescription( + const nsCString& aScheme, nsresult* aRv, nsString* aDescription) { + if (aScheme.Length() > MAX_SCHEME_LENGTH) { + *aRv = NS_ERROR_NOT_AVAILABLE; + return IPC_OK(); + } + nsCOMPtr<nsIExternalProtocolService> protoSvc = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID); + NS_ASSERTION(protoSvc, "No Helper App Service!"); + *aRv = protoSvc->GetApplicationDescription(aScheme, *aDescription); + return IPC_OK(); +} + +void HandlerServiceParent::ActorDestroy(ActorDestroyReason aWhy) {} diff --git a/uriloader/exthandler/HandlerServiceParent.h b/uriloader/exthandler/HandlerServiceParent.h new file mode 100644 index 0000000000..e98e2f589e --- /dev/null +++ b/uriloader/exthandler/HandlerServiceParent.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef handler_service_parent_h +#define handler_service_parent_h + +#include "mozilla/dom/PHandlerServiceParent.h" +#include "nsIMIMEInfo.h" + +class nsIHandlerApp; + +class HandlerServiceParent final : public mozilla::dom::PHandlerServiceParent { + public: + HandlerServiceParent(); + NS_INLINE_DECL_REFCOUNTING(HandlerServiceParent, final) + + private: + virtual ~HandlerServiceParent(); + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvFillHandlerInfo( + const HandlerInfo& aHandlerInfoData, const nsCString& aOverrideType, + HandlerInfo* handlerInfoData) override; + + mozilla::ipc::IPCResult RecvGetMIMEInfoFromOS(const nsCString& aMIMEType, + const nsCString& aExtension, + nsresult* aRv, + HandlerInfo* aHandlerInfoData, + bool* aFound) override; + + mozilla::ipc::IPCResult RecvExists(const HandlerInfo& aHandlerInfo, + bool* exists) override; + + mozilla::ipc::IPCResult RecvGetTypeFromExtension( + const nsCString& aFileExtension, nsCString* type) override; + + mozilla::ipc::IPCResult RecvExistsForProtocolOS( + const nsCString& aProtocolScheme, bool* aHandlerExists) override; + + mozilla::ipc::IPCResult RecvExistsForProtocol( + const nsCString& aProtocolScheme, bool* aHandlerExists) override; + + mozilla::ipc::IPCResult RecvGetApplicationDescription( + const nsCString& aScheme, nsresult* aRv, nsString* aDescription) override; + + /* + * Limit the length of MIME types, filename extensions, and protocol + * schemes we'll consider. + */ + static const size_t MAX_MIMETYPE_LENGTH = 129; /* Per RFC 6838, type and + subtype should be limited + to 64 characters. We add + one more to account for + a '/' separator. */ + static const size_t MAX_EXT_LENGTH = 64; /* Arbitratily chosen to be + longer than any known + extension */ + static const size_t MAX_SCHEME_LENGTH = 1024; /* Arbitratily chosen to be + longer than any known + protocol scheme */ +}; + +#endif diff --git a/uriloader/exthandler/PExternalHelperApp.ipdl b/uriloader/exthandler/PExternalHelperApp.ipdl new file mode 100644 index 0000000000..b3d1e7c736 --- /dev/null +++ b/uriloader/exthandler/PExternalHelperApp.ipdl @@ -0,0 +1,28 @@ +/* vim: set ft=cpp: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBrowser; +include protocol PContent; + +namespace mozilla { +namespace dom { + +refcounted protocol PExternalHelperApp +{ + manager PContent; + +parent: + async OnStartRequest(nsCString entityID); + async OnDataAvailable(nsCString data, uint64_t offset, uint32_t count); + async OnStopRequest(nsresult code); + +child: + async Cancel(nsresult aStatus); + async __delete__(); +}; + + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/PHandlerService.ipdl b/uriloader/exthandler/PHandlerService.ipdl new file mode 100644 index 0000000000..981c1839c4 --- /dev/null +++ b/uriloader/exthandler/PHandlerService.ipdl @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PContent; + +namespace mozilla { +namespace dom { + +struct HandlerApp { + nsString name; + nsString detailedDescription; +}; + +struct HandlerInfo { + nsCString type; + bool isMIMEInfo; + nsString description; + bool alwaysAskBeforeHandling; + nsCString[] extensions; + HandlerApp preferredApplicationHandler; + HandlerApp[] possibleApplicationHandlers; + long preferredAction; +}; + +sync refcounted protocol PHandlerService +{ + manager PContent; + +parent: + sync FillHandlerInfo(HandlerInfo aHandlerInfoData, + nsCString aOverrideType) + returns (HandlerInfo handlerInfoData); + + /* + * Check if an OS handler exists for the given protocol scheme. + */ + sync ExistsForProtocolOS(nsCString aProtocolScheme) + returns (bool exists); + + /* + * Check if a handler exists for the given protocol scheme. Check + * the datastore first and then fallback to an OS handler check. + */ + sync ExistsForProtocol(nsCString aProtocolScheme) + returns (bool exists); + + sync Exists(HandlerInfo aHandlerInfo) + returns (bool exists); + sync GetTypeFromExtension(nsCString aFileExtension) + returns (nsCString type); + sync GetMIMEInfoFromOS(nsCString aMIMEType, nsCString aExtension) + returns (nsresult rv, HandlerInfo handlerInfoData, bool found); + sync GetApplicationDescription(nsCString aScheme) + returns (nsresult rv, nsString description); + async __delete__(); +}; + + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/WebHandlerApp.jsm b/uriloader/exthandler/WebHandlerApp.jsm new file mode 100644 index 0000000000..bac75230f3 --- /dev/null +++ b/uriloader/exthandler/WebHandlerApp.jsm @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); + +function nsWebHandlerApp() {} + +nsWebHandlerApp.prototype = { + classDescription: "A web handler for protocols and content", + classID: Components.ID("8b1ae382-51a9-4972-b930-56977a57919d"), + contractID: "@mozilla.org/uriloader/web-handler-app;1", + QueryInterface: ChromeUtils.generateQI(["nsIWebHandlerApp", "nsIHandlerApp"]), + + _name: null, + _detailedDescription: null, + _uriTemplate: null, + + // nsIHandlerApp + + get name() { + return this._name; + }, + + set name(aName) { + this._name = aName; + }, + + get detailedDescription() { + return this._detailedDescription; + }, + + set detailedDescription(aDesc) { + this._detailedDescription = aDesc; + }, + + equals(aHandlerApp) { + if (!aHandlerApp) { + throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); + } + + if ( + aHandlerApp instanceof Ci.nsIWebHandlerApp && + aHandlerApp.uriTemplate && + this.uriTemplate && + aHandlerApp.uriTemplate == this.uriTemplate + ) { + return true; + } + return false; + }, + + launchWithURI(aURI, aBrowsingContext) { + // XXX need to strip passwd & username from URI to handle, as per the + // WhatWG HTML5 draft. nsSimpleURL, which is what we're going to get, + // can't do this directly. Ideally, we'd fix nsStandardURL to make it + // possible to turn off all of its quirks handling, and use that... + + // encode the URI to be handled + var escapedUriSpecToHandle = encodeURIComponent(aURI.spec); + + // insert the encoded URI and create the object version. + var uriSpecToSend = this.uriTemplate.replace("%s", escapedUriSpecToHandle); + var uriToSend = Services.io.newURI(uriSpecToSend); + + let policy = WebExtensionPolicy.getByURI(uriToSend); + let privateAllowed = !policy || policy.privateBrowsingAllowed; + + // If we're in a frame, check if we're a built-in scheme, in which case, + // override the target browsingcontext. It's not a good idea to try to + // load mail clients or other apps with potential for logged in data into + // iframes, and in any case it's unlikely to work due to framing + // restrictions employed by the target site. + if (aBrowsingContext && aBrowsingContext != aBrowsingContext.top) { + let { scheme } = aURI; + if (!scheme.startsWith("web+") && !scheme.startsWith("ext+")) { + aBrowsingContext = null; + } + } + + // if we have a context, use the URI loader to load there + if (aBrowsingContext) { + if (aBrowsingContext.usePrivateBrowsing && !privateAllowed) { + throw Components.Exception( + "Extension not allowed in private windows.", + Cr.NS_ERROR_FILE_NOT_FOUND + ); + } + + let triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + Services.tm.dispatchToMainThread(() => + aBrowsingContext.loadURI(uriSpecToSend, { triggeringPrincipal }) + ); + return; + } + + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // If this is an extension handler, check private browsing access. + if (!privateAllowed && PrivateBrowsingUtils.isWindowPrivate(win)) { + throw Components.Exception( + "Extension not allowed in private windows.", + Cr.NS_ERROR_FILE_NOT_FOUND + ); + } + + // If we get an exception, there are several possible reasons why: + // a) this gecko embedding doesn't provide an nsIBrowserDOMWindow + // implementation (i.e. doesn't support browser-style functionality), + // so we need to kick the URL out to the OS default browser. This is + // the subject of bug 394479. + // b) this embedding does provide an nsIBrowserDOMWindow impl, but + // there doesn't happen to be a browser window open at the moment; one + // should be opened. It's not clear whether this situation will really + // ever occur in real life. If it does, the only API that I can find + // that seems reasonably likely to work for most embedders is the + // command line handler. + // c) something else went wrong + // + // It's not clear how one would differentiate between the three cases + // above, so for now we don't catch the exception. + + // openURI + win.browserDOMWindow.openURI( + uriToSend, + null, // no window.opener + Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, + Ci.nsIBrowserDOMWindow.OPEN_NEW, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }, + + // nsIWebHandlerApp + + get uriTemplate() { + return this._uriTemplate; + }, + + set uriTemplate(aURITemplate) { + this._uriTemplate = aURITemplate; + }, +}; + +var EXPORTED_SYMBOLS = ["nsWebHandlerApp"]; diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.cpp b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp new file mode 100644 index 0000000000..de9f6be509 --- /dev/null +++ b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/java/GeckoAppShellWrappers.h" +#include "nsAndroidHandlerApp.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsAndroidHandlerApp, nsIHandlerApp, nsISharingHandlerApp) + +nsAndroidHandlerApp::nsAndroidHandlerApp(const nsAString& aName, + const nsAString& aDescription, + const nsAString& aPackageName, + const nsAString& aClassName, + const nsACString& aMimeType, + const nsAString& aAction) + : mName(aName), + mDescription(aDescription), + mPackageName(aPackageName), + mClassName(aClassName), + mMimeType(aMimeType), + mAction(aAction) {} + +nsAndroidHandlerApp::~nsAndroidHandlerApp() {} + +NS_IMETHODIMP +nsAndroidHandlerApp::GetName(nsAString& aName) { + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::SetName(const nsAString& aName) { + mName.Assign(aName); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::GetDetailedDescription(nsAString& aDescription) { + aDescription.Assign(mDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::SetDetailedDescription(const nsAString& aDescription) { + mDescription.Assign(aDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* aRetval) { + *aRetval = false; + if (!aHandlerApp) { + return NS_OK; + } + + nsAutoString name; + nsAutoString detailedDescription; + aHandlerApp->GetName(name); + aHandlerApp->GetDetailedDescription(detailedDescription); + + *aRetval = name.Equals(mName) && detailedDescription.Equals(mDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + nsCString uriSpec; + aURI->GetSpec(uriSpec); + return java::GeckoAppShell::OpenUriExternal(uriSpec, mMimeType, mPackageName, + mClassName, mAction, u""_ns) + ? NS_OK + : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::Share(const nsAString& data, const nsAString& title) { + return java::GeckoAppShell::OpenUriExternal(data, mMimeType, mPackageName, + mClassName, mAction, u""_ns) + ? NS_OK + : NS_ERROR_FAILURE; +} diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.h b/uriloader/exthandler/android/nsAndroidHandlerApp.h new file mode 100644 index 0000000000..dab4042fd8 --- /dev/null +++ b/uriloader/exthandler/android/nsAndroidHandlerApp.h @@ -0,0 +1,33 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAndroidHandlerApp_h +#define nsAndroidHandlerApp_h + +#include "nsMIMEInfoImpl.h" +#include "nsISharingHandlerApp.h" + +class nsAndroidHandlerApp : public nsISharingHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + NS_DECL_NSISHARINGHANDLERAPP + + nsAndroidHandlerApp(const nsAString& aName, const nsAString& aDescription, + const nsAString& aPackageName, + const nsAString& aClassName, const nsACString& aMimeType, + const nsAString& aAction); + + private: + virtual ~nsAndroidHandlerApp(); + + nsString mName; + nsString mDescription; + nsString mPackageName; + nsString mClassName; + nsCString mMimeType; + nsString mAction; +}; +#endif diff --git a/uriloader/exthandler/android/nsExternalURLHandlerService.cpp b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp new file mode 100644 index 0000000000..60ccbbdfe9 --- /dev/null +++ b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp @@ -0,0 +1,21 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsExternalURLHandlerService.h" +#include "nsMIMEInfoAndroid.h" + +NS_IMPL_ISUPPORTS(nsExternalURLHandlerService, nsIExternalURLHandlerService) + +nsExternalURLHandlerService::nsExternalURLHandlerService() {} + +nsExternalURLHandlerService::~nsExternalURLHandlerService() {} + +NS_IMETHODIMP +nsExternalURLHandlerService::GetURLHandlerInfoFromOS(nsIURI* aURL, bool* found, + nsIHandlerInfo** info) { + // We don't want to get protocol handlers from the OS in GV; the app + // should take care of that in NavigationDelegate.onLoadRequest(). + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/android/nsExternalURLHandlerService.h b/uriloader/exthandler/android/nsExternalURLHandlerService.h new file mode 100644 index 0000000000..18389c5286 --- /dev/null +++ b/uriloader/exthandler/android/nsExternalURLHandlerService.h @@ -0,0 +1,22 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSEXTERNALURLHANDLERSERVICE_H +#define NSEXTERNALURLHANDLERSERVICE_H + +#include "nsIExternalURLHandlerService.h" + +class nsExternalURLHandlerService final : public nsIExternalURLHandlerService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALURLHANDLERSERVICE + + nsExternalURLHandlerService(); + + private: + ~nsExternalURLHandlerService(); +}; + +#endif // NSEXTERNALURLHANDLERSERVICE_H diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp new file mode 100644 index 0000000000..6d361d32c2 --- /dev/null +++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp @@ -0,0 +1,414 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "nsMIMEInfoAndroid.h" +#include "AndroidBridge.h" +#include "nsAndroidHandlerApp.h" +#include "nsArrayUtils.h" +#include "nsISupportsUtils.h" +#include "nsStringEnumerator.h" +#include "nsNetUtil.h" +#include "mozilla/Utf8.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsMIMEInfoAndroid, nsIMIMEInfo, nsIHandlerInfo) + +NS_IMETHODIMP +nsMIMEInfoAndroid::LaunchDefaultWithFile(nsIFile* aFile) { + return LaunchWithFile(aFile); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::LoadUriInternal(nsIURI* aURI) { + nsCString uriSpec; + aURI->GetSpec(uriSpec); + + nsCString uriScheme; + aURI->GetScheme(uriScheme); + + nsAutoString mimeType; + if (mType.Equals(uriScheme) || mType.Equals(uriSpec)) { + mimeType.Truncate(); + } else { + CopyUTF8toUTF16(mType, mimeType); + } + + if (java::GeckoAppShell::OpenUriExternal(NS_ConvertUTF8toUTF16(uriSpec), + mimeType, u""_ns, u""_ns, u""_ns, + u""_ns)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +bool nsMIMEInfoAndroid::GetMimeInfoForMimeType(const nsACString& aMimeType, + nsMIMEInfoAndroid** aMimeInfo) { + RefPtr<nsMIMEInfoAndroid> info = new nsMIMEInfoAndroid(aMimeType); + mozilla::AndroidBridge* bridge = mozilla::AndroidBridge::Bridge(); + // we don't have access to the bridge, so just assume we can handle + // the mime type for now and let the system deal with it + if (!bridge) { + info.forget(aMimeInfo); + return false; + } + + nsIHandlerApp* systemDefault = nullptr; + + if (!IsUtf8(aMimeType)) return false; + + NS_ConvertUTF8toUTF16 mimeType(aMimeType); + + bridge->GetHandlersForMimeType(mimeType, info->mHandlerApps, &systemDefault); + + if (systemDefault) info->mPrefApp = systemDefault; + + nsAutoCString fileExt; + bridge->GetExtensionFromMimeType(aMimeType, fileExt); + info->SetPrimaryExtension(fileExt); + + uint32_t len; + info->mHandlerApps->GetLength(&len); + if (len == 1) { + info.forget(aMimeInfo); + return false; + } + + info.forget(aMimeInfo); + return true; +} + +bool nsMIMEInfoAndroid::GetMimeInfoForFileExt(const nsACString& aFileExt, + nsMIMEInfoAndroid** aMimeInfo) { + nsCString mimeType; + if (mozilla::AndroidBridge::Bridge()) + mozilla::AndroidBridge::Bridge()->GetMimeTypeFromExtensions(aFileExt, + mimeType); + + // "*/*" means that the bridge didn't know. + if (mimeType.Equals(nsDependentCString("*/*"), + nsCaseInsensitiveCStringComparator)) + return false; + + bool found = GetMimeInfoForMimeType(mimeType, aMimeInfo); + (*aMimeInfo)->SetPrimaryExtension(aFileExt); + return found; +} + +/** + * Returns MIME info for the aURL, which may contain the whole URL or only a + * protocol + */ +nsresult nsMIMEInfoAndroid::GetMimeInfoForURL(const nsACString& aURL, + bool* found, + nsIHandlerInfo** info) { + nsMIMEInfoAndroid* mimeinfo = new nsMIMEInfoAndroid(aURL); + NS_ADDREF(*info = mimeinfo); + *found = true; + + mozilla::AndroidBridge* bridge = mozilla::AndroidBridge::Bridge(); + if (!bridge) { + // we don't have access to the bridge, so just assume we can handle + // the protocol for now and let the system deal with it + return NS_OK; + } + + nsIHandlerApp* systemDefault = nullptr; + bridge->GetHandlersForURL(NS_ConvertUTF8toUTF16(aURL), mimeinfo->mHandlerApps, + &systemDefault); + + if (systemDefault) mimeinfo->mPrefApp = systemDefault; + + nsAutoCString fileExt; + nsAutoCString mimeType; + mimeinfo->GetType(mimeType); + bridge->GetExtensionFromMimeType(mimeType, fileExt); + mimeinfo->SetPrimaryExtension(fileExt); + + uint32_t len; + mimeinfo->mHandlerApps->GetLength(&len); + if (len == 1) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + *found = false; + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetType(nsACString& aType) { + aType.Assign(mType); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetDescription(nsAString& aDesc) { + aDesc.Assign(mDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetDescription(const nsAString& aDesc) { + mDescription.Assign(aDesc); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPreferredApplicationHandler(nsIHandlerApp** aApp) { + *aApp = mPrefApp; + NS_IF_ADDREF(*aApp); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetPreferredApplicationHandler(nsIHandlerApp* aApp) { + mPrefApp = aApp; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPossibleApplicationHandlers( + nsIMutableArray** aHandlerApps) { + if (!mHandlerApps) mHandlerApps = do_CreateInstance(NS_ARRAY_CONTRACTID); + + if (!mHandlerApps) return NS_ERROR_OUT_OF_MEMORY; + + *aHandlerApps = mHandlerApps; + NS_IF_ADDREF(*aHandlerApps); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetHasDefaultHandler(bool* aHasDefault) { + uint32_t len; + *aHasDefault = false; + if (!mHandlerApps) return NS_OK; + + if (NS_FAILED(mHandlerApps->GetLength(&len))) return NS_OK; + + if (len == 0) return NS_OK; + + *aHasDefault = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetDefaultDescription(nsAString& aDesc) { + aDesc.Assign(u""_ns); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + return mPrefApp->LaunchWithURI(aURI, aBrowsingContext); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPreferredAction(nsHandlerInfoAction* aPrefAction) { + *aPrefAction = mPrefAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetPreferredAction(nsHandlerInfoAction aPrefAction) { + mPrefAction = aPrefAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetAlwaysAskBeforeHandling(bool* aAlwaysAsk) { + *aAlwaysAsk = mAlwaysAsk; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetAlwaysAskBeforeHandling(bool aAlwaysAsk) { + mAlwaysAsk = aAlwaysAsk; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetFileExtensions(nsIUTF8StringEnumerator** aResult) { + return NS_NewUTF8StringEnumerator(aResult, &mExtensions, this); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetFileExtensions(const nsACString& aExtensions) { + mExtensions.Clear(); + nsACString::const_iterator start, end; + aExtensions.BeginReading(start); + aExtensions.EndReading(end); + while (start != end) { + nsACString::const_iterator cursor = start; + mozilla::Unused << FindCharInReadable(',', cursor, end); + AddUniqueExtension(Substring(start, cursor)); + // If a comma was found, skip it for the next search. + start = cursor != end ? ++cursor : cursor; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::ExtensionExists(const nsACString& aExtension, + bool* aRetVal) { + NS_ASSERTION(!aExtension.IsEmpty(), "no extension"); + + nsCString mimeType; + if (mozilla::AndroidBridge::Bridge()) { + mozilla::AndroidBridge::Bridge()->GetMimeTypeFromExtensions(aExtension, + mimeType); + } + + // "*/*" means the bridge didn't find anything (i.e., extension doesn't + // exist). + *aRetVal = !mimeType.Equals(nsDependentCString("*/*"), + nsCaseInsensitiveCStringComparator); + return NS_OK; +} + +void nsMIMEInfoAndroid::AddUniqueExtension(const nsACString& aExtension) { + if (!aExtension.IsEmpty() && + !mExtensions.Contains(aExtension, + nsCaseInsensitiveCStringArrayComparator())) { + mExtensions.AppendElement(aExtension); + } +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::AppendExtension(const nsACString& aExtension) { + MOZ_ASSERT(!aExtension.IsEmpty(), "No extension"); + AddUniqueExtension(aExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPrimaryExtension(nsACString& aPrimaryExtension) { + if (!mExtensions.Length()) { + aPrimaryExtension.Truncate(); + return NS_ERROR_NOT_INITIALIZED; + } + aPrimaryExtension = mExtensions[0]; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetPrimaryExtension(const nsACString& aExtension) { + if (MOZ_UNLIKELY(aExtension.IsEmpty())) { + // Don't assert since Java may return an empty extension for unknown types. + return NS_ERROR_INVALID_ARG; + } + int32_t i = mExtensions.IndexOf(aExtension, 0, + nsCaseInsensitiveCStringArrayComparator()); + if (i != -1) { + mExtensions.RemoveElementAt(i); + } + mExtensions.InsertElementAt(0, aExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetMIMEType(nsACString& aMIMEType) { + aMIMEType.Assign(mType); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::Equals(nsIMIMEInfo* aMIMEInfo, bool* aRetVal) { + if (!aMIMEInfo) return NS_ERROR_NULL_POINTER; + + nsAutoCString type; + nsresult rv = aMIMEInfo->GetMIMEType(type); + if (NS_FAILED(rv)) return rv; + + *aRetVal = mType.Equals(type); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPossibleLocalHandlers(nsIArray** aPossibleLocalHandlers) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::LaunchWithFile(nsIFile* aFile) { + nsCOMPtr<nsIURI> uri; + NS_NewFileURI(getter_AddRefs(uri), aFile); + return LoadUriInternal(uri); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::IsCurrentAppOSDefault(bool* aRetVal) { + // FIXME: this should in theory be meaningfully implemented. However, android + // implements its own version of nsIHandlerApp instances which internally + // have package and class names - but do not expose those. So to meaningfully + // compare the handler app would require access to those and knowing what + // our own package/class names are, and it's not clear how to do that. + // It also seems less important to do this right on Android, given that + // Android UI normally limits what apps you can associate with what files, so + // it shouldn't be possible to get into the same kind of loop as on desktop. + *aRetVal = false; + return NS_OK; +} + +nsMIMEInfoAndroid::nsMIMEInfoAndroid(const nsACString& aMIMEType) + : mType(aMIMEType), + mAlwaysAsk(true), + mPrefAction(nsIMIMEInfo::useHelperApp) { + mPrefApp = new nsMIMEInfoAndroid::SystemChooser(this); + nsresult rv; + mHandlerApps = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + mHandlerApps->AppendElement(mPrefApp); +} + +#define SYSTEMCHOOSER_NAME u"Android chooser" +#define SYSTEMCHOOSER_DESCRIPTION \ + u"Android's default handler application chooser" + +NS_IMPL_ISUPPORTS(nsMIMEInfoAndroid::SystemChooser, nsIHandlerApp) + +nsresult nsMIMEInfoAndroid::SystemChooser::GetName(nsAString& aName) { + aName.AssignLiteral(SYSTEMCHOOSER_NAME); + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::SetName(const nsAString&) { + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::GetDetailedDescription( + nsAString& aDesc) { + aDesc.AssignLiteral(SYSTEMCHOOSER_DESCRIPTION); + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::SetDetailedDescription( + const nsAString&) { + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::Equals(nsIHandlerApp* aHandlerApp, + bool* aRetVal) { + *aRetVal = false; + if (!aHandlerApp) { + return NS_OK; + } + + nsAutoString name; + nsAutoString detailedDescription; + aHandlerApp->GetName(name); + aHandlerApp->GetDetailedDescription(detailedDescription); + + *aRetVal = name.Equals(SYSTEMCHOOSER_NAME) && + detailedDescription.Equals(SYSTEMCHOOSER_DESCRIPTION); + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext*) { + return mOuter->LoadUriInternal(aURI); +} diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.h b/uriloader/exthandler/android/nsMIMEInfoAndroid.h new file mode 100644 index 0000000000..bdac8f97f9 --- /dev/null +++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.h @@ -0,0 +1,62 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoAndroid_h +#define nsMIMEInfoAndroid_h + +#include "nsMIMEInfoImpl.h" +#include "nsIMutableArray.h" +#include "nsAndroidHandlerApp.h" + +class nsMIMEInfoAndroid final : public nsIMIMEInfo { + public: + [[nodiscard]] static bool GetMimeInfoForMimeType( + const nsACString& aMimeType, nsMIMEInfoAndroid** aMimeInfo); + [[nodiscard]] static bool GetMimeInfoForFileExt( + const nsACString& aFileExt, nsMIMEInfoAndroid** aMimeInfo); + + [[nodiscard]] static nsresult GetMimeInfoForURL(const nsACString& aURL, + bool* found, + nsIHandlerInfo** info); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMIMEINFO + NS_DECL_NSIHANDLERINFO + + explicit nsMIMEInfoAndroid(const nsACString& aMIMEType); + + private: + ~nsMIMEInfoAndroid() {} + + /** + * Internal helper to avoid adding duplicates. + */ + void AddUniqueExtension(const nsACString& aExtension); + + [[nodiscard]] virtual nsresult LaunchDefaultWithFile(nsIFile* aFile); + [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI); + nsCOMPtr<nsIMutableArray> mHandlerApps; + nsCString mType; + nsTArray<nsCString> mExtensions; + bool mAlwaysAsk; + nsHandlerInfoAction mPrefAction; + nsString mDescription; + nsCOMPtr<nsIHandlerApp> mPrefApp; + + public: + class SystemChooser final : public nsIHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + explicit SystemChooser(nsMIMEInfoAndroid* aOuter) : mOuter(aOuter) {} + + private: + ~SystemChooser() {} + + nsMIMEInfoAndroid* mOuter; + }; +}; + +#endif /* nsMIMEInfoAndroid_h */ diff --git a/uriloader/exthandler/android/nsOSHelperAppService.cpp b/uriloader/exthandler/android/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..9849b66075 --- /dev/null +++ b/uriloader/exthandler/android/nsOSHelperAppService.cpp @@ -0,0 +1,70 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" +#include "nsMIMEInfoAndroid.h" +#include "AndroidBridge.h" + +nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() {} + +nsOSHelperAppService::~nsOSHelperAppService() {} + +nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + RefPtr<nsMIMEInfoAndroid> mimeInfo; + *aFound = false; + if (!aMIMEType.IsEmpty()) + *aFound = nsMIMEInfoAndroid::GetMimeInfoForMimeType( + aMIMEType, getter_AddRefs(mimeInfo)); + if (!*aFound) + *aFound = nsMIMEInfoAndroid::GetMimeInfoForFileExt( + aFileExt, getter_AddRefs(mimeInfo)); + + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + if (!*aFound) mimeInfo = new nsMIMEInfoAndroid(aMIMEType); + + mimeInfo.forget(aMIMEInfo); + return NS_OK; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aScheme, + bool* aExists) { + // Support any URI barring a couple schemes we use in testing; let the + // app decide what to do with them. + nsAutoCString scheme(aScheme); + *aExists = + !scheme.Equals("unsupported"_ns) && !scheme.Equals("unknownextproto"_ns); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsOSHelperAppService::GetProtocolHandlerInfoFromOS( + const nsACString& aScheme, bool* found, nsIHandlerInfo** info) { + // We don't want to get protocol handlers from the OS in GV; the app + // should take care of that in NavigationDelegate.onLoadRequest(). + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsIHandlerApp* nsOSHelperAppService::CreateAndroidHandlerApp( + const nsAString& aName, const nsAString& aDescription, + const nsAString& aPackageName, const nsAString& aClassName, + const nsACString& aMimeType, const nsAString& aAction) { + return new nsAndroidHandlerApp(aName, aDescription, aPackageName, aClassName, + aMimeType, aAction); +} diff --git a/uriloader/exthandler/android/nsOSHelperAppService.h b/uriloader/exthandler/android/nsOSHelperAppService.h new file mode 100644 index 0000000000..a333c4bcd3 --- /dev/null +++ b/uriloader/exthandler/android/nsOSHelperAppService.h @@ -0,0 +1,38 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h +#define nsOSHelperAppService_h + +#include "nsCExternalHandlerService.h" +#include "nsExternalHelperAppService.h" + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + nsresult GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + + [[nodiscard]] nsresult OSProtocolHandlerExists(const char* aScheme, + bool* aExists) override; + + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + static nsIHandlerApp* CreateAndroidHandlerApp( + const nsAString& aName, const nsAString& aDescription, + const nsAString& aPackageName, const nsAString& aClassName, + const nsACString& aMimeType, const nsAString& aAction = u""_ns); +}; + +#endif /* nsOSHelperAppService_h */ diff --git a/uriloader/exthandler/components.conf b/uriloader/exthandler/components.conf new file mode 100644 index 0000000000..1379c13ffd --- /dev/null +++ b/uriloader/exthandler/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{8b1ae382-51a9-4972-b930-56977a57919d}', + 'contract_ids': ['@mozilla.org/uriloader/web-handler-app;1'], + 'jsm': 'resource://gre/modules/WebHandlerApp.jsm', + 'constructor': 'nsWebHandlerApp', + }, +] diff --git a/uriloader/exthandler/docs/index.rst b/uriloader/exthandler/docs/index.rst new file mode 100644 index 0000000000..40088ab6d3 --- /dev/null +++ b/uriloader/exthandler/docs/index.rst @@ -0,0 +1,76 @@ +.. _external_helper_app_service: + +External Helper App Service +=========================== + +The external helper app service is responsible for deciding how to handle an +attempt to load come content that cannot be loaded by the browser itself. + +Part of this involves using the Handler Service which manages the users +preferences for what to do by default with different content. + +When a Link is Clicked +---------------------- + +When a link in a page is clicked (or a form submitted) ``nsDocShell`` tests +whether the target protocol can be loaded by the browser itself, this is based +on the preferences under ``network.protocol-handler``. When the browser cannot +load the protocol it calls into ``nsExternalHelperAppService::LoadURI``. + +Some validation checks are performed but ultimateley we look for a registered +protocol handler. First the OS is queried for an app registration for the +protocol and then the handler server is asked to fill in any user settings from +the internal database. If there were no settings from the handler service then +some defaults are applied in ``nsExternalHelperAppService::SetProtocolHandlerDefaults``. + +If there is a default handler app chosen and the settings say to use it without +asking then that happens. If not a dialog s shown asking the user what they +want to do. + +During a Load +------------- + +When content is already being loaded the :ref:`URI Loader Service <uri_loader_service>` +determines whether the browser can handle the content or not. If not it calls +into the external helper app server through ``nsExternalHelperAppService::DoContent``. + +The content type of the loading content is retrieved from the channel. A file +extension is also generated using the Content-Disposition header or if the load +is not a HTTP POST request the file extension is generated from the requested URL. + +We then query the MIME Service for an nsIMIMEInfo to find information about +apps that can handle the content type or file extension based on OS and user +settings, :ref:`see below <mime_service>` for further details. The result is +used to create a ``nsExternalAppHandler`` which is then used as a stream listener +for the content. + +The MIME info object contains settings that control whether to prompt the user +before doing anything and what the default action should be. If we need to ask +the user then a dialog is shown offering to let users cancel the load, save the +content to disk or send it to a registered application handler. + +Assuming the load isn't canceled the content is streamed to disk using a background +file saver with a target ``nsITransfer``. The ``nsITransfer`` is responsible for +showing the download in the UI. + +If the user opted to open the file with an application then once the transfer is +complete then ``nsIMIMEInfo::LaunchWithFile`` is used to +`launch the application <https://searchfox.org/mozilla-central/search?q=nsIMIMEInfo%3A%3ALaunchWithFile&path=>`_. + +.. _mime_service: + +MIME Service +------------ + +The MIME service is responsible for getting an ``nsIMIMEInfo`` object for a +content type or file extension: + +1. Fills out an ``nsIMIMEInfo`` based on OS provided information. This is platform + specific but should try to find the default application registered to handle + the content. +2. Ask the handler service to fill out the ``nsIMIMEInfo`` with information held + in browser settings. This will not overwrite a any application found from + the OS. +3. If one has not been found already then try to find a type description from + a `lookup table <https://searchfox.org/mozilla-central/search?q=extraMimeEntries[]&path=>`_ + or just by appending " File" to the file extension. diff --git a/uriloader/exthandler/mac/nsDecodeAppleFile.cpp b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp new file mode 100644 index 0000000000..29c1adf941 --- /dev/null +++ b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp @@ -0,0 +1,357 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsDecodeAppleFile.h" +#include "nsCRT.h" + +NS_IMPL_ADDREF(nsDecodeAppleFile) +NS_IMPL_RELEASE(nsDecodeAppleFile) + +NS_INTERFACE_MAP_BEGIN(nsDecodeAppleFile) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStream) + NS_INTERFACE_MAP_ENTRY(nsIOutputStream) +NS_INTERFACE_MAP_END + +nsDecodeAppleFile::nsDecodeAppleFile() { + m_state = parseHeaders; + m_dataBufferLength = 0; + m_dataBuffer = (unsigned char*)malloc(MAX_BUFFERSIZE); + m_entries = nullptr; + m_rfRefNum = -1; + m_totalDataForkWritten = 0; + m_totalResourceForkWritten = 0; + m_headerOk = false; + + m_comment[0] = 0; + memset(&m_dates, 0, sizeof(m_dates)); + memset(&m_finderInfo, 0, sizeof(m_dates)); + memset(&m_finderExtraInfo, 0, sizeof(m_dates)); +} + +nsDecodeAppleFile::~nsDecodeAppleFile() { + free(m_dataBuffer); + m_dataBuffer = nullptr; + if (m_entries) delete[] m_entries; +} + +NS_IMETHODIMP nsDecodeAppleFile::Initialize(nsIOutputStream* output, + nsIFile* file) { + m_output = output; + + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(file); + macFile->GetTargetFSSpec(&m_fsFileSpec); + + m_offset = 0; + m_dataForkOffset = 0; + + return NS_OK; +} + +NS_IMETHODIMP nsDecodeAppleFile::Close(void) { + nsresult rv; + rv = m_output->Close(); + + int32_t i; + + if (m_rfRefNum != -1) FSClose(m_rfRefNum); + + /* Check if the file is complete and if it's the case, write file attributes + */ + if (m_headerOk) { + bool dataOk = true; /* It's ok if the file doesn't have a datafork, + therefore set it to true by default. */ + if (m_headers.magic == APPLESINGLE_MAGIC) { + for (i = 0; i < m_headers.entriesCount; i++) + if (ENT_DFORK == m_entries[i].id) { + dataOk = (bool)(m_totalDataForkWritten == m_entries[i].length); + break; + } + } + + bool resourceOk = FALSE; + for (i = 0; i < m_headers.entriesCount; i++) + if (ENT_RFORK == m_entries[i].id) { + resourceOk = (bool)(m_totalResourceForkWritten == m_entries[i].length); + break; + } + + if (dataOk && resourceOk) { + HFileInfo* fpb; + CInfoPBRec cipbr; + + fpb = (HFileInfo*)&cipbr; + fpb->ioVRefNum = m_fsFileSpec.vRefNum; + fpb->ioDirID = m_fsFileSpec.parID; + fpb->ioNamePtr = m_fsFileSpec.name; + fpb->ioFDirIndex = 0; + PBGetCatInfoSync(&cipbr); + + /* set finder info */ + memcpy(&fpb->ioFlFndrInfo, &m_finderInfo, sizeof(FInfo)); + memcpy(&fpb->ioFlXFndrInfo, &m_finderExtraInfo, sizeof(FXInfo)); + fpb->ioFlFndrInfo.fdFlags &= + 0xfc00; /* clear flags maintained by finder */ + + /* set file dates */ + fpb->ioFlCrDat = m_dates.create - CONVERT_TIME; + fpb->ioFlMdDat = m_dates.modify - CONVERT_TIME; + fpb->ioFlBkDat = m_dates.backup - CONVERT_TIME; + + /* update file info */ + fpb->ioDirID = fpb->ioFlParID; + PBSetCatInfoSync(&cipbr); + + /* set comment */ + IOParam vinfo; + GetVolParmsInfoBuffer vp; + DTPBRec dtp; + + memset((void*)&vinfo, 0, sizeof(vinfo)); + vinfo.ioVRefNum = fpb->ioVRefNum; + vinfo.ioBuffer = (Ptr)&vp; + vinfo.ioReqCount = sizeof(vp); + if (PBHGetVolParmsSync((HParmBlkPtr)&vinfo) == noErr && + ((vp.vMAttrib >> bHasDesktopMgr) & 1)) { + memset((void*)&dtp, 0, sizeof(dtp)); + dtp.ioVRefNum = fpb->ioVRefNum; + if (PBDTGetPath(&dtp) == noErr) { + dtp.ioDTBuffer = (Ptr)&m_comment[1]; + dtp.ioNamePtr = fpb->ioNamePtr; + dtp.ioDirID = fpb->ioDirID; + dtp.ioDTReqCount = m_comment[0]; + if (PBDTSetCommentSync(&dtp) == noErr) PBDTFlushSync(&dtp); + } + } + } + } + + return rv; +} + +NS_IMETHODIMP nsDecodeAppleFile::Flush(void) { return m_output->Flush(); } + +NS_IMETHODIMP nsDecodeAppleFile::WriteFrom(nsIInputStream* inStr, + uint32_t count, uint32_t* _retval) { + return m_output->WriteFrom(inStr, count, _retval); +} + +NS_IMETHODIMP nsDecodeAppleFile::WriteSegments(nsReadSegmentFun reader, + void* closure, uint32_t count, + uint32_t* _retval) { + return m_output->WriteSegments(reader, closure, count, _retval); +} + +NS_IMETHODIMP nsDecodeAppleFile::IsNonBlocking(bool* aNonBlocking) { + return m_output->IsNonBlocking(aNonBlocking); +} + +NS_IMETHODIMP nsDecodeAppleFile::Write(const char* buffer, uint32_t bufferSize, + uint32_t* writeCount) { + /* WARNING: to simplify my life, I presume that I should get all appledouble + headers in the first block, else I would have to implement a buffer */ + + const char* buffPtr = buffer; + uint32_t dataCount; + int32_t i; + nsresult rv = NS_OK; + + *writeCount = 0; + + while (bufferSize > 0 && NS_SUCCEEDED(rv)) { + switch (m_state) { + case parseHeaders: + dataCount = sizeof(ap_header) - m_dataBufferLength; + if (dataCount > bufferSize) dataCount = bufferSize; + memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount); + m_dataBufferLength += dataCount; + + if (m_dataBufferLength == sizeof(ap_header)) { + memcpy(&m_headers, m_dataBuffer, sizeof(ap_header)); + + /* Check header to be sure we are dealing with the right kind of data, + * else just write it to the data fork. */ + if ((m_headers.magic == APPLEDOUBLE_MAGIC || + m_headers.magic == APPLESINGLE_MAGIC) && + m_headers.version == VERSION && m_headers.entriesCount) { + /* Just to be sure, the filler must contains only 0 */ + for (i = 0; i < 4 && m_headers.fill[i] == 0L; i++) + ; + if (i == 4) m_state = parseEntries; + } + m_dataBufferLength = 0; + + if (m_state == parseHeaders) { + dataCount = 0; + m_state = parseWriteThrough; + } + } + break; + + case parseEntries: + if (!m_entries) { + m_entries = new ap_entry[m_headers.entriesCount]; + if (!m_entries) return NS_ERROR_OUT_OF_MEMORY; + } + uint32_t entriesSize = sizeof(ap_entry) * m_headers.entriesCount; + dataCount = entriesSize - m_dataBufferLength; + if (dataCount > bufferSize) dataCount = bufferSize; + memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount); + m_dataBufferLength += dataCount; + + if (m_dataBufferLength == entriesSize) { + for (i = 0; i < m_headers.entriesCount; i++) { + memcpy(&m_entries[i], &m_dataBuffer[i * sizeof(ap_entry)], + sizeof(ap_entry)); + if (m_headers.magic == APPLEDOUBLE_MAGIC) { + uint32_t offset = m_entries[i].offset + m_entries[i].length; + if (offset > m_dataForkOffset) m_dataForkOffset = offset; + } + } + m_headerOk = true; + m_state = parseLookupPart; + } + break; + + case parseLookupPart: + /* which part are we parsing? */ + m_currentPartID = -1; + for (i = 0; i < m_headers.entriesCount; i++) + if (m_offset == m_entries[i].offset && m_entries[i].length) { + m_currentPartID = m_entries[i].id; + m_currentPartLength = m_entries[i].length; + m_currentPartCount = 0; + + switch (m_currentPartID) { + case ENT_DFORK: + m_state = parseDataFork; + break; + case ENT_RFORK: + m_state = parseResourceFork; + break; + + case ENT_COMMENT: + case ENT_DATES: + case ENT_FINFO: + m_dataBufferLength = 0; + m_state = parsePart; + break; + + default: + m_state = parseSkipPart; + break; + } + break; + } + + if (m_currentPartID == -1) { + /* maybe is the datafork of an appledouble file? */ + if (m_offset == m_dataForkOffset) { + m_currentPartID = ENT_DFORK; + m_currentPartLength = -1; + m_currentPartCount = 0; + m_state = parseDataFork; + } else + dataCount = 1; + } + break; + + case parsePart: + dataCount = m_currentPartLength - m_dataBufferLength; + if (dataCount > bufferSize) dataCount = bufferSize; + memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount); + m_dataBufferLength += dataCount; + + if (m_dataBufferLength == m_currentPartLength) { + switch (m_currentPartID) { + case ENT_COMMENT: + m_comment[0] = + m_currentPartLength > 255 ? 255 : m_currentPartLength; + memcpy(&m_comment[1], buffPtr, m_comment[0]); + break; + case ENT_DATES: + if (m_currentPartLength == sizeof(m_dates)) + memcpy(&m_dates, buffPtr, m_currentPartLength); + break; + case ENT_FINFO: + if (m_currentPartLength == + (sizeof(m_finderInfo) + sizeof(m_finderExtraInfo))) { + memcpy(&m_finderInfo, buffPtr, sizeof(m_finderInfo)); + memcpy(&m_finderExtraInfo, buffPtr + sizeof(m_finderInfo), + sizeof(m_finderExtraInfo)); + } + break; + } + m_state = parseLookupPart; + } + break; + + case parseSkipPart: + dataCount = m_currentPartLength - m_currentPartCount; + if (dataCount > bufferSize) + dataCount = bufferSize; + else + m_state = parseLookupPart; + break; + + case parseDataFork: + if (m_headers.magic == APPLEDOUBLE_MAGIC) + dataCount = bufferSize; + else { + dataCount = m_currentPartLength - m_currentPartCount; + if (dataCount > bufferSize) + dataCount = bufferSize; + else + m_state = parseLookupPart; + } + + if (m_output) { + uint32_t writeCount; + rv = m_output->Write((const char*)buffPtr, dataCount, &writeCount); + if (dataCount != writeCount) rv = NS_ERROR_FAILURE; + m_totalDataForkWritten += dataCount; + } + + break; + + case parseResourceFork: + dataCount = m_currentPartLength - m_currentPartCount; + if (dataCount > bufferSize) + dataCount = bufferSize; + else + m_state = parseLookupPart; + + if (m_rfRefNum == -1) { + if (noErr != FSpOpenRF(&m_fsFileSpec, fsWrPerm, &m_rfRefNum)) + return NS_ERROR_FAILURE; + } + + long count = dataCount; + if (noErr != FSWrite(m_rfRefNum, &count, buffPtr) || count != dataCount) + return NS_ERROR_FAILURE; + m_totalResourceForkWritten += dataCount; + break; + + case parseWriteThrough: + dataCount = bufferSize; + if (m_output) { + uint32_t writeCount; + rv = m_output->Write((const char*)buffPtr, dataCount, &writeCount); + if (dataCount != writeCount) rv = NS_ERROR_FAILURE; + } + break; + } + + if (dataCount) { + *writeCount += dataCount; + bufferSize -= dataCount; + buffPtr += dataCount; + m_currentPartCount += dataCount; + m_offset += dataCount; + dataCount = 0; + } + } + + return rv; +} diff --git a/uriloader/exthandler/mac/nsDecodeAppleFile.h b/uriloader/exthandler/mac/nsDecodeAppleFile.h new file mode 100644 index 0000000000..22279a8a49 --- /dev/null +++ b/uriloader/exthandler/mac/nsDecodeAppleFile.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDecodeAppleFile_h__ +#define nsDecodeAppleFile_h__ + +#include "nscore.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" + +/* +** applefile definitions used +*/ +#if PRAGMA_STRUCT_ALIGN +# pragma options align = mac68k +#endif + +#define APPLESINGLE_MAGIC 0x00051600L +#define APPLEDOUBLE_MAGIC 0x00051607L +#define VERSION 0x00020000 + +#define NUM_ENTRIES 6 + +#define ENT_DFORK 1L +#define ENT_RFORK 2L +#define ENT_NAME 3L +#define ENT_COMMENT 4L +#define ENT_DATES 8L +#define ENT_FINFO 9L + +#define CONVERT_TIME 1265437696L + +/* +** data type used in the header decoder. +*/ +typedef struct ap_header { + int32_t magic; + int32_t version; + int32_t fill[4]; + int16_t entriesCount; + +} ap_header; + +typedef struct ap_entry { + int32_t id; + int32_t offset; + int32_t length; + +} ap_entry; + +typedef struct ap_dates { + int32_t create, modify, backup, access; + +} ap_dates; + +#if PRAGMA_STRUCT_ALIGN +# pragma options align = reset +#endif + +/* +**Error codes +*/ +enum { errADNotEnoughData = -12099, errADNotSupported, errADBadVersion }; + +class nsDecodeAppleFile : public nsIOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + + nsDecodeAppleFile(); + virtual ~nsDecodeAppleFile(); + + [[nodiscard]] nsresult Initialize(nsIOutputStream* output, nsIFile* file); + + private: +#define MAX_BUFFERSIZE 1024 + enum ParserState { + parseHeaders, + parseEntries, + parseLookupPart, + parsePart, + parseSkipPart, + parseDataFork, + parseResourceFork, + parseWriteThrough + }; + + nsCOMPtr<nsIOutputStream> m_output; + FSSpec m_fsFileSpec; + SInt16 m_rfRefNum; + + unsigned char* m_dataBuffer; + int32_t m_dataBufferLength; + ParserState m_state; + ap_header m_headers; + ap_entry* m_entries; + int32_t m_offset; + int32_t m_dataForkOffset; + int32_t m_totalDataForkWritten; + int32_t m_totalResourceForkWritten; + bool m_headerOk; + + int32_t m_currentPartID; + int32_t m_currentPartLength; + int32_t m_currentPartCount; + + Str255 m_comment; + ap_dates m_dates; + FInfo m_finderInfo; + FXInfo m_finderExtraInfo; +}; + +#endif diff --git a/uriloader/exthandler/mac/nsLocalHandlerAppMac.h b/uriloader/exthandler/mac/nsLocalHandlerAppMac.h new file mode 100644 index 0000000000..7c77091b42 --- /dev/null +++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.h @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSLOCALHANDLERAPPMAC_H_ +#define NSLOCALHANDLERAPPMAC_H_ + +#include "nsLocalHandlerApp.h" + +class nsLocalHandlerAppMac : public nsLocalHandlerApp { + public: + nsLocalHandlerAppMac() {} + + nsLocalHandlerAppMac(const char16_t* aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + nsLocalHandlerAppMac(const nsAString& aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + virtual ~nsLocalHandlerAppMac() {} + + NS_IMETHOD LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override; + NS_IMETHOD GetName(nsAString& aName) override; +}; + +#endif /*NSLOCALHANDLERAPPMAC_H_*/ diff --git a/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm new file mode 100644 index 0000000000..7cb09a6622 --- /dev/null +++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <CoreFoundation/CoreFoundation.h> +#import <ApplicationServices/ApplicationServices.h> + +#include "nsObjCExceptions.h" +#include "nsLocalHandlerAppMac.h" +#include "nsILocalFileMac.h" +#include "nsIURI.h" + +// We override this to make sure app bundles display their pretty name (without .app suffix) +NS_IMETHODIMP nsLocalHandlerAppMac::GetName(nsAString& aName) { + if (mExecutable) { + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(mExecutable); + if (macFile) { + bool isPackage; + (void)macFile->IsPackage(&isPackage); + if (isPackage) return macFile->GetBundleDisplayName(aName); + } + } + + return nsLocalHandlerApp::GetName(aName); +} + +/** + * mostly copy/pasted from nsMacShellService.cpp (which is in browser/, + * so we can't depend on it here). This code probably really wants to live + * somewhere more central (see bug 389922). + */ +NS_IMETHODIMP +nsLocalHandlerAppMac::LaunchWithURI(nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv; + nsCOMPtr<nsILocalFileMac> lfm(do_QueryInterface(mExecutable, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + CFURLRef appURL; + rv = lfm->GetCFURL(&appURL); + if (NS_FAILED(rv)) return rv; + + nsAutoCString uriSpec; + aURI->GetAsciiSpec(uriSpec); + + const UInt8* uriString = reinterpret_cast<const UInt8*>(uriSpec.get()); + CFURLRef uri = + ::CFURLCreateWithBytes(NULL, uriString, uriSpec.Length(), kCFStringEncodingUTF8, NULL); + if (!uri) { + ::CFRelease(appURL); + return NS_ERROR_OUT_OF_MEMORY; + } + + CFArrayRef uris = ::CFArrayCreate(NULL, reinterpret_cast<const void**>(&uri), 1, NULL); + if (!uris) { + ::CFRelease(uri); + ::CFRelease(appURL); + return NS_ERROR_OUT_OF_MEMORY; + } + + LSLaunchURLSpec launchSpec; + launchSpec.appURL = appURL; + launchSpec.itemURLs = uris; + launchSpec.passThruParams = NULL; + launchSpec.launchFlags = kLSLaunchDefaults; + launchSpec.asyncRefCon = NULL; + + OSErr err = ::LSOpenFromURLSpec(&launchSpec, NULL); + + ::CFRelease(uris); + ::CFRelease(uri); + ::CFRelease(appURL); + + return err != noErr ? NS_ERROR_FAILURE : NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/uriloader/exthandler/mac/nsMIMEInfoMac.h b/uriloader/exthandler/mac/nsMIMEInfoMac.h new file mode 100644 index 0000000000..2b87601573 --- /dev/null +++ b/uriloader/exthandler/mac/nsMIMEInfoMac.h @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoMac_h_ +#define nsMIMEInfoMac_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoMac : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoMac(const char* aMIMEType = "") + : nsMIMEInfoImpl(aMIMEType) {} + explicit nsMIMEInfoMac(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoMac(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + + protected: + [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI) override; +#ifdef DEBUG + [[nodiscard]] virtual nsresult LaunchDefaultWithFile( + nsIFile* aFile) override { + MOZ_ASSERT_UNREACHABLE("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override; +}; + +#endif diff --git a/uriloader/exthandler/mac/nsMIMEInfoMac.mm b/uriloader/exthandler/mac/nsMIMEInfoMac.mm new file mode 100644 index 0000000000..ab4db322af --- /dev/null +++ b/uriloader/exthandler/mac/nsMIMEInfoMac.mm @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <ApplicationServices/ApplicationServices.h> + +#include "nsComponentManagerUtils.h" +#include "nsObjCExceptions.h" +#include "nsMIMEInfoMac.h" +#include "nsILocalFileMac.h" + +// We override this to make sure app bundles display their pretty name (without .app suffix) +NS_IMETHODIMP nsMIMEInfoMac::GetDefaultDescription(nsAString& aDefaultDescription) { + if (mDefaultApplication) { + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(mDefaultApplication); + if (macFile) { + bool isPackage; + (void)macFile->IsPackage(&isPackage); + if (isPackage) return macFile->GetBundleDisplayName(aDefaultDescription); + } + } + + return nsMIMEInfoImpl::GetDefaultDescription(aDefaultDescription); +} + +NS_IMETHODIMP +nsMIMEInfoMac::LaunchWithFile(nsIFile* aFile) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsCOMPtr<nsIFile> application; + nsresult rv; + + NS_ASSERTION(mClass == eMIMEInfo, "only MIME infos are currently allowed" + "to pass content by value"); + + if (mPreferredAction == useHelperApp) { + // we don't yet support passing content by value (rather than reference) + // to web apps. at some point, we will probably want to. + nsCOMPtr<nsILocalHandlerApp> localHandlerApp = do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = localHandlerApp->GetExecutable(getter_AddRefs(application)); + NS_ENSURE_SUCCESS(rv, rv); + + } else if (mPreferredAction == useSystemDefault) { + application = mDefaultApplication; + } else + return NS_ERROR_INVALID_ARG; + + // if we've already got an app, just QI so we have the launchWithDoc method + nsCOMPtr<nsILocalFileMac> app; + if (application) { + app = do_QueryInterface(application, &rv); + if (NS_FAILED(rv)) return rv; + } else { + // otherwise ask LaunchServices for an app directly + nsCOMPtr<nsILocalFileMac> tempFile = do_QueryInterface(aFile, &rv); + if (NS_FAILED(rv)) return rv; + + FSRef tempFileRef; + tempFile->GetFSRef(&tempFileRef); + + FSRef appFSRef; + if (::LSGetApplicationForItem(&tempFileRef, kLSRolesAll, &appFSRef, nullptr) == noErr) { + app = (do_CreateInstance("@mozilla.org/file/local;1")); + if (!app) return NS_ERROR_FAILURE; + app->InitWithFSRef(&appFSRef); + } else { + return NS_ERROR_FAILURE; + } + } + return app->LaunchWithDoc(aFile, false); + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsMIMEInfoMac::LoadUriInternal(nsIURI* aURI) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NS_ENSURE_ARG_POINTER(aURI); + + nsresult rv = NS_ERROR_FAILURE; + + nsAutoCString uri; + aURI->GetSpec(uri); + if (!uri.IsEmpty()) { + CFURLRef myURLRef = ::CFURLCreateWithBytes(kCFAllocatorDefault, (const UInt8*)uri.get(), + strlen(uri.get()), kCFStringEncodingUTF8, NULL); + if (myURLRef) { + OSStatus status = ::LSOpenCFURLRef(myURLRef, NULL); + if (status == noErr) rv = NS_OK; + ::CFRelease(myURLRef); + } + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/uriloader/exthandler/mac/nsOSHelperAppService.h b/uriloader/exthandler/mac/nsOSHelperAppService.h new file mode 100644 index 0000000000..aa97fee5b4 --- /dev/null +++ b/uriloader/exthandler/mac/nsOSHelperAppService.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. This is the Mac version. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsIMimeInfo; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + nsresult GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + [[nodiscard]] nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + [[nodiscard]] nsresult OSProtocolHandlerExists(const char* aScheme, + bool* aHandlerExists) override; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/mac/nsOSHelperAppService.mm b/uriloader/exthandler/mac/nsOSHelperAppService.mm new file mode 100644 index 0000000000..5585fb67d2 --- /dev/null +++ b/uriloader/exthandler/mac/nsOSHelperAppService.mm @@ -0,0 +1,591 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <sys/stat.h> +#include "mozilla/net/NeckoCommon.h" +#include "nsComponentManagerUtils.h" +#include "nsOSHelperAppService.h" +#include "nsObjCExceptions.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIFile.h" +#include "nsILocalFileMac.h" +#include "nsMimeTypes.h" +#include "nsMemory.h" +#include "nsCRT.h" +#include "nsMIMEInfoMac.h" +#include "nsEmbedCID.h" + +#import <CoreFoundation/CoreFoundation.h> +#import <ApplicationServices/ApplicationServices.h> + +// chrome URL's +#define HELPERAPPLAUNCHER_BUNDLE_URL "chrome://global/locale/helperAppLauncher.properties" +#define BRAND_BUNDLE_URL "chrome://branding/locale/brand.properties" + +nsresult GetDefaultBundleURL(const nsACString& aScheme, CFURLRef* aBundleURL) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + CFStringRef schemeCFString = ::CFStringCreateWithBytes( + kCFAllocatorDefault, (const UInt8*)PromiseFlatCString(aScheme).get(), aScheme.Length(), + kCFStringEncodingUTF8, false); + + if (schemeCFString) { + CFStringRef lookupCFString = + ::CFStringCreateWithFormat(NULL, NULL, CFSTR("%@:"), schemeCFString); + + if (lookupCFString) { + CFURLRef lookupCFURL = ::CFURLCreateWithString(NULL, lookupCFString, NULL); + + if (lookupCFURL) { + if (@available(macOS 10.10, *)) { + *aBundleURL = ::LSCopyDefaultApplicationURLForURL(lookupCFURL, kLSRolesAll, NULL); + if (*aBundleURL) { + rv = NS_OK; + } + } else { + OSStatus theErr = ::LSGetApplicationForURL(lookupCFURL, kLSRolesAll, NULL, aBundleURL); + if (theErr == noErr && *aBundleURL) { + rv = NS_OK; + } + } + + ::CFRelease(lookupCFURL); + } + + ::CFRelease(lookupCFString); + } + + ::CFRelease(schemeCFString); + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +using mozilla::LogLevel; + +/* This is an undocumented interface (in the Foundation framework) that has + * been stable since at least 10.2.8 and is still present on SnowLeopard. + * Furthermore WebKit has three public methods (in WebKitSystemInterface.h) + * that are thin wrappers around this interface's last three methods. So + * it's unlikely to change anytime soon. Now that we're no longer using + * Internet Config Services, this is the only way to look up a MIME type + * from an extension, or vice versa. + */ +@class NSURLFileTypeMappingsInternal; + +@interface NSURLFileTypeMappings : NSObject { + NSURLFileTypeMappingsInternal* _internal; +} + ++ (NSURLFileTypeMappings*)sharedMappings; +- (NSString*)MIMETypeForExtension:(NSString*)aString; +- (NSString*)preferredExtensionForMIMEType:(NSString*)aString; +- (NSArray*)extensionsForMIMEType:(NSString*)aString; +@end + +nsOSHelperAppService::~nsOSHelperAppService() {} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) { + // CFStringCreateWithBytes() can fail even if we're not out of memory -- + // for example if the 'bytes' parameter is something very weird (like + // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's + // specified in the 'encoding' parameter. See bug 548719. + CFStringRef schemeString = + ::CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)aProtocolScheme, + strlen(aProtocolScheme), kCFStringEncodingUTF8, false); + if (schemeString) { + // LSCopyDefaultHandlerForURLScheme() can fail to find the default handler + // for aProtocolScheme when it's never been explicitly set (using + // LSSetDefaultHandlerForURLScheme()). For example, Safari is the default + // handler for the "http" scheme on a newly installed copy of OS X. But + // this (presumably) wasn't done using LSSetDefaultHandlerForURLScheme(), + // so LSCopyDefaultHandlerForURLScheme() will fail to find Safari. To get + // around this we use LSCopyAllHandlersForURLScheme() instead -- which seems + // never to fail. + // http://lists.apple.com/archives/Carbon-dev/2007/May/msg00349.html + // http://www.realsoftware.com/listarchives/realbasic-nug/2008-02/msg00119.html + CFArrayRef handlerArray = ::LSCopyAllHandlersForURLScheme(schemeString); + *aHandlerExists = !!handlerArray; + if (handlerArray) ::CFRelease(handlerArray); + ::CFRelease(schemeString); + } else { + *aHandlerExists = false; + } + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + CFURLRef handlerBundleURL; + rv = GetDefaultBundleURL(aScheme, &handlerBundleURL); + + if (NS_SUCCEEDED(rv) && handlerBundleURL) { + CFBundleRef handlerBundle = CFBundleCreate(NULL, handlerBundleURL); + if (!handlerBundle) { + ::CFRelease(handlerBundleURL); + return NS_ERROR_OUT_OF_MEMORY; + } + + // Get the human-readable name of the bundle + CFStringRef bundleName = + (CFStringRef)::CFBundleGetValueForInfoDictionaryKey(handlerBundle, kCFBundleNameKey); + + if (bundleName) { + AutoTArray<UniChar, 255> buffer; + CFIndex bundleNameLength = ::CFStringGetLength(bundleName); + buffer.SetLength(bundleNameLength); + ::CFStringGetCharacters(bundleName, CFRangeMake(0, bundleNameLength), buffer.Elements()); + _retval.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), bundleNameLength); + rv = NS_OK; + } + ::CFRelease(handlerBundle); + ::CFRelease(handlerBundleURL); + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + CFURLRef handlerBundleURL; + rv = GetDefaultBundleURL(aScheme, &handlerBundleURL); + if (NS_SUCCEEDED(rv) && handlerBundleURL) { + // Ensure we don't accidentally return success if we can't get an app bundle. + rv = NS_ERROR_NOT_AVAILABLE; + CFBundleRef appBundle = ::CFBundleGetMainBundle(); + if (appBundle) { + CFURLRef selfURL = ::CFBundleCopyBundleURL(appBundle); + *_retval = ::CFEqual(selfURL, handlerBundleURL); + rv = NS_OK; + ::CFRelease(selfURL); + } + ::CFRelease(handlerBundleURL); + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath, + nsIFile** aFile) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv; + nsCOMPtr<nsILocalFileMac> localFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + CFURLRef pathAsCFURL; + CFStringRef pathAsCFString = ::CFStringCreateWithCharacters( + NULL, reinterpret_cast<const UniChar*>(aPlatformAppPath), NS_strlen(aPlatformAppPath)); + if (!pathAsCFString) return NS_ERROR_OUT_OF_MEMORY; + + if (::CFStringGetCharacterAtIndex(pathAsCFString, 0) == '/') { + // we have a Posix path + pathAsCFURL = + ::CFURLCreateWithFileSystemPath(nullptr, pathAsCFString, kCFURLPOSIXPathStyle, false); + if (!pathAsCFURL) { + ::CFRelease(pathAsCFString); + return NS_ERROR_OUT_OF_MEMORY; + } + } else { + // if it doesn't start with a / it's not an absolute Posix path + // let's check if it's a HFS path left over from old preferences + + // If it starts with a ':' char, it's not an absolute HFS path + // so bail for that, and also if it's empty + if (::CFStringGetLength(pathAsCFString) == 0 || + ::CFStringGetCharacterAtIndex(pathAsCFString, 0) == ':') { + ::CFRelease(pathAsCFString); + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + } + + pathAsCFURL = + ::CFURLCreateWithFileSystemPath(nullptr, pathAsCFString, kCFURLHFSPathStyle, false); + if (!pathAsCFURL) { + ::CFRelease(pathAsCFString); + return NS_ERROR_OUT_OF_MEMORY; + } + } + + rv = localFile->InitWithCFURL(pathAsCFURL); + ::CFRelease(pathAsCFString); + ::CFRelease(pathAsCFURL); + if (NS_FAILED(rv)) return rv; + *aFile = localFile; + NS_IF_ADDREF(*aFile); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Returns the MIME types an application bundle explicitly claims to handle. +// Returns NULL if aAppRef doesn't explicitly claim to handle any MIME types. +// If the return value is non-NULL, the caller is responsible for freeing it. +// This isn't necessarily the same as the MIME types the application bundle +// is registered to handle in the Launch Services database. (For example +// the Preview application is normally registered to handle the application/pdf +// MIME type, even though it doesn't explicitly claim to handle *any* MIME +// types in its Info.plist. This is probably because Preview does explicitly +// claim to handle the com.adobe.pdf UTI, and Launch Services somehow +// translates this into a claim to support the application/pdf MIME type. +// Launch Services doesn't provide any APIs (documented or undocumented) to +// query which MIME types a given application is registered to handle. So any +// app that wants this information (e.g. the Default Apps pref pane) needs to +// iterate through the entire Launch Services database -- a process which can +// take several seconds.) +static CFArrayRef GetMIMETypesHandledByApp(FSRef* aAppRef) { + CFURLRef appURL = ::CFURLCreateFromFSRef(kCFAllocatorDefault, aAppRef); + if (!appURL) { + return NULL; + } + CFDictionaryRef infoDict = ::CFBundleCopyInfoDictionaryForURL(appURL); + ::CFRelease(appURL); + if (!infoDict) { + return NULL; + } + CFTypeRef cfObject = ::CFDictionaryGetValue(infoDict, CFSTR("CFBundleDocumentTypes")); + if (!cfObject || (::CFGetTypeID(cfObject) != ::CFArrayGetTypeID())) { + ::CFRelease(infoDict); + return NULL; + } + + CFArrayRef docTypes = static_cast<CFArrayRef>(cfObject); + CFIndex docTypesCount = ::CFArrayGetCount(docTypes); + if (docTypesCount == 0) { + ::CFRelease(infoDict); + return NULL; + } + + CFMutableArrayRef mimeTypes = + ::CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + for (CFIndex i = 0; i < docTypesCount; ++i) { + cfObject = ::CFArrayGetValueAtIndex(docTypes, i); + if (!cfObject || (::CFGetTypeID(cfObject) != ::CFDictionaryGetTypeID())) { + continue; + } + CFDictionaryRef typeDict = static_cast<CFDictionaryRef>(cfObject); + + // When this key is present (on OS X 10.5 and later), its contents + // take precedence over CFBundleTypeMIMETypes (and CFBundleTypeExtensions + // and CFBundleTypeOSTypes). + cfObject = ::CFDictionaryGetValue(typeDict, CFSTR("LSItemContentTypes")); + if (cfObject && (::CFGetTypeID(cfObject) == ::CFArrayGetTypeID())) { + continue; + } + + cfObject = ::CFDictionaryGetValue(typeDict, CFSTR("CFBundleTypeMIMETypes")); + if (!cfObject || (::CFGetTypeID(cfObject) != ::CFArrayGetTypeID())) { + continue; + } + CFArrayRef mimeTypeHolder = static_cast<CFArrayRef>(cfObject); + CFArrayAppendArray(mimeTypes, mimeTypeHolder, + ::CFRangeMake(0, ::CFArrayGetCount(mimeTypeHolder))); + } + + ::CFRelease(infoDict); + if (!::CFArrayGetCount(mimeTypes)) { + ::CFRelease(mimeTypes); + mimeTypes = NULL; + } + return mimeTypes; +} + +nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSNULL; + MOZ_ASSERT(XRE_IsParentProcess()); + + *aFound = false; + + const nsCString& flatType = PromiseFlatCString(aMIMEType); + const nsCString& flatExt = PromiseFlatCString(aFileExt); + + MOZ_LOG(mLog, LogLevel::Debug, + ("Mac: HelperAppService lookup for type '%s' ext '%s'\n", flatType.get(), flatExt.get())); + + // Create a Mac-specific MIME info so we can use Mac-specific members. + RefPtr<nsMIMEInfoMac> mimeInfoMac = new nsMIMEInfoMac(aMIMEType); + + NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init]; + + OSStatus err; + bool haveAppForType = false; + bool haveAppForExt = false; + bool typeIsOctetStream = false; + bool typeAppIsDefault = false; + bool extAppIsDefault = false; + FSRef typeAppFSRef; + FSRef extAppFSRef; + + CFStringRef cfMIMEType = NULL; + + if (!aMIMEType.IsEmpty()) { + typeIsOctetStream = aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM); + CFURLRef appURL = NULL; + // CFStringCreateWithCString() can fail even if we're not out of memory -- + // for example if the 'cStr' parameter is something very weird (like + // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's + // specified in the 'encoding' parameter. See bug 548719. + cfMIMEType = ::CFStringCreateWithCString(NULL, flatType.get(), kCFStringEncodingUTF8); + if (cfMIMEType) { + err = ::LSCopyApplicationForMIMEType(cfMIMEType, kLSRolesAll, &appURL); + if ((err == noErr) && appURL && ::CFURLGetFSRef(appURL, &typeAppFSRef)) { + haveAppForType = true; + MOZ_LOG(mLog, LogLevel::Debug, + ("LSCopyApplicationForMIMEType found a default application\n")); + } + if (appURL) { + ::CFRelease(appURL); + } + } + } + if (!aFileExt.IsEmpty()) { + // CFStringCreateWithCString() can fail even if we're not out of memory -- + // for example if the 'cStr' parameter is something very weird (like + // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's + // specified in the 'encoding' parameter. See bug 548719. + CFStringRef cfExt = ::CFStringCreateWithCString(NULL, flatExt.get(), kCFStringEncodingUTF8); + if (cfExt) { + err = ::LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, cfExt, kLSRolesAll, + &extAppFSRef, nullptr); + if (err == noErr) { + haveAppForExt = true; + MOZ_LOG(mLog, LogLevel::Debug, ("LSGetApplicationForInfo found a default application\n")); + } + ::CFRelease(cfExt); + } + } + + if (haveAppForType && haveAppForExt) { + // Do aMIMEType and aFileExt match? + if (::FSCompareFSRefs((const FSRef*)&typeAppFSRef, (const FSRef*)&extAppFSRef) == noErr) { + typeAppIsDefault = true; + *aFound = true; + } + } else if (haveAppForType) { + // If aFileExt isn't empty, it doesn't match aMIMEType. + if (aFileExt.IsEmpty()) { + typeAppIsDefault = true; + *aFound = true; + } + } + + // If we have an app for the extension, and either don't have one for the + // type, or the type is application/octet-stream (ie "binary blob"), rely + // on the file extension. + if ((!haveAppForType || (!*aFound && typeIsOctetStream)) && haveAppForExt) { + // If aMIMEType isn't empty, it doesn't match aFileExt, which should mean + // that we haven't found a matching app. But make an exception for an app + // that also explicitly claims to handle aMIMEType, or which doesn't claim + // to handle any MIME types. This helps work around the following Apple + // design flaw: + // + // Launch Services is somewhat unreliable about registering Apple apps to + // handle MIME types. Probably this is because Apple has officially + // deprecated support for MIME types (in favor of UTIs). As a result, + // most of Apple's own apps don't explicitly claim to handle any MIME + // types (instead they claim to handle one or more UTIs). So Launch + // Services must contain logic to translate support for a given UTI into + // support for one or more MIME types, and it doesn't always do this + // correctly. For example DiskImageMounter isn't (by default) registered + // to handle the application/x-apple-diskimage MIME type. See bug 675356. + // + // Apple has also deprecated support for file extensions, and Apple apps + // also don't register to handle them. But for some reason Launch Services + // is (apparently) better about translating support for a given UTI into + // support for one or more file extensions. It's not at all clear why. + if (aMIMEType.IsEmpty() || typeIsOctetStream) { + extAppIsDefault = true; + *aFound = true; + } else { + CFArrayRef extAppMIMETypes = GetMIMETypesHandledByApp(&extAppFSRef); + if (extAppMIMETypes) { + if (cfMIMEType) { + if (::CFArrayContainsValue(extAppMIMETypes, + ::CFRangeMake(0, ::CFArrayGetCount(extAppMIMETypes)), + cfMIMEType)) { + extAppIsDefault = true; + *aFound = true; + } + } + ::CFRelease(extAppMIMETypes); + } else { + extAppIsDefault = true; + *aFound = true; + } + } + } + + if (cfMIMEType) { + ::CFRelease(cfMIMEType); + } + + if (aMIMEType.IsEmpty()) { + if (haveAppForExt) { + // If aMIMEType is empty and we've found a default app for aFileExt, try + // to get the MIME type from aFileExt. (It might also be worth doing + // this when aMIMEType isn't empty but haveAppForType is false -- but + // the doc for this method says that if we have a MIME type (in + // aMIMEType), we need to give it preference.) + NSURLFileTypeMappings* map = [NSURLFileTypeMappings sharedMappings]; + NSString* extStr = [NSString stringWithCString:flatExt.get() encoding:NSASCIIStringEncoding]; + NSString* typeStr = map ? [map MIMETypeForExtension:extStr] : NULL; + if (typeStr) { + nsAutoCString mimeType; + mimeType.Assign((char*)[typeStr cStringUsingEncoding:NSASCIIStringEncoding]); + mimeInfoMac->SetMIMEType(mimeType); + haveAppForType = true; + } else { + // Sometimes the OS won't give us a MIME type for an extension that's + // registered with Launch Services and has a default app: For example + // Real Player registers itself for the "ogg" extension and for the + // audio/x-ogg and application/x-ogg MIME types, but + // MIMETypeForExtension returns nil for the "ogg" extension even on + // systems where Real Player is installed. This is probably an Apple + // bug. But bad things happen if we return an nsIMIMEInfo structure + // with an empty MIME type and set *aFound to true. So in this + // case we need to set it to false here. + haveAppForExt = false; + extAppIsDefault = false; + *aFound = false; + } + } else { + // Otherwise set the MIME type to a reasonable fallback. + mimeInfoMac->SetMIMEType(nsLiteralCString(APPLICATION_OCTET_STREAM)); + } + } + + if (typeAppIsDefault || extAppIsDefault) { + if (haveAppForExt) mimeInfoMac->AppendExtension(aFileExt); + + nsresult rv; + nsCOMPtr<nsILocalFileMac> app(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + [localPool release]; + return rv; + } + + CFStringRef cfAppName = NULL; + if (typeAppIsDefault) { + app->InitWithFSRef(&typeAppFSRef); + ::LSCopyItemAttribute((const FSRef*)&typeAppFSRef, kLSRolesAll, kLSItemDisplayName, + (CFTypeRef*)&cfAppName); + } else { + app->InitWithFSRef(&extAppFSRef); + ::LSCopyItemAttribute((const FSRef*)&extAppFSRef, kLSRolesAll, kLSItemDisplayName, + (CFTypeRef*)&cfAppName); + } + if (cfAppName) { + AutoTArray<UniChar, 255> buffer; + CFIndex appNameLength = ::CFStringGetLength(cfAppName); + buffer.SetLength(appNameLength); + ::CFStringGetCharacters(cfAppName, CFRangeMake(0, appNameLength), buffer.Elements()); + nsAutoString appName; + appName.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), appNameLength); + mimeInfoMac->SetDefaultDescription(appName); + ::CFRelease(cfAppName); + } + + mimeInfoMac->SetDefaultApplication(app); + mimeInfoMac->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + } else { + mimeInfoMac->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + nsAutoCString mimeType; + mimeInfoMac->GetMIMEType(mimeType); + if (*aFound && !mimeType.IsEmpty()) { + // If we have a MIME type, make sure its extension list is included in our + // list. + NSURLFileTypeMappings* map = [NSURLFileTypeMappings sharedMappings]; + NSString* typeStr = [NSString stringWithCString:mimeType.get() encoding:NSASCIIStringEncoding]; + NSArray* extensionsList = map ? [map extensionsForMIMEType:typeStr] : NULL; + if (extensionsList) { + for (NSString* extension in extensionsList) { + nsAutoCString ext; + ext.Assign((char*)[extension cStringUsingEncoding:NSASCIIStringEncoding]); + mimeInfoMac->AppendExtension(ext); + } + } + + CFStringRef cfType = ::CFStringCreateWithCString(NULL, mimeType.get(), kCFStringEncodingUTF8); + if (cfType) { + CFStringRef cfTypeDesc = NULL; + if (::LSCopyKindStringForMIMEType(cfType, &cfTypeDesc) == noErr) { + AutoTArray<UniChar, 255> buffer; + CFIndex typeDescLength = ::CFStringGetLength(cfTypeDesc); + buffer.SetLength(typeDescLength); + ::CFStringGetCharacters(cfTypeDesc, CFRangeMake(0, typeDescLength), buffer.Elements()); + nsAutoString typeDesc; + typeDesc.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), typeDescLength); + mimeInfoMac->SetDescription(typeDesc); + } + if (cfTypeDesc) { + ::CFRelease(cfTypeDesc); + } + ::CFRelease(cfType); + } + } + + MOZ_LOG(mLog, LogLevel::Debug, ("OS gave us: type '%s' found '%i'\n", mimeType.get(), *aFound)); + + [localPool release]; + mimeInfoMac.forget(aMIMEInfo); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoMac* handlerInfo = new nsMIMEInfoMac(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + // As a workaround for the OS X problem described in bug 1391186, don't + // attempt to get/set the application description from the child process. + if (!mozilla::net::IsNeckoChild()) { + nsAutoString desc; + rv = GetApplicationDescription(aScheme, desc); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "GetApplicationDescription failed"); + handlerInfo->SetDefaultDescription(desc); + } + + return NS_OK; +} diff --git a/uriloader/exthandler/moz.build b/uriloader/exthandler/moz.build new file mode 100644 index 0000000000..0a668f9922 --- /dev/null +++ b/uriloader/exthandler/moz.build @@ -0,0 +1,152 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SPHINX_TREES["/uriloader/exthandler"] = "docs" + +TEST_DIRS += ["tests"] + +XPIDL_SOURCES += [ + "nsCExternalHandlerService.idl", + "nsIContentDispatchChooser.idl", + "nsIExternalHelperAppService.idl", + "nsIExternalProtocolService.idl", + "nsIExternalURLHandlerService.idl", + "nsIHandlerService.idl", + "nsIHelperAppLauncherDialog.idl", + "nsISharingHandlerApp.idl", +] + +XPIDL_MODULE = "exthandler" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + osdir = "win" + LOCAL_INCLUDES += ["win"] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + osdir = "mac" +elif CONFIG["MOZ_WIDGET_TOOLKIT"] in ("android", "uikit"): + osdir = CONFIG["MOZ_WIDGET_TOOLKIT"] +else: + osdir = "unix" + +EXPORTS += [osdir + "/nsOSHelperAppService.h"] + +EXPORTS += [ + "ContentHandlerService.h", + "nsExternalHelperAppService.h", + "nsMIMEInfoChild.h", + "nsOSHelperAppServiceChild.h", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + EXPORTS += [ + "%s/%s" % (osdir, f) + for f in [ + "nsExternalURLHandlerService.h", + ] + ] + +EXPORTS += [ + "nsLocalHandlerApp.h", +] + +EXPORTS.mozilla.dom += [ + "ExternalHelperAppChild.h", + "ExternalHelperAppParent.h", +] + +UNIFIED_SOURCES += [ + "ContentHandlerService.cpp", + "ExternalHelperAppChild.cpp", + "ExternalHelperAppParent.cpp", + "HandlerServiceParent.cpp", + "nsExternalHelperAppService.cpp", + "nsExternalProtocolHandler.cpp", + "nsLocalHandlerApp.cpp", + "nsMIMEInfoImpl.cpp", + "nsOSHelperAppServiceChild.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + UNIFIED_SOURCES += [ + "mac/nsLocalHandlerAppMac.mm", + "mac/nsMIMEInfoMac.mm", + "mac/nsOSHelperAppService.mm", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "uikit": + UNIFIED_SOURCES += [ + "uikit/nsLocalHandlerAppUIKit.mm", + "uikit/nsMIMEInfoUIKit.mm", + "uikit/nsOSHelperAppService.mm", + ] +else: + # These files can't be built in unified mode because they redefine LOG. + SOURCES += [ + osdir + "/nsOSHelperAppService.cpp", + ] + if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + UNIFIED_SOURCES += [ + "unix/nsGNOMERegistry.cpp", + "unix/nsMIMEInfoUnix.cpp", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + UNIFIED_SOURCES += [ + "android/nsAndroidHandlerApp.cpp", + "android/nsExternalURLHandlerService.cpp", + "android/nsMIMEInfoAndroid.cpp", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + UNIFIED_SOURCES += [ + "win/nsMIMEInfoWin.cpp", + ] + +if CONFIG["MOZ_ENABLE_DBUS"]: + UNIFIED_SOURCES += [ + "nsDBusHandlerApp.cpp", + ] + EXPORTS.mozilla += [ + "DBusHelpers.h", + ] + +EXTRA_COMPONENTS += [ + "HandlerService.js", + "HandlerService.manifest", +] + +EXTRA_JS_MODULES += [ + "WebHandlerApp.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +IPDL_SOURCES += [ + "PExternalHelperApp.ipdl", + "PHandlerService.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", + "/dom/ipc", + "/netwerk/base", + "/netwerk/protocol/http", +] + +if CONFIG["MOZ_ENABLE_DBUS"]: + CXXFLAGS += CONFIG["TK_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_DBUS_CFLAGS"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["TK_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] diff --git a/uriloader/exthandler/nsCExternalHandlerService.idl b/uriloader/exthandler/nsCExternalHandlerService.idl new file mode 100644 index 0000000000..bdf59e9ae6 --- /dev/null +++ b/uriloader/exthandler/nsCExternalHandlerService.idl @@ -0,0 +1,33 @@ +/* -*- Mode: IDL; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIExternalHelperAppService.idl" + +/* +nsCExternalHelperApp implements: +------------------------- +nsIExternalHelperAppService +*/ + +%{ C++ + +#define NS_EXTERNALHELPERAPPSERVICE_CONTRACTID \ +"@mozilla.org/uriloader/external-helper-app-service;1" + +#define NS_HANDLERSERVICE_CONTRACTID \ +"@mozilla.org/uriloader/handler-service;1" + +#define NS_EXTERNALPROTOCOLSERVICE_CONTRACTID \ +"@mozilla.org/uriloader/external-protocol-service;1" + +#define NS_MIMESERVICE_CONTRACTID \ +"@mozilla.org/mime;1" + +#define NS_LOCALHANDLERAPP_CONTRACTID \ +"@mozilla.org/uriloader/local-handler-app;1" + +%} + diff --git a/uriloader/exthandler/nsContentHandlerApp.h b/uriloader/exthandler/nsContentHandlerApp.h new file mode 100644 index 0000000000..f372d8735e --- /dev/null +++ b/uriloader/exthandler/nsContentHandlerApp.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsContentHandlerAppImpl_h__ +#define __nsContentHandlerAppImpl_h__ + +#include <contentaction/contentaction.h> +#include "nsString.h" +#include "nsIMIMEInfo.h" + +class nsContentHandlerApp : public nsIHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + + nsContentHandlerApp(nsString aName, nsCString aType, + ContentAction::Action& aAction); + virtual ~nsContentHandlerApp() {} + + protected: + nsString mName; + nsCString mType; + nsString mDetailedDescription; + + ContentAction::Action mAction; +}; +#endif diff --git a/uriloader/exthandler/nsDBusHandlerApp.cpp b/uriloader/exthandler/nsDBusHandlerApp.cpp new file mode 100644 index 0000000000..6155a1a951 --- /dev/null +++ b/uriloader/exthandler/nsDBusHandlerApp.cpp @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <dbus/dbus.h> +#include "mozilla/Components.h" +#include "mozilla/DBusHelpers.h" +#include "nsDBusHandlerApp.h" +#include "nsIURI.h" +#include "nsIClassInfoImpl.h" +#include "nsCOMPtr.h" +#include "nsCExternalHandlerService.h" + +using namespace mozilla; + +// XXX why does nsMIMEInfoImpl have a threadsafe nsISupports? do we need one +// here too? +NS_IMPL_CLASSINFO(nsDBusHandlerApp, nullptr, 0, + components::DBusHandlerApp::CID()) +NS_IMPL_ISUPPORTS_CI(nsDBusHandlerApp, nsIDBusHandlerApp, nsIHandlerApp) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIHandlerApp + +NS_IMETHODIMP nsDBusHandlerApp::GetName(nsAString& aName) { + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetName(const nsAString& aName) { + mName.Assign(aName); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetDetailedDescription( + const nsAString& aDescription) { + mDetailedDescription.Assign(aDescription); + + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetDetailedDescription( + nsAString& aDescription) { + aDescription.Assign(mDetailedDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsDBusHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* _retval) { + NS_ENSURE_ARG_POINTER(aHandlerApp); + + // If the handler app isn't a dbus handler app, then it's not the same app. + nsCOMPtr<nsIDBusHandlerApp> dbusHandlerApp = do_QueryInterface(aHandlerApp); + if (!dbusHandlerApp) { + *_retval = false; + return NS_OK; + } + nsAutoCString service; + nsAutoCString method; + + nsresult rv = dbusHandlerApp->GetService(service); + if (NS_FAILED(rv)) { + *_retval = false; + return NS_OK; + } + rv = dbusHandlerApp->GetMethod(method); + if (NS_FAILED(rv)) { + *_retval = false; + return NS_OK; + } + + *_retval = service.Equals(mService) && method.Equals(mMethod); + return NS_OK; +} + +NS_IMETHODIMP +nsDBusHandlerApp::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + nsAutoCString spec; + nsresult rv = aURI->GetAsciiSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + const char* uri = spec.get(); + + DBusError err; + dbus_error_init(&err); + + mozilla::UniquePtr<DBusConnection, mozilla::DBusConnectionDelete> connection( + dbus_bus_get_private(DBUS_BUS_SESSION, &err)); + + if (dbus_error_is_set(&err)) { + dbus_error_free(&err); + return NS_ERROR_FAILURE; + } + if (nullptr == connection) { + return NS_ERROR_FAILURE; + } + dbus_connection_set_exit_on_disconnect(connection.get(), false); + + RefPtr<DBusMessage> msg = + already_AddRefed<DBusMessage>(dbus_message_new_method_call( + mService.get(), mObjpath.get(), mInterface.get(), mMethod.get())); + + if (!msg) { + return NS_ERROR_FAILURE; + } + dbus_message_set_no_reply(msg, true); + + DBusMessageIter iter; + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &uri); + + if (dbus_connection_send(connection.get(), msg, nullptr)) { + dbus_connection_flush(connection.get()); + } else { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIDBusHandlerApp + +NS_IMETHODIMP nsDBusHandlerApp::GetService(nsACString& aService) { + aService.Assign(mService); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetService(const nsACString& aService) { + mService.Assign(aService); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetMethod(nsACString& aMethod) { + aMethod.Assign(mMethod); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetMethod(const nsACString& aMethod) { + mMethod.Assign(aMethod); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetDBusInterface(nsACString& aInterface) { + aInterface.Assign(mInterface); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetDBusInterface(const nsACString& aInterface) { + mInterface.Assign(aInterface); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetObjectPath(nsACString& aObjpath) { + aObjpath.Assign(mObjpath); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetObjectPath(const nsACString& aObjpath) { + mObjpath.Assign(aObjpath); + return NS_OK; +} diff --git a/uriloader/exthandler/nsDBusHandlerApp.h b/uriloader/exthandler/nsDBusHandlerApp.h new file mode 100644 index 0000000000..f45f917446 --- /dev/null +++ b/uriloader/exthandler/nsDBusHandlerApp.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsDBusHandlerAppImpl_h__ +#define __nsDBusHandlerAppImpl_h__ + +#include "nsString.h" +#include "nsIMIMEInfo.h" + +class nsDBusHandlerApp : public nsIDBusHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + NS_DECL_NSIDBUSHANDLERAPP + + nsDBusHandlerApp() {} + + protected: + virtual ~nsDBusHandlerApp() {} + + nsString mName; + nsString mDetailedDescription; + nsCString mService; + nsCString mMethod; + nsCString mInterface; + nsCString mObjpath; +}; +#endif diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp new file mode 100644 index 0000000000..b78eac71d5 --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -0,0 +1,3096 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "base/basictypes.h" + +/* This must occur *after* base/basictypes.h to avoid typedefs conflicts. */ +#include "mozilla/ArrayUtils.h" +#include "mozilla/Base64.h" +#include "mozilla/ResultExtensions.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/StaticPrefs_security.h" +#include "nsXULAppAPI.h" + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIChannel.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsICategoryManager.h" +#include "nsDependentSubstring.h" +#include "nsString.h" +#include "nsUnicharUtils.h" +#include "nsIStringEnumerator.h" +#include "nsMemory.h" +#include "nsIStreamListener.h" +#include "nsIMIMEService.h" +#include "nsILoadGroup.h" +#include "nsIWebProgressListener.h" +#include "nsITransfer.h" +#include "nsReadableUtils.h" +#include "nsIRequest.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIInterfaceRequestor.h" +#include "nsThreadUtils.h" +#include "nsIMutableArray.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsOSHelperAppService.h" +#include "nsOSHelperAppServiceChild.h" +#include "nsContentSecurityUtils.h" + +// used to access our datastore of user-configured helper applications +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "nsIHelperAppLauncherDialog.h" +#include "nsIContentDispatchChooser.h" +#include "nsNetUtil.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIIOService.h" +#include "nsNetCID.h" + +#include "nsIApplicationReputation.h" + +#include "nsDSURIContentListener.h" +#include "nsMimeTypes.h" +// used for header disposition information. +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIEncodedChannel.h" +#include "nsIMultiPartChannel.h" +#include "nsIFileChannel.h" +#include "nsIObserverService.h" // so we can be a profile change observer +#include "nsIPropertyBag2.h" // for the 64-bit content length + +#ifdef XP_MACOSX +# include "nsILocalFileMac.h" +#endif + +#include "nsPluginHost.h" +#include "nsEscape.h" + +#include "nsIStringBundle.h" // XXX needed to localize error msgs +#include "nsIPrompt.h" + +#include "nsITextToSubURI.h" // to unescape the filename + +#include "nsDocShellCID.h" + +#include "nsCRT.h" +#include "nsLocalHandlerApp.h" + +#include "nsIRandomGenerator.h" + +#include "ContentChild.h" +#include "nsXULAppAPI.h" +#include "nsPIDOMWindow.h" +#include "ExternalHelperAppChild.h" + +#ifdef XP_WIN +# include "nsWindowsHelpers.h" +#endif + +#include "mozilla/Components.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla; +using namespace mozilla::ipc; +using namespace mozilla::dom; + +// Download Folder location constants +#define NS_PREF_DOWNLOAD_DIR "browser.download.dir" +#define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList" +enum { + NS_FOLDER_VALUE_DESKTOP = 0, + NS_FOLDER_VALUE_DOWNLOADS = 1, + NS_FOLDER_VALUE_CUSTOM = 2 +}; + +LazyLogModule nsExternalHelperAppService::mLog("HelperAppService"); + +// Using level 3 here because the OSHelperAppServices use a log level +// of LogLevel::Debug (4), and we want less detailed output here +// Using 3 instead of LogLevel::Warning because we don't output warnings +#undef LOG +#define LOG(args) \ + MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args) +#define LOG_ENABLED() \ + MOZ_LOG_TEST(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info) + +static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] = + "browser.helperApps.neverAsk.saveToDisk"; +static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] = + "browser.helperApps.neverAsk.openFile"; + +// Helper functions for Content-Disposition headers + +/** + * Given a URI fragment, unescape it + * @param aFragment The string to unescape + * @param aURI The URI from which this fragment is taken. Only its character set + * will be used. + * @param aResult [out] Unescaped string. + */ +static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI, + nsAString& aResult) { + // We need the unescaper + nsresult rv; + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return textToSubURI->UnEscapeURIForUI(aFragment, aResult); +} + +/** + * UTF-8 version of UnescapeFragment. + * @param aFragment The string to unescape + * @param aURI The URI from which this fragment is taken. Only its character set + * will be used. + * @param aResult [out] Unescaped string, UTF-8 encoded. + * @note It is safe to pass the same string for aFragment and aResult. + * @note When this function fails, aResult will not be modified. + */ +static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI, + nsACString& aResult) { + nsAutoString result; + nsresult rv = UnescapeFragment(aFragment, aURI, result); + if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult); + return rv; +} + +/** + * Given a channel, returns the filename and extension the channel has. + * This uses the URL and other sources (nsIMultiPartChannel). + * Also gives back whether the channel requested external handling (i.e. + * whether Content-Disposition: attachment was sent) + * @param aChannel The channel to extract the filename/extension from + * @param aFileName [out] Reference to the string where the filename should be + * stored. Empty if it could not be retrieved. + * WARNING - this filename may contain characters which the OS does not + * allow as part of filenames! + * @param aExtension [out] Reference to the string where the extension should + * be stored. Empty if it could not be retrieved. Stored in UTF-8. + * @param aAllowURLExtension (optional) Get the extension from the URL if no + * Content-Disposition header is present. Default is true. + * @retval true The server sent Content-Disposition:attachment or equivalent + * @retval false Content-Disposition: inline or no content-disposition header + * was sent. + */ +static bool GetFilenameAndExtensionFromChannel(nsIChannel* aChannel, + nsString& aFileName, + nsCString& aExtension, + bool aAllowURLExtension = true) { + aExtension.Truncate(); + /* + * If the channel is an http or part of a multipart channel and we + * have a content disposition header set, then use the file name + * suggested there as the preferred file name to SUGGEST to the + * user. we shouldn't actually use that without their + * permission... otherwise just use our temp file + */ + bool handleExternally = false; + uint32_t disp; + nsresult rv = aChannel->GetContentDisposition(&disp); + bool gotFileNameFromURI = false; + if (NS_SUCCEEDED(rv)) { + aChannel->GetContentDispositionFilename(aFileName); + if (disp == nsIChannel::DISPOSITION_ATTACHMENT) handleExternally = true; + } + + // If the disposition header didn't work, try the filename from nsIURL + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsIURL> url(do_QueryInterface(uri)); + if (url && aFileName.IsEmpty()) { + if (aAllowURLExtension) { + url->GetFileExtension(aExtension); + UnescapeFragment(aExtension, url, aExtension); + + // Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + // In case the aExtension consisted only of the dot, the code below will + // extract an aExtension from the filename + aExtension.Trim(".", false); + } + + // try to extract the file name from the url and use that as a first pass as + // the leaf name of our temp file... + nsAutoCString leafName; + url->GetFileName(leafName); + if (!leafName.IsEmpty()) { + gotFileNameFromURI = true; + rv = UnescapeFragment(leafName, url, aFileName); + if (NS_FAILED(rv)) { + CopyUTF8toUTF16(leafName, aFileName); // use escaped name + } + } + } + + // If we have a filename and no extension, remove trailing dots from the + // filename and extract the extension if that is possible. + if (aExtension.IsEmpty() && !aFileName.IsEmpty()) { + // Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + aFileName.Trim(".", false); + // We can get an extension if the filename is from a header, or if getting + // it from the URL was allowed. + bool canGetExtensionFromFilename = + !gotFileNameFromURI || aAllowURLExtension; + // ... , or if the mimetype is meaningless and we have nothing to go on: + if (!canGetExtensionFromFilename) { + nsAutoCString contentType; + if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) { + canGetExtensionFromFilename = + contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) || + contentType.EqualsIgnoreCase("binary/octet-stream") || + contentType.EqualsIgnoreCase("application/x-msdownload"); + } + } + + if (canGetExtensionFromFilename) { + // XXX RFindCharInReadable!! + nsAutoString fileNameStr(aFileName); + int32_t idx = fileNameStr.RFindChar(char16_t('.')); + if (idx != kNotFound) + CopyUTF16toUTF8(StringTail(fileNameStr, fileNameStr.Length() - idx - 1), + aExtension); + } + } + + return handleExternally; +} + +/** + * Obtains the directory to use. This tends to vary per platform, and + * needs to be consistent throughout our codepaths. For platforms where + * helper apps use the downloads directory, this should be kept in + * sync with DownloadIntegration.jsm. + * + * Optionally skip availability of the directory and storage. + */ +static nsresult GetDownloadDirectory(nsIFile** _directory, + bool aSkipChecks = false) { + nsCOMPtr<nsIFile> dir; +#ifdef XP_MACOSX + // On OS X, we first try to get the users download location, if it's set. + switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) { + case NS_FOLDER_VALUE_DESKTOP: + (void)NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir)); + break; + case NS_FOLDER_VALUE_CUSTOM: { + Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(dir)); + if (!dir) break; + + // If we're not checking for availability we're done. + if (aSkipChecks) { + dir.forget(_directory); + return NS_OK; + } + + // We have the directory, and now we need to make sure it exists + bool dirExists = false; + (void)dir->Exists(&dirExists); + if (dirExists) break; + + nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_FAILED(rv)) { + dir = nullptr; + break; + } + } break; + case NS_FOLDER_VALUE_DOWNLOADS: + // This is just the OS default location, so fall out + break; + } + + if (!dir) { + // If not, we default to the OS X default download location. + nsresult rv = NS_GetSpecialDirectory(NS_OSX_DEFAULT_DOWNLOAD_DIR, + getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + } +#elif defined(ANDROID) + return NS_ERROR_FAILURE; +#else + // On all other platforms, we default to the systems temporary directory. + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + +# if defined(XP_UNIX) + // Ensuring that only the current user can read the file names we end up + // creating. Note that Creating directories with specified permission only + // supported on Unix platform right now. That's why above if exists. + + uint32_t permissions; + rv = dir->GetPermissions(&permissions); + NS_ENSURE_SUCCESS(rv, rv); + + if (permissions != PR_IRWXU) { + const char* userName = PR_GetEnv("USERNAME"); + if (!userName || !*userName) { + userName = PR_GetEnv("USER"); + } + if (!userName || !*userName) { + userName = PR_GetEnv("LOGNAME"); + } + if (!userName || !*userName) { + userName = "mozillaUser"; + } + + nsAutoString userDir; + userDir.AssignLiteral("mozilla_"); + userDir.AppendASCII(userName); + userDir.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_'); + + int counter = 0; + bool pathExists; + nsCOMPtr<nsIFile> finalPath; + + while (true) { + nsAutoString countedUserDir(userDir); + countedUserDir.AppendInt(counter, 10); + dir->Clone(getter_AddRefs(finalPath)); + finalPath->Append(countedUserDir); + + rv = finalPath->Exists(&pathExists); + NS_ENSURE_SUCCESS(rv, rv); + + if (pathExists) { + // If this path has the right permissions, use it. + rv = finalPath->GetPermissions(&permissions); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensuring the path is writable by the current user. + bool isWritable; + rv = finalPath->IsWritable(&isWritable); + NS_ENSURE_SUCCESS(rv, rv); + + if (permissions == PR_IRWXU && isWritable) { + dir = finalPath; + break; + } + } + + rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU); + if (NS_SUCCEEDED(rv)) { + dir = finalPath; + break; + } else if (rv != NS_ERROR_FILE_ALREADY_EXISTS) { + // Unexpected error. + return rv; + } + + counter++; + } + } + +# endif +#endif + + NS_ASSERTION(dir, "Somehow we didn't get a download directory!"); + dir.forget(_directory); + return NS_OK; +} + +/** + * Structure for storing extension->type mappings. + * @see defaultMimeEntries + */ +struct nsDefaultMimeTypeEntry { + const char* mMimeType; + const char* mFileExtension; +}; + +/** + * Default extension->mimetype mappings. These are not overridable. + * If you add types here, make sure they are lowercase, or you'll regret it. + */ +static const nsDefaultMimeTypeEntry defaultMimeEntries[] = { + // The following are those extensions that we're asked about during startup, + // sorted by order used + {IMAGE_GIF, "gif"}, + {TEXT_XML, "xml"}, + {APPLICATION_RDF, "rdf"}, + {IMAGE_PNG, "png"}, + // -- end extensions used during startup + {TEXT_CSS, "css"}, + {IMAGE_JPEG, "jpeg"}, + {IMAGE_JPEG, "jpg"}, + {IMAGE_SVG_XML, "svg"}, + {TEXT_HTML, "html"}, + {TEXT_HTML, "htm"}, + {APPLICATION_XPINSTALL, "xpi"}, + {"application/xhtml+xml", "xhtml"}, + {"application/xhtml+xml", "xht"}, + {TEXT_PLAIN, "txt"}, + {APPLICATION_JSON, "json"}, + {APPLICATION_XJAVASCRIPT, "js"}, + {APPLICATION_XJAVASCRIPT, "jsm"}, + {VIDEO_OGG, "ogv"}, + {VIDEO_OGG, "ogg"}, + {APPLICATION_OGG, "ogg"}, + {AUDIO_OGG, "oga"}, + {AUDIO_OGG, "opus"}, + {APPLICATION_PDF, "pdf"}, + {VIDEO_WEBM, "webm"}, + {AUDIO_WEBM, "webm"}, + {IMAGE_ICO, "ico"}, + {TEXT_PLAIN, "properties"}, + {TEXT_PLAIN, "locale"}, + {TEXT_PLAIN, "ftl"}, +#if defined(MOZ_WMF) + {VIDEO_MP4, "mp4"}, + {AUDIO_MP4, "m4a"}, + {AUDIO_MP3, "mp3"}, +#endif +#ifdef MOZ_RAW + {VIDEO_RAW, "yuv"} +#endif +}; + +/** + * This is a small private struct used to help us initialize some + * default mime types. + */ +struct nsExtraMimeTypeEntry { + const char* mMimeType; + const char* mFileExtensions; + const char* mDescription; +}; + +/** + * This table lists all of the 'extra' content types that we can deduce from + * particular file extensions. These entries also ensure that we provide a good + * descriptive name when we encounter files with these content types and/or + * extensions. These can be overridden by user helper app prefs. If you add + * types here, make sure they are lowercase, or you'll regret it. + */ +static const nsExtraMimeTypeEntry extraMimeEntries[] = { +#if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to + // look that up... + {APPLICATION_OCTET_STREAM, "exe,com", "Binary File"}, +#else + {APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"}, +#endif + {APPLICATION_GZIP2, "gz", "gzip"}, + {"application/x-arj", "arj", "ARJ file"}, + {"application/rtf", "rtf", "Rich Text Format File"}, + {APPLICATION_ZIP, "zip", "ZIP Archive"}, + {APPLICATION_XPINSTALL, "xpi", "XPInstall Install"}, + {APPLICATION_PDF, "pdf", "Portable Document Format"}, + {APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"}, + {APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"}, + {APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"}, +#ifdef MOZ_WIDGET_ANDROID + {"application/vnd.android.package-archive", "apk", "Android Package"}, +#endif + + // OpenDocument formats + {"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"}, + {"application/vnd.oasis.opendocument.presentation", "odp", + "OpenDocument Presentation"}, + {"application/vnd.oasis.opendocument.spreadsheet", "ods", + "OpenDocument Spreadsheet"}, + {"application/vnd.oasis.opendocument.graphics", "odg", + "OpenDocument Graphics"}, + + // Legacy Microsoft Office + {"application/msword", "doc", "Microsoft Word"}, + {"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"}, + {"application/vnd.ms-excel", "xls", "Microsoft Excel"}, + + // Office Open XML + {"application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "docx", "Microsoft Word (Open XML)"}, + {"application/" + "vnd.openxmlformats-officedocument.presentationml.presentation", + "pptx", "Microsoft PowerPoint (Open XML)"}, + {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsx", "Microsoft Excel (Open XML)"}, + + // Note: if you add new image types, please also update the list in + // contentAreaUtils.js to match. + {IMAGE_ART, "art", "ART Image"}, + {IMAGE_BMP, "bmp", "BMP Image"}, + {IMAGE_GIF, "gif", "GIF Image"}, + {IMAGE_ICO, "ico,cur", "ICO Image"}, + {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"}, + {IMAGE_PNG, "png", "PNG Image"}, + {IMAGE_APNG, "apng", "APNG Image"}, + {IMAGE_TIFF, "tiff,tif", "TIFF Image"}, + {IMAGE_XBM, "xbm", "XBM Image"}, + {IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"}, + {IMAGE_WEBP, "webp", "WebP Image"}, + {IMAGE_AVIF, "avif", "AV1 Image File"}, + + {MESSAGE_RFC822, "eml", "RFC-822 data"}, + {TEXT_PLAIN, "txt,text", "Text File"}, + {APPLICATION_JSON, "json", "JavaScript Object Notation"}, + {TEXT_VTT, "vtt", "Web Video Text Tracks"}, + {TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"}, + {TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"}, + {"application/xhtml+xml", "xhtml,xht", + "Extensible HyperText Markup Language"}, + {APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"}, + {APPLICATION_RDF, "rdf", "Resource Description Framework"}, + {"text/csv", "csv", "CSV File"}, + {TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"}, + {TEXT_CSS, "css", "Style Sheet"}, + {TEXT_VCARD, "vcf,vcard", "Contact Information"}, + {TEXT_CALENDAR, "ics", "iCalendar"}, + {VIDEO_OGG, "ogv", "Ogg Video"}, + {VIDEO_OGG, "ogg", "Ogg Video"}, + {APPLICATION_OGG, "ogg", "Ogg Video"}, + {AUDIO_OGG, "oga", "Ogg Audio"}, + {AUDIO_OGG, "opus", "Opus Audio"}, + {VIDEO_WEBM, "webm", "Web Media Video"}, + {AUDIO_WEBM, "webm", "Web Media Audio"}, + {AUDIO_MP3, "mp3", "MPEG Audio"}, + {VIDEO_MP4, "mp4", "MPEG-4 Video"}, + {AUDIO_MP4, "m4a", "MPEG-4 Audio"}, + {VIDEO_RAW, "yuv", "Raw YUV Video"}, + {AUDIO_WAV, "wav", "Waveform Audio"}, + {VIDEO_3GPP, "3gpp,3gp", "3GPP Video"}, + {VIDEO_3GPP2, "3g2", "3GPP2 Video"}, + {AUDIO_AAC, "aac", "AAC Audio"}, + {AUDIO_FLAC, "flac", "FLAC Audio"}, + {AUDIO_MIDI, "mid", "Standard MIDI Audio"}, + {APPLICATION_WASM, "wasm", "WebAssembly Module"}}; + +static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = { + {IMAGE_JPEG, "jfif"}}; + +/** + * File extensions for which decoding should be disabled. + * NOTE: These MUST be lower-case and ASCII. + */ +static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = { + {APPLICATION_GZIP, "gz"}, + {APPLICATION_GZIP, "tgz"}, + {APPLICATION_ZIP, "zip"}, + {APPLICATION_COMPRESS, "z"}, + {APPLICATION_GZIP, "svgz"}}; + +/** + * Mimetypes for which we enforce using a known extension. + * + * In addition to this list, we do this for all audio/, video/ and + * image/ mimetypes. + */ +static const char* forcedExtensionMimetypes[] = { + // OpenDocument formats + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.graphics", + + // Legacy Microsoft Office + "application/msword", "application/vnd.ms-powerpoint", + "application/vnd.ms-excel", + + // Office Open XML + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + + // Note: zip and json mimetypes are commonly used with a variety of + // extensions; don't add them here. It's a similar story for text/xml, + // but slightly worse because we can use it when sniffing for a mimetype + // if one hasn't been provided, so don't re-add that here either. + + APPLICATION_PDF, + + APPLICATION_OGG, + + APPLICATION_WASM, + + TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD}; + +/** + * Primary extensions of types whose descriptions should be overwritten. + * This extension is concatenated with "ExtHandlerDescription" to look up the + * description in unknownContentType.properties. + * NOTE: These MUST be lower-case and ASCII. + */ +static const char* descriptionOverwriteExtensions[] = { + "avif", "pdf", "svg", "webp", "xml", +}; + +static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton; + +/** + * On Mac child processes, return an nsOSHelperAppServiceChild for remoting + * OS calls to the parent process. On all other platforms use + * nsOSHelperAppService. + */ +/* static */ +already_AddRefed<nsExternalHelperAppService> +nsExternalHelperAppService::GetSingleton() { + if (!sExtHelperAppSvcSingleton) { +#ifdef XP_MACOSX + if (XRE_IsParentProcess()) { + sExtHelperAppSvcSingleton = new nsOSHelperAppService(); + } else { + sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild(); + } +#else + sExtHelperAppSvcSingleton = new nsOSHelperAppService(); +#endif /* XP_MACOSX */ + ClearOnShutdown(&sExtHelperAppSvcSingleton); + } + + return do_AddRef(sExtHelperAppSvcSingleton); +} + +NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService, + nsPIExternalAppLauncher, nsIExternalProtocolService, + nsIMIMEService, nsIObserver, nsISupportsWeakReference) + +nsExternalHelperAppService::nsExternalHelperAppService() {} +nsresult nsExternalHelperAppService::Init() { + // Add an observer for profile change + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) return NS_ERROR_FAILURE; + + nsresult rv = obs->AddObserver(this, "profile-before-change", true); + NS_ENSURE_SUCCESS(rv, rv); + return obs->AddObserver(this, "last-pb-context-exited", true); +} + +nsExternalHelperAppService::~nsExternalHelperAppService() {} + +nsresult nsExternalHelperAppService::DoContentContentProcessHelper( + const nsACString& aMimeContentType, nsIRequest* aRequest, + BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + // We need to get a hold of a ContentChild so that we can begin forwarding + // this data to the parent. In the HTTP case, this is unfortunate, since + // we're actually passing data from parent->child->parent wastefully, but + // the Right Fix will eventually be to short-circuit those channels on the + // parent side based on some sort of subscription concept. + using mozilla::dom::ContentChild; + using mozilla::dom::ExternalHelperAppChild; + ContentChild* child = ContentChild::GetSingleton(); + if (!child) { + return NS_ERROR_FAILURE; + } + + nsCString disp; + nsCOMPtr<nsIURI> uri; + int64_t contentLength = -1; + bool wasFileChannel = false; + uint32_t contentDisposition = -1; + nsAutoString fileName; + nsCOMPtr<nsILoadInfo> loadInfo; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + channel->GetContentDispositionHeader(disp); + loadInfo = channel->LoadInfo(); + + nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest)); + wasFileChannel = fileChan != nullptr; + } + + nsCOMPtr<nsIURI> referrer; + NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer)); + + Maybe<mozilla::net::LoadInfoArgs> loadInfoArgs; + MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs)); + + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aRequest)); + // Determine whether a new window was opened specifically for this request + bool shouldCloseWindow = false; + if (props) { + props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, + &shouldCloseWindow); + } + + // Now we build a protocol for forwarding our data to the parent. The + // protocol will act as a listener on the child-side and create a "real" + // helperAppService listener on the parent-side, via another call to + // DoContent. + RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild(); + MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor( + childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp, + contentDisposition, fileName, aForceSave, contentLength, wasFileChannel, + referrer, aContentContext, shouldCloseWindow)); + + NS_ADDREF(*aStreamListener = childListener); + + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + + RefPtr<nsExternalAppHandler> handler = + new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext, + this, fileName, reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + childListener->SetHandler(handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::CreateListener( + const nsACString& aMimeContentType, nsIRequest* aRequest, + BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + MOZ_ASSERT(!XRE_IsContentProcess()); + + nsAutoString fileName; + nsAutoCString fileExtension; + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + uint32_t contentDisposition = -1; + + // Get the file extension and name that we will need later + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsIURI> uri; + int64_t contentLength = -1; + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + + // Check if we have a POST request, in which case we don't want to use + // the url's extension + bool allowURLExt = !net::ChannelIsPost(channel); + + // Check if we had a query string - we don't want to check the URL + // extension if a query is present in the URI + // If we already know we don't want to check the URL extension, don't + // bother checking the query + if (uri && allowURLExt) { + nsCOMPtr<nsIURL> url = do_QueryInterface(uri); + + if (url) { + nsAutoCString query; + + // We only care about the query for HTTP and HTTPS URLs + if (uri->SchemeIs("http") || uri->SchemeIs("https")) { + url->GetQuery(query); + } + + // Only get the extension if the query is empty; if it isn't, then the + // extension likely belongs to a cgi script and isn't helpful + allowURLExt = query.IsEmpty(); + } + } + // Extract name & extension + bool isAttachment = GetFilenameAndExtensionFromChannel( + channel, fileName, fileExtension, allowURLExt); + LOG(("Found extension '%s' (filename is '%s', handling attachment: %i)", + fileExtension.get(), NS_ConvertUTF16toUTF8(fileName).get(), + isAttachment)); + if (isAttachment) { + reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST; + } + } + + LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n", + PromiseFlatCString(aMimeContentType).get(), fileExtension.get())); + + // We get the mime service here even though we're the default implementation + // of it, so it's possible to override only the mime service and not need to + // reimplement the whole external helper app service itself. + nsCOMPtr<nsIMIMEService> mimeSvc(do_GetService(NS_MIMESERVICE_CONTRACTID)); + NS_ENSURE_TRUE(mimeSvc, NS_ERROR_FAILURE); + + // Try to find a mime object by looking at the mime type/extension + nsCOMPtr<nsIMIMEInfo> mimeInfo; + if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT, + nsCaseInsensitiveCStringComparator)) { + nsAutoCString mimeType; + if (!fileExtension.IsEmpty()) { + mimeSvc->GetFromTypeAndExtension(""_ns, fileExtension, + getter_AddRefs(mimeInfo)); + if (mimeInfo) { + mimeInfo->GetMIMEType(mimeType); + + LOG(("OS-Provided mime type '%s' for extension '%s'\n", mimeType.get(), + fileExtension.get())); + } + } + + if (fileExtension.IsEmpty() || mimeType.IsEmpty()) { + // Extension lookup gave us no useful match + mimeSvc->GetFromTypeAndExtension( + nsLiteralCString(APPLICATION_OCTET_STREAM), fileExtension, + getter_AddRefs(mimeInfo)); + mimeType.AssignLiteral(APPLICATION_OCTET_STREAM); + } + + if (channel) { + channel->SetContentType(mimeType); + } + + // Don't overwrite SERVERREQUEST + if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) { + reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED; + } + } else { + mimeSvc->GetFromTypeAndExtension(aMimeContentType, fileExtension, + getter_AddRefs(mimeInfo)); + } + LOG(("Type/Ext lookup found 0x%p\n", mimeInfo.get())); + + // No mimeinfo -> we can't continue. probably OOM. + if (!mimeInfo) { + return NS_ERROR_OUT_OF_MEMORY; + } + + *aStreamListener = nullptr; + // We want the mimeInfo's primary extension to pass it to + // nsExternalAppHandler + nsAutoCString buf; + mimeInfo->GetPrimaryExtension(buf); + + // NB: ExternalHelperAppParent depends on this listener always being an + // nsExternalAppHandler. If this changes, make sure to update that code. + nsExternalAppHandler* handler = + new nsExternalAppHandler(mimeInfo, buf, aContentContext, aWindowContext, + this, fileName, reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + NS_ADDREF(*aStreamListener = handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::DoContent( + const nsACString& aMimeContentType, nsIRequest* aRequest, + nsIInterfaceRequestor* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + // Scripted interface requestors cannot return an instance of the + // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so + // get to the window via `nsIDOMWindow`. Unfortunately, at that point we + // don't know whether the thing we got is an inner or outer window, so have to + // work with either one. + RefPtr<BrowsingContext> bc; + nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext); + if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) { + bc = outerWindow->GetBrowsingContext(); + } else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow = + do_QueryInterface(domWindow)) { + bc = innerWindow->GetBrowsingContext(); + } + + if (XRE_IsContentProcess()) { + return DoContentContentProcessHelper(aMimeContentType, aRequest, bc, + aForceSave, aWindowContext, + aStreamListener); + } + + nsresult rv = CreateListener(aMimeContentType, aRequest, bc, aForceSave, + aWindowContext, aStreamListener); + return rv; +} + +NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension( + const nsACString& aExtension, const nsACString& aEncodingType, + bool* aApplyDecoding) { + *aApplyDecoding = true; + uint32_t i; + for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) { + if (aExtension.LowerCaseEqualsASCII( + nonDecodableExtensions[i].mFileExtension) && + aEncodingType.LowerCaseEqualsASCII( + nonDecodableExtensions[i].mMimeType)) { + *aApplyDecoding = false; + break; + } + } + return NS_OK; +} + +nsresult nsExternalHelperAppService::GetFileTokenForPath( + const char16_t* aPlatformAppPath, nsIFile** aFile) { + nsDependentString platformAppPath(aPlatformAppPath); + // First, check if we have an absolute path + nsIFile* localFile = nullptr; + nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile); + if (NS_SUCCEEDED(rv)) { + *aFile = localFile; + bool exists; + if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) { + NS_RELEASE(*aFile); + return NS_ERROR_FILE_NOT_FOUND; + } + return NS_OK; + } + + // Second, check if file exists in mozilla program directory + rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile); + if (NS_SUCCEEDED(rv)) { + rv = (*aFile)->Append(platformAppPath); + if (NS_SUCCEEDED(rv)) { + bool exists = false; + rv = (*aFile)->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) return NS_OK; + } + NS_RELEASE(*aFile); + } + + return NS_ERROR_NOT_AVAILABLE; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// begin external protocol service default implementation... +////////////////////////////////////////////////////////////////////////////////////////////////////// +NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsCOMPtr<nsIHandlerInfo> handlerInfo; + nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme), + getter_AddRefs(handlerInfo)); + if (NS_SUCCEEDED(rv)) { + // See if we have any known possible handler apps for this + nsCOMPtr<nsIMutableArray> possibleHandlers; + handlerInfo->GetPossibleApplicationHandlers( + getter_AddRefs(possibleHandlers)); + + uint32_t length; + possibleHandlers->GetLength(&length); + if (length) { + *aHandlerExists = true; + return NS_OK; + } + } + + // if not, fall back on an os-based handler + return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists); +} + +NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol( + const char* aProtocolScheme, bool* aResult) { + // check the per protocol setting first. it always takes precedence. + // if not set, then use the global setting. + + nsAutoCString prefName("network.protocol-handler.expose."); + prefName += aProtocolScheme; + bool val; + if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) { + *aResult = val; + return NS_OK; + } + + // by default, no protocol is exposed. i.e., by default all link clicks must + // go through the external protocol service. most applications override this + // default behavior. + *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false); + + return NS_OK; +} + +static const char kExternalProtocolPrefPrefix[] = + "network.protocol-handler.external."; +static const char kExternalProtocolDefaultPref[] = + "network.protocol-handler.external-default"; + +NS_IMETHODIMP +nsExternalHelperAppService::LoadURI(nsIURI* aURI, + nsIPrincipal* aTriggeringPrincipal, + BrowsingContext* aBrowsingContext) { + NS_ENSURE_ARG_POINTER(aURI); + + if (XRE_IsContentProcess()) { + mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal( + aURI, aTriggeringPrincipal, aBrowsingContext); + return NS_OK; + } + + nsAutoCString spec; + aURI->GetSpec(spec); + + if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI; + + spec.ReplaceSubstring("\"", "%22"); + spec.ReplaceSubstring("`", "%60"); + + nsCOMPtr<nsIIOService> ios(do_GetIOService()); + nsCOMPtr<nsIURI> uri; + nsresult rv = ios->NewURI(spec, nullptr, nullptr, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString scheme; + uri->GetScheme(scheme); + if (scheme.IsEmpty()) return NS_OK; // must have a scheme + + // Deny load if the prefs say to do so + nsAutoCString externalPref(kExternalProtocolPrefPrefix); + externalPref += scheme; + bool allowLoad = false; + if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) { + // no scheme-specific value, check the default + if (NS_FAILED( + Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) { + return NS_OK; // missing default pref + } + } + + if (!allowLoad) { + return NS_OK; // explicitly denied + } + + // Now check if the principal is allowed to access the navigated context. + // We allow navigating subframes, even if not same-origin - non-external + // links can always navigate everywhere, so this is a minor additional + // restriction, only aiming to prevent some types of spoofing attacks + // from otherwise disjoint browsingcontext trees. + if (aBrowsingContext && aTriggeringPrincipal && + !StaticPrefs::security_allow_disjointed_external_uri_loads() && + // Add-on principals are always allowed: + !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() && + // As is chrome code: + !aTriggeringPrincipal->IsSystemPrincipal()) { + RefPtr<BrowsingContext> bc = aBrowsingContext; + WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal(); + bool foundAccessibleFrame = false; + + // Also allow this load if the target is a toplevel BC and contains a + // non-web-controlled about:blank document + if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) { + RefPtr<nsIURI> uri = wgp->GetDocumentURI(); + foundAccessibleFrame = + uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank"); + } + + while (!foundAccessibleFrame) { + if (wgp) { + foundAccessibleFrame = + aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal()); + } + // We have to get the parent via the bc, because there may not + // be a window global for the innermost bc; see bug 1650162. + BrowsingContext* parent = bc->GetParent(); + if (!parent) { + break; + } + bc = parent; + wgp = parent->Canonical()->GetCurrentWindowGlobal(); + } + + if (!foundAccessibleFrame) { + // See if this navigation could have come from a subframe. + nsTArray<RefPtr<BrowsingContext>> contexts; + aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts); + for (const auto& kid : contexts) { + wgp = kid->Canonical()->GetCurrentWindowGlobal(); + if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) { + foundAccessibleFrame = true; + break; + } + } + } + + if (!foundAccessibleFrame) { + return NS_OK; // deny the load. + } + } + + nsCOMPtr<nsIHandlerInfo> handler; + rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIContentDispatchChooser> chooser = + do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return chooser->HandleURI(handler, uri, aTriggeringPrincipal, + aBrowsingContext); +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// Methods related to deleting temporary files on exit +////////////////////////////////////////////////////////////////////////////////////////////////////// + +/* static */ +nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper( + nsIFile* aTemporaryFile, nsCOMArray<nsIFile>& aFileList) { + bool isFile = false; + + // as a safety measure, make sure the nsIFile is really a file and not a + // directory object. + aTemporaryFile->IsFile(&isFile); + if (!isFile) return NS_OK; + + aFileList.AppendObject(aTemporaryFile); + + return NS_OK; +} + +NS_IMETHODIMP +nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) { + return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList); +} + +NS_IMETHODIMP +nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible( + nsIFile* aTemporaryFile) { + return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList); +} + +void nsExternalHelperAppService::ExpungeTemporaryFilesHelper( + nsCOMArray<nsIFile>& fileList) { + int32_t numEntries = fileList.Count(); + nsIFile* localFile; + for (int32_t index = 0; index < numEntries; index++) { + localFile = fileList[index]; + if (localFile) { + // First make the file writable, since the temp file is probably readonly. + localFile->SetPermissions(0600); + localFile->Remove(false); + } + } + + fileList.Clear(); +} + +void nsExternalHelperAppService::ExpungeTemporaryFiles() { + ExpungeTemporaryFilesHelper(mTemporaryFilesList); +} + +void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() { + ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList); +} + +static const char kExternalWarningPrefPrefix[] = + "network.protocol-handler.warn-external."; +static const char kExternalWarningDefaultPref[] = + "network.protocol-handler.warn-external-default"; + +NS_IMETHODIMP +nsExternalHelperAppService::GetProtocolHandlerInfo( + const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) { + // XXX enterprise customers should be able to turn this support off with a + // single master pref (maybe use one of the "exposed" prefs here?) + + bool exists; + nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo); + if (NS_FAILED(rv)) { + // Either it knows nothing, or we ran out of memory + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns); + if (NS_SUCCEEDED(rv)) return NS_OK; + } + } + + return SetProtocolHandlerDefaults(*aHandlerInfo, exists); +} + +NS_IMETHODIMP +nsExternalHelperAppService::SetProtocolHandlerDefaults( + nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) { + // this type isn't in our database, so we've only got an OS default handler, + // if one exists + + if (aOSHandlerExists) { + // we've got a default, so use it + aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault); + + // whether or not to ask the user depends on the warning preference + nsAutoCString scheme; + aHandlerInfo->GetType(scheme); + + nsAutoCString warningPref(kExternalWarningPrefPrefix); + warningPref += scheme; + bool warn; + if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) { + // no scheme-specific value, check the default + warn = Preferences::GetBool(kExternalWarningDefaultPref, true); + } + aHandlerInfo->SetAlwaysAskBeforeHandling(warn); + } else { + // If no OS default existed, we set the preferred action to alwaysAsk. + // This really means not initialized (i.e. there's no available handler) + // to all the code... + aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk); + } + + return NS_OK; +} + +// XPCOM profile change observer +NS_IMETHODIMP +nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* someData) { + if (!strcmp(aTopic, "profile-before-change")) { + ExpungeTemporaryFiles(); + } else if (!strcmp(aTopic, "last-pb-context-exited")) { + ExpungeTemporaryPrivateFiles(); + } + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// begin external app handler implementation +////////////////////////////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ADDREF(nsExternalAppHandler) +NS_IMPL_RELEASE(nsExternalAppHandler) + +NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher) + NS_INTERFACE_MAP_ENTRY(nsICancelable) + NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler) +NS_INTERFACE_MAP_END + +nsExternalAppHandler::nsExternalAppHandler( + nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension, + BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext, + nsExternalHelperAppService* aExtProtSvc, + const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave) + : mMimeInfo(aMIMEInfo), + mBrowsingContext(aBrowsingContext), + mWindowContext(aWindowContext), + mSuggestedFileName(aSuggestedFilename), + mForceSave(aForceSave), + mCanceled(false), + mStopRequestIssued(false), + mIsFileChannel(false), + mShouldCloseWindow(false), + mHandleInternally(false), + mReason(aReason), + mTempFileIsExecutable(false), + mTimeDownloadStarted(0), + mContentLength(-1), + mProgress(0), + mSaver(nullptr), + mDialogProgressListener(nullptr), + mTransfer(nullptr), + mRequest(nullptr), + mExtProtSvc(aExtProtSvc) { + // make sure the extention includes the '.' + if (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.') + mTempFileExtension = char16_t('.'); + AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension); + + // Get mSuggestedFileName's current file extension. + nsAutoString originalFileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos != kNotFound) { + mSuggestedFileName.Right(originalFileExt, + mSuggestedFileName.Length() - pos); + } + + // replace platform specific path separator and illegal characters to avoid + // any confusion. + // Try to keep the use of spaces or underscores in sync with the Downloads + // code sanitization in DownloadPaths.jsm + mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_'); + mSuggestedFileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' '); + mSuggestedFileName.ReplaceChar(char16_t(0), '_'); + mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS, '_'); + mTempFileExtension.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' '); + + // Remove unsafe bidi characters which might have spoofing implications (bug + // 511521). + const char16_t unsafeBidiCharacters[] = { + char16_t(0x061c), // Arabic Letter Mark + char16_t(0x200e), // Left-to-Right Mark + char16_t(0x200f), // Right-to-Left Mark + char16_t(0x202a), // Left-to-Right Embedding + char16_t(0x202b), // Right-to-Left Embedding + char16_t(0x202c), // Pop Directional Formatting + char16_t(0x202d), // Left-to-Right Override + char16_t(0x202e), // Right-to-Left Override + char16_t(0x2066), // Left-to-Right Isolate + char16_t(0x2067), // Right-to-Left Isolate + char16_t(0x2068), // First Strong Isolate + char16_t(0x2069), // Pop Directional Isolate + char16_t(0)}; + mSuggestedFileName.ReplaceChar(unsafeBidiCharacters, '_'); + mTempFileExtension.ReplaceChar(unsafeBidiCharacters, '_'); + + // Remove trailing or leading spaces that we may have generated while + // sanitizing. + mSuggestedFileName.CompressWhitespace(); + mTempFileExtension.CompressWhitespace(); + + EnsureCorrectExtension(originalFileExt); + + mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096); +} + +nsExternalAppHandler::~nsExternalAppHandler() { + MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted"); +} + +bool nsExternalAppHandler::ShouldForceExtension(const nsString& aFileExt) { + nsAutoCString MIMEType; + if (!mMimeInfo || NS_FAILED(mMimeInfo->GetMIMEType(MIMEType))) { + return false; + } + + bool canForce = StringBeginsWith(MIMEType, "image/"_ns) || + StringBeginsWith(MIMEType, "audio/"_ns) || + StringBeginsWith(MIMEType, "video/"_ns); + + if (!canForce && + StaticPrefs::browser_download_sanitize_non_media_extensions()) { + for (const char* mime : forcedExtensionMimetypes) { + if (MIMEType.Equals(mime)) { + canForce = true; + break; + } + } + } + if (!canForce) { + return false; + } + + // If we get here, we know for sure the mimetype allows us to overwrite the + // existing extension, if it's wrong. Return whether the extension is wrong: + + bool knownExtension = false; + // Note that aFileExt is either empty or consists of an extension + // *including the dot* which we remove for ExtensionExists(). + return ( + aFileExt.IsEmpty() || aFileExt.EqualsLiteral(".") || + (NS_SUCCEEDED(mMimeInfo->ExtensionExists( + Substring(NS_ConvertUTF16toUTF8(aFileExt), 1), &knownExtension)) && + !knownExtension)); +} + +void nsExternalAppHandler::EnsureCorrectExtension(const nsString& aFileExt) { + // If we don't have an extension (which will include the .), + // just short-circuit. + if (mTempFileExtension.Length() <= 1) { + return; + } + + // After removing trailing whitespaces from the name, if we have a + // temp file extension, there are broadly 2 cases where we want to + // replace the extension. + // First, if the file extension contains invalid characters. + // Second, for document type mimetypes, if the extension is either + // missing or not valid for this mimetype. + bool replaceExtension = + (aFileExt.FindCharInSet(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS) != + kNotFound) || + ShouldForceExtension(aFileExt); + + if (replaceExtension) { + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos != kNotFound) { + mSuggestedFileName = + Substring(mSuggestedFileName, 0, pos) + mTempFileExtension; + } else { + mSuggestedFileName.Append(mTempFileExtension); + } + } + + /* + * Ensure we don't double-append the file extension if it matches: + */ + if (replaceExtension || + aFileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator)) { + // Matches -> mTempFileExtension can be empty + mTempFileExtension.Truncate(); + } +} + +void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) { + MOZ_ASSERT(XRE_IsContentProcess(), "in child process"); + // Remove our request from the child loadGroup + RetargetLoadNotifications(request); +} + +NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener( + nsIWebProgressListener2* aWebProgressListener) { + // This is always called by nsHelperDlg.js. Go ahead and register the + // progress listener. At this point, we don't have mTransfer. + mDialogProgressListener = aWebProgressListener; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) { + if (mFinalFileDestination) + *aTarget = mFinalFileDestination; + else + *aTarget = mTempFile; + + NS_IF_ADDREF(*aTarget); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) { + // Use the real target if it's been set + if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec); + + // Otherwise, use the stored executable-ness of the temporary + *aExec = mTempFileIsExecutable; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) { + *aTime = mTimeDownloadStarted; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) { + *aContentLength = mContentLength; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId( + uint64_t* aBrowsingContextId) { + *aBrowsingContextId = mBrowsingContext->Id(); + return NS_OK; +} + +void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) { + // we are going to run the downloading of the helper app in our own little + // docloader / load group context. so go ahead and force the creation of a + // load group and doc loader for us to use... + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + if (!aChannel) return; + + bool isPrivate = NS_UsePrivateBrowsing(aChannel); + + nsCOMPtr<nsILoadGroup> oldLoadGroup; + aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup)); + + if (oldLoadGroup) { + oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED); + } + + aChannel->SetLoadGroup(nullptr); + aChannel->SetNotificationCallbacks(nullptr); + + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel); + if (pbChannel) { + pbChannel->SetPrivate(isPrivate); + } +} + +nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { + // First we need to try to get the destination directory for the temporary + // file. + nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // At this point, we do not have a filename for the temp file. For security + // purposes, this cannot be predictable, so we must use a cryptographic + // quality PRNG to generate one. + // We will request raw random bytes, and transform that to a base64 string, + // as all characters from the base64 set are acceptable for filenames. For + // each three bytes of random data, we will get four bytes of ASCII. Request + // a bit more, to be safe, and truncate to the length we want in the end. + + const uint32_t wantedFileNameLength = 8; + const uint32_t requiredBytesLength = + static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3); + + nsCOMPtr<nsIRandomGenerator> rg = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint8_t* buffer; + rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString tempLeafName; + nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer), + requiredBytesLength); + rv = Base64Encode(randomData, tempLeafName); + free(buffer); + buffer = nullptr; + NS_ENSURE_SUCCESS(rv, rv); + + tempLeafName.Truncate(wantedFileNameLength); + + // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need + // to replace illegal characters -- notably '/' + tempLeafName.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + + // now append our extension. + nsAutoCString ext; + mMimeInfo->GetPrimaryExtension(ext); + if (!ext.IsEmpty()) { + ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + if (ext.First() != '.') tempLeafName.Append('.'); + tempLeafName.Append(ext); + } + + // We need to temporarily create a dummy file with the correct + // file extension to determine the executable-ness, so do this before adding + // the extra .part extension. + nsCOMPtr<nsIFile> dummyFile; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the file name without .part + rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName)); + NS_ENSURE_SUCCESS(rv, rv); + rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + // Store executable-ness then delete + dummyFile->IsExecutable(&mTempFileIsExecutable); + dummyFile->Remove(false); + + // Add an additional .part to prevent the OS from running this file in the + // default application. + tempLeafName.AppendLiteral(".part"); + + rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName)); + // make this file unique!!! + NS_ENSURE_SUCCESS(rv, rv); + rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + // Now save the temp leaf name, minus the ".part" bit, so we can use it later. + // This is a bit broken in the case when createUnique actually had to append + // some numbers, because then we now have a filename like foo.bar-1.part and + // we'll end up with foo.bar-1.bar as our final filename if we end up using + // this. But the other options are all bad too.... Ideally we'd have a way + // to tell createUnique to put its unique marker before the extension that + // comes before ".part" or something. + rv = mTempFile->GetLeafName(mTempLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns), + NS_ERROR_UNEXPECTED); + + // Strip off the ".part" from mTempLeafName + mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1); + + MOZ_ASSERT(!mSaver, "Output file initialization called more than once!"); + mSaver = + do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mSaver->SetObserver(this); + if (NS_FAILED(rv)) { + mSaver = nullptr; + return rv; + } + + rv = mSaver->EnableSha256(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mSaver->EnableSignatureInfo(); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("Enabled hashing and signature verification")); + + rv = mSaver->SetTarget(mTempFile, false); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +void nsExternalAppHandler::MaybeApplyDecodingForExtension( + nsIRequest* aRequest) { + MOZ_ASSERT(aRequest); + + nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aRequest); + if (!encChannel) { + return; + } + + // Turn off content encoding conversions if needed + bool applyConversion = true; + + // First, check to see if conversion is already disabled. If so, we + // have nothing to do here. + encChannel->GetApplyConversion(&applyConversion); + if (!applyConversion) { + return; + } + + nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(mSourceUrl)); + if (sourceURL) { + nsAutoCString extension; + sourceURL->GetFileExtension(extension); + if (!extension.IsEmpty()) { + nsCOMPtr<nsIUTF8StringEnumerator> encEnum; + encChannel->GetContentEncodings(getter_AddRefs(encEnum)); + if (encEnum) { + bool hasMore; + nsresult rv = encEnum->HasMore(&hasMore); + if (NS_SUCCEEDED(rv) && hasMore) { + nsAutoCString encType; + rv = encEnum->GetNext(encType); + if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) { + MOZ_ASSERT(mExtProtSvc); + mExtProtSvc->ApplyDecodingForExtension(extension, encType, + &applyConversion); + } + } + } + } + } + + encChannel->SetApplyConversion(applyConversion); +} + +already_AddRefed<nsIInterfaceRequestor> +nsExternalAppHandler::GetDialogParent() { + nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext; + + if (!dialogParent && mBrowsingContext) { + dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow()); + } + if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) { + RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement(); + if (element) { + dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow()); + } + } + return dialogParent.forget(); +} + +NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { + MOZ_ASSERT(request, "OnStartRequest without request?"); + + // Set mTimeDownloadStarted here as the download has already started and + // we want to record the start time before showing the filepicker. + mTimeDownloadStarted = PR_Now(); + + mRequest = request; + + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + + nsresult rv; + nsAutoCString MIMEType; + if (mMimeInfo) { + mMimeInfo->GetMIMEType(MIMEType); + } + // Now get the URI + if (aChannel) { + aChannel->GetURI(getter_AddRefs(mSourceUrl)); + } + + mDownloadClassification = + nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType); + + if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) { + // If the download is rated as forbidden, + // cancel the request so no ui knows about this. + mCanceled = true; + request->Cancel(NS_ERROR_ABORT); + return NS_OK; + } + + nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(request)); + mIsFileChannel = fileChan != nullptr; + if (!mIsFileChannel) { + // It's possible that this request came from the child process and the + // file channel actually lives there. If this returns true, then our + // mSourceUrl will be an nsIFileURL anyway. + nsCOMPtr<dom::nsIExternalHelperAppParent> parent( + do_QueryInterface(request)); + mIsFileChannel = parent && parent->WasFileChannel(); + } + + // Get content length + if (aChannel) { + aChannel->GetContentLength(&mContentLength); + } + + if (mBrowsingContext) { + mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext); + mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow); + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv)); + // Determine whether a new window was opened specifically for this request + if (props) { + bool tmp = false; + if (NS_SUCCEEDED( + props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) { + mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp); + } + } + } + + // retarget all load notifications to our docloader instead of the original + // window's docloader... + RetargetLoadNotifications(request); + + // Close the underlying DOMWindow if it was opened specifically for the + // download. We don't run this in the content process, since we have + // an instance running in the parent as well, which will handle this + // if needed. + if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) { + mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow(); + } + + // In an IPC setting, we're allowing the child process, here, to make + // decisions about decoding the channel (e.g. decompression). It will + // still forward the decoded (uncompressed) data back to the parent. + // Con: Uncompressed data means more IPC overhead. + // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel. + // Parent process doesn't need to expect CPU time on decompression. + MaybeApplyDecodingForExtension(aChannel); + + // At this point, the child process has done everything it can usefully do + // for OnStartRequest. + if (XRE_IsContentProcess()) { + return NS_OK; + } + + rv = SetUpTempFile(aChannel); + if (NS_FAILED(rv)) { + nsresult transferError = rv; + + rv = CreateFailedTransfer(); + if (NS_FAILED(rv)) { + LOG( + ("Failed to create transfer to report failure." + "Will fallback to prompter!")); + } + + mCanceled = true; + request->Cancel(transferError); + + nsAutoString path; + if (mTempFile) mTempFile->GetPath(path); + + SendStatusChange(kWriteError, transferError, request, path); + + return NS_OK; + } + + // Inform channel it is open on behalf of a download to throttle it during + // page loads and prevent its caching. + nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel); + if (httpInternal) { + rv = httpInternal->SetChannelIsForDownload(true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (mSourceUrl->SchemeIs("data")) { + // In case we're downloading a data:// uri + // we don't want to apply AllowTopLevelNavigationToDataURI. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + loadInfo->SetForceAllowDataURI(true); + } + + // now that the temp file is set up, find out if we need to invoke a dialog + // asking the user what they want us to do with this content... + + // We can get here for three reasons: "can't handle", "sniffed type", or + // "server sent content-disposition:attachment". In the first case we want + // to honor the user's "always ask" pref; in the other two cases we want to + // honor it only if the default action is "save". Opening attachments in + // helper apps by default breaks some websites (especially if the attachment + // is one part of a multipart document). Opening sniffed content in helper + // apps by default introduces security holes that we'd rather not have. + + // So let's find out whether the user wants to be prompted. If he does not, + // check mReason and the preferred action to see what we should do. + + bool alwaysAsk = true; + mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); + if (alwaysAsk) { + // But we *don't* ask if this mimeInfo didn't come from + // our user configuration datastore and the user has said + // at some point in the distant past that they don't + // want to be asked. The latter fact would have been + // stored in pref strings back in the old days. + + bool mimeTypeIsInDatastore = false; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore); + } + if (!handlerSvc || !mimeTypeIsInDatastore) { + if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF, + MIMEType.get())) { + // Don't need to ask after all. + alwaysAsk = false; + // Make sure action matches pref (save to disk). + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF, + MIMEType.get())) { + // Don't need to ask after all. + alwaysAsk = false; + } + } + } else if (MIMEType.EqualsLiteral("text/plain")) { + nsAutoCString ext; + mMimeInfo->GetPrimaryExtension(ext); + // If people are sending us ApplicationReputation-eligible files with + // text/plain mimetypes, enforce asking the user what to do. + if (!ext.IsEmpty()) { + nsAutoCString dummyFileName("f"); + if (ext.First() != '.') { + dummyFileName.Append("."); + } + ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + nsCOMPtr<nsIApplicationReputationService> appRep = + components::ApplicationReputation::Service(); + appRep->IsBinary(dummyFileName + ext, &alwaysAsk); + } + } + + int32_t action = nsIMIMEInfo::saveToDisk; + mMimeInfo->GetPreferredAction(&action); + + // OK, now check why we're here + if (!alwaysAsk && mReason != nsIHelperAppLauncherDialog::REASON_CANTHANDLE) { + // Force asking if we're not saving. See comment back when we fetched the + // alwaysAsk boolean for details. + alwaysAsk = (action != nsIMIMEInfo::saveToDisk); + } + + // If we're not asking, check we actually know what to do: + if (!alwaysAsk) { + alwaysAsk = action != nsIMIMEInfo::saveToDisk && + action != nsIMIMEInfo::useHelperApp && + action != nsIMIMEInfo::useSystemDefault; + } + + // if we were told that we _must_ save to disk without asking, all the stuff + // before this is irrelevant; override it + if (mForceSave) { + alwaysAsk = false; + action = nsIMIMEInfo::saveToDisk; + } + + if (alwaysAsk) { + // Display the dialog + mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // this will create a reference cycle (the dialog holds a reference to us as + // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer. + nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent(); + rv = mDialog->Show(this, dialogParent, mReason); + + // what do we do if the dialog failed? I guess we should call Cancel and + // abort the load.... + } else { + // We need to do the save/open immediately, then. +#ifdef XP_WIN + /* We need to see whether the file we've got here could be + * executable. If it could, we had better not try to open it! + * We can skip this check, though, if we have a setting to open in a + * helper app. + * This code mirrors the code in + * nsExternalAppHandler::LaunchWithApplication so that what we + * test here is as close as possible to what will really be + * happening if we decide to execute + */ + nsCOMPtr<nsIHandlerApp> prefApp; + mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp)); + if (action != nsIMIMEInfo::useHelperApp || !prefApp) { + nsCOMPtr<nsIFile> fileToTest; + GetTargetFile(getter_AddRefs(fileToTest)); + if (fileToTest) { + bool isExecutable; + rv = fileToTest->IsExecutable(&isExecutable); + if (NS_FAILED(rv) || + isExecutable) { // checking NS_FAILED, because paranoia is good + action = nsIMIMEInfo::saveToDisk; + } + } else { // Paranoia is good here too, though this really should not + // happen + NS_WARNING( + "GetDownloadInfo returned a null file after the temp file has been " + "set up! "); + action = nsIMIMEInfo::saveToDisk; + } + } + +#endif + if (action == nsIMIMEInfo::useHelperApp || + action == nsIMIMEInfo::useSystemDefault) { + rv = LaunchWithApplication(mHandleInternally); + } else { + rv = PromptForSaveDestination(); + } + } + return NS_OK; +} + +// Convert error info into proper message text and send OnStatusChange +// notification to the dialog progress listener or nsITransfer implementation. +void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv, + nsIRequest* aRequest, + const nsString& path) { + const char* msgId = nullptr; + switch (rv) { + case NS_ERROR_OUT_OF_MEMORY: + // No memory + msgId = "noMemory"; + break; + + case NS_ERROR_FILE_DISK_FULL: + case NS_ERROR_FILE_NO_DEVICE_SPACE: + // Out of space on target volume. + msgId = "diskFull"; + break; + + case NS_ERROR_FILE_READ_ONLY: + // Attempt to write to read/only file. + msgId = "readOnly"; + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + if (type == kWriteError) { + // Attempt to write without sufficient permissions. +#if defined(ANDROID) + // On Android this means the SD card is present but + // unavailable (read-only). + msgId = "SDAccessErrorCardReadOnly"; +#else + msgId = "accessError"; +#endif + } else { + msgId = "launchError"; + } + break; + + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST: + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + // Helper app not found, let's verify this happened on launch + if (type == kLaunchError) { + msgId = "helperAppNotFound"; + break; + } +#if defined(ANDROID) + else if (type == kWriteError) { + // On Android this means the SD card is missing (not in + // SD slot). + msgId = "SDAccessErrorCardMissing"; + break; + } +#endif + [[fallthrough]]; + + default: + // Generic read/write/launch error message. + switch (type) { + case kReadError: + msgId = "readError"; + break; + case kWriteError: + msgId = "writeError"; + break; + case kLaunchError: + msgId = "launchError"; + break; + } + break; + } + + MOZ_LOG( + nsExternalHelperAppService::mLog, LogLevel::Error, + ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n", + msgId, type, mDialogProgressListener.get(), mTransfer.get(), + static_cast<uint32_t>(rv))); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get())); + + // Get properties file bundle and extract status string. + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::services::GetStringBundleService(); + if (stringService) { + nsCOMPtr<nsIStringBundle> bundle; + if (NS_SUCCEEDED(stringService->CreateBundle( + "chrome://global/locale/nsWebBrowserPersist.properties", + getter_AddRefs(bundle)))) { + nsAutoString msgText; + AutoTArray<nsString, 1> strings = {path}; + if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) { + if (mDialogProgressListener) { + // We have a listener, let it handle the error. + mDialogProgressListener->OnStatusChange( + nullptr, (type == kReadError) ? aRequest : nullptr, rv, + msgText.get()); + } else if (mTransfer) { + mTransfer->OnStatusChange(nullptr, + (type == kReadError) ? aRequest : nullptr, + rv, msgText.get()); + } else if (XRE_IsParentProcess()) { + // We don't have a listener. Simply show the alert ourselves. + nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent(); + nsresult qiRv; + nsCOMPtr<nsIPrompt> prompter(do_GetInterface(dialogParent, &qiRv)); + nsAutoString title; + bundle->FormatStringFromName("title", strings, title); + + MOZ_LOG( + nsExternalHelperAppService::mLog, LogLevel::Debug, + ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32 + ", title='%s', msg='%s'", + mBrowsingContext.get(), prompter.get(), + static_cast<uint32_t>(qiRv), NS_ConvertUTF16toUTF8(title).get(), + NS_ConvertUTF16toUTF8(msgText).get())); + + // If we didn't have a prompter we will try and get a window + // instead, get it's docshell and use it to alert the user. + if (!prompter) { + nsCOMPtr<nsPIDOMWindowOuter> window(do_GetInterface(dialogParent)); + if (!window || !window->GetDocShell()) { + return; + } + + prompter = do_GetInterface(window->GetDocShell(), &qiRv); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug, + ("No prompter from mBrowsingContext, using DocShell, " + "window=0x%p, docShell=0x%p, " + "prompter=0x%p, qi rv=0x%08" PRIX32, + window.get(), window->GetDocShell(), prompter.get(), + static_cast<uint32_t>(qiRv))); + + // If we still don't have a prompter, there's nothing else we + // can do so just return. + if (!prompter) { + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + ("No prompter from DocShell, no way to alert user")); + return; + } + } + + // We should always have a prompter at this point. + prompter->Alert(title.get(), msgText.get()); + } + } + } + } +} + +NS_IMETHODIMP +nsExternalAppHandler::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + nsresult rv = NS_OK; + // first, check to see if we've been canceled.... + if (mCanceled || !mSaver) { + // then go cancel our underlying channel too + return request->Cancel(NS_BINDING_ABORTED); + } + + // read the data out of the stream and write it to the temp file. + if (count > 0) { + mProgress += count; + + nsCOMPtr<nsIStreamListener> saver = do_QueryInterface(mSaver); + rv = saver->OnDataAvailable(request, inStr, sourceOffset, count); + if (NS_SUCCEEDED(rv)) { + // Send progress notification. + if (mTransfer) { + mTransfer->OnProgressChange64(nullptr, request, mProgress, + mContentLength, mProgress, + mContentLength); + } + } else { + // An error occurred, notify listener. + nsAutoString tempFilePath; + if (mTempFile) { + mTempFile->GetPath(tempFilePath); + } + SendStatusChange(kReadError, rv, request, tempFilePath); + + // Cancel the download. + Cancel(rv); + } + } + return rv; +} + +NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + LOG( + ("nsExternalAppHandler::OnStopRequest\n" + " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n", + mCanceled, mTransfer.get(), static_cast<uint32_t>(aStatus))); + + mStopRequestIssued = true; + + // Cancel if the request did not complete successfully. + if (!mCanceled && NS_FAILED(aStatus)) { + // Send error notification. + nsAutoString tempFilePath; + if (mTempFile) mTempFile->GetPath(tempFilePath); + SendStatusChange(kReadError, aStatus, request, tempFilePath); + + Cancel(aStatus); + } + + // first, check to see if we've been canceled.... + if (mCanceled || !mSaver) { + return NS_OK; + } + + return mSaver->Finish(NS_OK); +} + +NS_IMETHODIMP +nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver, + nsIFile* aTarget) { + return NS_OK; +} + +NS_IMETHODIMP +nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, + nsresult aStatus) { + LOG( + ("nsExternalAppHandler::OnSaveComplete\n" + " aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n", + aSaver, static_cast<uint32_t>(aStatus), mCanceled, mTransfer.get())); + + if (!mCanceled) { + // Save the hash and signature information + (void)mSaver->GetSha256Hash(mHash); + (void)mSaver->GetSignatureInfo(mSignatureInfo); + + // Free the reference that the saver keeps on us, even if we couldn't get + // the hash. + mSaver = nullptr; + + // Save the redirect information. + nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + nsresult rv = NS_OK; + nsCOMPtr<nsIMutableArray> redirectChain = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("nsExternalAppHandler: Got %zu redirects\n", + loadInfo->RedirectChain().Length())); + for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { + redirectChain->AppendElement(entry); + } + mRedirects = redirectChain; + } + + if (NS_FAILED(aStatus)) { + nsAutoString path; + mTempFile->GetPath(path); + + // It may happen when e10s is enabled that there will be no transfer + // object available to communicate status as expected by the system. + // Let's try and create a temporary transfer object to take care of this + // for us, we'll fall back to using the prompt service if we absolutely + // have to. + if (!mTransfer) { + // We don't care if this fails. + CreateFailedTransfer(); + } + + SendStatusChange(kWriteError, aStatus, nullptr, path); + if (!mCanceled) Cancel(aStatus); + return NS_OK; + } + } + + // Notify the transfer object that we are done if the user has chosen an + // action. If the user hasn't chosen an action, the progress listener + // (nsITransfer) will be notified in CreateTransfer. + if (mTransfer) { + NotifyTransfer(aStatus); + } + + return NS_OK; +} + +void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread"); + MOZ_ASSERT(mTransfer, "We must have an nsITransfer"); + + LOG(("Notifying progress listener")); + + if (NS_SUCCEEDED(aStatus)) { + (void)mTransfer->SetSha256Hash(mHash); + (void)mTransfer->SetSignatureInfo(mSignatureInfo); + (void)mTransfer->SetRedirects(mRedirects); + (void)mTransfer->OnProgressChange64( + nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength); + } + + (void)mTransfer->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_NETWORK, + aStatus); + + // This nsITransfer object holds a reference to us (we are its observer), so + // we need to release the reference to break a reference cycle (and therefore + // to prevent leaking). We do this even if the previous calls failed. + mTransfer = nullptr; +} + +NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = mMimeInfo; + NS_ADDREF(*aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) { + NS_ENSURE_ARG(aSourceURI); + *aSourceURI = mSourceUrl; + NS_IF_ADDREF(*aSourceURI); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName( + nsAString& aSuggestedFileName) { + aSuggestedFileName = mSuggestedFileName; + return NS_OK; +} + +nsresult nsExternalAppHandler::CreateTransfer() { + LOG(("nsExternalAppHandler::CreateTransfer")); + + MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread"); + // We are back from the helper app dialog (where the user chooses to save or + // open), but we aren't done processing the load. in this case, throw up a + // progress dialog so the user can see what's going on. + // Also, release our reference to mDialog. We don't need it anymore, and we + // need to break the reference cycle. + mDialog = nullptr; + if (!mDialogProgressListener) { + NS_WARNING("The dialog should nullify the dialog progress listener"); + } + // In case of a non acceptable download, we need to cancel the request and + // pass a FailedTransfer for the Download UI. + if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) { + mCanceled = true; + mRequest->Cancel(NS_ERROR_ABORT); + return CreateFailedTransfer(); + } + nsresult rv; + + // We must be able to create an nsITransfer object. If not, it doesn't matter + // much that we can't launch the helper application or save to disk. Work on + // a local copy rather than mTransfer until we know we succeeded, to make it + // clearer that this function is re-entrant. + nsCOMPtr<nsITransfer> transfer = + do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Initialize the download + nsCOMPtr<nsIURI> target; + rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest); + if (mBrowsingContext) { + rv = transfer->InitWithBrowsingContext( + mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile, + this, channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, mBrowsingContext, mHandleInternally); + } else { + rv = transfer->Init(mSourceUrl, target, u""_ns, mMimeInfo, + mTimeDownloadStarted, mTempFile, this, + channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification); + } + + NS_ENSURE_SUCCESS(rv, rv); + + // If we were cancelled since creating the transfer, just return. It is + // always ok to return NS_OK if we are cancelled. Callers of this function + // must call Cancel if CreateTransfer fails, but there's no need to cancel + // twice. + if (mCanceled) { + return NS_OK; + } + rv = transfer->OnStateChange(nullptr, mRequest, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); + NS_ENSURE_SUCCESS(rv, rv); + + if (mCanceled) { + return NS_OK; + } + + mRequest = nullptr; + // Finally, save the transfer to mTransfer. + mTransfer = transfer; + transfer = nullptr; + + // While we were bringing up the progress dialog, we actually finished + // processing the url. If that's the case then mStopRequestIssued will be + // true and OnSaveComplete has been called. + if (mStopRequestIssued && !mSaver && mTransfer) { + NotifyTransfer(NS_OK); + } + + return rv; +} + +nsresult nsExternalAppHandler::CreateFailedTransfer() { + nsresult rv; + nsCOMPtr<nsITransfer> transfer = + do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // If we don't have a download directory we're kinda screwed but it's OK + // we'll still report the error via the prompter. + nsCOMPtr<nsIFile> pseudoFile; + rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true); + NS_ENSURE_SUCCESS(rv, rv); + + // Append the default suggested filename. If the user restarts the transfer + // we will re-trigger a filename check anyway to ensure that it is unique. + rv = pseudoFile->Append(mSuggestedFileName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> pseudoTarget; + rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest); + if (mBrowsingContext) { + rv = transfer->InitWithBrowsingContext( + mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted, + nullptr, this, channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, mBrowsingContext, mHandleInternally); + } else { + rv = transfer->Init(mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, + mTimeDownloadStarted, nullptr, this, + channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification); + } + NS_ENSURE_SUCCESS(rv, rv); + + // Our failed transfer is ready. + mTransfer = std::move(transfer); + + return NS_OK; +} + +nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile) { + if (aFile) + ContinueSave(aFile); + else + Cancel(NS_BINDING_ABORTED); + + return NS_OK; +} + +void nsExternalAppHandler::RequestSaveDestination( + const nsString& aDefaultFile, const nsString& aFileExtension) { + // Display the dialog + // XXX Convert to use file picker? No, then embeddors could not do any sort of + // "AutoDownload" w/o showing a prompt + nsresult rv = NS_OK; + if (!mDialog) { + // Get helper app launcher dialog. + mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv); + if (rv != NS_OK) { + Cancel(NS_BINDING_ABORTED); + return; + } + } + + // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we + // can't unescape it because the dialog is implemented by a JS component which + // doesn't have a window so no unescape routine is defined... + + // Now, be sure to keep |this| alive, and the dialog + // If we don't do this, users that close the helper app dialog while the file + // picker is up would cause Cancel() to be called, and the dialog would be + // released, which would release this object too, which would crash. + // See Bug 249143 + RefPtr<nsExternalAppHandler> kungFuDeathGrip(this); + nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog); + nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent(); + + rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(), + aFileExtension.get(), mForceSave); + if (NS_FAILED(rv)) { + Cancel(NS_BINDING_ABORTED); + } +} + +// PromptForSaveDestination should only be called by the helper app dialog which +// allows the user to say launch with application or save to disk. +NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() { + if (mCanceled) return NS_OK; + + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + + if (mSuggestedFileName.IsEmpty()) { + RequestSaveDestination(mTempLeafName, mTempFileExtension); + } else { + nsAutoString fileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos >= 0) { + mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos); + } + if (fileExt.IsEmpty()) { + fileExt = mTempFileExtension; + } + + RequestSaveDestination(mSuggestedFileName, fileExt); + } + + return NS_OK; +} +nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) { + if (mCanceled) return NS_OK; + + MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file"); + + nsresult rv = NS_OK; + nsCOMPtr<nsIFile> fileToUse = aNewFileLocation; + mFinalFileDestination = fileToUse; + + // Move what we have in the final directory, but append .part + // to it, to indicate that it's unfinished. Do not call SetTarget on the + // saver if we are done (Finish has been called) but OnSaverComplete has not + // been called. + if (mFinalFileDestination && mSaver && !mStopRequestIssued) { + nsCOMPtr<nsIFile> movedFile; + mFinalFileDestination->Clone(getter_AddRefs(movedFile)); + if (movedFile) { + // Get the old leaf name and append .part to it + nsAutoString name; + mFinalFileDestination->GetLeafName(name); + name.AppendLiteral(".part"); + movedFile->SetLeafName(name); + + rv = mSaver->SetTarget(movedFile, true); + if (NS_FAILED(rv)) { + nsAutoString path; + mTempFile->GetPath(path); + SendStatusChange(kWriteError, rv, nullptr, path); + Cancel(rv); + return NS_OK; + } + + mTempFile = movedFile; + } + } + + // The helper app dialog has told us what to do and we have a final file + // destination. + rv = CreateTransfer(); + // If we fail to create the transfer, Cancel. + if (NS_FAILED(rv)) { + Cancel(rv); + return rv; + } + + return NS_OK; +} + +// LaunchWithApplication should only be called by the helper app dialog which +// allows the user to say launch with application or save to disk. +NS_IMETHODIMP nsExternalAppHandler::LaunchWithApplication( + bool aHandleInternally) { + if (mCanceled) return NS_OK; + + mHandleInternally = aHandleInternally; + + // Now check if the file is local, in which case we won't bother with saving + // it to a temporary directory and just launch it from where it is + nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl)); + if (fileUrl && mIsFileChannel) { + Cancel(NS_BINDING_ABORTED); + nsCOMPtr<nsIFile> file; + nsresult rv = fileUrl->GetFile(getter_AddRefs(file)); + + if (NS_SUCCEEDED(rv)) { + rv = mMimeInfo->LaunchWithFile(file); + if (NS_SUCCEEDED(rv)) return NS_OK; + } + nsAutoString path; + if (file) file->GetPath(path); + // If we get here, an error happened + SendStatusChange(kLaunchError, rv, nullptr, path); + return rv; + } + + // Now that the user has elected to launch the downloaded file with a helper + // app, we're justified in removing the 'salted' name. We'll rename to what + // was specified in mSuggestedFileName after the download is done prior to + // launching the helper app. So that any existing file of that name won't be + // overwritten we call CreateUnique(). Also note that we use the same + // directory as originally downloaded so the download can be renamed in place + // later. + nsCOMPtr<nsIFile> fileToUse; + (void)GetDownloadDirectory(getter_AddRefs(fileToUse)); + + if (mSuggestedFileName.IsEmpty()) { + // Keep using the leafname of the temp file, since we're just starting a + // helper + mSuggestedFileName = mTempLeafName; + } + +#ifdef XP_WIN + fileToUse->Append(mSuggestedFileName + mTempFileExtension); +#else + fileToUse->Append(mSuggestedFileName); +#endif + + nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + if (NS_SUCCEEDED(rv)) { + mFinalFileDestination = fileToUse; + // launch the progress window now that the user has picked the desired + // action. + rv = CreateTransfer(); + if (NS_FAILED(rv)) { + Cancel(rv); + } + } else { + // Cancel the download and report an error. We do not want to end up in + // a state where it appears that we have a normal download that is + // pointing to a file that we did not actually create. + nsAutoString path; + mTempFile->GetPath(path); + SendStatusChange(kWriteError, rv, nullptr, path); + Cancel(rv); + } + return rv; +} + +NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { + NS_ENSURE_ARG(NS_FAILED(aReason)); + + if (mCanceled) { + return NS_OK; + } + mCanceled = true; + + if (mSaver) { + // We are still writing to the target file. Give the saver a chance to + // close the target file, then notify the transfer object if necessary in + // the OnSaveComplete callback. + mSaver->Finish(aReason); + mSaver = nullptr; + } else { + if (mStopRequestIssued && mTempFile) { + // This branch can only happen when the user cancels the helper app dialog + // when the request has completed. The temp file has to be removed here, + // because mSaver has been released at that time with the temp file left. + (void)mTempFile->Remove(false); + } + + // Notify the transfer object that the download has been canceled, if the + // user has already chosen an action and we didn't notify already. + if (mTransfer) { + NotifyTransfer(aReason); + } + } + + // Break our reference cycle with the helper app dialog (set up in + // OnStartRequest) + mDialog = nullptr; + + mRequest = nullptr; + + // Release the listener, to break the reference cycle with it (we are the + // observer of the listener). + mDialogProgressListener = nullptr; + + return NS_OK; +} + +bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName, + const char* aContentType) { + // Search the obsolete pref strings. + nsAutoCString prefCString; + Preferences::GetCString(prefName, prefCString); + if (prefCString.IsEmpty()) { + // Default is true, if not found in the pref string. + return true; + } + + NS_UnescapeURL(prefCString); + nsACString::const_iterator start, end; + prefCString.BeginReading(start); + prefCString.EndReading(end); + return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start, + end); +} + +NS_IMETHODIMP +nsExternalAppHandler::GetName(nsACString& aName) { + aName.AssignLiteral("nsExternalAppHandler"); + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The following section contains our nsIMIMEService implementation and related +// methods. +// +////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// nsIMIMEService methods +NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension( + const nsACString& aMIMEType, const nsACString& aFileExt, + nsIMIMEInfo** _retval) { + MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(), + "Give me something to work with"); + MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound, + "The extension should never contain null characters"); + LOG(("Getting mimeinfo from type '%s' ext '%s'\n", + PromiseFlatCString(aMIMEType).get(), + PromiseFlatCString(aFileExt).get())); + + *_retval = nullptr; + + // OK... we need a type. Get one. + nsAutoCString typeToUse(aMIMEType); + if (typeToUse.IsEmpty()) { + nsresult rv = GetTypeFromExtension(aFileExt, typeToUse); + if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE; + } + + // We promise to only send lower case mime types to the OS + ToLowerCase(typeToUse); + + // First, ask the OS for a mime info + bool found; + nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LOG(("OS gave back 0x%p - found: %i\n", *_retval, found)); + // If we got no mimeinfo, something went wrong. Probably lack of memory. + if (!*_retval) return NS_ERROR_OUT_OF_MEMORY; + + // The handler service can make up for bad mime types by checking the file + // extension. If the mime type is known (in extras or in the handler + // service), we stop it doing so by flipping this bool to true. + bool trustMIMEType = false; + + // Check extras - not everything we support will be known by the OS store, + // unfortunately, and it may even miss some extensions that we know should + // be accepted. We only do this for non-octet-stream mimetypes, because + // our information for octet-stream would lead to us trying to open all such + // files as Binary file with exe, com or bin extension regardless of the + // real extension. + if (!typeToUse.Equals(APPLICATION_OCTET_STREAM, + nsCaseInsensitiveCStringComparator)) { + rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval); + LOG(("Searched extras (by type), rv 0x%08" PRIX32 "\n", + static_cast<uint32_t>(rv))); + trustMIMEType = NS_SUCCEEDED(rv); + found = found || NS_SUCCEEDED(rv); + } + + // Now, let's see if we can find something in our datastore. + // This will not overwrite the OS information that interests us + // (i.e. default application, default app. description) + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void)handlerSvc->Exists(*_retval, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns); + LOG(("Data source: Via type: retval 0x%08" PRIx32 "\n", + static_cast<uint32_t>(rv))); + trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + found = found || NS_SUCCEEDED(rv); + } + + // If we still haven't found anything, try finding a match for + // an extension in extras first: + if (!found && !aFileExt.IsEmpty()) { + rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval); + LOG(("Searched extras (by ext), rv 0x%08" PRIX32 "\n", + static_cast<uint32_t>(rv))); + } + + // Then check the handler service - but only do so if we really do not know + // the mimetype. This avoids overwriting good mimetype info with bad file + // extension info. + if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) { + nsAutoCString overrideType; + rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType); + if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) { + // We can't check handlerSvc->Exists() here, because we have a + // overideType. That's ok, it just results in some console noise. + // (If there's no handler for the override type, it throws) + rv = handlerSvc->FillHandlerInfo(*_retval, overrideType); + LOG(("Data source: Via ext: retval 0x%08" PRIx32 "\n", + static_cast<uint32_t>(rv))); + found = found || NS_SUCCEEDED(rv); + } + } + + // If we still don't have a match, at least set the file description + // to `${aFileExt} File` if it's empty: + if (!found && !aFileExt.IsEmpty()) { + // XXXzpao This should probably be localized + nsAutoCString desc(aFileExt); + desc.AppendLiteral(" File"); + (*_retval)->SetDescription(NS_ConvertASCIItoUTF16(desc)); + LOG(("Falling back to 'File' file description\n")); + } + + // Sometimes, OSes give us bad data. We have a set of forbidden extensions + // for some MIME types. If the primary extension is forbidden, + // overwrite it with a known-good one. See bug 1571247 for context. + nsAutoCString primaryExtension; + (*_retval)->GetPrimaryExtension(primaryExtension); + if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) { + if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) { + (*_retval)->GetPrimaryExtension(primaryExtension); + } + } + + // Finally, check if we got a file extension and if yes, if it is an + // extension on the mimeinfo, in which case we want it to be the primary one + if (!aFileExt.IsEmpty()) { + bool matches = false; + (*_retval)->ExtensionExists(aFileExt, &matches); + LOG(("Extension '%s' matches mime info: %i\n", + PromiseFlatCString(aFileExt).get(), matches)); + if (matches) { + nsAutoCString fileExt; + ToLowerCase(aFileExt, fileExt); + (*_retval)->SetPrimaryExtension(fileExt); + primaryExtension = fileExt; + } + } + + // Overwrite with a generic description if the primary extension for the + // type is in our list; these are file formats supported by Firefox and + // we don't want other brands positioning themselves as the sole viewer + // for a system. + if (!primaryExtension.IsEmpty()) { + for (const char* ext : descriptionOverwriteExtensions) { + if (primaryExtension.Equals(ext)) { + nsCOMPtr<nsIStringBundleService> bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIStringBundle> unknownContentTypeBundle; + rv = bundleService->CreateBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties", + getter_AddRefs(unknownContentTypeBundle)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString stringName(ext); + stringName.AppendLiteral("ExtHandlerDescription"); + nsAutoString handlerDescription; + rv = unknownContentTypeBundle->GetStringFromName(stringName.get(), + handlerDescription); + if (NS_SUCCEEDED(rv)) { + (*_retval)->SetDescription(handlerDescription); + } + } + break; + } + } + } + + if (LOG_ENABLED()) { + nsAutoCString type; + (*_retval)->GetMIMEType(type); + + LOG(("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(), + primaryExtension.get())); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt, + nsACString& aContentType) { + // OK. We want to try the following sources of mimetype information, in this + // order: + // 1. defaultMimeEntries array + // 2. OS-provided information + // 3. our "extras" array + // 4. Information from plugins + // 5. The "ext-to-type-mapping" category + // Note that, we are intentionally not looking at the handler service, because + // that can be affected by websites, which leads to undesired behavior. + + // Early return if called with an empty extension parameter + if (aFileExt.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // First of all, check our default entries + for (auto& entry : defaultMimeEntries) { + if (aFileExt.LowerCaseEqualsASCII(entry.mFileExtension)) { + aContentType = entry.mMimeType; + return NS_OK; + } + } + + // Ask OS. + if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) { + return NS_OK; + } + + // Check extras array. + bool found = GetTypeFromExtras(aFileExt, aContentType); + if (found) { + return NS_OK; + } + + // Try the plugins + RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst(); + if (pluginHost && + pluginHost->HavePluginForExtension(aFileExt, aContentType)) { + return NS_OK; + } + + // Let's see if an extension added something + nsCOMPtr<nsICategoryManager> catMan( + do_GetService("@mozilla.org/categorymanager;1")); + if (catMan) { + // The extension in the category entry is always stored as lowercase + nsAutoCString lowercaseFileExt(aFileExt); + ToLowerCase(lowercaseFileExt); + // Read the MIME type from the category entry, if available + nsCString type; + nsresult rv = + catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type); + if (NS_SUCCEEDED(rv)) { + aContentType = type; + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension( + const nsACString& aMIMEType, const nsACString& aFileExt, + nsACString& _retval) { + NS_ENSURE_ARG(!aMIMEType.IsEmpty()); + + nsCOMPtr<nsIMIMEInfo> mi; + nsresult rv = + GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi)); + if (NS_FAILED(rv)) return rv; + + return mi->GetPrimaryExtension(_retval); +} + +NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI( + nsIURI* aURI, nsACString& aContentType) { + NS_ENSURE_ARG_POINTER(aURI); + nsresult rv = NS_ERROR_NOT_AVAILABLE; + aContentType.Truncate(); + + // First look for a file to use. If we have one, we just use that. + nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI); + if (fileUrl) { + nsCOMPtr<nsIFile> file; + rv = fileUrl->GetFile(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + rv = GetTypeFromFile(file, aContentType); + if (NS_SUCCEEDED(rv)) { + // we got something! + return rv; + } + } + } + + // Now try to get an nsIURL so we don't have to do our own parsing + nsCOMPtr<nsIURL> url = do_QueryInterface(aURI); + if (url) { + nsAutoCString ext; + rv = url->GetFileExtension(ext); + if (NS_FAILED(rv)) return rv; + if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE; + + UnescapeFragment(ext, url, ext); + + return GetTypeFromExtension(ext, aContentType); + } + + // no url, let's give the raw spec a shot + nsAutoCString specStr; + rv = aURI->GetSpec(specStr); + if (NS_FAILED(rv)) return rv; + UnescapeFragment(specStr, aURI, specStr); + + // find the file extension (if any) + int32_t extLoc = specStr.RFindChar('.'); + int32_t specLength = specStr.Length(); + if (-1 != extLoc && extLoc != specLength - 1 && + // nothing over 20 chars long can be sanely considered an + // extension.... Dat dere would be just data. + specLength - extLoc < 20) { + return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType); + } + + // We found no information; say so. + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile( + nsIFile* aFile, nsACString& aContentType) { + NS_ENSURE_ARG_POINTER(aFile); + nsresult rv; + + // Get the Extension + nsAutoString fileName; + rv = aFile->GetLeafName(fileName); + if (NS_FAILED(rv)) return rv; + + nsAutoCString fileExt; + if (!fileName.IsEmpty()) { + int32_t len = fileName.Length(); + for (int32_t i = len; i >= 0; i--) { + if (fileName[i] == char16_t('.')) { + CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt); + break; + } + } + } + + if (fileExt.IsEmpty()) return NS_ERROR_FAILURE; + + return GetTypeFromExtension(fileExt, aContentType); +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras( + const nsACString& aContentType, bool aOverwriteDescription, + nsIMIMEInfo* aMIMEInfo) { + NS_ENSURE_ARG(aMIMEInfo); + + NS_ENSURE_ARG(!aContentType.IsEmpty()); + + // Look for default entry with matching mime type. + nsAutoCString MIMEType(aContentType); + ToLowerCase(MIMEType); + for (auto entry : extraMimeEntries) { + if (MIMEType.Equals(entry.mMimeType)) { + // This is the one. Set attributes appropriately. + nsDependentCString extensions(entry.mFileExtensions); + nsACString::const_iterator start, end; + extensions.BeginReading(start); + extensions.EndReading(end); + while (start != end) { + nsACString::const_iterator cursor = start; + mozilla::Unused << FindCharInReadable(',', cursor, end); + aMIMEInfo->AppendExtension(Substring(start, cursor)); + // If a comma was found, skip it for the next search. + start = cursor != end ? ++cursor : cursor; + } + + nsAutoString desc; + aMIMEInfo->GetDescription(desc); + if (aOverwriteDescription || desc.IsEmpty()) { + aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription)); + } + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras( + const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) { + nsAutoCString type; + bool found = GetTypeFromExtras(aExtension, type); + if (!found) return NS_ERROR_NOT_AVAILABLE; + return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo); +} + +bool nsExternalHelperAppService::MaybeReplacePrimaryExtension( + const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) { + for (const auto& entry : sForbiddenPrimaryExtensions) { + if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) { + nsDependentCString mime(entry.mMimeType); + for (const auto& extraEntry : extraMimeEntries) { + if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) { + nsDependentCString goodExts(extraEntry.mFileExtensions); + int32_t commaPos = goodExts.FindChar(','); + commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos; + auto goodExt = Substring(goodExts, 0, commaPos); + aMIMEInfo->SetPrimaryExtension(goodExt); + return true; + } + } + } + } + return false; +} + +bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension, + nsACString& aMIMEType) { + NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!"); + + // Look for default entry with matching extension. + nsDependentCString::const_iterator start, end, iter; + int32_t numEntries = ArrayLength(extraMimeEntries); + for (int32_t index = 0; index < numEntries; index++) { + nsDependentCString extList(extraMimeEntries[index].mFileExtensions); + extList.BeginReading(start); + extList.EndReading(end); + iter = start; + while (start != end) { + FindCharInReadable(',', iter, end); + if (Substring(start, iter) + .Equals(aExtension, nsCaseInsensitiveCStringComparator)) { + aMIMEType = extraMimeEntries[index].mMimeType; + return true; + } + if (iter != end) { + ++iter; + } + start = iter; + } + } + + return false; +} + +bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension( + const nsACString& aExtension, nsACString& aMIMEType) { + bool found = false; + nsCOMPtr<nsIMIMEInfo> mimeInfo; + nsresult rv = + GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo)); + return NS_SUCCEEDED(rv) && found && mimeInfo && + NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType)); +} + +nsresult nsExternalHelperAppService::GetMIMEInfoFromOS( + const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = nullptr; + *aFound = false; + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h new file mode 100644 index 0000000000..8c87587e9e --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.h @@ -0,0 +1,512 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsExternalHelperAppService_h__ +#define nsExternalHelperAppService_h__ + +#include "mozilla/Logging.h" +#include "prtime.h" + +#include "nsIExternalHelperAppService.h" +#include "nsIExternalProtocolService.h" +#include "nsIWebProgressListener2.h" +#include "nsIHelperAppLauncherDialog.h" + +#include "nsIMIMEInfo.h" +#include "nsIMIMEService.h" +#include "nsINamed.h" +#include "nsIStreamListener.h" +#include "nsIFile.h" +#include "nsString.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIChannel.h" +#include "nsIBackgroundFileSaver.h" + +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsCOMArray.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsExternalAppHandler; +class nsIMIMEInfo; +class nsITransfer; +class nsIPrincipal; +class MaybeCloseWindowHelper; + +#define EXTERNAL_APP_HANDLER_IID \ + { \ + 0x50eb7479, 0x71ff, 0x4ef8, { \ + 0xb3, 0x1e, 0x3b, 0x59, 0xc8, 0xab, 0xb9, 0x24 \ + } \ + } + +/** + * The helper app service. Responsible for handling content that Mozilla + * itself can not handle + * Note that this is an abstract class - we depend on appropriate subclassing + * on a per-OS basis to implement some methods. + */ +class nsExternalHelperAppService : public nsIExternalHelperAppService, + public nsPIExternalAppLauncher, + public nsIExternalProtocolService, + public nsIMIMEService, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALHELPERAPPSERVICE + NS_DECL_NSPIEXTERNALAPPLAUNCHER + NS_DECL_NSIMIMESERVICE + NS_DECL_NSIOBSERVER + + nsExternalHelperAppService(); + + /** + * Initializes internal state. Will be called automatically when + * this service is first instantiated. + */ + [[nodiscard]] nsresult Init(); + + /** + * nsIExternalProtocolService methods that we provide in this class. Other + * methods should be implemented by per-OS subclasses. + */ + NS_IMETHOD ExternalProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + NS_IMETHOD IsExposedProtocol(const char* aProtocolScheme, + bool* aResult) override; + NS_IMETHOD GetProtocolHandlerInfo(const nsACString& aScheme, + nsIHandlerInfo** aHandlerInfo) override; + NS_IMETHOD LoadURI(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal, + mozilla::dom::BrowsingContext* aBrowsingContext) override; + NS_IMETHOD SetProtocolHandlerDefaults(nsIHandlerInfo* aHandlerInfo, + bool aOSHandlerExists) override; + + /** + * Given a string identifying an application, create an nsIFile representing + * it. This function should look in $PATH for the application. + * The base class implementation will first try to interpret platformAppPath + * as an absolute path, and if that fails it will look for a file next to the + * mozilla executable. Subclasses can override this method if they want a + * different behaviour. + * @param platformAppPath A platform specific path to an application that we + * got out of the rdf data source. This can be a mac + * file spec, a unix path or a windows path depending + * on the platform + * @param aFile [out] An nsIFile representation of that platform + * application path. + */ + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile); + + NS_IMETHOD OSProtocolHandlerExists(const char* aScheme, bool* aExists) = 0; + + /** + * Given an extension, get a MIME type string. If not overridden by + * the OS-specific nsOSHelperAppService, will call into GetMIMEInfoFromOS + * with an empty mimetype. + * @return true if we successfully found a mimetype. + */ + virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension, + nsACString& aMIMEType); + + static already_AddRefed<nsExternalHelperAppService> GetSingleton(); + + protected: + virtual ~nsExternalHelperAppService(); + + /** + * Searches the "extra" array of MIMEInfo objects for an object + * with a specific type. If found, it will modify the passed-in + * MIMEInfo. Otherwise, it will return an error and the MIMEInfo + * will be untouched. + * @param aContentType The type to search for. + * @param aOverwriteDescription Whether to overwrite the description + * @param aMIMEInfo [inout] The mime info, if found + */ + nsresult FillMIMEInfoForMimeTypeFromExtras(const nsACString& aContentType, + bool aOverwriteDescription, + nsIMIMEInfo* aMIMEInfo); + /** + * Searches the "extra" array of MIMEInfo objects for an object + * with a specific extension. + * + * Does not change the MIME Type of the MIME Info. + * + * @see FillMIMEInfoForMimeTypeFromExtras + */ + nsresult FillMIMEInfoForExtensionFromExtras(const nsACString& aExtension, + nsIMIMEInfo* aMIMEInfo); + + /** + * Replace the primary extension of the mimeinfo object if it's in our + * list of forbidden extensions. This fixes up broken information + * provided to us by the OS. + */ + bool MaybeReplacePrimaryExtension(const nsACString& aPrimaryExtension, + nsIMIMEInfo* aMIMEInfo); + + /** + * Searches the "extra" array for a MIME type, and gets its extension. + * @param aExtension The extension to search for + * @param aMIMEType [out] The found MIME type. + * @return true if the extension was found, false otherwise. + */ + bool GetTypeFromExtras(const nsACString& aExtension, nsACString& aMIMEType); + + /** + * Logging Module. Usage: set MOZ_LOG=HelperAppService:level, where level + * should be 2 for errors, 3 for debug messages from the cross- platform + * nsExternalHelperAppService, and 4 for os-specific debug messages. + */ + static mozilla::LazyLogModule mLog; + + // friend, so that it can access the nspr log module. + friend class nsExternalAppHandler; + + /** + * Helper function for ExpungeTemporaryFiles and ExpungeTemporaryPrivateFiles + */ + static void ExpungeTemporaryFilesHelper(nsCOMArray<nsIFile>& fileList); + /** + * Helper function for DeleteTemporaryFileOnExit and + * DeleteTemporaryPrivateFileWhenPossible + */ + static nsresult DeleteTemporaryFileHelper(nsIFile* aTemporaryFile, + nsCOMArray<nsIFile>& aFileList); + /** + * Functions related to the tempory file cleanup service provided by + * nsExternalHelperAppService + */ + void ExpungeTemporaryFiles(); + /** + * Functions related to the tempory file cleanup service provided by + * nsExternalHelperAppService (for the temporary files added during + * the private browsing mode) + */ + void ExpungeTemporaryPrivateFiles(); + + /** + * Array for the files that should be deleted + */ + nsCOMArray<nsIFile> mTemporaryFilesList; + /** + * Array for the files that should be deleted (for the temporary files + * added during the private browsing mode) + */ + nsCOMArray<nsIFile> mTemporaryPrivateFilesList; + + private: + nsresult DoContentContentProcessHelper( + const nsACString& aMimeContentType, nsIRequest* aRequest, + mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener); +}; + +/** + * An external app handler is just a small little class that presents itself as + * a nsIStreamListener. It saves the incoming data into a temp file. The handler + * is bound to an application when it is created. When it receives an + * OnStopRequest it launches the application using the temp file it has + * stored the data into. We create a handler every time we have to process + * data using a helper app. + */ +class nsExternalAppHandler final : public nsIStreamListener, + public nsIHelperAppLauncher, + public nsIBackgroundFileSaverObserver, + public nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIHELPERAPPLAUNCHER + NS_DECL_NSICANCELABLE + NS_DECL_NSIBACKGROUNDFILESAVEROBSERVER + NS_DECL_NSINAMED + + NS_DECLARE_STATIC_IID_ACCESSOR(EXTERNAL_APP_HANDLER_IID) + + /** + * @param aMIMEInfo MIMEInfo object, representing the type of the + * content that should be handled + * @param aFileExtension The extension we need to append to our temp file, + * INCLUDING the ".". e.g. .mp3 + * @param aContentContext dom Window context, as passed to DoContent. + * @param aWindowContext Top level window context used in dialog parenting, + * as passed to DoContent. This parameter may be null, + * in which case dialogs will be parented to + * aContentContext. + * @param mExtProtSvc nsExternalHelperAppService on creation + * @param aFileName The filename to use + * @param aReason A constant from nsIHelperAppLauncherDialog + * indicating why the request is handled by a helper app. + */ + nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsACString& aFileExtension, + mozilla::dom::BrowsingContext* aBrowsingContext, + nsIInterfaceRequestor* aWindowContext, + nsExternalHelperAppService* aExtProtSvc, + const nsAString& aFilename, uint32_t aReason, + bool aForceSave); + + /** + * Clean up after the request was diverted to the parent process. + */ + void DidDivertRequest(nsIRequest* request); + + /** + * Apply content conversions if needed. + */ + void MaybeApplyDecodingForExtension(nsIRequest* request); + + void SetShouldCloseWindow() { mShouldCloseWindow = true; } + + protected: + ~nsExternalAppHandler(); + + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsIURI> mSourceUrl; + nsString mTempFileExtension; + nsString mTempLeafName; + + /** + * The MIME Info for this load. Will never be null. + */ + nsCOMPtr<nsIMIMEInfo> mMimeInfo; + + /** + * The BrowsingContext associated with this request to handle content. + */ + RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext; + + /** + * If set, the parent window helper app dialogs and file pickers + * should use in parenting. If null, we use mContentContext. + */ + nsCOMPtr<nsIInterfaceRequestor> mWindowContext; + + /** + * Used to close the window on a timer, to avoid any exceptions that are + * thrown if we try to close the window before it's fully loaded. + */ + RefPtr<MaybeCloseWindowHelper> mMaybeCloseWindowHelper; + + /** + * The following field is set if we were processing an http channel that had + * a content disposition header which specified the SUGGESTED file name we + * should present to the user in the save to disk dialog. + */ + nsString mSuggestedFileName; + + /** + * If set, this handler should forcibly save the file to disk regardless of + * MIME info settings or anything else, without ever popping up the + * unknown content type handling dialog. + */ + bool mForceSave; + + /** + * The canceled flag is set if the user canceled the launching of this + * application before we finished saving the data to a temp file. + */ + bool mCanceled; + + /** + * True if a stop request has been issued. + */ + bool mStopRequestIssued; + + bool mIsFileChannel; + + /** + * True if the ExternalHelperAppChild told us that we should close the window + * if we handle the content as a download. + */ + bool mShouldCloseWindow; + + /** + * True if the file should be handled internally. + */ + bool mHandleInternally; + + /** + * One of the REASON_ constants from nsIHelperAppLauncherDialog. Indicates the + * reason the dialog was shown (unknown content type, server requested it, + * etc). + */ + uint32_t mReason; + + /** + * Indicates if the nsContentSecurityUtils rate this download as + * acceptable, potentialy unwanted or illigal request. + * + */ + int32_t mDownloadClassification; + + /** + * Track the executable-ness of the temporary file. + */ + bool mTempFileIsExecutable; + + PRTime mTimeDownloadStarted; + int64_t mContentLength; + int64_t mProgress; /**< Number of bytes received (for sending progress + notifications). */ + + /** + * When we are told to save the temp file to disk (in a more permament + * location) before we are done writing the content to a temp file, then + * we need to remember the final destination until we are ready to use it. + */ + nsCOMPtr<nsIFile> mFinalFileDestination; + + uint32_t mBufferSize; + + /** + * This object handles saving the data received from the network to a + * temporary location first, and then move the file to its final location, + * doing all the input/output on a background thread. + */ + nsCOMPtr<nsIBackgroundFileSaver> mSaver; + + /** + * Stores the SHA-256 hash associated with the file that we downloaded. + */ + nsAutoCString mHash; + /** + * Stores the signature information of the downloaded file in an nsTArray of + * nsTArray of Array of bytes. If the file is unsigned this will be + * empty. + */ + nsTArray<nsTArray<nsTArray<uint8_t>>> mSignatureInfo; + /** + * Stores the redirect information associated with the channel. + */ + nsCOMPtr<nsIArray> mRedirects; + /** + * Get the dialog parent: the parent window that we can attach + * a dialog to when prompting the user for a download. + */ + already_AddRefed<nsIInterfaceRequestor> GetDialogParent(); + /** + * Creates the temporary file for the download and an output stream for it. + * Upon successful return, both mTempFile and mSaver will be valid. + */ + nsresult SetUpTempFile(nsIChannel* aChannel); + /** + * When we download a helper app, we are going to retarget all load + * notifications into our own docloader and load group instead of + * using the window which initiated the load....RetargetLoadNotifications + * contains that information... + */ + void RetargetLoadNotifications(nsIRequest* request); + /** + * Once the user tells us how they want to dispose of the content + * create an nsITransfer so they know what's going on. If this fails, the + * caller MUST call Cancel. + */ + nsresult CreateTransfer(); + + /** + * If we fail to create the necessary temporary file to initiate a transfer + * we will report the failure by creating a failed nsITransfer. + */ + nsresult CreateFailedTransfer(); + + /* + * The following two functions are part of the split of SaveToDisk + * to make it async, and works as following: + * + * SaveToDisk -------> RequestSaveDestination + * . + * . + * v + * ContinueSave <------- SaveDestinationAvailable + */ + + /** + * This is called by SaveToDisk to decide what's the final + * file destination chosen by the user or by auto-download settings. + */ + void RequestSaveDestination(const nsString& aDefaultFile, + const nsString& aDefaultFileExt); + + /** + * When SaveToDisk is called, it possibly delegates to RequestSaveDestination + * to decide the file destination. ContinueSave must then be called when + * the final destination is finally known. + * @param aFile The file that was chosen as the final destination. + * Must not be null. + */ + nsresult ContinueSave(nsIFile* aFile); + + /** + * Notify our nsITransfer object that we are done with the download. This is + * always called after the target file has been closed. + * + * @param aStatus + * NS_OK for success, or a failure code if the download failed. + * A partially downloaded file may still be available in this case. + */ + void NotifyTransfer(nsresult aStatus); + + /** + * Helper routine that searches a pref string for a given mime type + */ + bool GetNeverAskFlagFromPref(const char* prefName, const char* aContentType); + + /** + * Helper routine that checks whether we should enforce an extension + * for this file. + */ + bool ShouldForceExtension(const nsString& aFileExt); + + /** + * Helper routine to ensure that mSuggestedFileName ends in the correct + * extension, in case the original extension contains invalid characters + * or if this download is for a mimetype where we enforce using a specific + * extension (image/, video/, and audio/ based mimetypes, and a few specific + * document types). + * + * It also ensure that mTempFileExtension only contains an extension + * when it is different from mSuggestedFileName's extension. + */ + void EnsureCorrectExtension(const nsString& aFileExt); + + typedef enum { kReadError, kWriteError, kLaunchError } ErrorType; + /** + * Utility function to send proper error notification to web progress listener + */ + void SendStatusChange(ErrorType type, nsresult aStatus, nsIRequest* aRequest, + const nsString& path); + + /** + * Set in nsHelperDlgApp.js. This is always null after the user has chosen an + * action. + */ + nsCOMPtr<nsIWebProgressListener2> mDialogProgressListener; + /** + * Set once the user has chosen an action. This is null after the download + * has been canceled or completes. + */ + nsCOMPtr<nsITransfer> mTransfer; + + nsCOMPtr<nsIHelperAppLauncherDialog> mDialog; + + /** + + * The request that's being loaded. Initialized in OnStartRequest. + * Nulled out in OnStopRequest or once we know what we're doing + * with the data, whichever happens later. + */ + nsCOMPtr<nsIRequest> mRequest; + + RefPtr<nsExternalHelperAppService> mExtProtSvc; +}; +NS_DEFINE_STATIC_IID_ACCESSOR(nsExternalAppHandler, EXTERNAL_APP_HANDLER_IID) + +#endif // nsExternalHelperAppService_h__ diff --git a/uriloader/exthandler/nsExternalProtocolHandler.cpp b/uriloader/exthandler/nsExternalProtocolHandler.cpp new file mode 100644 index 0000000000..30b806d7c2 --- /dev/null +++ b/uriloader/exthandler/nsExternalProtocolHandler.cpp @@ -0,0 +1,545 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/BasePrincipal.h" +#include "nsIURI.h" +#include "nsExternalProtocolHandler.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsNetUtil.h" +#include "nsContentSecurityManager.h" +#include "nsExternalHelperAppService.h" + +// used to dispatch urls to default protocol handlers +#include "nsCExternalHandlerService.h" +#include "nsIExternalProtocolService.h" +#include "nsIChildChannel.h" +#include "nsIParentChannel.h" + +class nsILoadInfo; + +//////////////////////////////////////////////////////////////////////// +// a stub channel implemenation which will map calls to AsyncRead and +// OpenInputStream to calls in the OS for loading the url. +//////////////////////////////////////////////////////////////////////// + +class nsExtProtocolChannel : public nsIChannel, + public nsIChildChannel, + public nsIParentChannel { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUEST + NS_DECL_NSICHILDCHANNEL + NS_DECL_NSIPARENTCHANNEL + + nsExtProtocolChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo); + + private: + virtual ~nsExtProtocolChannel(); + + nsresult OpenURL(); + void Finish(nsresult aResult); + + nsCOMPtr<nsIURI> mUrl; + nsCOMPtr<nsIURI> mOriginalURI; + nsresult mStatus; + nsLoadFlags mLoadFlags; + bool mWasOpened; + bool mCanceled; + // Set true (as a result of ConnectParent invoked from child process) + // when this channel is on the parent process and is being used as + // a redirect target channel. It turns AsyncOpen into a no-op since + // we do it on the child. + bool mConnectedParent; + + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsCOMPtr<nsILoadInfo> mLoadInfo; + nsCOMPtr<nsIStreamListener> mListener; +}; + +NS_IMPL_ADDREF(nsExtProtocolChannel) +NS_IMPL_RELEASE(nsExtProtocolChannel) + +NS_INTERFACE_MAP_BEGIN(nsExtProtocolChannel) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChildChannel) + NS_INTERFACE_MAP_ENTRY(nsIParentChannel) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) +NS_INTERFACE_MAP_END + +nsExtProtocolChannel::nsExtProtocolChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mUrl(aURI), + mOriginalURI(aURI), + mStatus(NS_OK), + mLoadFlags(nsIRequest::LOAD_NORMAL), + mWasOpened(false), + mCanceled(false), + mConnectedParent(false), + mLoadInfo(aLoadInfo) {} + +nsExtProtocolChannel::~nsExtProtocolChannel() {} + +NS_IMETHODIMP nsExtProtocolChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + NS_IF_ADDREF(*aLoadGroup = mLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + mLoadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + NS_IF_ADDREF(*aCallbacks = mCallbacks); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) { + mCallbacks = aCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsExtProtocolChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + *aSecurityInfo = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetOriginalURI(nsIURI** aURI) { + NS_ADDREF(*aURI = mOriginalURI); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetOriginalURI(nsIURI* aURI) { + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetURI(nsIURI** aURI) { + *aURI = mUrl; + NS_IF_ADDREF(*aURI); + return NS_OK; +} + +nsresult nsExtProtocolChannel::OpenURL() { + nsresult rv = NS_ERROR_FAILURE; + nsCOMPtr<nsIExternalProtocolService> extProtService( + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID)); + + if (extProtService) { +#ifdef DEBUG + nsAutoCString urlScheme; + mUrl->GetScheme(urlScheme); + bool haveHandler = false; + extProtService->ExternalProtocolHandlerExists(urlScheme.get(), + &haveHandler); + NS_ASSERTION(haveHandler, + "Why do we have a channel for this url if we don't support " + "the protocol?"); +#endif + + RefPtr<mozilla::dom::BrowsingContext> ctx; + rv = mLoadInfo->GetTargetBrowsingContext(getter_AddRefs(ctx)); + if (NS_FAILED(rv)) { + goto finish; + } + + RefPtr<nsIPrincipal> principal = mLoadInfo->TriggeringPrincipal(); + rv = extProtService->LoadURI(mUrl, principal, ctx); + + if (NS_SUCCEEDED(rv) && mListener) { + mStatus = NS_ERROR_NO_CONTENT; + + RefPtr<nsExtProtocolChannel> self = this; + nsCOMPtr<nsIStreamListener> listener = mListener; + MessageLoop::current()->PostTask(NS_NewRunnableFunction( + "nsExtProtocolChannel::OpenURL", [self, listener]() { + listener->OnStartRequest(self); + listener->OnStopRequest(self, self->mStatus); + })); + } + } + +finish: + mCallbacks = nullptr; + mListener = nullptr; + return rv; +} + +NS_IMETHODIMP nsExtProtocolChannel::Open(nsIInputStream** aStream) { + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + return OpenURL(); +} + +NS_IMETHODIMP nsExtProtocolChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_FAILED(rv)) { + mCallbacks = nullptr; + return rv; + } + + if (mConnectedParent) { + return NS_OK; + } + + MOZ_ASSERT( + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); + + mWasOpened = true; + mListener = listener; + + return OpenURL(); +} + +NS_IMETHODIMP nsExtProtocolChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsExtProtocolChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsExtProtocolChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentType(nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentType( + const nsACString& aContentType) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentCharset( + nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentCharset( + const nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentDisposition( + uint32_t* aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentDisposition( + uint32_t aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentLength(int64_t* aContentLength) { + *aContentLength = -1; + return NS_OK; +} + +NS_IMETHODIMP +nsExtProtocolChannel::SetContentLength(int64_t aContentLength) { + MOZ_ASSERT_UNREACHABLE("SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetOwner(nsISupports** aPrincipal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetOwner(nsISupports* aPrincipal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// From nsIRequest +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsExtProtocolChannel::GetName(nsACString& result) { + return mUrl->GetSpec(result); +} + +NS_IMETHODIMP nsExtProtocolChannel::IsPending(bool* result) { + *result = false; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetStatus(nsresult* status) { + *status = mStatus; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Cancel(nsresult status) { + if (NS_SUCCEEDED(mStatus)) { + mStatus = status; + } + mCanceled = true; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Suspend() { + MOZ_ASSERT_UNREACHABLE("Suspend"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::Resume() { + MOZ_ASSERT_UNREACHABLE("Resume"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +/////////////////////////////////////////////////////////////////////// +// From nsIChildChannel +////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsExtProtocolChannel::ConnectParent(uint32_t registrarId) { + mozilla::dom::ContentChild::GetSingleton() + ->SendExtProtocolChannelConnectParent(registrarId); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::CompleteRedirectSetup( + nsIStreamListener* listener) { + // For redirects to external protocols we AsyncOpen on the child + // (not the parent) because child channel has the right docshell + // (which is needed for the select dialog). + return AsyncOpen(listener); +} + +/////////////////////////////////////////////////////////////////////// +// From nsIParentChannel (derives from nsIStreamListener) +////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsExtProtocolChannel::SetParentListener( + mozilla::net::ParentChannelListener* aListener) { + // This is called as part of the connect parent operation from + // ContentParent::RecvExtProtocolChannelConnectParent. Setting + // this flag tells this channel to not proceed and makes AsyncOpen + // just no-op. Actual operation will happen from the child process + // via CompleteRedirectSetup call on the child channel. + mConnectedParent = true; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetClassifierMatchedInfo( + const nsACString& aList, const nsACString& aProvider, + const nsACString& aFullHash) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::NotifyClassificationFlags( + uint32_t aClassificationFlags, bool aIsThirdParty) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::NotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Delete() { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetRemoteType(nsACString& aRemoteType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnStartRequest(nsIRequest* aRequest) { + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + MOZ_ASSERT(NS_FAILED(aStatusCode)); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnDataAvailable( + nsIRequest* aRequest, nsIInputStream* aInputStream, uint64_t aOffset, + uint32_t aCount) { + // no data is expected + MOZ_CRASH("No data expected from external protocol channel"); + return NS_ERROR_UNEXPECTED; +} + +/////////////////////////////////////////////////////////////////////// +// the default protocol handler implementation +////////////////////////////////////////////////////////////////////// + +nsExternalProtocolHandler::nsExternalProtocolHandler() { + m_schemeName = "default"; +} + +nsExternalProtocolHandler::~nsExternalProtocolHandler() {} + +NS_IMPL_ADDREF(nsExternalProtocolHandler) +NS_IMPL_RELEASE(nsExternalProtocolHandler) + +NS_INTERFACE_MAP_BEGIN(nsExternalProtocolHandler) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIProtocolHandler) + NS_INTERFACE_MAP_ENTRY(nsIProtocolHandler) + NS_INTERFACE_MAP_ENTRY(nsIExternalProtocolHandler) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP nsExternalProtocolHandler::GetScheme(nsACString& aScheme) { + aScheme = m_schemeName; + return NS_OK; +} + +NS_IMETHODIMP nsExternalProtocolHandler::GetDefaultPort(int32_t* aDefaultPort) { + *aDefaultPort = 0; + return NS_OK; +} + +NS_IMETHODIMP +nsExternalProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} +// returns TRUE if the OS can handle this protocol scheme and false otherwise. +bool nsExternalProtocolHandler::HaveExternalProtocolHandler(nsIURI* aURI) { + MOZ_ASSERT(aURI); + nsAutoCString scheme; + aURI->GetScheme(scheme); + + nsCOMPtr<nsIExternalProtocolService> extProtSvc( + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID)); + if (!extProtSvc) { + return false; + } + + bool haveHandler = false; + extProtSvc->ExternalProtocolHandlerExists(scheme.get(), &haveHandler); + return haveHandler; +} + +NS_IMETHODIMP nsExternalProtocolHandler::GetProtocolFlags(uint32_t* aUritype) { + // Make it norelative since it is a simple uri + *aUritype = URI_NORELATIVE | URI_NOAUTH | URI_LOADABLE_BY_ANYONE | + URI_NON_PERSISTABLE | URI_DOES_NOT_RETURN_DATA; + return NS_OK; +} + +NS_IMETHODIMP +nsExternalProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** aRetval) { + NS_ENSURE_TRUE(aURI, NS_ERROR_UNKNOWN_PROTOCOL); + NS_ENSURE_TRUE(aRetval, NS_ERROR_UNKNOWN_PROTOCOL); + + // Only try to return a channel if we have a protocol handler for the url. + // nsOSHelperAppService::LoadUriInternal relies on this to check trustedness + // for some platforms at least. (win uses ::ShellExecute and unix uses + // gnome_url_show.) + if (!HaveExternalProtocolHandler(aURI)) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + nsCOMPtr<nsIChannel> channel = new nsExtProtocolChannel(aURI, aLoadInfo); + channel.forget(aRetval); + return NS_OK; +} + +/////////////////////////////////////////////////////////////////////// +// External protocol handler interface implementation +////////////////////////////////////////////////////////////////////// +NS_IMETHODIMP nsExternalProtocolHandler::ExternalAppExistsForScheme( + const nsACString& aScheme, bool* _retval) { + nsCOMPtr<nsIExternalProtocolService> extProtSvc( + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID)); + if (extProtSvc) + return extProtSvc->ExternalProtocolHandlerExists( + PromiseFlatCString(aScheme).get(), _retval); + + // In case we don't have external protocol service. + *_retval = false; + return NS_OK; +} diff --git a/uriloader/exthandler/nsExternalProtocolHandler.h b/uriloader/exthandler/nsExternalProtocolHandler.h new file mode 100644 index 0000000000..373223bc39 --- /dev/null +++ b/uriloader/exthandler/nsExternalProtocolHandler.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsExternalProtocolHandler_h___ +#define nsExternalProtocolHandler_h___ + +#include "nsIExternalProtocolHandler.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsIURI; + +// protocol handlers need to support weak references if we want the netlib +// nsIOService to cache them. +class nsExternalProtocolHandler final : public nsIExternalProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIEXTERNALPROTOCOLHANDLER + + nsExternalProtocolHandler(); + + protected: + ~nsExternalProtocolHandler(); + + // helper function + bool HaveExternalProtocolHandler(nsIURI* aURI); + nsCString m_schemeName; +}; + +#endif // nsExternalProtocolHandler_h___ diff --git a/uriloader/exthandler/nsIContentDispatchChooser.idl b/uriloader/exthandler/nsIContentDispatchChooser.idl new file mode 100644 index 0000000000..d12c1aa907 --- /dev/null +++ b/uriloader/exthandler/nsIContentDispatchChooser.idl @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIHandlerInfo; +interface nsIPrincipal; +interface nsIURI; +webidl BrowsingContext; + +/** + * This is used to ask a user what they would like to do with a given piece of + * content. + */ +[scriptable, uuid(456ca3b2-02be-4f97-89a2-08c08d3ad88f)] +interface nsIContentDispatchChooser : nsISupports { + /** + * Opens the handler associated with the given resource. + * If the caller does not have permission or no handler is set, we ask the + * user to grant permission and pick a handler. + * + * @param aHander + * The interface describing the details of how this content should or + * can be handled. + * @param aURI + * The URI of the resource that we are asking about. + * @param aTriggeringPrincipal + * The principal making the request. + * @param aBrowsingContext + * The browsing context where the load should happen. + */ + void handleURI(in nsIHandlerInfo aHandler, + in nsIURI aURI, + in nsIPrincipal aTriggeringPrincipal, + in BrowsingContext aBrowsingContext); +}; + diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl new file mode 100644 index 0000000000..657e15bc07 --- /dev/null +++ b/uriloader/exthandler/nsIExternalHelperAppService.idl @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsICancelable.idl" + +interface nsIURI; +interface nsIRequest; +interface nsIStreamListener; +interface nsIFile; +interface nsIMIMEInfo; +interface nsIWebProgressListener2; +interface nsIInterfaceRequestor; +webidl BrowsingContext; + +/** + * The external helper app service is used for finding and launching + * platform specific external applications for a given mime content type. + */ +[scriptable, uuid(1E4F3AE1-B737-431F-A95D-31FA8DA70199)] +interface nsIExternalHelperAppService : nsISupports +{ + /** + * Binds an external helper application to a stream listener. The caller + * should pump data into the returned stream listener. When the OnStopRequest + * is issued, the stream listener implementation will launch the helper app + * with this data. + * @param aMimeContentType The content type of the incoming data + * @param aRequest The request corresponding to the incoming data + * @param aContentContext Used in processing content document refresh + * headers after target content is downloaded. + * @param aForceSave True to always save this content to disk, regardless of + * nsIMIMEInfo and other such influences. + * @param aWindowContext Used in parenting helper app dialogs, usually + * points to the parent browser window. This parameter may be null, + * in which case dialogs will be parented to aContentContext. + * @return A nsIStreamListener which the caller should pump the data into. + */ + nsIStreamListener doContent (in ACString aMimeContentType, + in nsIRequest aRequest, + in nsIInterfaceRequestor aContentContext, + in boolean aForceSave, + [optional] in nsIInterfaceRequestor aWindowContext); + + /** + * Binds an external helper application to a stream listener. The caller + * should pump data into the returned stream listener. When the OnStopRequest + * is issued, the stream listener implementation will launch the helper app + * with this data. + * Replaces doContent for native code, and uses BrowsingContext. + * + * @param aMimeContentType The content type of the incoming data + * @param aRequest The request corresponding to the incoming data + * @param aContentContext The BrowsingContext that the request was initiated + * by. Used for closing the window if we opened one specifically for this download. + * @param aForceSave True to always save this content to disk, regardless of + * nsIMIMEInfo and other such influences. + * @param aWindowContext Used in parenting helper app dialogs, usually + * points to the parent browser window. This parameter may be null, + * in which case dialogs will be parented to aContentContext. + * @return A nsIStreamListener which the caller should pump the data into. + */ + nsIStreamListener createListener (in ACString aMimeContentType, + in nsIRequest aRequest, + in BrowsingContext aContentContext, + in boolean aForceSave, + [optional] in nsIInterfaceRequestor aWindowContext); + + /** + * Returns true if data from a URL with this extension combination + * is to be decoded from aEncodingType prior to saving or passing + * off to helper apps, false otherwise. + */ + boolean applyDecodingForExtension(in AUTF8String aExtension, + in ACString aEncodingType); + +}; + +/** + * This is a private interface shared between external app handlers and the platform specific + * external helper app service + */ +[scriptable, uuid(6613e2e7-feab-4e3a-bb1f-b03200d544ec)] +interface nsPIExternalAppLauncher : nsISupports +{ + /** + * mscott --> eventually I should move this into a new service so other + * consumers can add temporary files they want deleted on exit. + * @param aTemporaryFile A temporary file we should delete on exit. + */ + void deleteTemporaryFileOnExit(in nsIFile aTemporaryFile); + /** + * Delete a temporary file created inside private browsing mode when + * the private browsing mode has ended. + */ + void deleteTemporaryPrivateFileWhenPossible(in nsIFile aTemporaryFile); +}; + +/** + * A helper app launcher is a small object created to handle the launching + * of an external application. + * + * Note that cancelling the load via the nsICancelable interface will release + * the reference to the launcher dialog. + */ +[scriptable, uuid(acf2a516-7d7f-4771-8b22-6c4a559c088e)] +interface nsIHelperAppLauncher : nsICancelable +{ + /** + * The mime info object associated with the content type this helper app + * launcher is currently attempting to load + */ + readonly attribute nsIMIMEInfo MIMEInfo; + + /** + * The source uri + */ + readonly attribute nsIURI source; + + /** + * The suggested name for this file + */ + readonly attribute AString suggestedFileName; + + /** + * Saves the final destination of the file. + * NOTE: This will release the reference to the nsIHelperAppLauncherDialog. + */ + void promptForSaveDestination(); + + /** + * Tell the launcher that we will want to open the file. + * NOTE: This will release the reference to the nsIHelperAppLauncherDialog. + * @param aHandleInternally TRUE if we should handle opening this internally. + */ + void launchWithApplication(in boolean aHandleInternally); + + /** + * Callback invoked by nsIHelperAppLauncherDialog::promptForSaveToFileAsync + * after the user has chosen a file through the File Picker (or dismissed it). + * @param aFile The file that was chosen by the user (or null if dialog was dismissed). + */ + void saveDestinationAvailable(in nsIFile aFile); + + /** + * The following methods are used by the progress dialog to get or set + * information on the current helper app launcher download. + * This reference will be released when the download is finished (after the + * listener receives the STATE_STOP notification). + */ + void setWebProgressListener(in nsIWebProgressListener2 aWebProgressListener); + + /** + * The file we are saving to + */ + readonly attribute nsIFile targetFile; + + /** + * The executable-ness of the target file + */ + readonly attribute boolean targetFileIsExecutable; + + /** + * Time when the download started + */ + readonly attribute PRTime timeDownloadStarted; + + /** + * The download content length, or -1 if the length is not available. + */ + readonly attribute int64_t contentLength; + + /** + * The browsingContext ID of the launcher's source + */ + readonly attribute uint64_t browsingContextId; +}; diff --git a/uriloader/exthandler/nsIExternalProtocolService.idl b/uriloader/exthandler/nsIExternalProtocolService.idl new file mode 100644 index 0000000000..66d459c491 --- /dev/null +++ b/uriloader/exthandler/nsIExternalProtocolService.idl @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURI; +interface nsIFile; +interface nsIPrincipal; +interface nsIInterfaceRequestor; +interface nsIHandlerInfo; + +webidl BrowsingContext; + +/** + * The external protocol service is used for finding and launching + * web handlers (a la registerProtocolHandler in the HTML5 draft) or + * platform-specific applications for handling particular protocols. + * + * You can ask the external protocol service if it has an external + * handler for a given protocol scheme. And you can ask it to load + * the url using the default handler. + */ +[scriptable, uuid(70f93b7a-3ec6-4bcb-b093-92d9984c9f83)] +interface nsIExternalProtocolService : nsISupports +{ + /** + * Check whether a handler for a specific protocol exists. Specifically, + * this looks to see whether there are any known possible application handlers + * in either the nsIHandlerService datastore or registered with the OS. + * + * @param aProtocolScheme The scheme from a url: http, ftp, mailto, etc. + * + * @return true if we have a handler and false otherwise. + * + * XXX shouldn't aProtocolScheme be an ACString like nsIURI::scheme? + */ + boolean externalProtocolHandlerExists(in string aProtocolScheme); + + /** + * Check whether a handler for a specific protocol is "exposed" as a visible + * feature of the current application. + * + * An exposed protocol handler is one that can be used in all contexts. A + * non-exposed protocol handler is one that can only be used internally by the + * application. For example, a non-exposed protocol would not be loaded by the + * application in response to a link click or a X-remote openURL command. + * Instead, it would be deferred to the system's external protocol handler. + * XXX shouldn't aProtocolScheme be an ACString like nsIURI::scheme? + */ + boolean isExposedProtocol(in string aProtocolScheme); + + /** + * Retrieve the handler for the given protocol. If neither the application + * nor the OS knows about a handler for the protocol, the object this method + * returns will represent a default handler for unknown content. + * + * @param aProtocolScheme the scheme from a URL: http, ftp, mailto, etc. + * + * Note: aProtocolScheme should not include a trailing colon, which is part + * of the URI syntax, not part of the scheme itself (i.e. pass "mailto" not + * "mailto:"). + * + * @return the handler, if any; otherwise a default handler + */ + nsIHandlerInfo getProtocolHandlerInfo(in ACString aProtocolScheme); + + /** + * Given a scheme, looks up the protocol info from the OS. This should be + * overridden by each OS's implementation. + * + * @param aScheme The protocol scheme we are looking for. + * @param aFound Was an OS default handler for this scheme found? + * @return An nsIHanderInfo for the protocol. + */ + nsIHandlerInfo getProtocolHandlerInfoFromOS(in ACString aProtocolScheme, + out boolean aFound); + + /** + * Set some sane defaults for a protocol handler object. + * + * @param aHandlerInfo nsIHandlerInfo object, as returned by + * getProtocolHandlerInfoFromOS + * @param aOSHandlerExists was the object above created for an extant + * OS default handler? This is generally the + * value of the aFound out param from + * getProtocolHandlerInfoFromOS. + */ + void setProtocolHandlerDefaults(in nsIHandlerInfo aHandlerInfo, + in boolean aOSHandlerExists); + + /** + * Used to load a URI via an external application. Might prompt the user for + * permission to load the external application. + * + * @param aURI + * The URI to load + * + * @param aTriggeringPrincipal + * The principal triggering this load. + * + * @param aBrowsingContext + * The context to parent the dialog against, and, if a web handler + * is chosen, it is loaded in this window as well. This parameter + * may be ultimately passed nsIURILoader.openURI in the case of a + * web handler, and aWindowContext is null or not present, web + * handlers will fail. We need to do better than that; bug 394483 + * filed in order to track. + * + * @note Embedders that do not expose the http protocol should not currently + * use web-based protocol handlers, as handoff won't work correctly + * (bug 394479). + */ + void loadURI(in nsIURI aURI, + [optional] in nsIPrincipal aTriggeringPrincipal, + [optional] in BrowsingContext aBrowsingContext); + + /** + * Gets a human-readable description for the application responsible for + * handling a specific protocol. + * + * @param aScheme The scheme to look up. For example, "mms". + * + * @throw NS_ERROR_NOT_IMPLEMENTED + * If getting descriptions for protocol helpers is not supported + * @throw NS_ERROR_NOT_AVAILABLE + * If no protocol helper exists for this scheme, or if it is not + * possible to get a description for it. + */ + AString getApplicationDescription(in AUTF8String aScheme); + + /** + * Check if this app is registered as the OS default for a given scheme. + * + * @param aScheme The scheme to look up. For example, "mms". + */ + bool isCurrentAppOSDefaultForProtocol(in AUTF8String aScheme); +}; diff --git a/uriloader/exthandler/nsIExternalURLHandlerService.idl b/uriloader/exthandler/nsIExternalURLHandlerService.idl new file mode 100644 index 0000000000..3573497591 --- /dev/null +++ b/uriloader/exthandler/nsIExternalURLHandlerService.idl @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIMIMEInfo.idl" + +/** + * The external URL handler service is used for finding + * platform-specific applications for handling particular URLs. + */ + +[scriptable, uuid(56c5c7d3-6fd3-43f8-9429-4397e111453a)] +interface nsIExternalURLHandlerService : nsISupports +{ + /** + * Given a URL, looks up the handler info from the OS. This should be + * overridden by each OS's implementation. + * + * @param aURL The URL we are looking for. + * @param aFound Was an OS default handler for this URL found? + * @return An nsIHanderInfo for the protocol. + */ + nsIHandlerInfo getURLHandlerInfoFromOS(in nsIURI aURL, + out boolean aFound); + +}; diff --git a/uriloader/exthandler/nsIHandlerService.idl b/uriloader/exthandler/nsIHandlerService.idl new file mode 100644 index 0000000000..34519f57cd --- /dev/null +++ b/uriloader/exthandler/nsIHandlerService.idl @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIHandlerInfo; +interface nsISimpleEnumerator; + +[scriptable, uuid(53f0ad17-ec62-46a1-adbc-efccc06babcd)] +interface nsIHandlerService : nsISupports +{ + /** + * Asynchronously performs any IO that the nsIHandlerService needs to do + * before it can be of use. + */ + void asyncInit(); + + /** + * Retrieve a list of all handlers in the datastore. This list is not + * guaranteed to be in any particular order, and callers should not assume + * it will remain in the same order in the future. + * + * @returns a list of all handlers in the datastore + */ + nsISimpleEnumerator enumerate(); + + /** + * Fill a handler info object with information from the datastore. + * + * Note: because of the way the external helper app service currently mixes + * OS and user handler info in the same handler info object, this method + * takes an existing handler info object (probably retrieved from the OS) + * and "fills it in" with information from the datastore, overriding any + * existing properties on the object with properties from the datastore. + * + * Ultimately, however, we're going to separate OS and user handler info + * into separate objects, at which point this method should be renamed to + * something like "get" or "retrieve", take a class and type (or perhaps + * a type whose class can be determined by querying the type, for example + * an nsIContentType which is also an nsIMIMEType or an nsIProtocolScheme), + * and return a handler info object representing only the user info. + * + * Note: if you specify an override type, then the service will fill in + * the handler info object with information about that type instead of + * the type specified by the object's nsIHandlerInfo::type attribute. + * + * This is useful when you are trying to retrieve information about a MIME + * type that doesn't exist in the datastore, but you have a file extension + * for that type, and nsIHandlerService::getTypeFromExtension returns another + * MIME type that does exist in the datastore and can handle that extension. + * + * For example, the user clicks on a link, and the content has a MIME type + * that isn't in the datastore, but the link has a file extension, and that + * extension is associated with another MIME type in the datastore (perhaps + * an unofficial MIME type preceded an official one, like with image/x-png + * and image/png). + * + * In that situation, you can call this method to fill in the handler info + * object with information about that other type by passing the other type + * as the aOverrideType parameter. + * + * @param aHandlerInfo the handler info object + * @param aOverrideType a type to use instead of aHandlerInfo::type + * + * Note: if there is no information in the datastore about this type, + * this method throws NS_ERROR_NOT_AVAILABLE. Callers are encouraged to + * check exists() before calling fillHandlerInfo(), to prevent spamming the + * console with XPCOM exception errors. + */ + void fillHandlerInfo(in nsIHandlerInfo aHandlerInfo, + in ACString aOverrideType); + + /** + * Save the preferred action, preferred handler, possible handlers, and + * always ask properties of the given handler info object to the datastore. + * Updates an existing record or creates a new one if necessary. + * + * Note: if preferred action is undefined or invalid, then we assume + * the default value nsIHandlerInfo::useHelperApp. + * + * @param aHandlerInfo the handler info object + */ + void store(in nsIHandlerInfo aHandlerInfo); + + /** + * Whether or not a record for the given handler info object exists + * in the datastore. If the datastore is corrupt (or some other error + * is caught in the implementation), false will be returned. + * + * @param aHandlerInfo a handler info object + * + * @returns whether or not a record exists + */ + boolean exists(in nsIHandlerInfo aHandlerInfo); + + /** + * Remove the given handler info object from the datastore. Deletes all + * records associated with the object, including the preferred handler, info, + * and type records plus the entry in the list of types, if they exist. + * Otherwise, it does nothing and does not return an error. + * + * @param aHandlerInfo the handler info object + */ + void remove(in nsIHandlerInfo aHandlerInfo); + + /** + * Get the MIME type mapped to the given file extension in the datastore. + * + * XXX If we ever support extension -> protocol scheme mappings, then this + * method should work for those as well. + * + * Note: in general, you should use nsIMIMEService::getTypeFromExtension + * to get a MIME type from a file extension, as that method checks a variety + * of other sources besides just the datastore. Use this only when you want + * to specifically get only the mapping available in the datastore. + * + * @param aFileExtension the file extension + * + * @returns the MIME type, if any; otherwise returns an empty string (""). + */ + ACString getTypeFromExtension(in ACString aFileExtension); + + /** + * Whether or not there is a handler known to the OS for the + * specified protocol type. + * + * @param aProtocolScheme scheme to check for support + * + * @returns whether or not a handler exists + */ + boolean existsForProtocolOS(in ACString aProtocolScheme); + + /** + * Whether or not there is a handler in the datastore or OS for + * the specified protocol type. If there is no handler in the datastore, + * falls back to a check for an OS handler. + * + * @param aProtocolScheme scheme to check for support + * + * @returns whether or not a handler exists + */ + boolean existsForProtocol(in ACString aProtocolScheme); + + /* + * Fill in a handler info object using information from the OS, taking into + * account the MIME type and file extension. When the OS handler + * for the MIME type and extension match, |aFound| is returned as true. If + * either the MIME type or extension is the empty string and a handler is + * found, |aFound| is returned as true. + */ + void getMIMEInfoFromOS(in nsIHandlerInfo aHandlerInfo, + in ACString aMIMEType, + in ACString aExtension, + out bool aFound); + + /* + * Get a description for the application responsible for handling + * the provided protocol. + */ + AString getApplicationDescription(in ACString aProtocolScheme); +}; diff --git a/uriloader/exthandler/nsIHelperAppLauncherDialog.idl b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl new file mode 100644 index 0000000000..9a7f76b7da --- /dev/null +++ b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIHelperAppLauncher; +interface nsIFile; +interface nsIInterfaceRequestor; + +/** + * This interface is used to display a confirmation dialog before + * launching a "helper app" to handle content not handled by + * Mozilla. + * + * Usage: Clients (of which there is one: the nsIExternalHelperAppService + * implementation in mozilla/uriloader/exthandler) create an instance of + * this interface (using the contract ID) and then call the show() method. + * + * The dialog is shown non-modally. The implementation of the dialog + * will access methods of the nsIHelperAppLauncher passed in to show() + * in order to cause a "save to disk" or "open using" action. + */ +[scriptable, uuid(bfc739f3-8d75-4034-a6f8-1039a5996bad)] +interface nsIHelperAppLauncherDialog : nsISupports { + /** + * This request is passed to the helper app dialog because Gecko can not + * handle content of this type. + */ + const unsigned long REASON_CANTHANDLE = 0; + + /** + * The server requested external handling. + */ + const unsigned long REASON_SERVERREQUEST = 1; + + /** + * Gecko detected that the type sent by the server (e.g. text/plain) does + * not match the actual type. + */ + const unsigned long REASON_TYPESNIFFED = 2; + + /** + * Show confirmation dialog for launching application (or "save to + * disk") for content specified by aLauncher. + * + * @param aLauncher + * A nsIHelperAppLauncher to be invoked when a file is selected. + * @param aWindowContext + * Window associated with action. + * @param aReason + * One of the constants from above. It indicates why the dialog is + * shown. Implementors should treat unknown reasons like + * REASON_CANTHANDLE. + */ + void show(in nsIHelperAppLauncher aLauncher, + in nsIInterfaceRequestor aWindowContext, + in unsigned long aReason); + + /** + * Async invoke a save-to-file dialog instead of the full fledged helper app + * dialog. When the file is chosen (or the dialog is closed), the callback + * in aLauncher (aLauncher.saveDestinationAvailable) is called with the + * selected file. + * + * @param aLauncher + * A nsIHelperAppLauncher to be invoked when a file is selected. + * @param aWindowContext + * Window associated with action. + * @param aDefaultFileName + * Default file name to provide (can be null) + * @param aSuggestedFileExtension + * Sugested file extension + * @param aForcePrompt + * Set to true to force prompting the user for thet file + * name/location, otherwise perferences may control if the user is + * prompted. + */ + void promptForSaveToFileAsync(in nsIHelperAppLauncher aLauncher, + in nsIInterfaceRequestor aWindowContext, + in wstring aDefaultFileName, + in wstring aSuggestedFileExtension, + in boolean aForcePrompt); +}; + + +%{C++ +#define NS_HELPERAPPLAUNCHERDLG_CONTRACTID "@mozilla.org/helperapplauncherdialog;1" +%} diff --git a/uriloader/exthandler/nsISharingHandlerApp.idl b/uriloader/exthandler/nsISharingHandlerApp.idl new file mode 100644 index 0000000000..14bdee1884 --- /dev/null +++ b/uriloader/exthandler/nsISharingHandlerApp.idl @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIMIMEInfo.idl" + +[scriptable, uuid(7111f769-53ec-41fd-b314-613661d5b6ba)] +interface nsISharingHandlerApp : nsIHandlerApp +{ + void share(in AString data, [optional] in AString title); +}; + diff --git a/uriloader/exthandler/nsLocalHandlerApp.cpp b/uriloader/exthandler/nsLocalHandlerApp.cpp new file mode 100644 index 0000000000..f1a9a779cf --- /dev/null +++ b/uriloader/exthandler/nsLocalHandlerApp.cpp @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsLocalHandlerApp.h" +#include "nsIURI.h" +#include "nsIProcess.h" + +// XXX why does nsMIMEInfoImpl have a threadsafe nsISupports? do we need one +// here too? +NS_IMPL_ISUPPORTS(nsLocalHandlerApp, nsILocalHandlerApp, nsIHandlerApp) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIHandlerApp + +NS_IMETHODIMP nsLocalHandlerApp::GetName(nsAString& aName) { + if (mName.IsEmpty() && mExecutable) { + // Don't want to cache this, just in case someone resets the app + // without changing the description.... + mExecutable->GetLeafName(aName); + } else { + aName.Assign(mName); + } + + return NS_OK; +} + +NS_IMETHODIMP nsLocalHandlerApp::SetName(const nsAString& aName) { + mName.Assign(aName); + + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::SetDetailedDescription(const nsAString& aDescription) { + mDetailedDescription.Assign(aDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::GetDetailedDescription(nsAString& aDescription) { + aDescription.Assign(mDetailedDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* _retval) { + NS_ENSURE_ARG_POINTER(aHandlerApp); + + *_retval = false; + + // If the handler app isn't a local handler app, then it's not the same app. + nsCOMPtr<nsILocalHandlerApp> localHandlerApp = do_QueryInterface(aHandlerApp); + if (!localHandlerApp) return NS_OK; + + // If either handler app doesn't have an executable, then they aren't + // the same app. + nsCOMPtr<nsIFile> executable; + nsresult rv = localHandlerApp->GetExecutable(getter_AddRefs(executable)); + if (NS_FAILED(rv)) return rv; + + // Equality for two empty nsIHandlerApp + if (!executable && !mExecutable) { + *_retval = true; + return NS_OK; + } + + // At least one is set so they are not equal + if (!mExecutable || !executable) return NS_OK; + + // Check the command line parameter list lengths + uint32_t len; + localHandlerApp->GetParameterCount(&len); + if (mParameters.Length() != len) return NS_OK; + + // Check the command line params lists + for (uint32_t idx = 0; idx < mParameters.Length(); idx++) { + nsAutoString param; + if (NS_FAILED(localHandlerApp->GetParameter(idx, param)) || + !param.Equals(mParameters[idx])) + return NS_OK; + } + + return executable->Equals(mExecutable, _retval); +} + +NS_IMETHODIMP +nsLocalHandlerApp::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + // pass the entire URI to the handler. + nsAutoCString spec; + aURI->GetAsciiSpec(spec); + return LaunchWithIProcess(spec); +} + +nsresult nsLocalHandlerApp::LaunchWithIProcess(const nsCString& aArg) { + nsresult rv; + nsCOMPtr<nsIProcess> process = do_CreateInstance(NS_PROCESS_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + if (NS_FAILED(rv = process->Init(mExecutable))) return rv; + + const char* string = aArg.get(); + + return process->Run(false, &string, 1); +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsILocalHandlerApp + +NS_IMETHODIMP +nsLocalHandlerApp::GetExecutable(nsIFile** aExecutable) { + NS_IF_ADDREF(*aExecutable = mExecutable); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::SetExecutable(nsIFile* aExecutable) { + mExecutable = aExecutable; + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::GetParameterCount(uint32_t* aParameterCount) { + *aParameterCount = mParameters.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::ClearParameters() { + mParameters.Clear(); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::AppendParameter(const nsAString& aParam) { + mParameters.AppendElement(aParam); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::GetParameter(uint32_t parameterIndex, nsAString& _retval) { + if (mParameters.Length() <= parameterIndex) return NS_ERROR_INVALID_ARG; + + _retval.Assign(mParameters[parameterIndex]); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::ParameterExists(const nsAString& aParam, bool* _retval) { + *_retval = mParameters.Contains(aParam); + return NS_OK; +} diff --git a/uriloader/exthandler/nsLocalHandlerApp.h b/uriloader/exthandler/nsLocalHandlerApp.h new file mode 100644 index 0000000000..3ea8e3e4fc --- /dev/null +++ b/uriloader/exthandler/nsLocalHandlerApp.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsLocalHandlerAppImpl_h__ +#define __nsLocalHandlerAppImpl_h__ + +#include "nsString.h" +#include "nsIMIMEInfo.h" +#include "nsIFile.h" +#include "nsTArray.h" + +class nsLocalHandlerApp : public nsILocalHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + NS_DECL_NSILOCALHANDLERAPP + + nsLocalHandlerApp() {} + + nsLocalHandlerApp(const char16_t* aName, nsIFile* aExecutable) + : mName(aName), mExecutable(aExecutable) {} + + nsLocalHandlerApp(const nsAString& aName, nsIFile* aExecutable) + : mName(aName), mExecutable(aExecutable) {} + + protected: + virtual ~nsLocalHandlerApp() {} + + nsString mName; + nsString mDetailedDescription; + nsTArray<nsString> mParameters; + nsCOMPtr<nsIFile> mExecutable; + + /** + * Launches this application with a single argument (typically either + * a file path or a URI spec). This is meant as a helper method for + * implementations of (e.g.) LaunchWithURI. + * + * @param aApp The application to launch (may not be null) + * @param aArg The argument to pass on the command line + */ + nsresult LaunchWithIProcess(const nsCString& aArg); +}; + +// any platforms that need a platform-specific class instead of just +// using nsLocalHandlerApp need to add an include and a typedef here. +#ifdef XP_MACOSX +# ifndef NSLOCALHANDLERAPPMAC_H_ +# include "mac/nsLocalHandlerAppMac.h" +typedef nsLocalHandlerAppMac PlatformLocalHandlerApp_t; +# endif +#else +typedef nsLocalHandlerApp PlatformLocalHandlerApp_t; +#endif + +#endif // __nsLocalHandlerAppImpl_h__ diff --git a/uriloader/exthandler/nsMIMEInfoChild.h b/uriloader/exthandler/nsMIMEInfoChild.h new file mode 100644 index 0000000000..707f19aa67 --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoChild.h @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoChild_h +#define nsMIMEInfoChild_h + +#include "nsMIMEInfoImpl.h" + +/* + * A platform-generic nsMIMEInfo implementation to be used in child process + * generic code that needs a MIMEInfo with limited functionality. + */ +class nsChildProcessMIMEInfo : public nsMIMEInfoImpl { + public: + explicit nsChildProcessMIMEInfo(const char* aMIMEType = "") + : nsMIMEInfoImpl(aMIMEType) {} + + explicit nsChildProcessMIMEInfo(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + + nsChildProcessMIMEInfo(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; + + NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; + + protected: + [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; + +#ifdef DEBUG + [[nodiscard]] virtual nsresult LaunchDefaultWithFile( + nsIFile* aFile) override { + return NS_ERROR_UNEXPECTED; + } +#endif + [[nodiscard]] static nsresult OpenApplicationWithURI(nsIFile* aApplication, + const nsCString& aURI) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; +}; + +#endif diff --git a/uriloader/exthandler/nsMIMEInfoImpl.cpp b/uriloader/exthandler/nsMIMEInfoImpl.cpp new file mode 100644 index 0000000000..3800d41d57 --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoImpl.cpp @@ -0,0 +1,467 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoImpl.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsStringEnumerator.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsEscape.h" +#include "nsCURILoader.h" +#include "nsCExternalHandlerService.h" +#include "nsIExternalProtocolService.h" +#include "mozilla/StaticPtr.h" + +static bool sInitializedOurData = false; +StaticRefPtr<nsIFile> sOurAppFile; + +static already_AddRefed<nsIFile> GetCanonicalExecutable(nsIFile* aFile) { + nsCOMPtr<nsIFile> binary = aFile; +#ifdef XP_MACOSX + nsAutoString leafName; + if (binary) { + binary->GetLeafName(leafName); + } + while (binary && !StringEndsWith(leafName, u".app"_ns)) { + nsCOMPtr<nsIFile> parent; + binary->GetParent(getter_AddRefs(parent)); + binary = parent; + if (binary) { + binary->GetLeafName(leafName); + } + } +#endif + return binary.forget(); +} + +static void EnsureAppDetailsAvailable() { + if (sInitializedOurData) { + return; + } + sInitializedOurData = true; + nsCOMPtr<nsIFile> binary; + XRE_GetBinaryPath(getter_AddRefs(binary)); + sOurAppFile = GetCanonicalExecutable(binary); + ClearOnShutdown(&sOurAppFile); +} + +// nsISupports methods +NS_IMPL_ADDREF(nsMIMEInfoBase) +NS_IMPL_RELEASE(nsMIMEInfoBase) + +NS_INTERFACE_MAP_BEGIN(nsMIMEInfoBase) + NS_INTERFACE_MAP_ENTRY(nsIHandlerInfo) + // This is only an nsIMIMEInfo if it's a MIME handler. + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIMIMEInfo, mClass == eMIMEInfo) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIHandlerInfo) +NS_INTERFACE_MAP_END + +// nsMIMEInfoImpl methods + +// Constructors for a MIME handler. +nsMIMEInfoBase::nsMIMEInfoBase(const char* aMIMEType) + : mSchemeOrType(aMIMEType), + mClass(eMIMEInfo), + mPreferredAction(nsIMIMEInfo::saveToDisk), + mAlwaysAskBeforeHandling(true) {} + +nsMIMEInfoBase::nsMIMEInfoBase(const nsACString& aMIMEType) + : mSchemeOrType(aMIMEType), + mClass(eMIMEInfo), + mPreferredAction(nsIMIMEInfo::saveToDisk), + mAlwaysAskBeforeHandling(true) {} + +// Constructor for a handler that lets the caller specify whether this is a +// MIME handler or a protocol handler. In the long run, these will be distinct +// classes (f.e. nsMIMEInfo and nsProtocolInfo), but for now we reuse this class +// for both and distinguish between the two kinds of handlers via the aClass +// argument to this method, which can be either eMIMEInfo or eProtocolInfo. +nsMIMEInfoBase::nsMIMEInfoBase(const nsACString& aType, HandlerClass aClass) + : mSchemeOrType(aType), + mClass(aClass), + mPreferredAction(nsIMIMEInfo::saveToDisk), + mAlwaysAskBeforeHandling(true) {} + +nsMIMEInfoBase::~nsMIMEInfoBase() {} + +NS_IMETHODIMP +nsMIMEInfoBase::GetFileExtensions(nsIUTF8StringEnumerator** aResult) { + return NS_NewUTF8StringEnumerator(aResult, &mExtensions, this); +} + +NS_IMETHODIMP +nsMIMEInfoBase::ExtensionExists(const nsACString& aExtension, bool* _retval) { + MOZ_ASSERT(!aExtension.IsEmpty(), "no extension"); + *_retval = mExtensions.Contains(aExtension, + nsCaseInsensitiveCStringArrayComparator()); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPrimaryExtension(nsACString& _retval) { + if (!mExtensions.Length()) { + _retval.Truncate(); + return NS_ERROR_NOT_INITIALIZED; + } + _retval = mExtensions[0]; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetPrimaryExtension(const nsACString& aExtension) { + if (MOZ_UNLIKELY(aExtension.IsEmpty())) { + MOZ_ASSERT(false, "No extension"); + return NS_ERROR_INVALID_ARG; + } + int32_t i = mExtensions.IndexOf(aExtension, 0, + nsCaseInsensitiveCStringArrayComparator()); + if (i != -1) { + mExtensions.RemoveElementAt(i); + } + mExtensions.InsertElementAt(0, aExtension); + return NS_OK; +} + +void nsMIMEInfoBase::AddUniqueExtension(const nsACString& aExtension) { + if (!aExtension.IsEmpty() && + !mExtensions.Contains(aExtension, + nsCaseInsensitiveCStringArrayComparator())) { + mExtensions.AppendElement(aExtension); + } +} + +NS_IMETHODIMP +nsMIMEInfoBase::AppendExtension(const nsACString& aExtension) { + MOZ_ASSERT(!aExtension.IsEmpty(), "No extension"); + AddUniqueExtension(aExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetType(nsACString& aType) { + if (mSchemeOrType.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + aType = mSchemeOrType; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetMIMEType(nsACString& aMIMEType) { + if (mSchemeOrType.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + aMIMEType = mSchemeOrType; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetDescription(nsAString& aDescription) { + aDescription = mDescription; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetDescription(const nsAString& aDescription) { + mDescription = aDescription; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) { + if (!aMIMEInfo) return NS_ERROR_NULL_POINTER; + + nsAutoCString type; + nsresult rv = aMIMEInfo->GetMIMEType(type); + if (NS_FAILED(rv)) return rv; + + *_retval = mSchemeOrType.Equals(type); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetFileExtensions(const nsACString& aExtensions) { + mExtensions.Clear(); + nsACString::const_iterator start, end; + aExtensions.BeginReading(start); + aExtensions.EndReading(end); + while (start != end) { + nsACString::const_iterator cursor = start; + mozilla::Unused << FindCharInReadable(',', cursor, end); + AddUniqueExtension(Substring(start, cursor)); + // If a comma was found, skip it for the next search. + start = cursor != end ? ++cursor : cursor; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetDefaultDescription(nsAString& aDefaultDescription) { + aDefaultDescription = mDefaultAppDescription; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPreferredApplicationHandler( + nsIHandlerApp** aPreferredAppHandler) { + *aPreferredAppHandler = mPreferredApplication; + NS_IF_ADDREF(*aPreferredAppHandler); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetPreferredApplicationHandler( + nsIHandlerApp* aPreferredAppHandler) { + mPreferredApplication = aPreferredAppHandler; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPossibleApplicationHandlers( + nsIMutableArray** aPossibleAppHandlers) { + if (!mPossibleApplications) + mPossibleApplications = do_CreateInstance(NS_ARRAY_CONTRACTID); + + if (!mPossibleApplications) return NS_ERROR_OUT_OF_MEMORY; + + *aPossibleAppHandlers = mPossibleApplications; + NS_IF_ADDREF(*aPossibleAppHandlers); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPreferredAction(nsHandlerInfoAction* aPreferredAction) { + *aPreferredAction = mPreferredAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetPreferredAction(nsHandlerInfoAction aPreferredAction) { + mPreferredAction = aPreferredAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetAlwaysAskBeforeHandling(bool* aAlwaysAsk) { + *aAlwaysAsk = mAlwaysAskBeforeHandling; + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetAlwaysAskBeforeHandling(bool aAlwaysAsk) { + mAlwaysAskBeforeHandling = aAlwaysAsk; + return NS_OK; +} + +/* static */ +nsresult nsMIMEInfoBase::GetLocalFileFromURI(nsIURI* aURI, nsIFile** aFile) { + nsresult rv; + + nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIFile> file; + rv = fileUrl->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + file.forget(aFile); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::LaunchWithFile(nsIFile* aFile) { + nsresult rv; + + // it doesn't make any sense to call this on protocol handlers + NS_ASSERTION(mClass == eMIMEInfo, + "nsMIMEInfoBase should have mClass == eMIMEInfo"); + + if (mPreferredAction == useSystemDefault) { + return LaunchDefaultWithFile(aFile); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND; + + // at the moment, we only know how to hand files off to local handlers + nsCOMPtr<nsILocalHandlerApp> localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + return LaunchWithIProcess(executable, aFile->NativePath()); + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMIMEInfoBase::LaunchWithURI(nsIURI* aURI, BrowsingContext* aBrowsingContext) { + // This is only being called with protocol handlers + NS_ASSERTION(mClass == eProtocolInfo, + "nsMIMEInfoBase should be a protocol handler"); + + if (mPreferredAction == useSystemDefault) { + // First, ensure we're not accidentally going to call ourselves. + // That'd lead to an infinite loop (see bug 215554). + nsCOMPtr<nsIExternalProtocolService> extProtService = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID); + if (!extProtService) { + return NS_ERROR_FAILURE; + } + nsAutoCString scheme; + aURI->GetScheme(scheme); + bool isDefault = false; + nsresult rv = + extProtService->IsCurrentAppOSDefaultForProtocol(scheme, &isDefault); + if (NS_SUCCEEDED(rv) && isDefault) { + // Lie. This will trip the handler service into showing a dialog asking + // what the user wants. + return NS_ERROR_FILE_NOT_FOUND; + } + return LoadUriInternal(aURI); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND; + + EnsureAppDetailsAvailable(); + nsCOMPtr<nsILocalHandlerApp> localPreferredHandler = + do_QueryInterface(mPreferredApplication); + if (localPreferredHandler) { + nsCOMPtr<nsIFile> executable; + localPreferredHandler->GetExecutable(getter_AddRefs(executable)); + executable = GetCanonicalExecutable(executable); + bool isOurExecutable = false; + if (!executable || + NS_FAILED(executable->Equals(sOurAppFile, &isOurExecutable)) || + isOurExecutable) { + // Lie. This will trip the handler service into showing a dialog asking + // what the user wants. + return NS_ERROR_FILE_NOT_FOUND; + } + } + return mPreferredApplication->LaunchWithURI(aURI, aBrowsingContext); + } + + return NS_ERROR_INVALID_ARG; +} + +void nsMIMEInfoBase::CopyBasicDataTo(nsMIMEInfoBase* aOther) { + aOther->mSchemeOrType = mSchemeOrType; + aOther->mDefaultAppDescription = mDefaultAppDescription; + aOther->mExtensions = mExtensions.Clone(); +} + +/* static */ +already_AddRefed<nsIProcess> nsMIMEInfoBase::InitProcess(nsIFile* aApp, + nsresult* aResult) { + NS_ASSERTION(aApp, "Unexpected null pointer, fix caller"); + + nsCOMPtr<nsIProcess> process = + do_CreateInstance(NS_PROCESS_CONTRACTID, aResult); + if (NS_FAILED(*aResult)) return nullptr; + + *aResult = process->Init(aApp); + if (NS_FAILED(*aResult)) return nullptr; + + return process.forget(); +} + +/* static */ +nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp, + const nsCString& aArg) { + nsresult rv; + nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv); + if (NS_FAILED(rv)) return rv; + + const char* string = aArg.get(); + + return process->Run(false, &string, 1); +} + +/* static */ +nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp, + const nsString& aArg) { + nsresult rv; + nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv); + if (NS_FAILED(rv)) return rv; + + const char16_t* string = aArg.get(); + + return process->Runw(false, &string, 1); +} + +/* static */ +nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp, const int aArgc, + const char16_t** aArgv) { + nsresult rv; + nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + return process->Runw(false, aArgv, aArgc); +} + +// nsMIMEInfoImpl implementation +NS_IMETHODIMP +nsMIMEInfoImpl::GetDefaultDescription(nsAString& aDefaultDescription) { + if (mDefaultAppDescription.IsEmpty() && mDefaultApplication) { + // Don't want to cache this, just in case someone resets the app + // without changing the description.... + mDefaultApplication->GetLeafName(aDefaultDescription); + } else { + aDefaultDescription = mDefaultAppDescription; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoImpl::GetHasDefaultHandler(bool* _retval) { + *_retval = !mDefaultAppDescription.IsEmpty(); + if (mDefaultApplication) { + bool exists; + *_retval = NS_SUCCEEDED(mDefaultApplication->Exists(&exists)) && exists; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoImpl::IsCurrentAppOSDefault(bool* _retval) { + *_retval = false; + if (mDefaultApplication) { + // Determine if the default executable is our executable. + EnsureAppDetailsAvailable(); + bool isSame = false; + nsresult rv = mDefaultApplication->Equals(sOurAppFile, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + } + return NS_OK; +} + +nsresult nsMIMEInfoImpl::LaunchDefaultWithFile(nsIFile* aFile) { + if (!mDefaultApplication) return NS_ERROR_FILE_NOT_FOUND; + + return LaunchWithIProcess(mDefaultApplication, aFile->NativePath()); +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPossibleLocalHandlers(nsIArray** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/nsMIMEInfoImpl.h b/uriloader/exthandler/nsMIMEInfoImpl.h new file mode 100644 index 0000000000..f2fd792efb --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoImpl.h @@ -0,0 +1,213 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef __nsmimeinfoimpl_h___ +#define __nsmimeinfoimpl_h___ + +#include "nsIMIMEInfo.h" +#include "nsAtom.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIMutableArray.h" +#include "nsIFile.h" +#include "nsCOMPtr.h" +#include "nsIURI.h" +#include "nsIProcess.h" +#include "mozilla/dom/BrowsingContext.h" + +/** + * UTF8 moz-icon URI string for the default handler application's icon, if + * available. + */ +#define PROPERTY_DEFAULT_APP_ICON_URL "defaultApplicationIconURL" +/** + * UTF8 moz-icon URI string for the user's preferred handler application's + * icon, if available. + */ +#define PROPERTY_CUSTOM_APP_ICON_URL "customApplicationIconURL" + +/** + * Basic implementation of nsIMIMEInfo. Incomplete - it is meant to be + * subclassed, and GetHasDefaultHandler as well as LaunchDefaultWithFile need to + * be implemented. + */ +class nsMIMEInfoBase : public nsIMIMEInfo { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + // I'd use NS_DECL_NSIMIMEINFO, but I don't want GetHasDefaultHandler + NS_IMETHOD GetFileExtensions(nsIUTF8StringEnumerator** _retval) override; + NS_IMETHOD SetFileExtensions(const nsACString& aExtensions) override; + NS_IMETHOD ExtensionExists(const nsACString& aExtension, + bool* _retval) override; + NS_IMETHOD AppendExtension(const nsACString& aExtension) override; + NS_IMETHOD GetPrimaryExtension(nsACString& aPrimaryExtension) override; + NS_IMETHOD SetPrimaryExtension(const nsACString& aPrimaryExtension) override; + NS_IMETHOD GetType(nsACString& aType) override; + NS_IMETHOD GetMIMEType(nsACString& aMIMEType) override; + NS_IMETHOD GetDescription(nsAString& aDescription) override; + NS_IMETHOD SetDescription(const nsAString& aDescription) override; + NS_IMETHOD Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) override; + NS_IMETHOD GetPreferredApplicationHandler( + nsIHandlerApp** aPreferredAppHandler) override; + NS_IMETHOD SetPreferredApplicationHandler( + nsIHandlerApp* aPreferredAppHandler) override; + NS_IMETHOD GetPossibleApplicationHandlers( + nsIMutableArray** aPossibleAppHandlers) override; + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override; + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + NS_IMETHOD LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override; + NS_IMETHOD GetPreferredAction(nsHandlerInfoAction* aPreferredAction) override; + NS_IMETHOD SetPreferredAction(nsHandlerInfoAction aPreferredAction) override; + NS_IMETHOD GetAlwaysAskBeforeHandling( + bool* aAlwaysAskBeforeHandling) override; + NS_IMETHOD SetAlwaysAskBeforeHandling(bool aAlwaysAskBeforeHandling) override; + NS_IMETHOD GetPossibleLocalHandlers(nsIArray** _retval) override; + + enum HandlerClass { eMIMEInfo, eProtocolInfo }; + + // nsMIMEInfoBase methods + explicit nsMIMEInfoBase(const char* aMIMEType = ""); + explicit nsMIMEInfoBase(const nsACString& aMIMEType); + nsMIMEInfoBase(const nsACString& aType, HandlerClass aClass); + + void SetMIMEType(const nsACString& aMIMEType) { mSchemeOrType = aMIMEType; } + + void SetDefaultDescription(const nsString& aDesc) { + mDefaultAppDescription = aDesc; + } + + /** + * Copies basic data of this MIME Info Implementation to the given other + * MIME Info. The data consists of the MIME Type, the (default) description, + * the MacOS type and creator, and the extension list (this object's + * extension list will replace aOther's list, not append to it). This + * function also ensures that aOther's primary extension will be the same as + * the one of this object. + */ + void CopyBasicDataTo(nsMIMEInfoBase* aOther); + + /** + * Return whether this MIMEInfo has any extensions + */ + bool HasExtensions() const { return mExtensions.Length() != 0; } + + protected: + virtual ~nsMIMEInfoBase(); // must be virtual, as the the base class's + // Release should call the subclass's destructor + + /** + * Launch the default application for the given file. + * For even more control over the launching, override launchWithFile. + * Also see the comment about nsIMIMEInfo in general, above. + * + * @param aFile The file that should be opened + */ + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) = 0; + + /** + * Loads the URI with the OS default app. + * + * @param aURI The URI to pass off to the OS. + */ + virtual nsresult LoadUriInternal(nsIURI* aURI) = 0; + + static already_AddRefed<nsIProcess> InitProcess(nsIFile* aApp, + nsresult* aResult); + + /** + * This method can be used to launch the file or URI with a single + * argument (typically either a file path or a URI spec). This is + * meant as a helper method for implementations of + * LaunchWithURI/LaunchDefaultWithFile. + * + * @param aApp The application to launch (may not be null) + * @param aArg The argument to pass on the command line + */ + static nsresult LaunchWithIProcess(nsIFile* aApp, const nsCString& aArg); + static nsresult LaunchWithIProcess(nsIFile* aApp, const nsString& aArg); + static nsresult LaunchWithIProcess(nsIFile* aApp, const int aArgc, + const char16_t** aArgv); + + /** + * Given a file: nsIURI, return the associated nsIFile + * + * @param aURI the file: URI in question + * @param aFile the associated nsIFile (out param) + */ + static nsresult GetLocalFileFromURI(nsIURI* aURI, nsIFile** aFile); + + /** + * Internal helper to avoid adding duplicates. + */ + void AddUniqueExtension(const nsACString& aExtension); + + // member variables + nsTArray<nsCString> + mExtensions; ///< array of file extensions associated w/ this MIME obj + nsString mDescription; ///< human readable description + nsCString mSchemeOrType; + HandlerClass mClass; + nsCOMPtr<nsIHandlerApp> mPreferredApplication; + nsCOMPtr<nsIMutableArray> mPossibleApplications; + nsHandlerInfoAction + mPreferredAction; ///< preferred action to associate with this type + nsString mPreferredAppDescription; + nsString mDefaultAppDescription; + bool mAlwaysAskBeforeHandling; +}; + +/** + * This is a complete implementation of nsIMIMEInfo, and contains all necessary + * methods. However, depending on your platform you may want to use a different + * way of launching applications. This class stores the default application in a + * member variable and provides a function for setting it. For local + * applications, launching is done using nsIProcess, native path of the file to + * open as first argument. + */ +class nsMIMEInfoImpl : public nsMIMEInfoBase { + public: + explicit nsMIMEInfoImpl(const char* aMIMEType = "") + : nsMIMEInfoBase(aMIMEType) {} + explicit nsMIMEInfoImpl(const nsACString& aMIMEType) + : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoImpl(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoBase(aType, aClass) {} + virtual ~nsMIMEInfoImpl() {} + + // nsIMIMEInfo methods + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override; + NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override; + + // additional methods + /** + * Sets the default application. Supposed to be only called by the OS Helper + * App Services; the default application is immutable after it is first set. + */ + void SetDefaultApplication(nsIFile* aApp) { + if (!mDefaultApplication) mDefaultApplication = aApp; + } + + protected: + // nsMIMEInfoBase methods + /** + * The base class implementation is to use LaunchWithIProcess in combination + * with mDefaultApplication. Subclasses can override that behaviour. + */ + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override; + + /** + * Loads the URI with the OS default app. This should be overridden by each + * OS's implementation. + */ + virtual nsresult LoadUriInternal(nsIURI* aURI) override = 0; + + nsCOMPtr<nsIFile> + mDefaultApplication; ///< default application associated with this type. +}; + +#endif //__nsmimeinfoimpl_h___ diff --git a/uriloader/exthandler/nsOSHelperAppServiceChild.cpp b/uriloader/exthandler/nsOSHelperAppServiceChild.cpp new file mode 100644 index 0000000000..1e1442fac6 --- /dev/null +++ b/uriloader/exthandler/nsOSHelperAppServiceChild.cpp @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <sys/stat.h> +#include "mozilla/Logging.h" +#include "mozilla/net/NeckoCommon.h" +#include "nsOSHelperAppServiceChild.h" +#include "nsMIMEInfoChild.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIFile.h" +#include "nsIHandlerService.h" +#include "nsMimeTypes.h" +#include "nsMIMEInfoImpl.h" +#include "nsMemory.h" +#include "nsCRT.h" +#include "nsEmbedCID.h" + +#undef LOG +#define LOG(args) \ + MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args) +#undef LOG_ERR +#define LOG_ERR(args) \ + MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Error, args) +#undef LOG_ENABLED +#define LOG_ENABLED() \ + MOZ_LOG_TEST(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info) + +nsresult nsOSHelperAppServiceChild::ExternalProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG_ERR(("nsOSHelperAppServiceChild error: no handler service")); + return rv; + } + + nsAutoCString scheme(aProtocolScheme); + *aHandlerExists = false; + rv = handlerSvc->ExistsForProtocol(scheme, aHandlerExists); + LOG( + ("nsOSHelperAppServiceChild::ExternalProtocolHandlerExists(): " + "protocol: %s, result: %" PRId32, + aProtocolScheme, static_cast<uint32_t>(rv))); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + return rv; +} + +nsresult nsOSHelperAppServiceChild::OSProtocolHandlerExists(const char* aScheme, + bool* aExists) { + // Use ExternalProtocolHandlerExists() which includes the + // OS-level check and remotes the call to the parent process. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::GetApplicationDescription(const nsACString& aScheme, + nsAString& aRetVal) { + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG_ERR(("nsOSHelperAppServiceChild error: no handler service")); + return rv; + } + + rv = handlerSvc->GetApplicationDescription(aScheme, aRetVal); + LOG( + ("nsOSHelperAppServiceChild::GetApplicationDescription(): " + "scheme: %s, result: %" PRId32 ", description: %s", + PromiseFlatCString(aScheme).get(), static_cast<uint32_t>(rv), + NS_ConvertUTF16toUTF8(aRetVal).get())); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + return rv; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + RefPtr<nsChildProcessMIMEInfo> mimeInfo = + new nsChildProcessMIMEInfo(aMIMEType); + + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + nsresult rv = + handlerSvc->GetMIMEInfoFromOS(mimeInfo, aMIMEType, aFileExt, aFound); + LOG( + ("nsOSHelperAppServiceChild::GetMIMEInfoFromOS(): " + "MIME type: %s, extension: %s, result: %" PRId32, + PromiseFlatCString(aMIMEType).get(), + PromiseFlatCString(aFileExt).get(), static_cast<uint32_t>(rv))); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + if (NS_FAILED(rv)) { + return rv; + } + } else { + LOG_ERR(("nsOSHelperAppServiceChild error: no handler service")); + *aFound = false; + } + + mimeInfo.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::GetProtocolHandlerInfoFromOS( + const nsACString& aScheme, bool* aFound, nsIHandlerInfo** aRetVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* aRetVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsOSHelperAppServiceChild::GetFileTokenForPath( + const char16_t* platformAppPath, nsIFile** aFile) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/nsOSHelperAppServiceChild.h b/uriloader/exthandler/nsOSHelperAppServiceChild.h new file mode 100644 index 0000000000..f37caf010d --- /dev/null +++ b/uriloader/exthandler/nsOSHelperAppServiceChild.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppServiceChild_h__ +#define nsOSHelperAppServiceChild_h__ + +#include "nsExternalHelperAppService.h" + +class nsIMIMEInfo; + +/* + * Provides a generic implementation of the nsExternalHelperAppService + * platform-specific methods by remoting calls to the parent process. + * Only provides implementations for the methods needed in unprivileged + * child processes. + */ +class nsOSHelperAppServiceChild : public nsExternalHelperAppService { + public: + nsOSHelperAppServiceChild() : nsExternalHelperAppService(){}; + virtual ~nsOSHelperAppServiceChild() = default; + + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + NS_IMETHOD ExternalProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + + NS_IMETHOD OSProtocolHandlerExists(const char* aScheme, + bool* aExists) override; + + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& aRetVal) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; +}; + +#endif // nsOSHelperAppServiceChild_h__ diff --git a/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm b/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm new file mode 100644 index 0000000000..72c551d654 --- /dev/null +++ b/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm @@ -0,0 +1,241 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Shared functions for tests related to invoking external handler applications. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["HandlerServiceTestUtils"]; + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gHandlerService", + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService" +); + +var HandlerServiceTestUtils = { + /** + * Retrieves the names of all the MIME types and protocols configured in the + * handler service instance currently under testing. + * + * @return Array of strings like "example/type" or "example-scheme", sorted + * alphabetically regardless of category. + */ + getAllHandlerInfoTypes() { + return Array.from(gHandlerService.enumerate(), info => info.type).sort(); + }, + + /** + * Retrieves all the configured handlers for MIME types and protocols. + * + * @note The nsIHandlerInfo instances returned by the "enumerate" method + * cannot be used for testing because they incorporate information from + * the operating system and also from the default nsIHandlerService + * instance, independently from what instance is under testing. + * + * @return Array of nsIHandlerInfo instances sorted by their "type" property. + */ + getAllHandlerInfos() { + return this.getAllHandlerInfoTypes().map(type => this.getHandlerInfo(type)); + }, + + /** + * Retrieves an nsIHandlerInfo for the given MIME type or protocol, which + * incorporates information from the operating system and also from the + * handler service instance currently under testing. + * + * @note If the handler service instance currently under testing is not the + * default one and the requested type is a MIME type, the returned + * nsIHandlerInfo will include information from the default + * nsIHandlerService instance. This cannot be avoided easily because the + * getMIMEInfoFromOS method is not exposed to JavaScript. + * + * @param type + * MIME type or scheme name of the nsIHandlerInfo to retrieve. + * + * @return The populated nsIHandlerInfo instance. + */ + getHandlerInfo(type) { + if (type.includes("/")) { + // We have to use the getFromTypeAndExtension method because we don't have + // access to getMIMEInfoFromOS. This means that we have to reset the data + // that may have been imported from the default nsIHandlerService instance + // and is not overwritten by fillHandlerInfo later. + let handlerInfo = gMIMEService.getFromTypeAndExtension(type, null); + if (AppConstants.platform == "android") { + // On Android, the first handler application is always the internal one. + while (handlerInfo.possibleApplicationHandlers.length > 1) { + handlerInfo.possibleApplicationHandlers.removeElementAt(1); + } + } else { + handlerInfo.possibleApplicationHandlers.clear(); + } + handlerInfo.setFileExtensions(""); + // Populate the object from the handler service instance under testing. + if (gHandlerService.exists(handlerInfo)) { + gHandlerService.fillHandlerInfo(handlerInfo, ""); + } + return handlerInfo; + } + + // Populate the protocol information from the handler service instance under + // testing, like the nsIExternalProtocolService::GetProtocolHandlerInfo + // method does on the default nsIHandlerService instance. + let osDefaultHandlerFound = {}; + let handlerInfo = gExternalProtocolService.getProtocolHandlerInfoFromOS( + type, + osDefaultHandlerFound + ); + if (gHandlerService.exists(handlerInfo)) { + gHandlerService.fillHandlerInfo(handlerInfo, ""); + } else { + gExternalProtocolService.setProtocolHandlerDefaults( + handlerInfo, + osDefaultHandlerFound.value + ); + } + return handlerInfo; + }, + + /** + * Creates an nsIHandlerInfo for the given MIME type or protocol, initialized + * to the default values for the current platform. + * + * @note For this method to work, the specified MIME type or protocol must not + * be configured in the default handler service instance or the one + * under testing, and must not be registered in the operating system. + * + * @param type + * MIME type or scheme name of the nsIHandlerInfo to create. + * + * @return The blank nsIHandlerInfo instance. + */ + getBlankHandlerInfo(type) { + let handlerInfo = this.getHandlerInfo(type); + + let preferredAction, preferredApplicationHandler; + if (AppConstants.platform == "android") { + // On Android, the default preferredAction for MIME types is useHelperApp. + // For protocols, we always behave as if an operating system provided + // handler existed, and as such we initialize them to useSystemDefault. + // This is because the AndroidBridge is not available in xpcshell tests. + preferredAction = type.includes("/") + ? Ci.nsIHandlerInfo.useHelperApp + : Ci.nsIHandlerInfo.useSystemDefault; + // On Android, the default handler application is always the internal one. + preferredApplicationHandler = { + name: "Android chooser", + }; + } else { + // On Desktop, the default preferredAction for MIME types is saveToDisk, + // while for protocols it is alwaysAsk. + preferredAction = type.includes("/") + ? Ci.nsIHandlerInfo.saveToDisk + : Ci.nsIHandlerInfo.alwaysAsk; + preferredApplicationHandler = null; + } + + this.assertHandlerInfoMatches(handlerInfo, { + type, + preferredAction, + alwaysAskBeforeHandling: true, + preferredApplicationHandler, + }); + return handlerInfo; + }, + + /** + * Checks whether an nsIHandlerInfo instance matches the provided object. + */ + assertHandlerInfoMatches(handlerInfo, expected) { + let expectedInterface = expected.type.includes("/") + ? Ci.nsIMIMEInfo + : Ci.nsIHandlerInfo; + Assert.ok(handlerInfo instanceof expectedInterface); + Assert.equal(handlerInfo.type, expected.type); + + if (!expected.preferredActionOSDependent) { + Assert.equal(handlerInfo.preferredAction, expected.preferredAction); + Assert.equal( + handlerInfo.alwaysAskBeforeHandling, + expected.alwaysAskBeforeHandling + ); + } + + if (expectedInterface == Ci.nsIMIMEInfo) { + let fileExtensionsEnumerator = handlerInfo.getFileExtensions(); + for (let expectedFileExtension of expected.fileExtensions || []) { + Assert.equal(fileExtensionsEnumerator.getNext(), expectedFileExtension); + } + Assert.ok(!fileExtensionsEnumerator.hasMore()); + } + + if (expected.preferredApplicationHandler) { + this.assertHandlerAppMatches( + handlerInfo.preferredApplicationHandler, + expected.preferredApplicationHandler + ); + } else { + Assert.equal(handlerInfo.preferredApplicationHandler, null); + } + + let handlerAppsArrayEnumerator = handlerInfo.possibleApplicationHandlers.enumerate(); + if (AppConstants.platform == "android") { + // On Android, the first handler application is always the internal one. + this.assertHandlerAppMatches(handlerAppsArrayEnumerator.getNext(), { + name: "Android chooser", + }); + } + for (let expectedHandlerApp of expected.possibleApplicationHandlers || []) { + this.assertHandlerAppMatches( + handlerAppsArrayEnumerator.getNext(), + expectedHandlerApp + ); + } + Assert.ok(!handlerAppsArrayEnumerator.hasMoreElements()); + }, + + /** + * Checks whether an nsIHandlerApp instance matches the provided object. + */ + assertHandlerAppMatches(handlerApp, expected) { + Assert.ok(handlerApp instanceof Ci.nsIHandlerApp); + Assert.equal(handlerApp.name, expected.name); + if (expected.executable) { + Assert.ok(handlerApp instanceof Ci.nsILocalHandlerApp); + Assert.ok(expected.executable.equals(handlerApp.executable)); + } else if (expected.uriTemplate) { + Assert.ok(handlerApp instanceof Ci.nsIWebHandlerApp); + Assert.equal(handlerApp.uriTemplate, expected.uriTemplate); + } else if (expected.service) { + Assert.ok(handlerApp instanceof Ci.nsIDBusHandlerApp); + Assert.equal(handlerApp.service, expected.service); + Assert.equal(handlerApp.method, expected.method); + Assert.equal(handlerApp.dBusInterface, expected.dBusInterface); + Assert.equal(handlerApp.objectPath, expected.objectPath); + } + }, +}; diff --git a/uriloader/exthandler/tests/WriteArgument.cpp b/uriloader/exthandler/tests/WriteArgument.cpp new file mode 100644 index 0000000000..603965150e --- /dev/null +++ b/uriloader/exthandler/tests/WriteArgument.cpp @@ -0,0 +1,20 @@ +#include <stdio.h> +#include "prenv.h" + +int main(int argc, char* argv[]) { + if (argc != 2) return 1; + + const char* value = PR_GetEnv("WRITE_ARGUMENT_FILE"); + + if (!value) return 2; + + FILE* outfile = fopen(value, "w"); + if (!outfile) return 3; + + // We only need to write out the first argument (no newline). + fputs(argv[argc - 1], outfile); + + fclose(outfile); + + return 0; +} diff --git a/uriloader/exthandler/tests/mochitest/.eslintrc.js b/uriloader/exthandler/tests/mochitest/.eslintrc.js new file mode 100644 index 0000000000..7612459de1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test", "plugin:mozilla/mochitest-test"], +}; diff --git a/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js new file mode 100644 index 0000000000..d08d72b048 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js @@ -0,0 +1,38 @@ +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); + +const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1"; +const HELPERAPP_DIALOG_CID = Components.ID( + Cc[HELPERAPP_DIALOG_CONTRACT].number +); + +const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator) + .generateUUID(); +/* eslint-env mozilla/frame-script */ +function HelperAppLauncherDialog() {} +HelperAppLauncherDialog.prototype = { + show(aLauncher, aWindowContext, aReason) { + sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName); + }, + QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]), +}; + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory( + FAKE_CID, + "", + HELPERAPP_DIALOG_CONTRACT, + ComponentUtils._getFactory(HelperAppLauncherDialog) +); + +addMessageListener("unregister", function() { + registrar.registerFactory( + HELPERAPP_DIALOG_CID, + "", + HELPERAPP_DIALOG_CONTRACT, + null + ); + sendAsyncMessage("unregistered"); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser.ini b/uriloader/exthandler/tests/mochitest/browser.ini new file mode 100644 index 0000000000..0d5b20bef8 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser.ini @@ -0,0 +1,51 @@ +[DEFAULT] +head = head.js +support-files = + protocolHandler.html + +[browser_auto_close_window.js] +run-if = e10s # test relies on e10s behavior +support-files = + download_page.html + download.bin + download.sjs +[browser_download_always_ask_preferred_app.js] +[browser_download_privatebrowsing.js] +[browser_download_open_with_internal_handler.js] +support-files = + file_pdf_application_pdf.pdf + file_pdf_application_pdf.pdf^headers^ + file_pdf_application_unknown.pdf + file_pdf_application_unknown.pdf^headers^ + file_pdf_binary_octet_stream.pdf + file_pdf_binary_octet_stream.pdf^headers^ + file_txt_attachment_test.txt + file_txt_attachment_test.txt^headers^ + file_xml_attachment_binary_octet_stream.xml + file_xml_attachment_binary_octet_stream.xml^headers^ + file_xml_attachment_test.xml + file_xml_attachment_test.xml^headers^ +[browser_download_urlescape.js] +support-files = + file_with@@funny_name.png + file_with@@funny_name.png^headers^ + file_with[funny_name.webm + file_with[funny_name.webm^headers^ +[browser_extension_correction.js] +support-files = + file_as.exe + file_as.exe^headers^ +[browser_open_internal_choice_persistence.js] +support-files = + file_pdf_application_pdf.pdf + file_pdf_application_pdf.pdf^headers^ +[browser_protocol_ask_dialog.js] +support-files = + file_nested_protocol_request.html +[browser_first_prompt_not_blocked_without_user_interaction.js] +support-files = + file_external_protocol_iframe.html +[browser_protocol_ask_dialog_permission.js] +[browser_protocolhandler_loop.js] +[browser_remember_download_option.js] +[browser_web_protocol_handlers.js] diff --git a/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js new file mode 100644 index 0000000000..2e1d17e139 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); + +const ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const PAGE_URL = ROOT + "download_page.html"; +const SJS_URL = ROOT + "download.sjs"; + +const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1"; +const HELPERAPP_DIALOG_CID = Components.ID( + Cc[HELPERAPP_DIALOG_CONTRACT_ID].number +); +const MOCK_HELPERAPP_DIALOG_CID = Components.ID( + "{2f372b6f-56c9-46d5-af0d-9f09bb69860c}" +); + +let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +let curDialogResolve = null; + +function HelperAppLauncherDialog() {} + +HelperAppLauncherDialog.prototype = { + show(aLauncher, aWindowContext, aReason) { + ok(true, "Showing the helper app dialog"); + curDialogResolve(aWindowContext); + executeSoon(() => { + aLauncher.cancel(Cr.NS_ERROR_ABORT); + }); + }, + QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]), +}; + +function promiseHelperAppDialog() { + return new Promise(resolve => { + curDialogResolve = resolve; + }); +} + +let mockHelperAppService; + +add_task(async function setup() { + // Replace the real helper app dialog with our own. + mockHelperAppService = ComponentUtils._getFactory(HelperAppLauncherDialog); + registrar.registerFactory( + MOCK_HELPERAPP_DIALOG_CID, + "", + HELPERAPP_DIALOG_CONTRACT_ID, + mockHelperAppService + ); +}); + +add_task(async function simple_navigation() { + // Tests that simple navigation gives us the right windowContext (that is, + // the window that we're using). + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#regular_load", + {}, + browser + ); + let windowContext = await dialogAppeared; + + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + }); +}); + +// Given a browser pointing to download_page.html, clicks on the link that +// opens with target="_blank" (i.e. a new tab) and ensures that we +// automatically open and close that tab. +async function testNewTab(browser) { + let dialogAppeared = promiseHelperAppDialog(); + let tabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ).then(event => { + return [event.target, BrowserTestUtils.waitForTabClosing(event.target)]; + }); + + await BrowserTestUtils.synthesizeMouseAtCenter("#target_blank", {}, browser); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + let [tab, closingPromise] = await tabOpened; + await closingPromise; + is(tab.linkedBrowser, null, "tab was opened and closed"); +} + +add_task(async function target_blank() { + // Tests that a link with target=_blank opens a new tab and closes it, + // returning the window that we're using for navigation. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + await testNewTab(browser); + }); +}); + +add_task(async function target_blank_no_opener() { + // Tests that a link with target=_blank and no opener opens a new tab + // and closes it, returning the window that we're using for navigation. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let tabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ).then(event => { + return [event.target, BrowserTestUtils.waitForTabClosing(event.target)]; + }); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#target_blank_no_opener", + {}, + browser + ); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + let [tab, closingPromise] = await tabOpened; + await closingPromise; + is(tab.linkedBrowser, null, "tab was opened and closed"); + }); +}); + +add_task(async function open_in_new_tab_no_opener() { + // Tests that a link with target=_blank and no opener opens a new tab + // and closes it, returning the window that we're using for navigation. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let tabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ).then(event => { + return [event.target, BrowserTestUtils.waitForTabClosing(event.target)]; + }); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#open_in_new_tab_no_opener", + {}, + browser + ); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + let [tab, closingPromise] = await tabOpened; + await closingPromise; + is(tab.linkedBrowser, null, "tab was opened and closed"); + }); +}); + +add_task(async function new_window() { + // Tests that a link that forces us to open a new window (by specifying a + // width and a height in window.open) opens a new window for the load, + // realizes that we need to close that window and returns the *original* + // window as the window context. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let windowOpened = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.synthesizeMouseAtCenter("#new_window", {}, browser); + let win = await windowOpened; + // Now allow request to complete: + fetch(SJS_URL + "?finish"); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + + // The window should close on its own. If not, this test will time out. + await BrowserTestUtils.domWindowClosed(win); + ok(win.closed, "window was opened and closed"); + + is( + await fetch(SJS_URL + "?reset").then(r => r.text()), + "OK", + "Test reseted" + ); + }); +}); + +add_task(async function new_window_no_opener() { + // Tests that a link that forces us to open a new window (by specifying a + // width and a height in window.open) opens a new window for the load, + // realizes that we need to close that window and returns the *original* + // window as the window context. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let windowOpened = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#new_window_no_opener", + {}, + browser + ); + let win = await windowOpened; + // Now allow request to complete: + fetch(SJS_URL + "?finish"); + + await dialogAppeared; + + // The window should close on its own. If not, this test will time out. + await BrowserTestUtils.domWindowClosed(win); + ok(win.closed, "window was opened and closed"); + + is( + await fetch(SJS_URL + "?reset").then(r => r.text()), + "OK", + "Test reseted" + ); + }); +}); + +add_task(async function nested_window_opens() { + // Tests that the window auto-closing feature works if the download is + // initiated by a window that, itself, has an opener (see bug 1373109). + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + outerBrowser + ) { + let secondTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + `${PAGE_URL}?newwin`, + true + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "#open_in_new_tab", + {}, + outerBrowser + ); + let secondTab = await secondTabPromise; + let nestedBrowser = secondTab.linkedBrowser; + + await SpecialPowers.spawn(nestedBrowser, [], function() { + ok(content.opener, "this window has an opener"); + }); + + await testNewTab(nestedBrowser); + + isnot( + secondTab.linkedBrowser, + null, + "the page that triggered the download is still open" + ); + BrowserTestUtils.removeTab(secondTab); + }); +}); + +add_task(async function cleanup() { + // Unregister our factory from XPCOM and restore the original CID. + registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID, mockHelperAppService); + registrar.registerFactory( + HELPERAPP_DIALOG_CID, + "", + HELPERAPP_DIALOG_CONTRACT_ID, + null + ); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js new file mode 100644 index 0000000000..bd421d51f3 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js @@ -0,0 +1,25 @@ +add_task(async function() { + // Create mocked objects for test + let launcher = createMockedObjects(false); + // Open helper app dialog with mocked launcher + let dlg = await openHelperAppDialog(launcher); + let doc = dlg.document; + let location = doc.getElementById("source"); + let expectedValue = launcher.source.prePath; + if (location.value != expectedValue) { + info("Waiting for dialog to be populated."); + await BrowserTestUtils.waitForAttribute("value", location, expectedValue); + } + is( + doc.getElementById("mode").selectedItem.id, + "open", + "Should be opening the file." + ); + ok( + !dlg.document.getElementById("openHandler").selectedItem.hidden, + "Should not have selected a hidden item." + ); + let helperAppDialogHiddenPromise = BrowserTestUtils.windowClosed(dlg); + doc.getElementById("unknownContentType").cancelDialog(); + await helperAppDialogHiddenPromise; +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js new file mode 100644 index 0000000000..ef7174f30f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js @@ -0,0 +1,612 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/Downloads.jsm", this); +const { DownloadIntegration } = ChromeUtils.import( + "resource://gre/modules/DownloadIntegration.jsm" +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +function waitForAcceptButtonToGetEnabled(doc) { + let dialog = doc.querySelector("#unknownContentType"); + let button = dialog.getButton("accept"); + return TestUtils.waitForCondition( + () => !button.disabled, + "Wait for Accept button to get enabled" + ); +} + +async function waitForPdfJS(browser, url) { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.eventBusDispatchToDOM", true]], + }); + // Runs tests after all "load" event handlers have fired off + let loadPromise = BrowserTestUtils.waitForContentEvent( + browser, + "documentloaded", + false, + null, + true + ); + await SpecialPowers.spawn(browser, [url], contentUrl => { + content.location = contentUrl; + }); + return loadPromise; +} + +add_task(async function setup() { + // Remove the security delay for the dialog during the test. + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.dialog_enable_delay", 0], + ["browser.helperApps.showOpenOptionForPdfJS", true], + ["browser.helperApps.showOpenOptionForViewableInternally", true], + ], + }); + + // Restore handlers after the whole test has run + const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + const registerRestoreHandler = function(type, ext) { + const mimeInfo = mimeSvc.getFromTypeAndExtension(type, ext); + const existed = handlerSvc.exists(mimeInfo); + registerCleanupFunction(() => { + if (existed) { + handlerSvc.store(mimeInfo); + } else { + handlerSvc.remove(mimeInfo); + } + }); + }; + registerRestoreHandler("application/pdf", "pdf"); + registerRestoreHandler("binary/octet-stream", "pdf"); + registerRestoreHandler("application/unknown", "pdf"); +}); + +/** + * Check that loading a PDF file with content-disposition: attachment + * shows an option to open with the internal handler, and that the + * internal option handler is not present when the download button + * is clicked from pdf.js. + */ +add_task(async function test_check_open_with_internal_handler() { + for (let file of [ + "file_pdf_application_pdf.pdf", + "file_pdf_binary_octet_stream.pdf", + ]) { + info("Testing with " + file); + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + // Add an extra tab after the loading tab so we can test that + // pdf.js is opened in the adjacent tab and not at the end of + // the tab strip. + let extraTab = await BrowserTestUtils.addTab(gBrowser, "about:blank"); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + let internalHandlerRadio = doc.querySelector("#handleInternally"); + + await waitForAcceptButtonToGetEnabled(doc); + + ok(!internalHandlerRadio.hidden, "The option should be visible for PDF"); + ok(internalHandlerRadio.selected, "The option should be selected"); + + let downloadFinishedPromise = promiseDownloadFinished(publicList); + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + let dialog = doc.querySelector("#unknownContentType"); + let button = dialog.getButton("accept"); + button.disabled = false; + dialog.acceptDialog(); + info("waiting for new tab to open"); + let newTab = await newTabPromise; + + is( + newTab._tPos - 1, + loadingTab._tPos, + "pdf.js should be opened in an adjacent tab" + ); + + await ContentTask.spawn(newTab.linkedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.readyState == "complete" + ); + }); + + let publicDownloads = await publicList.getAll(); + is( + publicDownloads.length, + 1, + "download should appear in publicDownloads list" + ); + + let download = await downloadFinishedPromise; + + let subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + // Current tab has file: URI and TEST_PATH is http uri, so uri will be different + BrowserTestUtils.loadURI(newTab.linkedBrowser, TEST_PATH + file); + let subDialogWindow = await subdialogPromise; + let subDoc = subDialogWindow.document; + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(subDoc); + let subInternalHandlerRadio = subDoc.querySelector("#handleInternally"); + ok( + !subInternalHandlerRadio.hidden, + "This option should be shown when the dialog is shown for another PDF" + ); + // Cancel dialog + subDoc.querySelector("#unknownContentType").cancelDialog(); + + subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + await SpecialPowers.spawn(newTab.linkedBrowser, [], async () => { + let downloadButton; + await ContentTaskUtils.waitForCondition(() => { + downloadButton = content.document.querySelector("#download"); + return !!downloadButton; + }); + ok(downloadButton, "Download button should be present in pdf.js"); + downloadButton.click(); + }); + info( + "Waiting for unknown content type dialog to appear from pdf.js download button click" + ); + subDialogWindow = await subdialogPromise; + subDoc = subDialogWindow.document; + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(subDoc); + subInternalHandlerRadio = subDoc.querySelector("#handleInternally"); + ok( + subInternalHandlerRadio.hidden, + "The option should be hidden when the dialog is opened from pdf.js" + ); + subDoc.querySelector("#open").click(); + + let tabOpenListener = () => { + ok( + false, + "A new tab should not be opened when accepting the dialog with 'open-with-external-app' chosen" + ); + }; + gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener); + + let oldLaunchFile = DownloadIntegration.launchFile; + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = async () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + downloadFinishedPromise = promiseDownloadFinished(publicList); + + info("Accepting the dialog"); + subDoc.querySelector("#unknownContentType").acceptDialog(); + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + DownloadIntegration.launchFile = oldLaunchFile; + + // Remove the first file (can't do this sooner or the second load fails): + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + + gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener); + BrowserTestUtils.removeTab(loadingTab); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(extraTab); + + // Remove the remaining file once complete. + download = await downloadFinishedPromise; + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + await publicList.removeFinished(); + } +}); + +/** + * Test that choosing to open in an external application doesn't + * open the PDF into pdf.js + */ +add_task(async function test_check_open_with_external_application() { + for (let file of [ + "file_pdf_application_pdf.pdf", + "file_pdf_binary_octet_stream.pdf", + ]) { + info("Testing with " + file); + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + + let oldLaunchFile = DownloadIntegration.launchFile; + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + let doc = dialogWindow.document; + await waitForAcceptButtonToGetEnabled(doc); + let dialog = doc.querySelector("#unknownContentType"); + doc.querySelector("#open").click(); + let button = dialog.getButton("accept"); + button.disabled = false; + info("Accepting the dialog"); + dialog.acceptDialog(); + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + DownloadIntegration.launchFile = oldLaunchFile; + + let publicDownloads = await publicList.getAll(); + is( + publicDownloads.length, + 1, + "download should appear in publicDownloads list" + ); + let download = publicDownloads[0]; + ok( + !download.launchWhenSucceeded, + "launchWhenSucceeded should be false after launchFile is called" + ); + + BrowserTestUtils.removeTab(loadingTab); + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + await publicList.removeFinished(); + } +}); + +/** + * Test that choosing to open a PDF with an external application works and + * then downloading the same file again and choosing Open with Firefox opens + * the download in Firefox. + */ +add_task(async function test_check_open_with_external_then_internal() { + // This test only runs on Windows because appPicker.xhtml is only used on Windows. + if (AppConstants.platform != "win") { + return; + } + + // This test covers a bug that only occurs when the mimeInfo is set to Always Ask + const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + const mimeInfo = mimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + mimeInfo.preferredAction = mimeInfo.alwaysAsk; + mimeInfo.alwaysAskBeforeHandling = true; + handlerSvc.store(mimeInfo); + + for (let [file, mimeType] of [ + ["file_pdf_application_pdf.pdf", "application/pdf"], + ["file_pdf_binary_octet_stream.pdf", "binary/octet-stream"], + ["file_pdf_application_unknown.pdf", "application/unknown"], + ]) { + info("Testing with " + file); + let originalMimeInfo = mimeSvc.getFromTypeAndExtension(mimeType, "pdf"); + + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + // Open a new tab to the PDF file which will trigger the Unknown Content Type dialog + // and choose to open the PDF with an external application. + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + + let oldLaunchFile = DownloadIntegration.launchFile; + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + let doc = dialogWindow.document; + await waitForAcceptButtonToGetEnabled(doc); + let dialog = doc.querySelector("#unknownContentType"); + let openHandlerMenulist = doc.querySelector("#openHandler"); + let originalDefaultHandler = openHandlerMenulist.label; + doc.querySelector("#open").click(); + doc.querySelector("#openHandlerPopup").click(); + let oldOpenDialog = dialogWindow.openDialog; + dialogWindow.openDialog = (location, unused2, unused3, params) => { + is(location, "chrome://global/content/appPicker.xhtml", "app picker"); + let handlerApp = params.mimeInfo.possibleLocalHandlers.queryElementAt( + 0, + Ci.nsILocalHandlerApp + ); + ok(handlerApp.executable, "handlerApp should be executable"); + ok(handlerApp.executable.isFile(), "handlerApp should be a file"); + params.handlerApp = handlerApp; + }; + doc.querySelector("#choose").click(); + dialogWindow.openDialog = oldOpenDialog; + await TestUtils.waitForCondition( + () => originalDefaultHandler != openHandlerMenulist.label, + "waiting for openHandler to get updated" + ); + let newDefaultHandler = openHandlerMenulist.label; + info(`was ${originalDefaultHandler}, now ${newDefaultHandler}`); + let button = dialog.getButton("accept"); + button.disabled = false; + info("Accepting the dialog"); + dialog.acceptDialog(); + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + BrowserTestUtils.removeTab(loadingTab); + + // Now, open a new tab to the PDF file which will trigger the Unknown Content Type dialog + // and choose to open the PDF internally. The previously used external application should be shown as + // the external option. + dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + + DownloadIntegration.launchFile = () => { + ok(false, "The file should not be launched with an external application"); + }; + + doc = dialogWindow.document; + await waitForAcceptButtonToGetEnabled(doc); + openHandlerMenulist = doc.querySelector("#openHandler"); + is(openHandlerMenulist.label, newDefaultHandler, "'new' handler"); + dialog = doc.querySelector("#unknownContentType"); + doc.querySelector("#handleInternally").click(); + info("Accepting the dialog"); + button = dialog.getButton("accept"); + button.disabled = false; + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + dialog.acceptDialog(); + + info("waiting for new tab to open"); + let newTab = await newTabPromise; + + await ContentTask.spawn(newTab.linkedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.readyState == "complete" + ); + }); + + is( + newTab.linkedBrowser.contentPrincipal.origin, + "resource://pdf.js", + "PDF should be opened with pdf.js" + ); + + BrowserTestUtils.removeTab(loadingTab); + BrowserTestUtils.removeTab(newTab); + + // Now trigger the dialog again and select the system + // default option to reset the state for the next iteration of the test. + // Reset the state for the next iteration of the test. + handlerSvc.store(originalMimeInfo); + DownloadIntegration.launchFile = oldLaunchFile; + let [download] = await publicList.getAll(); + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + await publicList.removeFinished(); + } +}); + +/** + * Check that the "Open with internal handler" option is presented + * for other viewable internally types. + */ +add_task( + async function test_internal_handler_hidden_with_viewable_internally_type() { + for (let [file, checkDefault] of [ + // The default for binary/octet-stream is changed by the PDF tests above, + // this may change given bug 1659008, so I'm just ignoring the default for now. + ["file_xml_attachment_binary_octet_stream.xml", false], + ["file_xml_attachment_test.xml", true], + ]) { + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + let internalHandlerRadio = doc.querySelector("#handleInternally"); + + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(doc); + + ok(!internalHandlerRadio.hidden, "The option should be visible for XML"); + if (checkDefault) { + ok(internalHandlerRadio.selected, "The option should be selected"); + } + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + } + } +); + +/** + * Check that the "Open with internal handler" option is not presented + * for non-PDF, non-viewable-internally types. + */ +add_task(async function test_internal_handler_hidden_with_other_type() { + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_txt_attachment_test.txt" + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(doc); + + let internalHandlerRadio = doc.querySelector("#handleInternally"); + ok( + internalHandlerRadio.hidden, + "The option should be hidden for unknown file type" + ); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); +}); + +/** + * Check that the "Open with internal handler" option is not presented + * when the feature is disabled for PDFs. + */ +add_task(async function test_internal_handler_hidden_with_pdf_pref_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.helperApps.showOpenOptionForPdfJS", false]], + }); + for (let file of [ + "file_pdf_application_pdf.pdf", + "file_pdf_binary_octet_stream.pdf", + ]) { + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + + await waitForAcceptButtonToGetEnabled(doc); + + let internalHandlerRadio = doc.querySelector("#handleInternally"); + ok( + internalHandlerRadio.hidden, + "The option should be hidden for PDF when the pref is false" + ); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + } +}); + +/** + * Check that the "Open with internal handler" option is not presented + * for other viewable internally types when disabled. + */ +add_task( + async function test_internal_handler_hidden_with_viewable_internally_pref_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.helperApps.showOpenOptionForViewableInternally", false]], + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_xml_attachment_test.xml" + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + + await waitForAcceptButtonToGetEnabled(doc); + + let internalHandlerRadio = doc.querySelector("#handleInternally"); + ok( + internalHandlerRadio.hidden, + "The option should be hidden for XML when the pref is false" + ); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + } +); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js new file mode 100644 index 0000000000..02cf6c3941 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that downloads started from a private window by clicking on a link end + * up in the global list of private downloads (see bug 1367581). + */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/Downloads.jsm", this); +ChromeUtils.import("resource://gre/modules/DownloadPaths.jsm", this); +ChromeUtils.import("resource://testing-common/FileTestUtils.jsm", this); +ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", this); + +add_task(async function test_setup() { + // Save downloads to disk without showing the dialog. + let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", { + QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]), + show(launcher) { + launcher.promptForSaveDestination(); + }, + promptForSaveToFileAsync(launcher) { + // The dialog should create the empty placeholder file. + let file = FileTestUtils.getTempFile(); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + launcher.saveDestinationAvailable(file); + }, + }); + registerCleanupFunction(() => { + MockRegistrar.unregister(cid); + }); +}); + +add_task(async function test_download_privatebrowsing() { + let privateList = await Downloads.getList(Downloads.PRIVATE); + let publicList = await Downloads.getList(Downloads.PUBLIC); + + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + try { + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + `data:text/html,<a download href="data:text/plain,">download</a>` + ); + + let promiseNextPrivateDownload = new Promise(resolve => { + privateList.addView({ + onDownloadAdded(download) { + privateList.removeView(this); + resolve(download); + }, + }); + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + content.document.querySelector("a").click(); + }); + + // Wait for the download to finish so the file can be safely deleted later. + let download = await promiseNextPrivateDownload; + await download.whenSucceeded(); + + // Clean up after checking that there are no new public downloads either. + let publicDownloads = await publicList.getAll(); + Assert.equal(publicDownloads.length, 0); + await privateList.removeFinished(); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js new file mode 100644 index 0000000000..ffab8146b6 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +registerCleanupFunction(() => MockFilePicker.cleanup()); + +/** + * Check downloading files URL-escapes content-disposition + * information when necessary. + */ +add_task(async function test_check_filename_urlescape() { + let pendingPromise; + let pendingTest = ""; + let expectedFileName = ""; + MockFilePicker.showCallback = function(fp) { + info(`${pendingTest} - Filepicker shown, checking filename`); + is( + fp.defaultString, + expectedFileName, + `${pendingTest} - Should have escaped filename` + ); + ok( + pendingPromise, + `${pendingTest} - Should have expected this picker open.` + ); + if (pendingPromise) { + pendingPromise.resolve(); + } + return Ci.nsIFilePicker.returnCancel; + }; + function runTestFor(fileName, selector) { + return BrowserTestUtils.withNewTab(TEST_PATH + fileName, async browser => { + expectedFileName = fileName; + let tabLabel = gBrowser.getTabForBrowser(browser).getAttribute("label"); + ok( + tabLabel.startsWith(fileName), + `"${tabLabel}" should have been escaped.` + ); + + pendingTest = "save browser"; + pendingPromise = PromiseUtils.defer(); + // First try to save the browser + saveBrowser(browser); + await pendingPromise.promise; + + // Next, try the context menu: + pendingTest = "save from context menu"; + pendingPromise = PromiseUtils.defer(); + let menu = document.getElementById("contentAreaContextMenu"); + let menuShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + BrowserTestUtils.synthesizeMouse( + selector, + 5, + 5, + { type: "contextmenu", button: 2 }, + browser + ); + await menuShown; + gContextMenu.saveMedia(); + menu.hidePopup(); + await pendingPromise.promise; + pendingPromise = null; + }); + } + await runTestFor("file_with@@funny_name.png", "img"); + await runTestFor("file_with[funny_name.webm", "video"); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_extension_correction.js b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js new file mode 100644 index 0000000000..b806ee9ace --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +let gPathsToRemove = []; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.download.useDownloadDir", true]], + }); + registerCleanupFunction(async () => { + for (let path of gPathsToRemove) { + // IOUtils.remove ignores non-existing files out of the box. + await IOUtils.remove(path); + } + let publicList = await Downloads.getList(Downloads.PUBLIC); + await publicList.removeFinished(); + }); +}); + +async function testLinkWithoutExtension(type, shouldHaveExtension) { + info("Checking " + type); + + let task = function() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [type], mimetype => { + let link = content.document.createElement("a"); + link.textContent = "Click me"; + link.href = "data:" + mimetype + ",hello"; + link.download = "somefile"; + content.document.body.appendChild(link); + link.click(); + }); + }; + await checkDownloadWithExtensionState(task, { type, shouldHaveExtension }); +} + +async function checkDownloadWithExtensionState( + task, + { type, shouldHaveExtension, expectedName = null } +) { + let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + + await task(); + + info("Waiting for dialog."); + let win = await winPromise; + + let actualName = win.document.getElementById("location").value; + if (shouldHaveExtension) { + expectedName ??= "somefile." + getMIMEInfoForType(type).primaryExtension; + is(actualName, expectedName, `${type} should get an extension`); + } else { + expectedName ??= "somefile"; + is(actualName, expectedName, `${type} should not get an extension`); + } + + let closedPromise = BrowserTestUtils.windowClosed(win); + + if (shouldHaveExtension) { + // Wait for the download. + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloadFinishedPromise = promiseDownloadFinished(publicList); + + // Then pick "save" in the dialog. + let dialog = win.document.getElementById("unknownContentType"); + win.document.getElementById("save").click(); + let button = dialog.getButton("accept"); + button.disabled = false; + dialog.acceptDialog(); + + // Wait for the download to finish and check the extension is correct. + let download = await downloadFinishedPromise; + is( + PathUtils.filename(download.target.path), + expectedName, + `Downloaded file should also match ${expectedName}` + ); + gPathsToRemove.push(download.target.path); + let pathToRemove = download.target.path; + // Avoid one file interfering with subsequent files. + await publicList.removeFinished(); + await IOUtils.remove(pathToRemove); + } else { + // We just cancel out for files that would end up without a path, as we'd + // prompt for a filename. + win.close(); + } + return closedPromise; +} + +/** + * Check that for document types, images, videos and audio files, + * we enforce a useful extension. + */ +add_task(async function test_enforce_useful_extension() { + await BrowserTestUtils.withNewTab("data:text/html,", async browser => { + await testLinkWithoutExtension("image/png", true); + await testLinkWithoutExtension("audio/ogg", true); + await testLinkWithoutExtension("video/webm", true); + await testLinkWithoutExtension("application/msword", true); + await testLinkWithoutExtension("application/pdf", true); + + await testLinkWithoutExtension("application/x-gobbledygook", false); + await testLinkWithoutExtension("application/octet-stream", false); + await testLinkWithoutExtension("binary/octet-stream", false); + await testLinkWithoutExtension("application/x-msdownload", false); + }); +}); + +/** + * Check that we still use URL extension info when we don't have anything else, + * despite bogus local info. + */ +add_task(async function test_broken_saved_handlerinfo_and_useless_mimetypes() { + let bogusType = getMIMEInfoForType("binary/octet-stream"); + bogusType.setFileExtensions(["jpg"]); + let handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + handlerSvc.store(bogusType); + let tabToClean = null; + let task = function() { + return BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_as.exe?foo=bar" + ).then(tab => { + return (tabToClean = tab); + }); + }; + await checkDownloadWithExtensionState(task, { + type: "binary/octet-stream", + shouldHaveExtension: true, + expectedName: "file_as.exe", + }); + // Downloads should really close their tabs... + if (tabToClean?.isConnected) { + BrowserTestUtils.removeTab(tabToClean); + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js new file mode 100644 index 0000000000..b6f401e5e1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(setupMailHandler); + +add_task(async function test_open_without_user_interaction() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.disable_open_during_load", true], + ["dom.block_external_protocol_in_iframes", true], + ["dom.delay.block_external_protocol_in_iframes.enabled", false], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.loadURI( + tab.linkedBrowser, + TEST_PATH + "file_external_protocol_iframe.html" + ); + + let dialog = await dialogWindowPromise; + ok(dialog, "Should show the dialog even without user interaction"); + + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + + // Adding another iframe without user interaction should be blocked. + let blockedWarning = new Promise(resolve => { + Services.console.registerListener(function onMessage(msg) { + let { message, logLevel } = msg; + if (logLevel != Ci.nsIConsoleMessage.warn) { + return; + } + if (!message.includes("Iframe with external protocol was blocked")) { + return; + } + Services.console.unregisterListener(onMessage); + resolve(); + }); + }); + + info("Adding another frame without user interaction"); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let frame = content.document.createElement("iframe"); + frame.src = "mailto:foo@baz.com"; + content.document.body.appendChild(frame); + }); + + await blockedWarning; + + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js new file mode 100644 index 0000000000..a36443edd1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js @@ -0,0 +1,255 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/Downloads.jsm", this); +const { DownloadIntegration } = ChromeUtils.import( + "resource://gre/modules/DownloadIntegration.jsm" +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +function waitForAcceptButtonToGetEnabled(doc) { + let dialog = doc.querySelector("#unknownContentType"); + let button = dialog.getButton("accept"); + return TestUtils.waitForCondition( + () => !button.disabled, + "Wait for Accept button to get enabled" + ); +} + +async function waitForPdfJS(browser, url) { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.eventBusDispatchToDOM", true]], + }); + // Runs tests after all "load" event handlers have fired off + let loadPromise = BrowserTestUtils.waitForContentEvent( + browser, + "documentloaded", + false, + null, + true + ); + await SpecialPowers.spawn(browser, [url], contentUrl => { + content.location = contentUrl; + }); + return loadPromise; +} + +add_task(async function setup() { + // Remove the security delay for the dialog during the test. + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.dialog_enable_delay", 0], + ["browser.helperApps.showOpenOptionForPdfJS", true], + ["browser.helperApps.showOpenOptionForViewableInternally", true], + ], + }); + + // Restore handlers after the whole test has run + const registerRestoreHandler = function(type, ext) { + const mimeInfo = gMimeSvc.getFromTypeAndExtension(type, ext); + const existed = gHandlerSvc.exists(mimeInfo); + registerCleanupFunction(() => { + if (existed) { + gHandlerSvc.store(mimeInfo); + } else { + gHandlerSvc.remove(mimeInfo); + } + }); + }; + registerRestoreHandler("application/pdf", "pdf"); +}); + +const { handleInternally, saveToDisk, useSystemDefault } = Ci.nsIHandlerInfo; + +const kTestCases = [ + { + description: + "Saving to disk when internal handling is the default shouldn't change prefs.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: false }, + dialogActions(doc) { + let saveItem = doc.querySelector("#save"); + saveItem.click(); + ok(saveItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: false, + expectedPreferredAction: handleInternally, + expectedAsk: false, + }, + { + description: + "Opening externally when internal handling is the default shouldn't change prefs.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: false }, + dialogActions(doc) { + let openItem = doc.querySelector("#open"); + openItem.click(); + ok(openItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: true, + expectedPreferredAction: handleInternally, + expectedAsk: false, + }, + { + description: + "Saving to disk when internal handling is the default *should* change prefs if checkbox is ticked.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: false }, + dialogActions(doc) { + let saveItem = doc.querySelector("#save"); + saveItem.click(); + ok(saveItem.selected, "The 'save' option should now be selected"); + let checkbox = doc.querySelector("#rememberChoice"); + checkbox.checked = true; + checkbox.doCommand(); + }, + expectTab: false, + expectLaunch: false, + expectedPreferredAction: saveToDisk, + expectedAsk: false, + }, + { + description: + "Saving to disk when asking is the default should change persisted default.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: true }, + dialogActions(doc) { + let saveItem = doc.querySelector("#save"); + saveItem.click(); + ok(saveItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: false, + expectedPreferredAction: saveToDisk, + expectedAsk: true, + }, + { + description: + "Opening externally when asking is the default should change persisted default.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: true }, + dialogActions(doc) { + let openItem = doc.querySelector("#open"); + openItem.click(); + ok(openItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: true, + expectedPreferredAction: useSystemDefault, + expectedAsk: true, + }, +]; + +function ensureMIMEState({ preferredAction, alwaysAsk }) { + const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + mimeInfo.preferredAction = preferredAction; + mimeInfo.alwaysAskBeforeHandling = alwaysAsk; + gHandlerSvc.store(mimeInfo); +} + +/** + * Test that if we have PDFs set to handle internally, and the user chooses to + * do something else with it, we do not alter the saved state. + */ +add_task(async function test_check_saving_handler_choices() { + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + for (let testCase of kTestCases) { + let file = "file_pdf_application_pdf.pdf"; + info("Testing with " + file + "; " + testCase.description); + ensureMIMEState(testCase.preDialogState); + + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + let internalHandlerRadio = doc.querySelector("#handleInternally"); + + await waitForAcceptButtonToGetEnabled(doc); + + ok(!internalHandlerRadio.hidden, "The option should be visible for PDF"); + ok( + internalHandlerRadio.selected, + "The Firefox option should be selected by default" + ); + + const { expectTab, expectLaunch, description } = testCase; + // Prep to intercept things so we only see the results we want. + let tabOpenListener = ev => { + ok( + expectTab, + `A new tab should ${expectTab ? "" : "not "}be opened - ${description}` + ); + BrowserTestUtils.removeTab(ev.target); + }; + gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener); + + let oldLaunchFile = DownloadIntegration.launchFile; + let fileLaunched = PromiseUtils.defer(); + DownloadIntegration.launchFile = () => { + ok( + expectLaunch, + `The file should ${ + expectLaunch ? "" : "not " + }be launched with an external application - ${description}` + ); + fileLaunched.resolve(); + }; + let downloadFinishedPromise = promiseDownloadFinished(publicList); + + await testCase.dialogActions(doc); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.acceptDialog(); + + let download = await downloadFinishedPromise; + if (expectLaunch) { + await fileLaunched.promise; + } + DownloadIntegration.launchFile = oldLaunchFile; + gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener); + + is( + (await publicList.getAll()).length, + 1, + "download should appear in public list" + ); + + // Check mime info: + const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + gHandlerSvc.fillHandlerInfo(mimeInfo, ""); + is( + mimeInfo.preferredAction, + testCase.expectedPreferredAction, + "preferredAction - " + description + ); + is( + mimeInfo.alwaysAskBeforeHandling, + testCase.expectedAsk, + "alwaysAsk - " + description + ); + + BrowserTestUtils.removeTab(loadingTab); + await publicList.removeFinished(); + if (download?.target.exists) { + try { + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js new file mode 100644 index 0000000000..5f60e39bb1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js @@ -0,0 +1,398 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const CONTENT_HANDLING_URL = + "chrome://mozapps/content/handling/appChooser.xhtml"; + +add_task(setupMailHandler); + +/** + * Check that if we open the protocol handler dialog from a subframe, we close + * it when closing the tab. + */ +add_task(async function test_closed_by_tab_closure() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_nested_protocol_request.html" + ); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed."); +}); + +/** + * Check that if we open the protocol handler dialog from a subframe, we close + * it when navigating the tab to a non-same-origin URL. + */ +add_task(async function test_closed_by_tab_navigation() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_nested_protocol_request.html" + ); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + info( + "Set up unload handler to ensure we don't break when the window global gets cleared" + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + content.addEventListener("unload", function() {}); + }); + + info("Navigating tab to a different but same origin page."); + BrowserTestUtils.loadURI(tab.linkedBrowser, TEST_PATH); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, TEST_PATH); + ok(dialog._frame.contentWindow, "Dialog should stay open."); + + // The use of weak references in various parts of the code means that we're + // susceptible to dropping crucial bits of our implementation on the floor, + // if they get GC'd, and then the test hangs. + // Do a bunch of GC/CC runs so that if we ever break, it's deterministic. + let numCycles = 3; + for (let i = 0; i < numCycles; i++) { + Cu.forceGC(); + Cu.forceCC(); + } + + info("Now navigate to a cross-origin page."); + const CROSS_ORIGIN_TEST_PATH = TEST_PATH.replace(".com", ".org"); + BrowserTestUtils.loadURI(tab.linkedBrowser, CROSS_ORIGIN_TEST_PATH); + let loadPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + CROSS_ORIGIN_TEST_PATH + ); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed."); + + // Avoid errors from aborted loads by waiting for it to finish. + await loadPromise; + gBrowser.removeTab(tab); +}); + +/** + * Check that we cannot open more than one of these dialogs. + */ +add_task(async function test_multiple_dialogs() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_nested_protocol_request.html" + ); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + + // Navigate the parent frame: + await ContentTask.spawn(tab.linkedBrowser, [], () => + content.eval("location.href = 'mailto:help@example.com'") + ); + + // Wait for a few ticks: + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 100)); + // Check we only have one dialog + + let tabDialogBox = gBrowser.getTabDialogBox(tab.linkedBrowser); + let dialogs = tabDialogBox + .getTabDialogManager() + ._dialogs.filter(d => d._openedURL == CONTENT_HANDLING_URL); + + is(dialogs.length, 1, "Should only have 1 dialog open"); + + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + dialog = await dialogClosedPromise; + + ok(!dialog._openedURL, "The dialog should have been closed."); + + // Then reopen the dialog again, to make sure we don't keep blocking: + dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Second dialog URL is as expected" + ); + + dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed again."); +}); + +/** + * Check that navigating invisible frames to external-proto URLs + * is handled correctly. + */ +add_task(async function invisible_iframes() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + // Ensure we notice the dialog opening: + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + let frame = content.document.createElement("iframe"); + frame.style.display = "none"; + frame.src = "mailto:help@example.com"; + content.document.body.append(frame); + }); + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog opens as expected for invisible iframe" + ); + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + await dialogClosedPromise; + gBrowser.removeTab(tab); +}); + +/** + * Check that nested iframes are handled correctly. + */ +add_task(async function nested_iframes() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + // Ensure we notice the dialog opening: + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + let innerLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + "https://example.org/" + ); + info("Constructing top frame"); + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + let frame = content.document.createElement("iframe"); + frame.src = "https://example.org/"; // cross-origin frame. + content.document.body.prepend(frame); + + content.eval( + `window.addEventListener("message", e => e.source.location = "mailto:help@example.com");` + ); + }); + + await innerLoaded; + let parentBC = tab.linkedBrowser.browsingContext; + + info("Creating innermost frame"); + await SpecialPowers.spawn(parentBC.children[0], [], async function() { + let innerFrame = content.document.createElement("iframe"); + let frameLoaded = ContentTaskUtils.waitForEvent(innerFrame, "load", true); + content.document.body.prepend(innerFrame); + await frameLoaded; + }); + + info("Posting event from innermost frame"); + await SpecialPowers.spawn( + parentBC.children[0].children[0], + [], + async function() { + // Top browsing context needs reference to the innermost, which is cross origin. + content.eval("top.postMessage('hello', '*')"); + } + ); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog opens as expected for deeply nested cross-origin iframe" + ); + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + await dialogClosedPromise; + gBrowser.removeTab(tab); +}); + +add_task(async function test_oop_iframe() { + const URI = `data:text/html,<div id="root"><iframe src="http://example.com/document-builder.sjs?html=<a href='mailto:help@example.com'>Mail it</a>"></iframe></div>`; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed."); +}); + +/** + * Check that a cross-origin iframe can navigate the top frame + * to an external protocol. + */ +add_task(async function xorigin_iframe_can_navigate_top() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + // Ensure we notice the dialog opening: + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + let innerLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + "https://example.org/" + ); + info("Constructing frame"); + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + let frame = content.document.createElement("iframe"); + frame.src = "https://example.org/"; // cross-origin frame. + content.document.body.prepend(frame); + }); + await innerLoaded; + + info("Navigating top bc from frame"); + let parentBC = tab.linkedBrowser.browsingContext; + await SpecialPowers.spawn(parentBC.children[0], [], async function() { + content.eval("window.top.location.href = 'mailto:example@example.com';"); + }); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog opens as expected for navigating the top frame from an x-origin frame." + ); + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + await dialogClosedPromise; + gBrowser.removeTab(tab); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js new file mode 100644 index 0000000000..7050616dc3 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js @@ -0,0 +1,754 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm", + this +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +// Testing multiple protocol / origin combinations takes long on debug. +requestLongerTimeout(3); + +const DIALOG_URL_APP_CHOOSER = + "chrome://mozapps/content/handling/appChooser.xhtml"; +const DIALOG_URL_PERMISSION = + "chrome://mozapps/content/handling/permissionDialog.xhtml"; + +const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler"; +const PERMISSION_KEY_DELIMITER = "^"; + +const TEST_PROTOS = ["foo", "bar"]; + +let testDir = getChromeDir(getResolvedURI(gTestPath)); + +const ORIGIN1 = "https://example.com"; +const ORIGIN2 = "https://example.org"; +const ORIGIN3 = Services.io.newFileURI(testDir).spec; +const PRINCIPAL1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + ORIGIN1 +); +const PRINCIPAL2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + ORIGIN2 +); +const PRINCIPAL3 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + ORIGIN3 +); + +let testExtension; + +/** + * Get the open protocol handler permission key for a given protocol scheme. + * @param {string} aProtocolScheme - Scheme of protocol to construct permission + * key with. + */ +function getSkipProtoDialogPermissionKey(aProtocolScheme) { + return ( + PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + aProtocolScheme + ); +} + +/** + * Creates dummy web protocol handlers used for testing. + */ +function initTestHandlers() { + TEST_PROTOS.forEach(scheme => { + let webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + webHandler.name = scheme + "Handler"; + webHandler.uriTemplate = ORIGIN1 + "/?url=%s"; + + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(scheme); + handlerInfo.possibleApplicationHandlers.appendElement(webHandler); + handlerInfo.preferredApplicationHandler = webHandler; + + gHandlerService.store(handlerInfo); + }); +} + +/** + * Update whether the protocol handler dialog is shown for our test protocol + + * handler. + * @param {string} scheme - Scheme of the protocol to change the ask state for. + * @param {boolean} ask - true => show dialog, false => skip dialog. + */ +function updateAlwaysAsk(scheme, ask) { + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme); + handlerInfo.alwaysAskBeforeHandling = ask; + gHandlerService.store(handlerInfo); +} + +/** + * Test whether the protocol handler dialog is set to show for our + * test protocol + handler. + * @param {string} scheme - Scheme of the protocol to test the ask state for. + * @param {boolean} ask - true => show dialog, false => skip dialog. + */ +function testAlwaysAsk(scheme, ask) { + is( + HandlerServiceTestUtils.getHandlerInfo(scheme).alwaysAskBeforeHandling, + ask, + "Should have correct alwaysAsk state." + ); +} + +/** + * Open a test URL with the desired scheme. + * By default the load is triggered by the content principal of the browser. + * @param {MozBrowser} browser - Browser to load the test URL in. + * @param {string} scheme - Scheme of the test URL. + * @param {Object} [opts] - Options for the triggering principal. + * @param {nsIPrincipal} [opts.triggeringPrincipal] - Principal to trigger the + * load with. Defaults to the browsers content principal. + * @param {boolean} [opts.useNullPrincipal] - If true, we will trigger the load + * with a null principal. + * @param {boolean} [opts.useExtensionPrincipal] - If true, we will trigger the + * load with an extension. + * @param {boolean} [opts.omitTriggeringPrincipal] - If true, we will directly + * call the protocol handler dialogs without a principal. + */ +async function triggerOpenProto( + browser, + scheme, + { + triggeringPrincipal = browser.contentPrincipal, + useNullPrincipal = false, + useExtensionPrincipal = false, + omitTriggeringPrincipal = false, + } = {} +) { + let uri = `${scheme}://test`; + + if (useNullPrincipal) { + // Create and load iframe with data URI. + // This will be a null principal. + ContentTask.spawn(browser, { uri }, args => { + let frame = content.document.createElement("iframe"); + frame.src = `data:text/html,<script>location.href="${args.uri}"</script>`; + content.document.body.appendChild(frame); + }); + return; + } + + if (useExtensionPrincipal) { + const EXTENSION_DATA = { + manifest: { + content_scripts: [ + { + matches: [browser.currentURI.spec], + js: ["navigate.js"], + }, + ], + }, + files: { + "navigate.js": `window.location.href = "${uri}";`, + }, + }; + + testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await testExtension.startup(); + return; + } + + if (omitTriggeringPrincipal) { + // Directly call ContentDispatchChooser without a triggering principal + let contentDispatchChooser = Cc[ + "@mozilla.org/content-dispatch-chooser;1" + ].createInstance(Ci.nsIContentDispatchChooser); + + let handler = HandlerServiceTestUtils.getHandlerInfo(scheme); + + contentDispatchChooser.handleURI( + handler, + Services.io.newURI(uri), + null, + browser.browsingContext + ); + return; + } + + info("Loading uri: " + uri); + browser.loadURI(uri, { triggeringPrincipal }); +} + +/** + * Navigates to a test URL with the given protocol scheme and waits for the + * result. + * @param {MozBrowser} browser - Browser to navigate. + * @param {string} scheme - Scheme of the test url. e.g. irc + * @param {Object} [options] - Test options. + * @param {Object} [options.permDialogOptions] - Test options for the permission + * dialog. If defined, we expect this dialog to be shown. + * @param {Object} [options.chooserDialogOptions] - Test options for the chooser + * dialog. If defined, we expect this dialog to be shown. + * @param {Object} [options.loadOptions] - Options for triggering the protocol + * load which causes the dialog to show. + * @param {nsIPrincipal} [options.triggeringPrincipal] - Principal to trigger + * the load with. Defaults to the browsers content principal. + * @returns {Promise} - A promise which resolves once the test is complete. + */ +async function testOpenProto( + browser, + scheme, + { permDialogOptions, chooserDialogOptions, loadOptions } = {} +) { + let permDialogOpenPromise; + let chooserDialogOpenPromise; + + if (permDialogOptions) { + info("Should see permission dialog"); + permDialogOpenPromise = waitForProtocolPermissionDialog(browser, true); + } + + if (chooserDialogOptions) { + info("Should see chooser dialog"); + chooserDialogOpenPromise = waitForProtocolAppChooserDialog(browser, true); + } + await triggerOpenProto(browser, scheme, loadOptions); + let webHandlerLoadedPromise; + + let webHandlerShouldOpen = + (!permDialogOptions && !chooserDialogOptions) || + ((permDialogOptions?.actionConfirm || permDialogOptions?.actionChangeApp) && + chooserDialogOptions?.actionConfirm); + + // Register web handler load listener if we expect to trigger it. + if (webHandlerShouldOpen) { + webHandlerLoadedPromise = waitForHandlerURL(browser, scheme); + } + + if (permDialogOpenPromise) { + let dialog = await permDialogOpenPromise; + let dialogEl = getDialogElementFromSubDialog(dialog); + let dialogType = getDialogType(dialog); + + let { + hasCheckbox, + hasChangeApp, + chooserIsNext, + actionCheckbox, + actionConfirm, + actionChangeApp, + } = permDialogOptions; + + if (actionChangeApp) { + actionConfirm = false; + } + + await testCheckbox(dialogEl, dialogType, { + hasCheckbox, + actionCheckbox, + }); + + // Check the button label depending on whether we would show the chooser + // dialog next or directly open the handler. + let acceptBtnLabel = dialogEl.getButton("accept")?.label; + if (chooserIsNext) { + is( + acceptBtnLabel, + "Choose Application", + "Accept button has choose app label" + ); + } else { + is(acceptBtnLabel, "Open Link", "Accept button has open link label"); + } + + let changeAppLink = dialogEl.ownerDocument.getElementById("change-app"); + if (typeof hasChangeApp == "boolean") { + ok(changeAppLink, "Permission dialog should have changeApp link label"); + is( + !changeAppLink.hidden, + hasChangeApp, + "Permission dialog change app link label" + ); + } + + if (actionChangeApp) { + let dialogClosedPromise = waitForProtocolPermissionDialog(browser, false); + changeAppLink.click(); + await dialogClosedPromise; + } else { + await closeDialog(browser, dialog, actionConfirm, scheme); + } + } + + if (chooserDialogOpenPromise) { + let dialog = await chooserDialogOpenPromise; + let dialogEl = getDialogElementFromSubDialog(dialog); + let dialogType = getDialogType(dialog); + + let { hasCheckbox, actionCheckbox, actionConfirm } = chooserDialogOptions; + + await testCheckbox(dialogEl, dialogType, { + hasCheckbox, + actionCheckbox, + }); + + await closeDialog(browser, dialog, actionConfirm, scheme); + } + + if (webHandlerShouldOpen) { + info("Waiting for web handler to open"); + await webHandlerLoadedPromise; + } else { + info("Web handler open canceled"); + } + + // Clean up test extension if needed. + await testExtension?.unload(); +} + +/** + * Inspects the checkbox state and interacts with it. + * @param {dialog} dialogEl + * @param {string} dialogType - String identifier of dialog type. + * Either "permission" or "chooser". + * @param {Object} options - Test Options. + * @param {boolean} [options.hasCheckbox] - Whether the dialog is expected to + * have a visible checkbox. + * @param {boolean} [options.hasCheckboxState] - The check state of the checkbox + * to test for. true = checked, false = unchecked. + * @param {boolean} [options.actionCheckbox] - The state to set on the checkbox. + * true = checked, false = unchecked. + */ +async function testCheckbox( + dialogEl, + dialogType, + { hasCheckbox, hasCheckboxState = false, actionCheckbox } +) { + let checkbox = dialogEl.ownerDocument.getElementById("remember"); + if (typeof hasCheckbox == "boolean") { + let hiddenEl; + if (dialogType == "permission") { + hiddenEl = checkbox.parentElement; + } else { + hiddenEl = checkbox; + } + is( + checkbox && !hiddenEl.hidden, + hasCheckbox, + "Dialog checkbox has correct visibility." + ); + } + + if (typeof hasCheckboxState == "boolean") { + is(checkbox.checked, hasCheckboxState, "Dialog checkbox has correct state"); + } + + if (typeof actionCheckbox == "boolean") { + checkbox.focus(); + await EventUtils.synthesizeKey("VK_SPACE", undefined, dialogEl.ownerWindow); + } +} + +/** + * Get the dialog element which is a child of the SubDialogs browser frame. + * @param {SubDialog} subDialog - Dialog to get the dialog element for. + */ +function getDialogElementFromSubDialog(subDialog) { + let dialogEl = subDialog._frame.contentDocument.querySelector("dialog"); + ok(dialogEl, "SubDialog should have dialog element"); + return dialogEl; +} + +/** + * Wait for the test handler to be opened. + * @param {MozBrowser} browser - The browser the load should occur in. + * @param {string} scheme - Scheme which triggered the handler to open. + */ +function waitForHandlerURL(browser, scheme) { + return BrowserTestUtils.browserLoaded( + browser, + false, + url => url == `${ORIGIN1}/?url=${scheme}%3A%2F%2Ftest` + ); +} + +/** + * Test for open-protocol-handler permission. + * @param {nsIPrincipal} principal - The principal to test the permission on. + * @param {string} scheme - Scheme to generate permission key. + * @param {boolean} hasPerm - Whether we expect the princial to set the + * permission (true), or not (false). + */ +function testPermission(principal, scheme, hasPerm) { + let permKey = getSkipProtoDialogPermissionKey(scheme); + let result = Services.perms.testPermissionFromPrincipal(principal, permKey); + let message = `${permKey} ${hasPerm ? "is" : "is not"} set for ${ + principal.origin + }.`; + is(result == Services.perms.ALLOW_ACTION, hasPerm, message); +} + +/** + * Get the checkbox element of the dialog used to remember the handler choice or + * store the permission. + * @param {SubDialog} dialog - Protocol handler dialog embedded in a SubDialog. + * @param {string} dialogType - Type of the dialog which holds the checkbox. + * @returns {HTMLInputElement} - Checkbox of the dialog. + */ +function getDialogCheckbox(dialog, dialogType) { + let id; + if (dialogType == "permission") { + id = "remember-permission"; + } else { + id = "remember"; + } + return dialog._frame.contentDocument.getElementById(id); +} + +function getDialogType(dialog) { + let url = dialog._frame.currentURI.spec; + + if (url === DIALOG_URL_PERMISSION) { + return "permission"; + } + if (url === DIALOG_URL_APP_CHOOSER) { + return "chooser"; + } + throw new Error("Dialog with unexpected url"); +} + +/** + * Exit a protocol handler SubDialog and wait for it to be fully closed. + * @param {MozBrowser} browser - Browser element of the tab where the dialog is + * shown. + * @param {SubDialog} dialog - SubDialog object which holds the protocol handler + * @param {boolean} confirm - Whether to confirm (true) or cancel (false) the + * dialog. + * @param {string} scheme - The scheme of the protocol the dialog is opened for. + * dialog. + */ +async function closeDialog(browser, dialog, confirm, scheme) { + let dialogClosedPromise = waitForSubDialog(browser, dialog._openedURL, false); + let dialogEl = getDialogElementFromSubDialog(dialog); + + if (confirm) { + if (getDialogType(dialog) == "chooser") { + // Select our test protocol handler + let listItem = dialogEl.ownerDocument.querySelector( + `richlistitem[name="${scheme}Handler"]` + ); + listItem.click(); + } + + dialogEl.setAttribute("buttondisabledaccept", false); + dialogEl.acceptDialog(); + } else { + dialogEl.cancelDialog(); + } + + return dialogClosedPromise; +} + +registerCleanupFunction(function() { + // Clean up test handlers + TEST_PROTOS.forEach(scheme => { + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme); + gHandlerService.remove(handlerInfo); + }); + + // Clear permissions + Services.perms.removeAll(); +}); + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", true]], + }); + initTestHandlers(); +}); + +/** + * Tests that when "remember" is unchecked, we only allow the protocol to be + * opened once and don't store any permission. + */ +add_task(async function test_permission_allow_once() { + for (let scheme of TEST_PROTOS) { + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // No permission should be set + testPermission(PRINCIPAL1, scheme, false); + testPermission(PRINCIPAL2, scheme, false); + + // No preferred app should be set + testAlwaysAsk(scheme, true); + + // If we open again we should see the permission dialog + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: false, + }, + }); + }); + } +}); + +/** + * Tests that when checking the "remember" checkbox, the protocol permission + * is set correctly and allows the caller to skip the permission dialog in + * subsequent calls. + */ +add_task(async function test_permission_allow_persist() { + for (let [origin, principal] of [ + [ORIGIN1, PRINCIPAL1], + [ORIGIN3, PRINCIPAL3], + ]) { + for (let scheme of TEST_PROTOS) { + info("Testing with origin " + origin); + info("testing with principal of origin " + principal.origin); + info("testing with protocol " + scheme); + + // Set a permission for an unrelated protocol. + // We should still see the permission dialog. + Services.perms.addFromPrincipal( + principal, + getSkipProtoDialogPermissionKey("foobar"), + Services.perms.ALLOW_ACTION + ); + + await BrowserTestUtils.withNewTab(origin, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionCheckbox: true, + actionConfirm: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // Permission should be set + testPermission(principal, scheme, true); + testPermission(PRINCIPAL2, scheme, false); + + // No preferred app should be set + testAlwaysAsk(scheme, true); + + // If we open again with the origin where we granted permission, we should + // directly get the chooser dialog. + await BrowserTestUtils.withNewTab(origin, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, + }, + }); + }); + + // If we open with the other origin, we should see the permission dialog + await BrowserTestUtils.withNewTab(ORIGIN2, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: false, + }, + }); + }); + + // Cleanup permissions + Services.perms.removeAll(); + } + } +}); + +/** + * Tests that if a preferred protocol handler is set, the permission dialog + * shows the application name and a link which leads to the app chooser. + */ +add_task(async function test_permission_application_set() { + let scheme = TEST_PROTOS[0]; + updateAlwaysAsk(scheme, false); + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: true, + chooserIsNext: false, + actionChangeApp: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // Cleanup + updateAlwaysAsk(scheme, true); +}); + +/** + * Tests that we correctly handle system principals. They should always + * skip the permission dialog. + */ +add_task(async function test_permission_system_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { hasCheckbox: true, actionConfirm: false }, + loadOptions: { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }, + }); + }); +}); + +/** + * Tests that we don't show the permission dialog if the permission is disabled + * by pref. + */ +add_task(async function test_permission_disabled() { + let scheme = TEST_PROTOS[0]; + + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", false]], + }); + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + await SpecialPowers.popPrefEnv(); +}); + +/** + * Tests that we directly open the handler if permission and handler are set. + */ +add_task(async function test_app_and_permission_set() { + let scheme = TEST_PROTOS[1]; + + updateAlwaysAsk(scheme, false); + Services.perms.addFromPrincipal( + PRINCIPAL2, + getSkipProtoDialogPermissionKey(scheme), + Services.perms.ALLOW_ACTION + ); + + await BrowserTestUtils.withNewTab(ORIGIN2, async browser => { + await testOpenProto(browser, scheme); + }); + + // Cleanup + Services.perms.removeAll(); + updateAlwaysAsk(scheme, true); +}); + +/** + * Tests that the alwaysAsk state is not updated if the user cancels the dialog + */ +add_task(async function test_change_app_checkbox_cancel() { + let scheme = TEST_PROTOS[0]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionCheckbox: true, // Activate checkbox + actionConfirm: false, // Cancel dialog + }, + }); + }); + + // Should not have applied value from checkbox + testAlwaysAsk(scheme, true); +}); + +/** + * Tests that the external protocol dialogs behave correctly when a null + * principal is passed. + */ +add_task(async function test_null_principal() { + let scheme = TEST_PROTOS[0]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + loadOptions: { + useNullPrincipal: true, + }, + permDialogOptions: { + hasCheckbox: false, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Tests that the external protocol dialogs behave correctly when no principal + * is passed. + */ +add_task(async function test_no_principal() { + let scheme = TEST_PROTOS[1]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + loadOptions: { + omitTriggeringPrincipal: true, + }, + permDialogOptions: { + hasCheckbox: false, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Tests that we skip the permission dialog for extension callers. + */ +add_task(async function test_extension_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + loadOptions: { + useExtensionPrincipal: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js new file mode 100644 index 0000000000..b9ba4a7955 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_helperapp() { + // Set up the test infrastructure: + const kProt = "foopydoopydoo"; + const extProtocolSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + let handlerInfo = extProtocolSvc.getProtocolHandlerInfo(kProt); + if (handlerSvc.exists(handlerInfo)) { + handlerSvc.fillHandlerInfo(handlerInfo, ""); + } + // Say we want to use a specific app: + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.alwaysAskBeforeHandling = false; + + // Say it's us: + let selfFile = Services.dirsvc.get("XREExeF", Ci.nsIFile); + // Make sure it's the .app + if (AppConstants.platform == "macosx") { + while ( + !selfFile.leafName.endsWith(".app") && + !selfFile.leafName.endsWith(".app/") + ) { + selfFile = selfFile.parent; + } + } + let selfHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + selfHandlerApp.executable = selfFile; + handlerInfo.possibleApplicationHandlers.appendElement(selfHandlerApp); + handlerInfo.preferredApplicationHandler = selfHandlerApp; + handlerSvc.store(handlerInfo); + + await BrowserTestUtils.withNewTab("about:blank", async browser => { + // Now, do some safety stubbing. If we do end up recursing we spawn + // infinite tabs. We definitely don't want that. Avoid it by stubbing + // our external URL handling bits: + let oldAddTab = gBrowser.addTab; + registerCleanupFunction( + () => (gBrowser.addTab = gBrowser.loadOneTab = oldAddTab) + ); + let wrongThingHappenedPromise = new Promise(resolve => { + gBrowser.addTab = gBrowser.loadOneTab = function(aURI) { + ok(false, "Tried to open unexpected URL in a tab: " + aURI); + resolve(null); + // Pass a dummy object to avoid upsetting BrowserContentHandler - + // if it thinks opening the tab failed, it tries to open a window instead, + // which we can't prevent as easily, and at which point we still end up + // with runaway tabs. + return {}; + }; + }); + + let askedUserPromise = waitForProtocolAppChooserDialog(browser, true); + + BrowserTestUtils.loadURI(browser, kProt + ":test"); + let dialog = await Promise.race([ + wrongThingHappenedPromise, + askedUserPromise, + ]); + ok(dialog, "Should have gotten a dialog"); + + let closePromise = waitForProtocolAppChooserDialog(browser, false); + dialog.close(); + await closePromise; + askedUserPromise = null; + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js new file mode 100644 index 0000000000..28bd50a120 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js @@ -0,0 +1,61 @@ +add_task(async function() { + // create mocked objects + let launcher = createMockedObjects(true); + + // open helper app dialog with mocked launcher + let dlg = await openHelperAppDialog(launcher); + + let doc = dlg.document; + let dialogElement = doc.getElementById("unknownContentType"); + + // Set remember choice + ok( + !doc.getElementById("rememberChoice").checked, + "Remember choice checkbox should be not checked." + ); + doc.getElementById("rememberChoice").checked = true; + + // Make sure the mock handler information is not in nsIHandlerService + ok( + !gHandlerSvc.exists(launcher.MIMEInfo), + "Should not be in nsIHandlerService." + ); + + // close the dialog by pushing the ok button. + let dialogClosedPromise = BrowserTestUtils.windowClosed(dlg); + // Make sure the ok button is enabled, since the ok button might be disabled by + // EnableDelayHelper mechanism. Please refer the detailed + // https://searchfox.org/mozilla-central/source/toolkit/components/prompts/src/SharedPromptUtils.jsm#53 + dialogElement.getButton("accept").disabled = false; + dialogElement.acceptDialog(); + await dialogClosedPromise; + + // check the mocked handler information is saved in nsIHandlerService + ok(gHandlerSvc.exists(launcher.MIMEInfo), "Should be in nsIHandlerService."); + // check the extension. + var mimeType = gHandlerSvc.getTypeFromExtension("abc"); + is(mimeType, launcher.MIMEInfo.type, "Got correct mime type."); + for (let handlerInfo of gHandlerSvc.enumerate()) { + if (handlerInfo.type == launcher.MIMEInfo.type) { + // check the alwaysAskBeforeHandling + ok( + !handlerInfo.alwaysAskBeforeHandling, + "Should turn off the always ask." + ); + // check the preferredApplicationHandler + ok( + handlerInfo.preferredApplicationHandler.equals( + launcher.MIMEInfo.preferredApplicationHandler + ), + "Should be equal to the mockedHandlerApp." + ); + // check the perferredAction + is( + handlerInfo.preferredAction, + launcher.MIMEInfo.preferredAction, + "Should be equal to Ci.nsIHandlerInfo.useHelperApp." + ); + break; + } + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js new file mode 100644 index 0000000000..66f461834c --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js @@ -0,0 +1,124 @@ +let testURL = + "https://example.com/browser/" + + "uriloader/exthandler/tests/mochitest/protocolHandler.html"; + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", false]], + }); + + // Load a page registering a protocol handler. + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.loadURI(browser, testURL); + await BrowserTestUtils.browserLoaded(browser, false, testURL); + + // Register the protocol handler by clicking the notificationbar button. + let notificationValue = "Protocol Registration: web+testprotocol"; + let getNotification = () => + gBrowser.getNotificationBox().getNotificationWithValue(notificationValue); + await BrowserTestUtils.waitForCondition(getNotification); + let notification = getNotification(); + let button = notification.querySelector("button"); + ok(button, "got registration button"); + button.click(); + + // Set the new handler as default. + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol"); + is( + protoInfo.preferredAction, + protoInfo.useHelperApp, + "using a helper application is the preferred action" + ); + ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set"); + let handlers = protoInfo.possibleApplicationHandlers; + is(1, handlers.length, "only one handler registered for web+testprotocol"); + let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp); + ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler"); + is( + handler.uriTemplate, + "https://example.com/foobar?uri=%s", + "correct url template" + ); + protoInfo.preferredApplicationHandler = handler; + protoInfo.alwaysAskBeforeHandling = false; + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + handlerSvc.store(protoInfo); + + const expectedURL = + "https://example.com/foobar?uri=web%2Btestprotocol%3Atest"; + + // Create a framed link: + await SpecialPowers.spawn(browser, [], async function() { + let iframe = content.document.createElement("iframe"); + iframe.src = `data:text/html,<a href="web+testprotocol:test">Click me</a>`; + content.document.body.append(iframe); + // Can't return this promise because it resolves to the event object. + await ContentTaskUtils.waitForEvent(iframe, "load"); + iframe.contentDocument.querySelector("a").click(); + }); + let kidContext = browser.browsingContext.children[0]; + await TestUtils.waitForCondition(() => { + let spec = kidContext.currentWindowGlobal?.documentURI?.spec || ""; + return spec == expectedURL; + }); + is( + kidContext.currentWindowGlobal.documentURI.spec, + expectedURL, + "Should load in frame." + ); + + // Middle-click a testprotocol link and check the new tab is correct + let link = "#link"; + + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL); + await BrowserTestUtils.synthesizeMouseAtCenter(link, { button: 1 }, browser); + let tab = await promiseTabOpened; + gBrowser.selectedTab = tab; + is( + gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + BrowserTestUtils.removeTab(tab); + + // Shift-click the testprotocol link and check the new window. + let newWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: expectedURL, + }); + await BrowserTestUtils.synthesizeMouseAtCenter( + link, + { shiftKey: true }, + browser + ); + let win = await newWindowPromise; + await BrowserTestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == expectedURL + ); + is( + win.gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + await BrowserTestUtils.closeWindow(win); + + // Click the testprotocol link and check the url in the current tab. + let loadPromise = BrowserTestUtils.browserLoaded(browser); + await BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser); + await loadPromise; + await BrowserTestUtils.waitForCondition(() => gURLBar.value != testURL); + is( + gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + + // Cleanup. + protoInfo.preferredApplicationHandler = null; + handlers.removeElementAt(0); + handlerSvc.store(protoInfo); +}); diff --git a/uriloader/exthandler/tests/mochitest/download.bin b/uriloader/exthandler/tests/mochitest/download.bin new file mode 100644 index 0000000000..0e4b0c7bae --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download.bin @@ -0,0 +1 @@ +abc123 diff --git a/uriloader/exthandler/tests/mochitest/download.sjs b/uriloader/exthandler/tests/mochitest/download.sjs new file mode 100644 index 0000000000..bee7bd7015 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download.sjs @@ -0,0 +1,38 @@ +"use strict"; + +Cu.import("resource://gre/modules/Timer.jsm"); + +function actuallyHandleRequest(req, res) { + res.setHeader("Content-Type", "application/octet-stream", false); + res.write("abc123"); + res.finish(); +} + +function handleRequest(req, res) { + if (req.queryString.includes('finish')) { + res.write("OK"); + let downloadReq = null; + getObjectState("downloadReq", o => { downloadReq = o }); + // Two possibilities: either the download request has already reached us, or not. + if (downloadReq) { + downloadReq.wrappedJSObject.callback(); + } else { + // Set a variable to allow the request to complete immediately: + setState("finishReq", "true"); + } + } else if (req.queryString.includes('reset')) { + res.write("OK"); + setObjectState("downloadReq", null); + setState("finishReq", "false"); + } else { + res.processAsync(); + if (getState("finishReq") === "true") { + actuallyHandleRequest(req, res); + } else { + let o = {callback() { actuallyHandleRequest(req, res) }}; + o.wrappedJSObject = o; + o.QueryInterface = () => o; + setObjectState("downloadReq", o); + } + } +} diff --git a/uriloader/exthandler/tests/mochitest/download_page.html b/uriloader/exthandler/tests/mochitest/download_page.html new file mode 100644 index 0000000000..5a264888fa --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download_page.html @@ -0,0 +1,22 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<html> +<head> + <meta charset=UTF-8> + <title>Test page for link clicking</title> + <script type="text/javascript"> + function launch_download(extra) { + window.open("download.sjs", "_blank", "height=100,width=100" + extra); + } + </script> +</head> +<body> + <a href="download.bin" id="regular_load">regular load</a> + <a href="download.bin" id="target_blank" target="_blank" rel="opener">target blank</a> + <a href="#" onclick="launch_download(''); return false" id="new_window">new window</a> + <a href="#" onclick="window.open('download_page.html?newwin'); return false" id="open_in_new_tab">click to reopen</a> + <a href="download.bin" id="target_blank_no_opener" rel="noopener" target="_blank">target blank (noopener)</a> + <a href="#" onclick="window.open('download.bin', '_blank', 'noopener'); return false" id="open_in_new_tab_no_opener">click to reopen (noopener)</a> + <a href="#" onclick="launch_download(',noopener'); return false" id="new_window_no_opener">new window (noopener)</a> +</body> diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe b/uriloader/exthandler/tests/mochitest/file_as.exe new file mode 100644 index 0000000000..f2f5ab47f3 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_as.exe @@ -0,0 +1 @@ +Not actually an executable... but let's pretend! diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ new file mode 100644 index 0000000000..89f22e30be --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ @@ -0,0 +1,2 @@ +Content-Type: binary/octet-stream +Content-Disposition: attachment diff --git a/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html new file mode 100644 index 0000000000..eb2fb74441 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html @@ -0,0 +1 @@ +<iframe src="mailto:foo@bar.com"></iframe> diff --git a/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html new file mode 100644 index 0000000000..b1bb863f89 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html @@ -0,0 +1 @@ +<iframe srcdoc="<a href='mailto:help@example.com'>Mail someone</a>"></iframe> diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ new file mode 100644 index 0000000000..d1d59b9754 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ @@ -0,0 +1,2 @@ +content-disposition: attachment; filename=file_pdf_application_pdf.pdf; filename*=UTF-8''file_pdf_application_pdf.pdf +content-type: application/pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ new file mode 100644 index 0000000000..157c0e0943 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ @@ -0,0 +1,2 @@ +content-disposition: attachment; filename=file_pdf_application_unknown.pdf; filename*=UTF-8''file_pdf_application_unknown.pdf +content-type: application/unknown diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ new file mode 100644 index 0000000000..6358f54f48 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename="file_pdf_binary_octet_stream.pdf"; filename*=UTF-8''file_pdf_binary_octet_stream.pdf +Content-Type: binary/octet-stream diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ new file mode 100644 index 0000000000..37823166a4 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename=file_text_attachment_test.txt +Content-Type: text/plain diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png Binary files differnew file mode 100644 index 0000000000..743292dc6f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ new file mode 100644 index 0000000000..06e0cd957f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: inline; filename=file_with%40%40funny_name.png +Content-Type: image/png diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm Binary files differnew file mode 100644 index 0000000000..7bc738b8b4 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ new file mode 100644 index 0000000000..b77e9d3687 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: inline; filename=file_with%5Bfunny_name.webm +Content-Type: video/webm diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml new file mode 100644 index 0000000000..3a5792586a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml @@ -0,0 +1,4 @@ +<?xml version = "1.0" encoding = "utf-8"?> + +<something> +</something> diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ new file mode 100644 index 0000000000..5bdc4448e8 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment +Content-Type: binary/octet-stream diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml new file mode 100644 index 0000000000..3a5792586a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml @@ -0,0 +1,4 @@ +<?xml version = "1.0" encoding = "utf-8"?> + +<something> +</something> diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ new file mode 100644 index 0000000000..ac0355d976 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename=file_xml_attachment_test.xml +Content-Type: text/xml diff --git a/uriloader/exthandler/tests/mochitest/handlerApp.xhtml b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml new file mode 100644 index 0000000000..e519e80029 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml @@ -0,0 +1,28 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Pseudo Web Handler App</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="onLoad()"> +Pseudo Web Handler App + +<script class="testbody" type="text/javascript"> +<![CDATA[ +function onLoad() { + // if we have a window.opener, this must be the windowContext + // instance of this test. check that we got the URI right and clean up. + if (window.opener) { + window.opener.is(location.search, + "?uri=" + encodeURIComponent(window.opener.testURI), + "uri passed to web-handler app"); + window.opener.SimpleTest.finish(); + } + + window.close(); +} +]]> +</script> + +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/handlerApps.js b/uriloader/exthandler/tests/mochitest/handlerApps.js new file mode 100644 index 0000000000..aa841f13be --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/handlerApps.js @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// handlerApp.xhtml grabs this for verification purposes via window.opener +var testURI = "webcal://127.0.0.1/rheeeeet.html"; + +const Cc = SpecialPowers.Cc; + +function test() { + // set up the web handler object + var webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(SpecialPowers.Ci.nsIWebHandlerApp); + webHandler.name = "Test Web Handler App"; + webHandler.uriTemplate = + "https://example.com/tests/uriloader/exthandler/tests/mochitest/" + + "handlerApp.xhtml?uri=%s"; + + // set up the uri to test with + /* eslint-disable mozilla/use-services */ + + var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + SpecialPowers.Ci.nsIIOService + ); + + var uri = ioService.newURI(testURI); + + // create a window, and launch the handler in it + var newWindow = window.open("", "handlerWindow", "height=300,width=300"); + var windowContext = SpecialPowers.wrap(newWindow).docShell; + + webHandler.launchWithURI(uri, windowContext); + + // if we get this far without an exception, we've at least partly passed + // (remaining check in handlerApp.xhtml) + ok(true, "webHandler launchWithURI (existing window/tab) started"); + + // make the web browser launch in its own window/tab + webHandler.launchWithURI(uri); + + // if we get this far without an exception, we've passed + ok(true, "webHandler launchWithURI (new window/tab) test started"); + + // set up the local handler object + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(SpecialPowers.Ci.nsILocalHandlerApp); + localHandler.name = "Test Local Handler App"; + + // get a local app that we know will be there and do something sane + /* eslint-disable mozilla/use-services */ + + var osString = Cc["@mozilla.org/xre/app-info;1"].getService( + SpecialPowers.Ci.nsIXULRuntime + ).OS; + + var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService( + SpecialPowers.Ci.nsIDirectoryServiceProvider + ); + if (osString == "WINNT") { + var windowsDir = dirSvc.getFile("WinD", {}); + var exe = windowsDir.clone().QueryInterface(SpecialPowers.Ci.nsIFile); + exe.appendRelativePath("SYSTEM32\\HOSTNAME.EXE"); + } else if (osString == "Darwin") { + var localAppsDir = dirSvc.getFile("LocApp", {}); + exe = localAppsDir.clone(); + exe.append("iCal.app"); // lingers after the tests finish, but this seems + // seems better than explicitly killing it, since + // developers who run the tests locally may well + // information in their running copy of iCal + + if (navigator.userAgent.match(/ SeaMonkey\//)) { + // SeaMonkey tinderboxes don't like to have iCal lingering (and focused) + // on next test suite run(s). + todo(false, "On SeaMonkey, testing OS X as generic Unix. (Bug 749872)"); + + // assume a generic UNIX variant + exe = Cc["@mozilla.org/file/local;1"].createInstance( + SpecialPowers.Ci.nsIFile + ); + exe.initWithPath("/bin/echo"); + } + } else { + // assume a generic UNIX variant + exe = Cc["@mozilla.org/file/local;1"].createInstance( + SpecialPowers.Ci.nsIFile + ); + exe.initWithPath("/bin/echo"); + } + + localHandler.executable = exe; + localHandler.launchWithURI(ioService.newURI(testURI)); + + // if we get this far without an exception, we've passed + ok(true, "localHandler launchWithURI test"); + + // if we ever decide that killing iCal is the right thing to do, change + // the if statement below from "NOTDarwin" to "Darwin" + if (osString == "NOTDarwin") { + var killall = Cc["@mozilla.org/file/local;1"].createInstance( + SpecialPowers.Ci.nsIFile + ); + killall.initWithPath("/usr/bin/killall"); + + var process = Cc["@mozilla.org/process/util;1"].createInstance( + SpecialPowers.Ci.nsIProcess + ); + process.init(killall); + + var args = ["iCal"]; + process.run(false, args, args.length); + } + + SimpleTest.waitForExplicitFinish(); +} + +test(); diff --git a/uriloader/exthandler/tests/mochitest/head.js b/uriloader/exthandler/tests/mochitest/head.js new file mode 100644 index 0000000000..3b0d8a4072 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/head.js @@ -0,0 +1,277 @@ +var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); +var { HandlerServiceTestUtils } = ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm" +); + +var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); +var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +function createMockedHandlerApp() { + // Mock the executable + let mockedExecutable = FileUtils.getFile("TmpD", ["mockedExecutable"]); + if (!mockedExecutable.exists()) { + mockedExecutable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + } + + // Mock the handler app + let mockedHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + mockedHandlerApp.executable = mockedExecutable; + mockedHandlerApp.detailedDescription = "Mocked handler app"; + + registerCleanupFunction(function() { + // remove the mocked executable from disk. + if (mockedExecutable.exists()) { + mockedExecutable.remove(true); + } + }); + + return mockedHandlerApp; +} + +function createMockedObjects(createHandlerApp) { + // Mock the mime info + let internalMockedMIME = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + internalMockedMIME.alwaysAskBeforeHandling = true; + internalMockedMIME.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + internalMockedMIME.appendExtension("abc"); + if (createHandlerApp) { + let mockedHandlerApp = createMockedHandlerApp(); + internalMockedMIME.description = mockedHandlerApp.detailedDescription; + internalMockedMIME.possibleApplicationHandlers.appendElement( + mockedHandlerApp + ); + internalMockedMIME.preferredApplicationHandler = mockedHandlerApp; + } + + // Proxy for the mocked MIME info for faking the read-only attributes + let mockedMIME = new Proxy(internalMockedMIME, { + get(target, property) { + switch (property) { + case "hasDefaultHandler": + return true; + case "defaultDescription": + return "Default description"; + default: + return target[property]; + } + }, + }); + + // Mock the launcher: + let mockedLauncher = { + MIMEInfo: mockedMIME, + source: Services.io.newURI("http://www.mozilla.org/"), + suggestedFileName: "test_download_dialog.abc", + targetFileIsExecutable: false, + saveToDisk() {}, + cancel() {}, + launchWithApplication() {}, + setWebProgressListener() {}, + saveDestinationAvailable() {}, + contentLength: 42, + targetFile: null, // never read + // PRTime is microseconds since epoch, Date.now() returns milliseconds: + timeDownloadStarted: Date.now() * 1000, + QueryInterface: ChromeUtils.generateQI([ + "nsICancelable", + "nsIHelperAppLauncher", + ]), + }; + + registerCleanupFunction(function() { + // remove the mocked mime info from database. + let mockHandlerInfo = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + if (gHandlerSvc.exists(mockHandlerInfo)) { + gHandlerSvc.remove(mockHandlerInfo); + } + }); + + return mockedLauncher; +} + +async function openHelperAppDialog(launcher) { + let helperAppDialog = Cc[ + "@mozilla.org/helperapplauncherdialog;1" + ].createInstance(Ci.nsIHelperAppLauncherDialog); + + let helperAppDialogShownPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + try { + helperAppDialog.show(launcher, window, "foopy"); + } catch (ex) { + ok( + false, + "Trying to show unknownContentType.xhtml failed with exception: " + ex + ); + Cu.reportError(ex); + } + let dlg = await helperAppDialogShownPromise; + + is( + dlg.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Got correct dialog" + ); + + return dlg; +} + +async function waitForSubDialog(browser, url, state) { + let eventStr = state ? "dialogopen" : "dialogclose"; + + let tabDialogBox = gBrowser.getTabDialogBox(browser); + let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack; + + let checkFn; + + if (state) { + checkFn = dialogEvent => dialogEvent.detail.dialog?._openedURL == url; + } + + let event = await BrowserTestUtils.waitForEvent( + dialogStack, + eventStr, + true, + checkFn + ); + + let { dialog } = event.detail; + + // If the dialog is closing wait for it to be fully closed before resolving + if (!state) { + await dialog._closingPromise; + } + + return event.detail.dialog; +} + +/** + * Wait for protocol permission dialog open/close. + * @param {MozBrowser} browser - Browser element the dialog belongs to. + * @param {boolean} state - true: dialog open, false: dialog close + * @returns {Promise<SubDialog>} - Returns a promise which resolves with the + * SubDialog object of the dialog which closed or opened. + */ +async function waitForProtocolPermissionDialog(browser, state) { + return waitForSubDialog( + browser, + "chrome://mozapps/content/handling/permissionDialog.xhtml", + state + ); +} + +/** + * Wait for protocol app chooser dialog open/close. + * @param {MozBrowser} browser - Browser element the dialog belongs to. + * @param {boolean} state - true: dialog open, false: dialog close + * @returns {Promise<SubDialog>} - Returns a promise which resolves with the + * SubDialog object of the dialog which closed or opened. + */ +async function waitForProtocolAppChooserDialog(browser, state) { + return waitForSubDialog( + browser, + "chrome://mozapps/content/handling/appChooser.xhtml", + state + ); +} + +async function promiseDownloadFinished(list) { + return new Promise(resolve => { + list.addView({ + onDownloadChanged(download) { + info("Download changed!"); + if (download.succeeded || download.error) { + info("Download succeeded or errored"); + list.removeView(this); + resolve(download); + } + }, + }); + }); +} + +function setupMailHandler() { + let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + let gOldMailHandlers = []; + + // Remove extant web handlers because they have icons that + // we fetch from the web, which isn't allowed in tests. + let handlers = mailHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + gOldMailHandlers.push(handler); + // If we get here, this is a web handler app. Remove it: + handlers.removeElementAt(i); + } catch (ex) {} + } + + let previousHandling = mailHandlerInfo.alwaysAskBeforeHandling; + mailHandlerInfo.alwaysAskBeforeHandling = true; + + // Create a dummy web mail handler so we always know the mailto: protocol. + // Without this, the test fails on VMs without a default mailto: handler, + // because no dialog is ever shown, as we ignore subframe navigations to + // protocols that cannot be handled. + let dummy = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + dummy.name = "Handler 1"; + dummy.uriTemplate = "https://example.com/first/%s"; + mailHandlerInfo.possibleApplicationHandlers.appendElement(dummy); + + gHandlerSvc.store(mailHandlerInfo); + registerCleanupFunction(() => { + // Re-add the original protocol handlers: + let mailHandlers = mailHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + // See if this is a web handler. If it is, it'll throw, otherwise, + // we will remove it. + mailHandlers.queryElementAt(i, Ci.nsIWebHandlerApp); + mailHandlers.removeElementAt(i); + } catch (ex) {} + } + for (let h of gOldMailHandlers) { + mailHandlers.appendElement(h); + } + mailHandlerInfo.alwaysAskBeforeHandling = previousHandling; + gHandlerSvc.store(mailHandlerInfo); + }); +} + +let gDownloadDir; + +async function setDownloadDir() { + let tmpDir = await PathUtils.getTempDir(); + tmpDir = PathUtils.join( + tmpDir, + "testsavedir" + Math.floor(Math.random() * 2 ** 32) + ); + // Create this dir if it doesn't exist (ignores existing dirs) + await IOUtils.makeDirectory(tmpDir); + registerCleanupFunction(async function() { + try { + await IOUtils.remove(tmpDir, { recursive: true }); + } catch (e) { + Cu.reportError(e); + } + }); + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setCharPref("browser.download.dir", tmpDir); + return tmpDir; +} + +add_task(async function test_common_initialize() { + gDownloadDir = await setDownloadDir(); + Services.prefs.setCharPref("browser.download.loglevel", "Debug"); +}); diff --git a/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs new file mode 100644 index 0000000000..d12e2904d9 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) + return; + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader("Content-Type", "image/png; name=\"" + name + "\""); + response.setHeader("Content-Disposition", "attachment; filename=\"" + name + "\""); +} diff --git a/uriloader/exthandler/tests/mochitest/mochitest.ini b/uriloader/exthandler/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..be3b6bb37e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/mochitest.ini @@ -0,0 +1,23 @@ +[DEFAULT] +support-files = + handlerApp.xhtml + handlerApps.js + +[test_handlerApps.xhtml] +skip-if = (toolkit == 'android' || os == 'mac') || e10s # OS X: bug 786938 +scheme = https +[test_invalidCharFileExtension.xhtml] +skip-if = toolkit == 'android' && !is_fennec # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js + invalidCharFileExtension.sjs +[test_nullCharFile.xhtml] +skip-if = toolkit == 'android' && !is_fennec # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js +[test_unknown_ext_protocol_handlers.html] +[test_unsafeBidiChars.xhtml] +skip-if = toolkit == 'android' && !is_fennec # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js + unsafeBidiFileName.sjs diff --git a/uriloader/exthandler/tests/mochitest/protocolHandler.html b/uriloader/exthandler/tests/mochitest/protocolHandler.html new file mode 100644 index 0000000000..eff8a53aab --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/protocolHandler.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <title>Protocol handler</title> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + </head> + <body> + <script type="text/javascript"> + navigator.registerProtocolHandler("web+testprotocol", + "https://example.com/foobar?uri=%s", + "Test Protocol"); + </script> + <a id="link" href="web+testprotocol:test">testprotocol link</a> + </body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml new file mode 100644 index 0000000000..d6166fd270 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml @@ -0,0 +1,11 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handler Apps </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="handlerApps.js"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml new file mode 100644 index 0000000000..4ee1a6a1c1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml @@ -0,0 +1,47 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handling of unsafe bidi chars</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe id="test"></iframe> +<script type="text/javascript"> +var tests = [ + ["test.png:large", "test.png"], + ["test.png/large", "test.png"], + [":test.png::large:", "test.png"], +]; + +add_task(async function() { + let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js"); + let chromeScript = SpecialPowers.loadChromeScript(url); + + for (let [name, expected] of tests) { + let promiseName = new Promise(function(resolve) { + chromeScript.addMessageListener("suggestedFileName", + function listener(data) { + chromeScript.removeMessageListener("suggestedFileName", listener); + resolve(data); + }); + }); + document.getElementById("test").src = + "invalidCharFileExtension.sjs?name=" + encodeURIComponent(name); + is((await promiseName), expected, "got the expected sanitized name"); + } + + let promise = new Promise(function(resolve) { + chromeScript.addMessageListener("unregistered", function listener() { + chromeScript.removeMessageListener("unregistered", listener); + resolve(); + }); + }); + chromeScript.sendAsyncMessage("unregister"); + await promise; + + chromeScript.destroy(); +}); +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml new file mode 100644 index 0000000000..9bb1140718 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml @@ -0,0 +1,49 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handling of null char</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe id="test"></iframe> +<script type="text/javascript"> +var tests = [ + ["test.html\u0000.png", "test.html_.png"], + ["test.html.\u0000png", "test.html._png"], +]; + +add_task(async function() { + let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js"); + let chromeScript = SpecialPowers.loadChromeScript(url); + + for (let [name, expected] of tests) { + let promiseName = new Promise(function(resolve) { + chromeScript.addMessageListener("suggestedFileName", + function listener(data) { + chromeScript.removeMessageListener("suggestedFileName", listener); + resolve(data); + }); + }); + const a = document.createElement('a'); + // Pass an unknown mimetype so we don't "correct" the extension: + a.href = "data:application/baconizer;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="; + a.download = name; + a.dispatchEvent(new MouseEvent('click')); + is((await promiseName), expected, "got the expected sanitized name"); + } + + let promise = new Promise(function(resolve) { + chromeScript.addMessageListener("unregistered", function listener() { + chromeScript.removeMessageListener("unregistered", listener); + resolve(); + }); + }); + chromeScript.sendAsyncMessage("unregister"); + await promise; + + chromeScript.destroy(); +}); +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html new file mode 100644 index 0000000000..f8727db605 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for no error reporting for unknown external protocols</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe id="testFrame"></iframe> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +window.onload = () => { + let testFrame = document.getElementById("testFrame"); + + try { + testFrame.contentWindow.location.href = "unknownextproto:"; + ok(true, "There is no error reporting for unknown external protocol navigation."); + } catch (e) { + ok(false, "There should be no error reporting for unknown external protocol navigation."); + } + + SimpleTest.finish(); +}; +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml new file mode 100644 index 0000000000..4f62b32d99 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml @@ -0,0 +1,72 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handling of unsafe bidi chars</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe id="test"></iframe> +<script type="text/javascript"> +var unsafeBidiChars = [ + "\xe2\x80\xaa", // LRE + "\xe2\x80\xab", // RLE + "\xe2\x80\xac", // PDF + "\xe2\x80\xad", // LRO + "\xe2\x80\xae", // RLO +]; + +var tests = [ + "{1}.test", + "{1}File.test", + "Fi{1}le.test", + "File{1}.test", + "File.{1}test", + "File.te{1}st", + "File.test{1}", + "File.{1}", +]; + +function replace(name, x) { + return name.replace(/\{1\}/, x); +} + +function sanitize(name) { + return replace(name, "_"); +} + +add_task(async function() { + let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js"); + let chromeScript = SpecialPowers.loadChromeScript(url); + + for (let test of tests) { + for (let char of unsafeBidiChars) { + let promiseName = new Promise(function(resolve) { + chromeScript.addMessageListener("suggestedFileName", + function listener(data) { + chromeScript.removeMessageListener("suggestedFileName", listener); + resolve(data); + }); + }); + let name = replace(test, char); + let expected = sanitize(test); + document.getElementById("test").src = + "unsafeBidiFileName.sjs?name=" + encodeURIComponent(name); + is((await promiseName), expected, "got the expected sanitized name"); + } + } + + let promise = new Promise(function(resolve) { + chromeScript.addMessageListener("unregistered", function listener() { + chromeScript.removeMessageListener("unregistered", listener); + resolve(); + }); + }); + chromeScript.sendAsyncMessage("unregister"); + await promise; + + chromeScript.destroy(); +}); +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs new file mode 100644 index 0000000000..48301be5b4 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) + return; + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader("Content-Type", "application/octet-stream; name=\"" + name + "\""); + response.setHeader("Content-Disposition", "inline; filename=\"" + name + "\""); +} diff --git a/uriloader/exthandler/tests/moz.build b/uriloader/exthandler/tests/moz.build new file mode 100644 index 0000000000..3d96aaa448 --- /dev/null +++ b/uriloader/exthandler/tests/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ["mochitest/mochitest.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] + +BROWSER_CHROME_MANIFESTS += ["mochitest/browser.ini"] + +TESTING_JS_MODULES += [ + "HandlerServiceTestUtils.jsm", +] + +GeckoSimplePrograms( + [ + "WriteArgument", + ], + linkage=None, +) + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.xpcshell.uriloader.exthandler.tests.unit += [ + "!WriteArgument%s" % CONFIG["BIN_SUFFIX"] + ] + +USE_LIBS += [ + "nspr", +] diff --git a/uriloader/exthandler/tests/unit/handlers.json b/uriloader/exthandler/tests/unit/handlers.json new file mode 100644 index 0000000000..40e88f930a --- /dev/null +++ b/uriloader/exthandler/tests/unit/handlers.json @@ -0,0 +1,90 @@ +{ + "defaultHandlersVersion": { + "en-US": 999 + }, + "mimeTypes": { + "example/type.handleinternally": { + "unknownProperty": "preserved", + "action": 3, + "extensions": [ + "example_one" + ] + }, + "example/type.savetodisk": { + "action": 0, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + } + ], + "extensions": [ + "example_two", + "example_three" + ] + }, + "example/type.usehelperapp": { + "action": 2, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + }, + { + "name": "Example Possible Handler One", + "uriTemplate": "http://www.example.com/?id=1&url=%s" + }, + { + "name": "Example Possible Handler Two", + "uriTemplate": "http://www.example.com/?id=2&url=%s" + } + ], + "extensions": [ + "example_two", + "example_three" + ] + }, + "example/type.usesystemdefault": { + "action": 4, + "handlers": [ + null, + { + "name": "Example Possible Handler", + "uriTemplate": "http://www.example.com/?url=%s" + } + ] + } + }, + "schemes": { + "examplescheme.usehelperapp": { + "action": 2, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + }, + { + "name": "Example Possible Handler One", + "uriTemplate": "http://www.example.com/?id=1&url=%s" + }, + { + "name": "Example Possible Handler Two", + "uriTemplate": "http://www.example.com/?id=2&url=%s" + } + ] + }, + "examplescheme.usesystemdefault": { + "action": 4, + "handlers": [ + null, + { + "name": "Example Possible Handler", + "uriTemplate": "http://www.example.com/?url=%s" + } + ] + } + } +} diff --git a/uriloader/exthandler/tests/unit/head.js b/uriloader/exthandler/tests/unit/head.js new file mode 100644 index 0000000000..3330a309be --- /dev/null +++ b/uriloader/exthandler/tests/unit/head.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Initialization for tests related to invoking external handler applications. + */ + +"use strict"; + +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { OS, require } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm", + this +); +var { TestUtils } = ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gHandlerService", + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService" +); + +do_get_profile(); + +let jsonPath = OS.Path.join(OS.Constants.Path.profileDir, "handlers.json"); + +/** + * Unloads the nsIHandlerService data store, so the back-end file can be + * accessed or modified, and the new data will be loaded at the next access. + */ +let unloadHandlerStore = async function() { + // If this function is called before the nsIHandlerService instance has been + // initialized for the first time, the observer below will not be registered. + // We have to force initialization to prevent the function from stalling. + gHandlerService; + + let promise = TestUtils.topicObserved("handlersvc-json-replace-complete"); + Services.obs.notifyObservers(null, "handlersvc-json-replace"); + await promise; +}; + +/** + * Unloads the data store and deletes it. + */ +let deleteHandlerStore = async function() { + await unloadHandlerStore(); + + await OS.File.remove(jsonPath, { ignoreAbsent: true }); +}; + +/** + * Unloads the data store and replaces it with the test data file. + */ +let copyTestDataToHandlerStore = async function() { + await unloadHandlerStore(); + + await OS.File.copy(do_get_file("handlers.json").path, jsonPath); +}; + +/** + * Ensures the files are removed and the services unloaded when the tests end. + */ +registerCleanupFunction(async function test_terminate() { + await deleteHandlerStore(); +}); diff --git a/uriloader/exthandler/tests/unit/mailcap b/uriloader/exthandler/tests/unit/mailcap new file mode 100644 index 0000000000..dc93ef8042 --- /dev/null +++ b/uriloader/exthandler/tests/unit/mailcap @@ -0,0 +1,2 @@ +text/plain; cat '%s'; needsterminal +text/plain; sed '%s' diff --git a/uriloader/exthandler/tests/unit/test_badMIMEType.js b/uriloader/exthandler/tests/unit/test_badMIMEType.js new file mode 100644 index 0000000000..49c5e8d848 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_badMIMEType.js @@ -0,0 +1,29 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + // "text/plain" has an 0xFF character appended to it. This means it's an + // invalid string, which is tricky to enter using a text editor (I used + // emacs' hexl-mode). It also means an ordinary text editor might drop it + // or convert it to something that *is* valid (in UTF8). So we measure + // its length to make sure this hasn't happened. + var badMimeType = "text/plainÿ"; + Assert.equal(badMimeType.length, 11); + try { + Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getFromTypeAndExtension(badMimeType, "txt"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException) || + e.result != Cr.NS_ERROR_NOT_AVAILABLE + ) { + throw e; + } + // This is an expected exception, thrown if the type can't be determined + } + // Not crashing is good enough + Assert.equal(true, true); +} diff --git a/uriloader/exthandler/tests/unit/test_defaults_handlerService.js b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js new file mode 100644 index 0000000000..f9f9feda23 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); + +const kDefaultHandlerList = Services.prefs + .getChildList("gecko.handlerService.schemes") + .filter(p => { + try { + let val = Services.prefs.getComplexValue(p, Ci.nsIPrefLocalizedString) + .data; + return !!val; + } catch (ex) { + return false; + } + }); + +add_task(async function test_check_defaults_get_added() { + let protocols = new Set( + kDefaultHandlerList.map(p => p.match(/schemes\.(\w+)/)[1]) + ); + for (let protocol of protocols) { + const kPrefStr = `schemes.${protocol}.`; + let matchingPrefs = kDefaultHandlerList.filter(p => p.includes(kPrefStr)); + let protocolHandlerCount = matchingPrefs.length / 2; + Assert.ok( + protocolHandlerCount, + `Prefs for ${protocol} have at least 1 protocol handler` + ); + Assert.ok( + gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry, + `Expect stub for ${protocol}` + ); + let info = gExternalProtocolService.getProtocolHandlerInfo(protocol, {}); + Assert.ok( + info, + `Should be able to get protocol handler info for ${protocol}` + ); + let handlers = Array.from( + info.possibleApplicationHandlers.enumerate(Ci.nsIHandlerApp) + ); + handlers = handlers.filter(h => h instanceof Ci.nsIWebHandlerApp); + Assert.equal( + handlers.length, + protocolHandlerCount, + `Default web handlers for ${protocol} should match` + ); + let { alwaysAskBeforeHandling, preferredAction } = info; + // Actually store something, pretending there was a change: + let infoToWrite = gExternalProtocolService.getProtocolHandlerInfo( + protocol, + {} + ); + gHandlerService.store(infoToWrite); + ok( + !gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry, + "Expect stub entry info to go away" + ); + + let newInfo = gExternalProtocolService.getProtocolHandlerInfo(protocol, {}); + Assert.equal( + alwaysAskBeforeHandling, + newInfo.alwaysAskBeforeHandling, + protocol + " - always ask shouldn't change" + ); + Assert.equal( + preferredAction, + newInfo.preferredAction, + protocol + " - preferred action shouldn't change" + ); + await deleteHandlerStore(); + } +}); + +add_task(async function test_check_default_modification() { + let mailtoHandlerCount = + kDefaultHandlerList.filter(p => p.includes("mailto")).length / 2; + Assert.ok(mailtoHandlerCount, "Prefs have at least 1 mailto handler"); + Assert.ok( + true, + JSON.stringify(gHandlerService.wrappedJSObject._store.data.schemes.mailto) + ); + Assert.ok( + gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry, + "Expect stub for mailto" + ); + let mailInfo = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + mailInfo.alwaysAskBeforeHandling = false; + mailInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault; + gHandlerService.store(mailInfo); + Assert.ok( + !gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry, + "Stub entry should be removed immediately." + ); + let newMail = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + Assert.equal(newMail.preferredAction, Ci.nsIHandlerInfo.useSystemDefault); + Assert.equal(newMail.alwaysAskBeforeHandling, false); + await deleteHandlerStore(); +}); + +/** + * Check that we don't add bogus handlers. + */ +add_task(async function test_check_restrictions() { + const kTestData = { + testdeleteme: [ + ["Delete me", ""], + ["Delete me insecure", "http://example.com/%s"], + ["Delete me no substitution", "https://example.com/"], + ["Keep me", "https://example.com/%s"], + ], + testreallydeleteme: [ + // used to check we remove the entire entry. + ["Delete me", "http://example.com/%s"], + ], + }; + for (let [scheme, handlers] of Object.entries(kTestData)) { + let count = 1; + for (let [name, uriTemplate] of handlers) { + let pref = `gecko.handlerService.schemes.${scheme}.${count}.`; + let obj = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + obj.data = name; + Services.prefs.setComplexValue( + pref + "name", + Ci.nsIPrefLocalizedString, + obj + ); + obj.data = uriTemplate; + Services.prefs.setComplexValue( + pref + "uriTemplate", + Ci.nsIPrefLocalizedString, + obj + ); + count++; + } + } + + gHandlerService.wrappedJSObject._injectDefaultProtocolHandlers(); + let schemeData = gHandlerService.wrappedJSObject._store.data.schemes; + + Assert.ok(schemeData.testdeleteme, "Expect an entry for testdeleteme"); + Assert.ok( + schemeData.testdeleteme.stubEntry, + "Expect a stub entry for testdeleteme" + ); + + Assert.deepEqual( + schemeData.testdeleteme.handlers, + [null, { name: "Keep me", uriTemplate: "https://example.com/%s" }], + "Expect only one handler is kept." + ); + + Assert.ok(!schemeData.testreallydeleteme, "No entry for reallydeleteme"); +}); diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js new file mode 100644 index 0000000000..62031da1e6 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gBundleService", + "@mozilla.org/intl/stringbundle;1", + "nsIStringBundleService" +); + +// PDF files should always have a generic description instead +// of relying on what is registered with the Operating System. +add_task(async function test_check_unknown_mime_type() { + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let pdfType = mimeService.getTypeFromExtension("pdf"); + Assert.equal(pdfType, "application/pdf"); + let extension = mimeService.getPrimaryExtension("application/pdf", ""); + Assert.equal(extension, "pdf", "Expect pdf extension when given mime"); + let mimeInfo = gMIMEService.getFromTypeAndExtension("", "pdf"); + let stringBundle = gBundleService.createBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties" + ); + Assert.equal( + mimeInfo.description, + stringBundle.GetStringFromName("pdfExtHandlerDescription"), + "PDF has generic description" + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js new file mode 100644 index 0000000000..9beef9d9c5 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Zip files can be opened by Windows explorer, so we should always be able to +// determine a description and default handler for them. However, things can +// get messy if they are sent to us with a mime type other than what Windows +// considers the "right" mimetype (application/x-zip-compressed), like +// application/zip, which is what most places (IANA, macOS, probably all linux +// distros, Apache, etc.) think is the "right" mimetype. +add_task(async function test_check_unknown_mime_type() { + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let zipType = mimeService.getTypeFromExtension("zip"); + Assert.equal(zipType, "application/x-zip-compressed"); + try { + let extension = mimeService.getPrimaryExtension("application/zip", ""); + Assert.equal( + extension, + "zip", + "Expect our own info to provide an extension for zip files." + ); + } catch (ex) { + Assert.ok(false, "We shouldn't throw when getting zip info."); + } + let found = {}; + let mimeInfo = mimeService.getMIMEInfoFromOS("application/zip", "zip", found); + Assert.ok( + mimeInfo.hasDefaultHandler, + "Should have a default app for zip files" + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js new file mode 100644 index 0000000000..7202db58de --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js @@ -0,0 +1,65 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 508030 <https://bugzilla.mozilla.org/show_bug.cgi?id=508030>: + * nsIMIMEService.getTypeFromExtension fails to find a match in the + * "ext-to-type-mapping" category if the provided extension is not lowercase. + */ +function run_test() { + // --- Common services --- + + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + + const categoryManager = Services.catMan; + + // --- Test procedure --- + + const kTestExtension = "testextension"; + const kTestExtensionMixedCase = "testExtensIon"; + const kTestMimeType = "application/x-testextension"; + + // Ensure that the test extension is not initially recognized by the operating + // system or the "ext-to-type-mapping" category. + try { + // Try and get the MIME type associated with the extension. + mimeService.getTypeFromExtension(kTestExtension); + // The line above should have thrown an exception. + do_throw("nsIMIMEService.getTypeFromExtension succeeded unexpectedly"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException) || + e.result != Cr.NS_ERROR_NOT_AVAILABLE + ) { + throw e; + } + // This is an expected exception, thrown if the type can't be determined. + // Any other exception would cause the test to fail. + } + + // Add a temporary category entry mapping the extension to the MIME type. + categoryManager.addCategoryEntry( + "ext-to-type-mapping", + kTestExtension, + kTestMimeType, + false, + true + ); + + // Check that the mapping is recognized in the simple case. + var type = mimeService.getTypeFromExtension(kTestExtension); + Assert.equal(type, kTestMimeType); + + // Check that the mapping is recognized even if the extension has mixed case. + type = mimeService.getTypeFromExtension(kTestExtensionMixedCase); + Assert.equal(type, kTestMimeType); + + // Clean up after ourselves. + categoryManager.deleteCategoryEntry( + "ext-to-type-mapping", + kTestExtension, + false + ); +} diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js new file mode 100644 index 0000000000..dad5530856 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js @@ -0,0 +1,190 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 484579 <https://bugzilla.mozilla.org/show_bug.cgi?id=484579>: + * nsIMIMEService.getTypeFromExtension may fail unexpectedly on Windows when + * "Content Type" is empty in the registry. + */ + +// We must use a file extension that isn't listed in nsExternalHelperAppService's +// defaultMimeEntries, otherwise the code takes a shortcut skipping the registry. +const FILE_EXTENSION = ".nfo"; +// This is used to ensure the test properly used the mock, so that if we change +// the underlying code, it won't be skipped. +let gTestUsedOurMock = false; + +function run_test() { + // Activate the override of the file association data in the registry. + registerMockWindowsRegKeyFactory(); + + // Check the mock has been properly activated. + let regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + regKey.open( + Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT, + FILE_EXTENSION, + Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE + ); + Assert.equal( + regKey.readStringValue("content type"), + "", + "Check the mock replied as expected." + ); + Assert.ok(gTestUsedOurMock, "The test properly used the mock registry"); + // Reset gTestUsedOurMock, because we just used it. + gTestUsedOurMock = false; + // Try and get the MIME type associated with the extension. If this + // operation does not throw an unexpected exception, the test succeeds. + Assert.throws( + () => { + Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromExtension(FILE_EXTENSION); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should throw a NOT_AVAILABLE exception" + ); + + Assert.ok(gTestUsedOurMock, "The test properly used the mock registry"); +} + +/** + * Constructs a new mock registry key by wrapping the provided object. + * + * This mock implementation is tailored for this test, and forces consumers + * of the readStringValue method to believe that the "Content Type" value of + * the FILE_EXTENSION key under HKEY_CLASSES_ROOT is an empty string. + * + * The same value read from "HKEY_LOCAL_MACHINE\SOFTWARE\Classes" is not + * affected. + * + * @param aWrappedObject An actual nsIWindowsRegKey implementation. + */ +function MockWindowsRegKey(aWrappedObject) { + this._wrappedObject = aWrappedObject; + + // This function creates a forwarding function for wrappedObject + function makeForwardingFunction(functionName) { + return function() { + return aWrappedObject[functionName].apply(aWrappedObject, arguments); + }; + } + + // Forward all the functions that are not explicitly overridden + for (var propertyName in aWrappedObject) { + if (!(propertyName in this)) { + if (typeof aWrappedObject[propertyName] == "function") { + this[propertyName] = makeForwardingFunction(propertyName); + } else { + this[propertyName] = aWrappedObject[propertyName]; + } + } + } +} + +MockWindowsRegKey.prototype = { + // --- Overridden nsISupports interface functions --- + + QueryInterface: ChromeUtils.generateQI(["nsIWindowsRegKey"]), + + // --- Overridden nsIWindowsRegKey interface functions --- + + open(aRootKey, aRelPath, aMode) { + // Remember the provided root key and path + this._rootKey = aRootKey; + this._relPath = aRelPath; + + // Create the actual registry key + return this._wrappedObject.open(aRootKey, aRelPath, aMode); + }, + + openChild(aRelPath, aMode) { + // Open the child key and wrap it + var innerKey = this._wrappedObject.openChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + createChild(aRelPath, aMode) { + // Create the child key and wrap it + var innerKey = this._wrappedObject.createChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + get childCount() { + return this._wrappedObject.childCount; + }, + + get valueCount() { + return this._wrappedObject.valueCount; + }, + + readStringValue(aName) { + // If this is the key under test, return a fake value + if ( + this._rootKey == Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT && + this._relPath.toLowerCase() == FILE_EXTENSION && + aName.toLowerCase() == "content type" + ) { + gTestUsedOurMock = true; + return ""; + } + // Return the real value from the registry + return this._wrappedObject.readStringValue(aName); + }, +}; + +function registerMockWindowsRegKeyFactory() { + const kMockCID = Components.ID("{9b23dfe9-296b-4740-ba1c-d39c9a16e55e}"); + const kWindowsRegKeyContractID = "@mozilla.org/windows-registry-key;1"; + // Preserve the original CID. + let originalWindowsRegKeyCID = Cc[kWindowsRegKeyContractID].number; + + info("Create a mock RegKey factory"); + let originalRegKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + let mockWindowsRegKeyFactory = { + createInstance(outer, iid) { + if (outer != null) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + info("Create a mock wrapper around RegKey"); + var key = new MockWindowsRegKey(originalRegKey); + return key.QueryInterface(iid); + }, + }; + info("Register the mock RegKey factory"); + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + kMockCID, + "Mock Windows Registry Key Implementation", + kWindowsRegKeyContractID, + mockWindowsRegKeyFactory + ); + + registerCleanupFunction(() => { + // Free references to the mock factory + registrar.unregisterFactory(kMockCID, mockWindowsRegKeyFactory); + // Restore the original factory + registrar.registerFactory( + Components.ID(originalWindowsRegKeyCID), + "", + kWindowsRegKeyContractID, + null + ); + }); +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService.js b/uriloader/exthandler/tests/unit/test_handlerService.js new file mode 100644 index 0000000000..610eb5b749 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService.js @@ -0,0 +1,474 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + //* *************************************************************************// + // Constants + + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + + const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + + const prefSvc = Services.prefs; + + const env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + const rootPrefBranch = prefSvc.getBranch(""); + + let noMailto = false; + if (mozinfo.os == "win") { + // Check mailto handler from registry. + // If registry entry is nothing, no mailto handler + let regSvc = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + regSvc.open(regSvc.ROOT_KEY_CLASSES_ROOT, "mailto", regSvc.ACCESS_READ); + noMailto = false; + } catch (ex) { + noMailto = true; + } + regSvc.close(); + } + + if (mozinfo.os == "linux") { + // Check mailto handler from GIO + // If there isn't one, then we have no mailto handler + let gIOSvc = Cc["@mozilla.org/gio-service;1"].createInstance( + Ci.nsIGIOService + ); + try { + gIOSvc.getAppForURIScheme("mailto"); + noMailto = false; + } catch (ex) { + noMailto = true; + } + } + + //* *************************************************************************// + // Sample Data + + // It doesn't matter whether or not this nsIFile is actually executable, + // only that it has a path and exists. Since we don't know any executable + // that exists on all platforms (except possibly the application being + // tested, but there doesn't seem to be a way to get a reference to that + // from the directory service), we use the temporary directory itself. + var executable = Services.dirsvc.get("TmpD", Ci.nsIFile); + // XXX We could, of course, create an actual executable in the directory: + // executable.append("localhandler"); + // if (!executable.exists()) + // executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandler.name = "Local Handler"; + localHandler.executable = executable; + + var webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + webHandler.name = "Web Handler"; + webHandler.uriTemplate = "http://www.example.com/?%s"; + + // FIXME: these tests create and manipulate enough variables that it would + // make sense to move each test into its own scope so we don't run the risk + // of one test stomping on another's data. + + //* *************************************************************************// + // Test Default Properties + + // Get a handler info for a MIME type that neither the application nor + // the OS knows about and make sure its properties are set to the proper + // default values. + + var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + // Make sure it's also an nsIHandlerInfo. + Assert.ok(handlerInfo instanceof Ci.nsIHandlerInfo); + + Assert.equal(handlerInfo.type, "nonexistent/type"); + + // Deprecated property, but we should still make sure it's set correctly. + Assert.equal(handlerInfo.MIMEType, "nonexistent/type"); + + // These properties are the ones the handler service knows how to store. + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk); + Assert.equal(handlerInfo.preferredApplicationHandler, null); + Assert.equal(handlerInfo.possibleApplicationHandlers.length, 0); + Assert.ok(handlerInfo.alwaysAskBeforeHandling); + + // These properties are initialized to default values by the service, + // so we might as well make sure they're initialized to the right defaults. + Assert.equal(handlerInfo.description, ""); + Assert.equal(handlerInfo.hasDefaultHandler, false); + Assert.equal(handlerInfo.defaultDescription, ""); + + // test some default protocol info properties + var haveDefaultHandlersVersion = false; + try { + // If we have a defaultHandlersVersion pref, then assume that we're in the + // firefox tree and that we'll also have default handlers. + // Bug 395131 has been filed to make this test work more generically + // by providing our own prefs for this test rather than this icky + // special casing. + rootPrefBranch.getCharPref("gecko.handlerService.defaultHandlersVersion"); + haveDefaultHandlersVersion = true; + } catch (ex) {} + + const kExternalWarningDefault = + "network.protocol-handler.warn-external-default"; + prefSvc.setBoolPref(kExternalWarningDefault, true); + + // XXX add more thorough protocol info property checking + + // no OS default handler exists + var protoInfo = protoSvc.getProtocolHandlerInfo("x-moz-rheet"); + Assert.equal(protoInfo.preferredAction, protoInfo.alwaysAsk); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: false + const kExternalWarningPrefPrefix = "network.protocol-handler.warn-external."; + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", false); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + Assert.ok(!protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", true); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + // OS handler isn't included in possibleApplicationHandlers, so length is 0 + // Once they become instances of nsILocalHandlerApp, this number will need + // to change. + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: false + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) { + Assert.equal(2, protoInfo.possibleApplicationHandlers.length); + } else { + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + } + + // Win7+ or Linux's GIO might not have a default mailto: handler + if (noMailto) { + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } else { + Assert.ok(!protoInfo.alwaysAskBeforeHandling); + } + + // OS default exists, injected default exists, explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", true); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) { + Assert.equal(2, protoInfo.possibleApplicationHandlers.length); + // Win7+ or Linux's GIO may have no default mailto: handler, so we'd ask + // anyway. Otherwise, the default handlers will not have stored preferred + // actions etc., so re-requesting them after the warning pref has changed + // will use the updated pref value. So both when we have and do not have + // a default mailto: handler, we'll ask: + Assert.ok(protoInfo.alwaysAskBeforeHandling); + // As soon as anyone actually stores updated defaults into the profile + // database, that default will stop tracking the warning pref. + } else { + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } + + if (haveDefaultHandlersVersion) { + // Now set the value stored in RDF to true, and the pref to false, to make + // sure we still get the right value. (Basically, same thing as above but + // with the values reversed.) + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo.alwaysAskBeforeHandling = true; + handlerSvc.store(protoInfo); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + Assert.equal(2, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } + + //* *************************************************************************// + // Test Round-Trip Data Integrity + + // Test round-trip data integrity by setting the properties of the handler + // info object to different values, telling the handler service to store the + // object, and then retrieving a new info object for the same type and making + // sure its properties are identical. + + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.preferredApplicationHandler = localHandler; + handlerInfo.alwaysAskBeforeHandling = false; + + handlerSvc.store(handlerInfo); + + handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + + Assert.notEqual(handlerInfo.preferredApplicationHandler, null); + var preferredHandler = handlerInfo.preferredApplicationHandler; + Assert.equal(typeof preferredHandler, "object"); + Assert.equal(preferredHandler.name, "Local Handler"); + Assert.ok(preferredHandler instanceof Ci.nsILocalHandlerApp); + preferredHandler.QueryInterface(Ci.nsILocalHandlerApp); + Assert.equal(preferredHandler.executable.path, localHandler.executable.path); + + Assert.ok(!handlerInfo.alwaysAskBeforeHandling); + + // Make sure the handler service's enumerate method lists all known handlers. + var handlerInfo2 = mimeSvc.getFromTypeAndExtension("nonexistent/type2", null); + handlerSvc.store(handlerInfo2); + var handlerTypes = ["nonexistent/type", "nonexistent/type2"]; + if (haveDefaultHandlersVersion) { + handlerTypes.push("mailto"); + handlerTypes.push("irc"); + handlerTypes.push("ircs"); + } + for (let handler of handlerSvc.enumerate()) { + Assert.notEqual(handlerTypes.indexOf(handler.type), -1); + handlerTypes.splice(handlerTypes.indexOf(handler.type), 1); + } + Assert.equal(handlerTypes.length, 0); + // Make sure the handler service's remove method removes a handler record. + handlerSvc.remove(handlerInfo2); + let handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) { + Assert.notEqual( + handlers.getNext().QueryInterface(Ci.nsIHandlerInfo).type, + handlerInfo2.type + ); + } + + // Make sure we can store and retrieve a handler info object with no preferred + // handler. + var noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/no-preferred-handler", + null + ); + handlerSvc.store(noPreferredHandlerInfo); + noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/no-preferred-handler", + null + ); + Assert.equal(noPreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure that the handler service removes an existing handler record + // if we store a handler info object with no preferred handler. + var removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + removePreferredHandlerInfo.preferredApplicationHandler = localHandler; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + removePreferredHandlerInfo.preferredApplicationHandler = null; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + Assert.equal(removePreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure we can store and retrieve a handler info object with possible + // handlers. We test both adding and removing handlers. + + // Get a handler info and make sure it has no possible handlers. + var possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Store and re-retrieve the handler and make sure it still has no possible + // handlers. + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Add two handlers, store the object, re-retrieve it, and make sure it has + // two handlers. + possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler); + possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 2); + + // Figure out which is the local and which is the web handler and the index + // in the array of the local handler, which is the one we're going to remove + // to test removal of a handler. + var handler1 = possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 0, + Ci.nsIHandlerApp + ); + var handler2 = possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 1, + Ci.nsIHandlerApp + ); + var localPossibleHandler, webPossibleHandler, localIndex; + if (handler1 instanceof Ci.nsILocalHandlerApp) { + [localPossibleHandler, webPossibleHandler, localIndex] = [ + handler1, + handler2, + 0, + ]; + } else { + [localPossibleHandler, webPossibleHandler, localIndex] = [ + handler2, + handler1, + 1, + ]; + } + localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp); + webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp); + + // Make sure the two handlers are the ones we stored. + Assert.equal(localPossibleHandler.name, localHandler.name); + Assert.ok(localPossibleHandler.equals(localHandler)); + Assert.equal(webPossibleHandler.name, webHandler.name); + Assert.ok(webPossibleHandler.equals(webHandler)); + + // Remove a handler, store the object, re-retrieve it, and make sure + // it only has one handler. + possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 1); + + // Make sure the handler is the one we didn't remove. + webPossibleHandler = possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 0, + Ci.nsIWebHandlerApp + ); + Assert.equal(webPossibleHandler.name, webHandler.name); + Assert.ok(webPossibleHandler.equals(webHandler)); + + // //////////////////////////////////////////////////// + // handler info command line parameters and equality + var localApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + var handlerApp = localApp.QueryInterface(Ci.nsIHandlerApp); + + Assert.ok(handlerApp.equals(localApp)); + + localApp.executable = executable; + + Assert.equal(0, localApp.parameterCount); + localApp.appendParameter("-test1"); + Assert.equal(1, localApp.parameterCount); + localApp.appendParameter("-test2"); + Assert.equal(2, localApp.parameterCount); + Assert.ok(localApp.parameterExists("-test1")); + Assert.ok(localApp.parameterExists("-test2")); + Assert.ok(!localApp.parameterExists("-false")); + localApp.clearParameters(); + Assert.equal(0, localApp.parameterCount); + + var localApp2 = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + + localApp2.executable = executable; + + localApp.clearParameters(); + Assert.ok(localApp.equals(localApp2)); + + // equal: + // cut -d 1 -f 2 + // cut -d 1 -f 2 + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test3"); + Assert.ok(localApp.equals(localApp2)); + + // not equal: + // cut -d 1 -f 2 + // cut -f 1 -d 2 + + localApp.clearParameters(); + localApp2.clearParameters(); + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test3"); + Assert.ok(!localApp2.equals(localApp)); + + var str; + str = localApp.getParameter(0); + Assert.equal(str, "-test1"); + str = localApp.getParameter(1); + Assert.equal(str, "-test2"); + str = localApp.getParameter(2); + Assert.equal(str, "-test3"); + + // FIXME: test round trip integrity for a protocol. + // FIXME: test round trip integrity for a handler info with a web handler. + + //* *************************************************************************// + // getTypeFromExtension tests + + // test nonexistent extension + var lolType = handlerSvc.getTypeFromExtension("lolcat"); + Assert.equal(lolType, ""); + + // add a handler for the extension + var lolHandler = mimeSvc.getFromTypeAndExtension("application/lolcat", null); + + Assert.ok(!lolHandler.extensionExists("lolcat")); + lolHandler.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + lolHandler.preferredApplicationHandler = localHandler; + lolHandler.alwaysAskBeforeHandling = false; + lolHandler.appendExtension("lolcat"); + + // store the handler + Assert.ok(!handlerSvc.exists(lolHandler)); + handlerSvc.store(lolHandler); + Assert.ok(handlerSvc.exists(lolHandler)); + + // test now-existent extension + lolType = handlerSvc.getTypeFromExtension("lolcat"); + Assert.equal(lolType, "application/lolcat"); + + // test mailcap entries with needsterminal are ignored on non-Windows non-Mac. + if (mozinfo.os != "win" && mozinfo.os != "mac") { + env.set("PERSONAL_MAILCAP", do_get_file("mailcap").path); + handlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", null); + Assert.equal( + handlerInfo.preferredAction, + Ci.nsIHandlerInfo.useSystemDefault + ); + Assert.equal(handlerInfo.defaultDescription, "sed"); + } +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService_store.js b/uriloader/exthandler/tests/unit/test_handlerService_store.js new file mode 100644 index 0000000000..aa2efde822 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService_store.js @@ -0,0 +1,771 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests the nsIHandlerService interface. + */ + +// Set up an nsIWebHandlerApp instance that can be used in multiple tests. +let webHandlerApp = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" +].createInstance(Ci.nsIWebHandlerApp); +webHandlerApp.name = "Web Handler"; +webHandlerApp.uriTemplate = "https://www.example.com/?url=%s"; +let expectedWebHandlerApp = { + name: webHandlerApp.name, + uriTemplate: webHandlerApp.uriTemplate, +}; + +// Set up an nsILocalHandlerApp instance that can be used in multiple tests. The +// executable should exist, but it doesn't need to point to an actual file, so +// we simply initialize it to the path of an existing directory. +let localHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" +].createInstance(Ci.nsILocalHandlerApp); +localHandlerApp.name = "Local Handler"; +localHandlerApp.executable = FileUtils.getFile("TmpD", []); +let expectedLocalHandlerApp = { + name: localHandlerApp.name, + executable: localHandlerApp.executable, +}; + +/** + * Returns a new nsIHandlerInfo instance initialized to known values that don't + * depend on the platform and are easier to verify later. + * + * @param type + * Because the "preferredAction" is initialized to saveToDisk, this + * should represent a MIME type rather than a protocol. + */ +function getKnownHandlerInfo(type) { + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(type); + handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfo.alwaysAskBeforeHandling = false; + return handlerInfo; +} + +/** + * Checks that the information stored in the handler service instance under + * testing matches the test data files. + */ +function assertAllHandlerInfosMatchTestData() { + let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + + // It's important that the MIME types we check here do not exist at the + // operating system level, otherwise the list of handlers and file extensions + // will be merged. The current implementation avoids duplicate entries. + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.handleinternally", + preferredAction: Ci.nsIHandlerInfo.handleInternally, + alwaysAskBeforeHandling: false, + fileExtensions: ["example_one"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.savetodisk", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + ], + fileExtensions: ["example_two", "example_three"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.usehelperapp", + preferredAction: Ci.nsIHandlerInfo.useHelperApp, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + { + name: "Example Possible Handler One", + uriTemplate: "http://www.example.com/?id=1&url=%s", + }, + { + name: "Example Possible Handler Two", + uriTemplate: "http://www.example.com/?id=2&url=%s", + }, + ], + fileExtensions: ["example_two", "example_three"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.usesystemdefault", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "examplescheme.usehelperapp", + preferredAction: Ci.nsIHandlerInfo.useHelperApp, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + { + name: "Example Possible Handler One", + uriTemplate: "http://www.example.com/?id=1&url=%s", + }, + { + name: "Example Possible Handler Two", + uriTemplate: "http://www.example.com/?id=2&url=%s", + }, + ], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "examplescheme.usesystemdefault", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + + Assert.equal(handlerInfos.length, 0); +} + +/** + * Loads data from a file in a predefined format, verifying that the format is + * recognized and all the known properties are loaded and saved. + */ +add_task(async function test_store_fillHandlerInfo_predefined() { + // Test that the file format used in previous versions can be loaded. + await copyTestDataToHandlerStore(); + await assertAllHandlerInfosMatchTestData(); + + // Keep a copy of the nsIHandlerInfo instances, then delete the handler store + // and populate it with the known data. Since the handler store is empty, the + // default handlers for the current locale are also injected, so we have to + // delete them manually before adding the other nsIHandlerInfo instances. + let testHandlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + await deleteHandlerStore(); + for (let handlerInfo of HandlerServiceTestUtils.getAllHandlerInfos()) { + gHandlerService.remove(handlerInfo); + } + for (let handlerInfo of testHandlerInfos) { + gHandlerService.store(handlerInfo); + } + + // Test that the known data still matches after saving it and reloading. + await unloadHandlerStore(); + await assertAllHandlerInfosMatchTestData(); +}); + +/** + * Check that "store" is able to add new instances, that "remove" and "exists" + * work, and that "fillHandlerInfo" throws when the instance does not exist. + */ +add_task(async function test_store_remove_exists() { + // Test both MIME types and protocols. + for (let type of [ + "example/type.usehelperapp", + "examplescheme.usehelperapp", + ]) { + // Create new nsIHandlerInfo instances before loading the test data. + await deleteHandlerStore(); + let handlerInfoPresent = HandlerServiceTestUtils.getHandlerInfo(type); + let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + + // Set up known properties that we can verify later. + handlerInfoAbsent.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfoAbsent.alwaysAskBeforeHandling = false; + + await copyTestDataToHandlerStore(); + + Assert.ok(gHandlerService.exists(handlerInfoPresent)); + Assert.ok(!gHandlerService.exists(handlerInfoAbsent)); + + gHandlerService.store(handlerInfoAbsent); + gHandlerService.remove(handlerInfoPresent); + + await unloadHandlerStore(); + + Assert.ok(!gHandlerService.exists(handlerInfoPresent)); + Assert.ok(gHandlerService.exists(handlerInfoAbsent)); + + Assert.throws( + () => gHandlerService.fillHandlerInfo(handlerInfoPresent, ""), + ex => ex.result == Cr.NS_ERROR_NOT_AVAILABLE + ); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: type + "2", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + }); + } +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredAction" that is alwaysAsk or has an unknown value, but the + * action always becomes useHelperApp when reloading. + */ +add_task(async function test_store_preferredAction() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + + for (let preferredAction of [Ci.nsIHandlerInfo.alwaysAsk, 999]) { + handlerInfo.preferredAction = preferredAction; + gHandlerService.store(handlerInfo); + gHandlerService.fillHandlerInfo(handlerInfo, ""); + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + } +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance containing an + * nsILocalHandlerApp instance pointing to an executable that doesn't exist, but + * this entry is ignored when reloading. + */ +add_task(async function test_store_localHandlerApp_missing() { + if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + let missingHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + missingHandlerApp.name = "Non-existing Handler"; + missingHandlerApp.executable = FileUtils.getFile("TmpD", ["nonexisting"]); + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = missingHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(missingHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [expectedWebHandlerApp], + }); +}); + +/** + * Test saving and reloading an instance of nsIDBusHandlerApp. + */ +add_task(async function test_store_dBusHandlerApp() { + if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + // Set up an nsIDBusHandlerApp instance for testing. + let dBusHandlerApp = Cc[ + "@mozilla.org/uriloader/dbus-handler-app;1" + ].createInstance(Ci.nsIDBusHandlerApp); + dBusHandlerApp.name = "DBus Handler"; + dBusHandlerApp.service = "test.method.server"; + dBusHandlerApp.method = "Method"; + dBusHandlerApp.dBusInterface = "test.method.Type"; + dBusHandlerApp.objectPath = "/test/method/Object"; + let expectedDBusHandlerApp = { + name: dBusHandlerApp.name, + service: dBusHandlerApp.service, + method: dBusHandlerApp.method, + dBusInterface: dBusHandlerApp.dBusInterface, + objectPath: dBusHandlerApp.objectPath, + }; + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = dBusHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(dBusHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedDBusHandlerApp, + possibleApplicationHandlers: [expectedDBusHandlerApp], + }); +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredApplicationHandler" and no "possibleApplicationHandlers", but the + * former is always included in the latter list when reloading. + */ +add_task( + async function test_store_possibleApplicationHandlers_includes_preferred() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = localHandlerApp; + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/new" + ); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedLocalHandlerApp, + possibleApplicationHandlers: [expectedLocalHandlerApp], + }); + } +); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredApplicationHandler" that is not the first element in + * "possibleApplicationHandlers", but the former is always included as the first + * element of the latter list when reloading. + */ +add_task( + async function test_store_possibleApplicationHandlers_preferred_first() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = webHandlerApp; + // The preferred handler is appended after the other one. + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/new" + ); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedWebHandlerApp, + possibleApplicationHandlers: [ + expectedWebHandlerApp, + expectedLocalHandlerApp, + ], + }); + } +); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with an + * uppercase file extension, but it is converted to lowercase when reloading. + */ +add_task(async function test_store_fileExtensions_lowercase() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("EXTENSION_test2"); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + fileExtensions: ["extension_test1", "extension_test2"], + }); +}); + +/** + * Tests that appendExtension doesn't add duplicates, and that anyway duplicates + * from possibleApplicationHandlers are removed when saving and reloading. + */ +add_task(async function test_store_no_duplicates() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = webHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("extension_test2"); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("EXTENSION_test1"); + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), [ + "extension_test1", + "extension_test2", + ]); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedWebHandlerApp, + possibleApplicationHandlers: [ + expectedWebHandlerApp, + expectedLocalHandlerApp, + ], + fileExtensions: ["extension_test1", "extension_test2"], + }); +}); + +/** + * Tests that setFileExtensions doesn't add duplicates. + */ +add_task(async function test_setFileExtensions_no_duplicates() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.setFileExtensions("a,b,A,b,c,a"); + let expected = ["a", "b", "c"]; + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected); + // Test empty extensions, also at begin and end. + handlerInfo.setFileExtensions(",a,,b,A,c,"); + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected); +}); + +/** + * Tests that "store" deletes properties that have their default values from + * the data store. + * + * File extensions are never deleted once they have been associated. + */ +add_task(async function test_store_deletes_properties_except_extensions() { + await deleteHandlerStore(); + + // Prepare an nsIHandlerInfo instance with all the properties set to values + // that will result in deletions. The preferredAction is also set to a defined + // value so we can more easily verify it later. + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo( + "example/type.savetodisk" + ); + handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfo.alwaysAskBeforeHandling = false; + + // All the properties for "example/type.savetodisk" are present in the test + // data, so we load the data before overwriting their values. + await copyTestDataToHandlerStore(); + gHandlerService.store(handlerInfo); + + // Now we can reload the data and verify that no extra values have been kept. + await unloadHandlerStore(); + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/type.savetodisk" + ); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/type.savetodisk", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + fileExtensions: ["example_two", "example_three"], + }); +}); + +/** + * Tests the "overrideType" argument of "fillHandlerInfo". + */ +add_task(async function test_fillHandlerInfo_overrideType() { + // Test both MIME types and protocols. + for (let type of [ + "example/type.usesystemdefault", + "examplescheme.usesystemdefault", + ]) { + await deleteHandlerStore(); + + // Create new nsIHandlerInfo instances before loading the test data. + let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + + // Fill the nsIHandlerInfo instance using the type that actually exists. + await copyTestDataToHandlerStore(); + gHandlerService.fillHandlerInfo(handlerInfoAbsent, type); + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfoAbsent, { + // While the data is populated from another type, the type is unchanged. + type: type + "2", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + } +}); + +/** + * Tests "getTypeFromExtension" including unknown extensions. + */ +add_task(async function test_getTypeFromExtension() { + await copyTestDataToHandlerStore(); + + Assert.equal(gHandlerService.getTypeFromExtension(""), ""); + Assert.equal(gHandlerService.getTypeFromExtension("example_unknown"), ""); + Assert.equal( + gHandlerService.getTypeFromExtension("example_one"), + "example/type.handleinternally" + ); + Assert.equal( + gHandlerService.getTypeFromExtension("EXAMPLE_one"), + "example/type.handleinternally" + ); +}); + +/** + * Checks that the information stored in the handler service instance under + * testing matches the default handlers for the English locale. + */ +function assertAllHandlerInfosMatchDefaultHandlers() { + let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + + for (let type of ["irc", "ircs"]) { + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type, + preferredActionOSDependent: true, + possibleApplicationHandlers: [ + { + name: "Mibbit", + uriTemplate: "https://www.mibbit.com/?url=%s", + }, + ], + }); + } + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "mailto", + preferredActionOSDependent: true, + possibleApplicationHandlers: [ + { + name: "Yahoo! Mail", + uriTemplate: "https://compose.mail.yahoo.com/?To=%s", + }, + { + name: "Gmail", + uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s", + }, + ], + }); + + Assert.equal(handlerInfos.length, 0); +} + +/** + * Tests the default protocol handlers imported from the locale-specific data. + */ +add_task(async function test_default_protocol_handlers() { + if ( + !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion") + ) { + info("This platform or locale does not have default handlers."); + return; + } + + // This will inject the default protocol handlers for the current locale. + await deleteHandlerStore(); + + await assertAllHandlerInfosMatchDefaultHandlers(); +}); + +/** + * Tests that the default protocol handlers are not imported again from the + * locale-specific data if they already exist. + */ +add_task(async function test_default_protocol_handlers_no_duplicates() { + if ( + !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion") + ) { + info("This platform or locale does not have default handlers."); + return; + } + + // This will inject the default protocol handlers for the current locale. + await deleteHandlerStore(); + + // Remove the "irc" handler so we can verify that the injection is repeated. + let ircHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("irc"); + gHandlerService.remove(ircHandlerInfo); + + let originalDefaultHandlersVersion = Services.prefs.getComplexValue( + "gecko.handlerService.defaultHandlersVersion", + Ci.nsIPrefLocalizedString + ); + + // Set the preference to an arbitrarily high number to force injecting again. + Services.prefs.setStringPref( + "gecko.handlerService.defaultHandlersVersion", + "999" + ); + + await unloadHandlerStore(); + + // Check that "irc" exists to make sure that the injection was repeated. + Assert.ok(gHandlerService.exists(ircHandlerInfo)); + + // There should be no duplicate handlers in the protocols. + await assertAllHandlerInfosMatchDefaultHandlers(); + + Services.prefs.setStringPref( + "gecko.handlerService.defaultHandlersVersion", + originalDefaultHandlersVersion + ); +}); + +/** + * Ensures forward compatibility by checking that the "store" method preserves + * unknown properties in the test data. + */ +add_task(async function test_store_keeps_unknown_properties() { + // Create a new nsIHandlerInfo instance before loading the test data. + await deleteHandlerStore(); + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/type.handleinternally" + ); + + await copyTestDataToHandlerStore(); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + let data = JSON.parse(new TextDecoder().decode(await OS.File.read(jsonPath))); + Assert.equal( + data.mimeTypes["example/type.handleinternally"].unknownProperty, + "preserved" + ); +}); + +/** + * Runs the asyncInit method, ensuring that it successfully inits the store + * and calls the handlersvc-store-initialized topic. + */ +add_task(async function test_async_init() { + await deleteHandlerStore(); + await copyTestDataToHandlerStore(); + gHandlerService.asyncInit(); + await TestUtils.topicObserved("handlersvc-store-initialized"); + await assertAllHandlerInfosMatchTestData(); + + await unloadHandlerStore(); +}); + +/** + * Races the asyncInit method against the sync init (implicit in enumerate), + * to ensure that the store will be synchronously initialized without any + * ill effects. + */ +add_task(async function test_race_async_init() { + await deleteHandlerStore(); + await copyTestDataToHandlerStore(); + let storeInitialized = false; + // Pass a callback to synchronously observe the topic, as a promise would + // resolve asynchronously + TestUtils.topicObserved("handlersvc-store-initialized", () => { + storeInitialized = true; + return true; + }); + gHandlerService.asyncInit(); + Assert.ok(!storeInitialized); + gHandlerService.enumerate(); + Assert.ok(storeInitialized); + await assertAllHandlerInfosMatchTestData(); + + await unloadHandlerStore(); +}); + +/** + * Test saving and reloading an instance of nsIGIOMimeApp. + */ +add_task(async function test_store_gioHandlerApp() { + if (!("@mozilla.org/gio-service;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + // Create dummy exec file that following won't fail because file not found error + let dummyHandlerFile = FileUtils.getFile("TmpD", ["dummyHandler"]); + dummyHandlerFile.createUnique( + Ci.nsIFile.NORMAL_FILE_TYPE, + parseInt("777", 8) + ); + + // Set up an nsIGIOMimeApp instance for testing. + let handlerApp = Cc["@mozilla.org/gio-service;1"] + .getService(Ci.nsIGIOService) + .createAppFromCommand(dummyHandlerFile.path, "Dummy GIO handler"); + let expectedGIOMimeHandlerApp = { + name: handlerApp.name, + command: handlerApp.command, + }; + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = handlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(handlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedGIOMimeHandlerApp, + possibleApplicationHandlers: [expectedGIOMimeHandlerApp, webHandlerApp], + }); + + await OS.File.remove(dummyHandlerFile.path); + + // After removing dummyHandlerFile, the handler should disappear from the + // list of possibleApplicationHandlers and preferredAppHandler should be null. + actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: null, + possibleApplicationHandlers: [webHandlerApp], + }); +}); diff --git a/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js b/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js new file mode 100644 index 0000000000..161cce8d33 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ContentDispatchChooserTelemetry } = ChromeUtils.import( + "resource://gre/modules/ContentDispatchChooser.jsm" +); + +let telemetryLabels = Services.telemetry.getCategoricalLabels() + .EXTERNAL_PROTOCOL_HANDLER_DIALOG_CONTEXT_SCHEME; + +let schemeToLabel = ContentDispatchChooserTelemetry.SCHEME_TO_LABEL; +let schemePrefixToLabel = + ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL; + +/** + * Test for scheme-label mappings of protocol ask dialog telemetry. + */ +add_task(async function test_telemetry_label_maps() { + let mapValues = Object.values(schemeToLabel).concat( + Object.values(schemePrefixToLabel) + ); + + // Scheme - label maps must have valid label values. + mapValues.forEach(label => { + // Mapped labels must be valid. + Assert.ok(telemetryLabels.includes(label), `Exists label: ${label}`); + }); + + // Uppercase labels must have a mapping. + telemetryLabels.forEach(label => { + Assert.equal( + label == "OTHER" || mapValues.includes(label), + label == label.toUpperCase(), + `Exists label: ${label}` + ); + }); + + Object.keys(schemeToLabel).forEach(key => { + // Schemes which have a mapping must not exist as as label. + Assert.ok(!telemetryLabels.includes(key), `Not exists label: ${key}`); + + // There must be no key duplicates across the two maps. + Assert.ok(!schemePrefixToLabel[key], `No duplicate key: ${key}`); + }); +}); + +/** + * Tests the getTelemetryLabel method. + */ +add_task(async function test_telemetry_getTelemetryLabel() { + // Method should return the correct mapping. + Object.keys(schemeToLabel).forEach(scheme => { + Assert.equal( + schemeToLabel[scheme], + ContentDispatchChooserTelemetry._getTelemetryLabel(scheme) + ); + }); + + // Passing null to _getTelemetryLabel should throw. + Assert.throws(() => { + ContentDispatchChooserTelemetry._getTelemetryLabel(null); + }, /Invalid scheme/); + + // Replace maps with test data + ContentDispatchChooserTelemetry.SCHEME_TO_LABEL = { + foo: "FOOLABEL", + bar: "BARLABEL", + }; + + ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL = { + fooPrefix: "FOOPREFIXLABEL", + barPrefix: "BARPREFIXLABEL", + fo: "PREFIXLABEL", + }; + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("foo"), + "FOOLABEL", + "Non prefix mapping should have priority" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("bar"), + "BARLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix"), + "FOOPREFIXLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix1"), + "FOOPREFIXLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix2"), + "FOOPREFIXLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("doesnotexist"), + "OTHER", + "Should return the correct label for unknown scheme" + ); + + // Restore maps + ContentDispatchChooserTelemetry.SCHEME_TO_LABEL = schemeToLabel; + ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL = schemePrefixToLabel; +}); diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js new file mode 100644 index 0000000000..638128d11b --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.js @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Encoded test URI to work on all platforms/independent of file encoding +const kTestURI = "http://\u65e5\u672c\u8a93.jp/"; +const kExpectedURI = "http://xn--wgv71a309e.jp/"; +const kOutputFile = "result.txt"; + +// Try several times in case the box we're running on is slow. +const kMaxCheckExistAttempts = 30; // seconds +var gCheckExistsAttempts = 0; + +const tempDir = do_get_tempdir(); + +function checkFile() { + // This is where we expect the output + var tempFile = tempDir.clone(); + tempFile.append(kOutputFile); + + if (!tempFile.exists()) { + if (gCheckExistsAttempts >= kMaxCheckExistAttempts) { + do_throw( + "Expected File " + + tempFile.path + + " does not exist after " + + kMaxCheckExistAttempts + + " seconds" + ); + } else { + ++gCheckExistsAttempts; + // Wait a bit longer then try again + do_timeout(1000, checkFile); + return; + } + } + + // Now read it + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + fstream.init(tempFile, -1, 0, 0); + sstream.init(fstream); + + // Read the first line only as that's the one we expect WriteArguments + // to be writing the argument to. + var data = sstream.read(4096); + + sstream.close(); + fstream.close(); + + // Now remove the old file + tempFile.remove(false); + + // This currently fails on Mac with an argument like -psn_0_nnnnnn + // This seems to be to do with how the executable is called, but I couldn't + // find a way around it. + // Additionally the lack of OS detection in xpcshell tests sucks, so we'll + // have to check for the argument mac gives us. + if (data.substring(0, 7) != "-psn_0_") { + Assert.equal(data, kExpectedURI); + } + + do_test_finished(); +} + +function run_test() { + if (mozinfo.os == "mac") { + dump("INFO | test_punycodeURIs.js | Skipping test on mac, bug 599475"); + return; + } + + // set up the uri to test with + var ioService = Services.io; + + // set up the local handler object + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandler.name = "Test Local Handler App"; + + // WriteArgument will just dump its arguments to a file for us. + var processDir = do_get_cwd(); + var exe = processDir.clone(); + exe.append("WriteArgument"); + + if (!exe.exists()) { + // Maybe we are on windows + exe.leafName = "WriteArgument.exe"; + if (!exe.exists()) { + do_throw("Could not locate the WriteArgument tests executable\n"); + } + } + + var outFile = tempDir.clone(); + outFile.append(kOutputFile); + + // Set an environment variable for WriteArgument to pick up + var envSvc = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + // The Write Argument file needs to know where its libraries are, so + // just force the path variable + // For mac + var greDir = Services.dirsvc.get("GreD", Ci.nsIFile); + + envSvc.set("DYLD_LIBRARY_PATH", greDir.path); + // For Linux + envSvc.set("LD_LIBRARY_PATH", greDir.path); + // XXX: handle windows + + // Now tell it where we want the file. + envSvc.set("WRITE_ARGUMENT_FILE", outFile.path); + + var uri = ioService.newURI(kTestURI); + + // Just check we've got these matching, if we haven't there's a problem + // with ascii spec or our test case. + Assert.equal(uri.asciiSpec, kExpectedURI); + + localHandler.executable = exe; + localHandler.launchWithURI(uri); + + do_test_pending(); + do_timeout(1000, checkFile); +} diff --git a/uriloader/exthandler/tests/unit/xpcshell.ini b/uriloader/exthandler/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..d7bf9e54d6 --- /dev/null +++ b/uriloader/exthandler/tests/unit/xpcshell.ini @@ -0,0 +1,28 @@ +[DEFAULT] +head = head.js +run-sequentially = Bug 912235 - Intermittent failures +firefox-appdir = browser + +[test_defaults_handlerService.js] +# No default stored handlers on android given lack of support. +# No default stored handlers on Thunderbird. +skip-if = os == "android" || appname == "thunderbird" +[test_getMIMEInfo_pdf.js] +[test_getMIMEInfo_unknown_mime_type.js] +run-if = os == "win" # Windows only test +[test_getTypeFromExtension_ext_to_type_mapping.js] +[test_getTypeFromExtension_with_empty_Content_Type.js] +run-if = os == "win" # Windows only test +[test_badMIMEType.js] +[test_handlerService.js] +skip-if = (verify && (os == 'win')) +support-files = mailcap +# Bug 676997: test consistently fails on Android +fail-if = os == "android" +[test_handlerService_store.js] +# Disabled for 1563343 -- the app should determine possible handlers in GV. +fail-if = os == "android" +support-files = handlers.json +[test_punycodeURIs.js] +[test_protocol_ask_dialog_telemetry.js] +skip-if = os == "android" # Desktop telemetry diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h new file mode 100644 index 0000000000..35672ed20c --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nslocalhandlerappuikit_h_ +#define nslocalhandlerappuikit_h_ + +#include "nsLocalHandlerApp.h" + +class nsLocalHandlerAppUIKit final : public nsLocalHandlerApp { + public: + nsLocalHandlerAppUIKit() {} + ~nsLocalHandlerAppUIKit() {} + + nsLocalHandlerAppUIKit(const char16_t* aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + nsLocalHandlerAppUIKit(const nsAString& aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + NS_IMETHOD LaunchWithURI(nsIURI* aURI, + BrowsingContext* aBrowsingContext) override; +}; + +#endif /* nslocalhandlerappuikit_h_ */ diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm new file mode 100644 index 0000000000..b9fe2766ba --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <CoreFoundation/CoreFoundation.h> + +#include "nsLocalHandlerAppUIKit.h" +#include "nsIURI.h" + +NS_IMETHODIMP +nsLocalHandlerAppUIKit::LaunchWithURI(nsIURI* aURI, nsIInterfaceRequestor* aWindowContext) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h new file mode 100644 index 0000000000..8638b94f75 --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUIKit_h_ +#define nsMIMEInfoUIKit_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUIKit final : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoUIKit(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUIKit(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + + protected: + virtual nsresult LoadUriInternal(nsIURI* aURI); +#ifdef DEBUG + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) { + MOZ_ASSERT_UNREACHABLE("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif +}; + +#endif diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm new file mode 100644 index 0000000000..2ed0c1eb2e --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUIKit.h" + +NS_IMETHODIMP +nsMIMEInfoUIKit::LaunchWithFile(nsIFile* aFile) { return NS_ERROR_NOT_IMPLEMENTED; } + +nsresult nsMIMEInfoUIKit::LoadUriInternal(nsIURI* aURI) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.h b/uriloader/exthandler/uikit/nsOSHelperAppService.h new file mode 100644 index 0000000000..8c1b1fb6b3 --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and +// is implemented on each platform. It contains platform specific code for +// finding helper applications for a given mime type in addition to launching +// those applications. This is the UIKit version. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsOSHelperAppService final : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval); + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval); + + // method overrides --> used to hook the mime service into internet config.... + NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo); + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval); + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile); + + nsresult OSProtocolHandlerExists(const char* aScheme, + bool* aHandlerExists) override; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.mm b/uriloader/exthandler/uikit/nsOSHelperAppService.mm new file mode 100644 index 0000000000..c0eb14d28d --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" + +nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() {} + +nsOSHelperAppService::~nsOSHelperAppService() {} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) { + *aHandlerExists = false; + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, bool* _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath, + nsIFile** aFile) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetFromTypeAndExtension(const nsACString& aType, const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo) { + return nsExternalHelperAppService::GetFromTypeAndExtension(aType, aFileExt, aMIMEInfo); +} + +NS_IMETHODIMP nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = nullptr; + *aFound = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const char* aScheme, bool* found, + nsIHandlerInfo** _retval) { + *found = false; + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.cpp b/uriloader/exthandler/unix/nsGNOMERegistry.cpp new file mode 100644 index 0000000000..6136709d91 --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsGNOMERegistry.h" +#include "nsString.h" +#include "nsMIMEInfoUnix.h" +#include "nsIGIOService.h" + +/* static */ +bool nsGNOMERegistry::HandlerExists(const char* aProtocolScheme) { + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return false; + } + + nsCOMPtr<nsIHandlerApp> app; + return NS_SUCCEEDED(giovfs->GetAppForURIScheme( + nsDependentCString(aProtocolScheme), getter_AddRefs(app))); +} + +// XXX Check HandlerExists() before calling LoadURL. + +/* static */ +nsresult nsGNOMERegistry::LoadURL(nsIURI* aURL) { + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return NS_ERROR_FAILURE; + } + + return giovfs->ShowURI(aURL); +} + +/* static */ +void nsGNOMERegistry::GetAppDescForScheme(const nsACString& aScheme, + nsAString& aDesc) { + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) return; + + nsCOMPtr<nsIHandlerApp> app; + if (NS_FAILED(giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(app)))) + return; + + app->GetName(aDesc); +} + +/* static */ +already_AddRefed<nsMIMEInfoBase> nsGNOMERegistry::GetFromExtension( + const nsACString& aFileExt) { + nsAutoCString mimeType; + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + // Get the MIME type from the extension, then call GetFromType to + // fill in the MIMEInfo. + if (NS_FAILED(giovfs->GetMimeTypeFromExtension(aFileExt, mimeType)) || + mimeType.EqualsLiteral("application/octet-stream")) { + return nullptr; + } + + RefPtr<nsMIMEInfoBase> mi = GetFromType(mimeType); + if (mi) { + mi->AppendExtension(aFileExt); + } + + return mi.forget(); +} + +/* static */ +already_AddRefed<nsMIMEInfoBase> nsGNOMERegistry::GetFromType( + const nsACString& aMIMEType) { + RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(aMIMEType); + NS_ENSURE_TRUE(mimeInfo, nullptr); + + nsAutoString name; + nsAutoCString description; + + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + nsCOMPtr<nsIHandlerApp> handlerApp; + if (NS_FAILED( + giovfs->GetAppForMimeType(aMIMEType, getter_AddRefs(handlerApp))) || + !handlerApp) { + return nullptr; + } + handlerApp->GetName(name); + giovfs->GetDescriptionForMimeType(aMIMEType, description); + + mimeInfo->SetDefaultDescription(name); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + mimeInfo->SetDescription(NS_ConvertUTF8toUTF16(description)); + + return mimeInfo.forget(); +} diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.h b/uriloader/exthandler/unix/nsGNOMERegistry.h new file mode 100644 index 0000000000..ea626c5b50 --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.h @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsGNOMERegistry_h +#define nsGNOMERegistry_h + +#include "nsIURI.h" +#include "nsCOMPtr.h" + +class nsMIMEInfoBase; + +class nsGNOMERegistry { + public: + static bool HandlerExists(const char* aProtocolScheme); + + static nsresult LoadURL(nsIURI* aURL); + + static void GetAppDescForScheme(const nsACString& aScheme, nsAString& aDesc); + + static already_AddRefed<nsMIMEInfoBase> GetFromExtension( + const nsACString& aFileExt); + + static already_AddRefed<nsMIMEInfoBase> GetFromType( + const nsACString& aMIMEType); +}; + +#endif // nsGNOMERegistry_h diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp new file mode 100644 index 0000000000..7cbefcce3e --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUnix.h" +#include "nsGNOMERegistry.h" +#include "nsIGIOService.h" +#include "nsNetCID.h" +#include "nsIIOService.h" +#ifdef MOZ_ENABLE_DBUS +# include "nsDBusHandlerApp.h" +#endif + +nsresult nsMIMEInfoUnix::LoadUriInternal(nsIURI* aURI) { + return nsGNOMERegistry::LoadURL(aURI); +} + +NS_IMETHODIMP +nsMIMEInfoUnix::GetHasDefaultHandler(bool* _retval) { + // if mDefaultApplication is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (mDefaultApplication) return nsMIMEInfoImpl::GetHasDefaultHandler(_retval); + + *_retval = false; + + if (mClass == eProtocolInfo) { + *_retval = nsGNOMERegistry::HandlerExists(mSchemeOrType.get()); + } else { + RefPtr<nsMIMEInfoBase> mimeInfo = + nsGNOMERegistry::GetFromType(mSchemeOrType); + if (!mimeInfo) { + nsAutoCString ext; + nsresult rv = GetPrimaryExtension(ext); + if (NS_SUCCEEDED(rv)) { + mimeInfo = nsGNOMERegistry::GetFromExtension(ext); + } + } + if (mimeInfo) *_retval = true; + } + + if (*_retval) return NS_OK; + + return NS_OK; +} + +nsresult nsMIMEInfoUnix::LaunchDefaultWithFile(nsIFile* aFile) { + // if mDefaultApplication is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (mDefaultApplication) return nsMIMEInfoImpl::LaunchDefaultWithFile(aFile); + + nsAutoCString nativePath; + aFile->GetNativePath(nativePath); + + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return NS_ERROR_FAILURE; + } + + // nsGIOMimeApp->Launch wants a URI string instead of local file + nsresult rv; + nsCOMPtr<nsIIOService> ioservice = + do_GetService(NS_IOSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> uri; + rv = ioservice->NewFileURI(aFile, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHandlerApp> app; + if (NS_FAILED( + giovfs->GetAppForMimeType(mSchemeOrType, getter_AddRefs(app))) || + !app) { + return NS_ERROR_FILE_NOT_FOUND; + } + + return app->LaunchWithURI(uri, nullptr); +} diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.h b/uriloader/exthandler/unix/nsMIMEInfoUnix.h new file mode 100644 index 0000000000..2e32be4915 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUnix_h_ +#define nsMIMEInfoUnix_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUnix : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoUnix(const char* aMIMEType = "") + : nsMIMEInfoImpl(aMIMEType) {} + explicit nsMIMEInfoUnix(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUnix(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + static bool HandlerExists(const char* aProtocolScheme); + + protected: + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + + virtual nsresult LoadUriInternal(nsIURI* aURI) override; + + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override; +}; + +#endif // nsMIMEInfoUnix_h_ diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.cpp b/uriloader/exthandler/unix/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..8ffa98c40b --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp @@ -0,0 +1,1409 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <sys/stat.h> + +#include "nsOSHelperAppService.h" +#include "nsMIMEInfoUnix.h" +#ifdef MOZ_WIDGET_GTK +# include "nsGNOMERegistry.h" +#endif +#include "nsISupports.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsIFileStreams.h" +#include "nsILineInputStream.h" +#include "nsIFile.h" +#include "nsIProcess.h" +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsComponentManagerUtils.h" +#include "nsCRT.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsXULAppAPI.h" +#include "ContentHandlerService.h" +#include "prenv.h" // for PR_GetEnv() +#include "mozilla/Preferences.h" +#include "nsMimeTypes.h" + +using namespace mozilla; + +#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(mLog, mozilla::LogLevel::Debug) + +static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter); +static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter); + +inline bool IsNetscapeFormat(const nsACString& aBuffer); + +nsOSHelperAppService::~nsOSHelperAppService() {} + +/* + * Take a command with all the mailcap escapes in it and unescape it + * Ideally this needs the mime type, mime type options, and location of the + * temporary file, but this last can't be got from here + */ +// static +nsresult nsOSHelperAppService::UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand) { + LOG(("-- UnescapeCommand")); + LOG(("Command to escape: '%s'\n", + NS_LossyConvertUTF16toASCII(aEscapedCommand).get())); + // XXX This function will need to get the mime type and various stuff like + // that being passed in to work properly + + LOG( + ("UnescapeCommand really needs some work -- it should actually do some " + "unescaping\n")); + + CopyUTF16toUTF8(aEscapedCommand, aUnEscapedCommand); + LOG(("Escaped command: '%s'\n", PromiseFlatCString(aUnEscapedCommand).get())); + return NS_OK; +} + +/* Put aSemicolon_iter at the first non-escaped semicolon after + * aStart_iter but before aEnd_iter + */ + +static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter) { + bool semicolonFound = false; + while (aSemicolon_iter != aEnd_iter && !semicolonFound) { + switch (*aSemicolon_iter) { + case '\\': + aSemicolon_iter.advance(2); + break; + case ';': + semicolonFound = true; + break; + default: + ++aSemicolon_iter; + break; + } + } + return NS_OK; +} + +static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter) { + nsAString::const_iterator iter(aStart_iter); + + // skip leading whitespace + while (iter != aEnd_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeStart = iter; + + // find major/minor separator ('/') + while (iter != aEnd_iter && *iter != '/') { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeEnd = iter; + + // skip '/' + ++iter; + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMinorTypeStart = iter; + + // find end of minor type, delimited by whitespace or ';' + while (iter != aEnd_iter && !nsCRT::IsAsciiSpace(*iter) && *iter != ';') { + ++iter; + } + + aMinorTypeEnd = iter; + + return NS_OK; +} + +// static +nsresult nsOSHelperAppService::GetFileLocation(const char* aPrefName, + const char* aEnvVarName, + nsAString& aFileLocation) { + LOG(("-- GetFileLocation. Pref: '%s' EnvVar: '%s'\n", aPrefName, + aEnvVarName)); + MOZ_ASSERT(aPrefName, "Null pref name passed; don't do that!"); + + aFileLocation.Truncate(); + /* The lookup order is: + 1) user pref + 2) env var + 3) pref + */ + NS_ENSURE_TRUE(Preferences::GetRootBranch(), NS_ERROR_FAILURE); + + /* + If we have an env var we should check whether the pref is a user + pref. If we do not, we don't care. + */ + if (Preferences::HasUserValue(aPrefName) && + NS_SUCCEEDED(Preferences::GetString(aPrefName, aFileLocation))) { + return NS_OK; + } + + if (aEnvVarName && *aEnvVarName) { + char* prefValue = PR_GetEnv(aEnvVarName); + if (prefValue && *prefValue) { + // the pref is in the system charset and it's a filepath... The + // natural way to do the charset conversion is by just initing + // an nsIFile with the native path and asking it for the Unicode + // version. + nsresult rv; + nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->InitWithNativePath(nsDependentCString(prefValue)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->GetPath(aFileLocation); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + } + + return Preferences::GetString(aPrefName, aFileLocation); +} + +/* Get the mime.types file names from prefs and look up info in them + based on extension */ +// static +nsresult nsOSHelperAppService::LookUpTypeAndDescription( + const nsAString& aFileExtension, nsAString& aMajorType, + nsAString& aMinorType, nsAString& aDescription, bool aUserData) { + LOG(("-- LookUpTypeAndDescription for extension '%s'\n", + NS_LossyConvertUTF16toASCII(aFileExtension).get())); + nsAutoString mimeFileName; + + const char* filenamePref = aUserData ? "helpers.private_mime_types_file" + : "helpers.global_mime_types_file"; + + nsresult rv = GetFileLocation(filenamePref, nullptr, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetTypeAndDescriptionFromMimetypesFile( + mimeFileName, aFileExtension, aMajorType, aMinorType, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +inline bool IsNetscapeFormat(const nsACString& aBuffer) { + return StringBeginsWith( + aBuffer, + nsLiteralCString( + "#--Netscape Communications Corporation MIME Information")) || + StringBeginsWith(aBuffer, "#--MCOM MIME Information"_ns); +} + +/* + * Create a file stream and line input stream for the filename. + * Leaves the first line of the file in aBuffer and sets the format to + * true for netscape files and false for normail ones + */ +// static +nsresult nsOSHelperAppService::CreateInputStream( + const nsAString& aFilename, nsIFileInputStream** aFileInputStream, + nsILineInputStream** aLineInputStream, nsACString& aBuffer, + bool* aNetscapeFormat, bool* aMore) { + LOG(("-- CreateInputStream")); + nsresult rv = NS_OK; + + nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = file->InitWithPath(aFilename); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFileInputStream> fileStream( + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = fileStream->Init(file, -1, -1, false); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsILineInputStream> lineStream(do_QueryInterface(fileStream, &rv)); + + if (NS_FAILED(rv)) { + LOG(("Interface trouble in stream land!")); + return rv; + } + + rv = lineStream->ReadLine(aBuffer, aMore); + if (NS_FAILED(rv)) { + fileStream->Close(); + return rv; + } + + *aNetscapeFormat = IsNetscapeFormat(aBuffer); + + *aFileInputStream = fileStream; + NS_ADDREF(*aFileInputStream); + *aLineInputStream = lineStream; + NS_ADDREF(*aLineInputStream); + + return NS_OK; +} + +/* Open the file, read the first line, decide what type of file it is, + then get info based on extension */ +// static +nsresult nsOSHelperAppService::GetTypeAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aFileExtension, + nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription) { + LOG(("-- GetTypeAndDescriptionFromMimetypesFile\n")); + LOG(("Getting type and description from types file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using extension '%s'\n", + NS_LossyConvertUTF16toASCII(aFileExtension).get())); + nsCOMPtr<nsIFileInputStream> mimeFile; + nsCOMPtr<nsILineInputStream> mimeTypes; + bool netscapeFormat; + nsAutoString buf; + nsAutoCString cBuf; + bool more = false; + nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), + getter_AddRefs(mimeTypes), cBuf, + &netscapeFormat, &more); + + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsAutoStringN<101> entry; + nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG(("Bogus entry; trying 'normal' mode\n")); + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG(("Bogus entry; trying 'Netscape' mode\n")); + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv)) { // entry parses + nsAString::const_iterator start, end; + extensions.BeginReading(start); + extensions.EndReading(end); + nsAString::const_iterator iter(start); + + while (start != end) { + FindCharInReadable(',', iter, end); + if (Substring(start, iter) + .Equals(aFileExtension, + nsCaseInsensitiveStringComparator)) { + // it's a match. Assign the type and description and run + aMajorType.Assign(Substring(majorTypeStart, majorTypeEnd)); + aMinorType.Assign(Substring(minorTypeStart, minorTypeEnd)); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } + if (iter != end) { + ++iter; + } + start = iter; + } + } else { + LOG(("Failed to parse entry: %s\n", + NS_LossyConvertUTF16toASCII(entry).get())); + } + // truncate the entry for the next iteration + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* Get the mime.types file names from prefs and look up info in them + based on mimetype */ +// static +nsresult nsOSHelperAppService::LookUpExtensionsAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aFileExtensions, nsAString& aDescription) { + LOG(("-- LookUpExtensionsAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsAutoString mimeFileName; + + nsresult rv = + GetFileLocation("helpers.private_mime_types_file", nullptr, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile( + mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + if (NS_FAILED(rv) || aFileExtensions.IsEmpty()) { + rv = GetFileLocation("helpers.global_mime_types_file", nullptr, + mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile( + mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + } + return rv; +} + +/* Open the file, read the first line, decide what type of file it is, + then get info based on extension */ +// static +nsresult nsOSHelperAppService::GetExtensionsAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aFileExtensions, + nsAString& aDescription) { + LOG(("-- GetExtensionsAndDescriptionFromMimetypesFile\n")); + LOG(("Getting extensions and description from types file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsCOMPtr<nsIFileInputStream> mimeFile; + nsCOMPtr<nsILineInputStream> mimeTypes; + bool netscapeFormat; + nsAutoCString cBuf; + nsAutoString buf; + bool more = false; + nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), + getter_AddRefs(mimeTypes), cBuf, + &netscapeFormat, &more); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsAutoStringN<101> entry; + nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG(("Bogus entry; trying 'normal' mode\n")); + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG(("Bogus entry; trying 'Netscape' mode\n")); + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, majorTypeEnd) + .Equals(aMajorType, nsCaseInsensitiveStringComparator) && + Substring(minorTypeStart, minorTypeEnd) + .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { + // it's a match + aFileExtensions.Assign(extensions); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } + if (NS_FAILED(rv)) { + LOG(("Failed to parse entry: %s\n", + NS_LossyConvertUTF16toASCII(entry).get())); + } + + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* + * This parses a Netscape format mime.types entry. There are two + * possible formats: + * + * type=foo/bar; options exts="baz" description="Some type" + * + * and + * + * type=foo/bar; options description="Some type" exts="baz" + */ +// static +nsresult nsOSHelperAppService::ParseNetscapeMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG(("-- ParseNetscapeMIMETypesEntry\n")); + NS_ASSERTION(!aEntry.IsEmpty(), + "Empty Netscape MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, match_start, match_end; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); + // if we're pointing to a quote, don't advance -- we don't want to + // include the quote.... + if (*end_iter != '"') ++end_iter; + match_start = start_iter; + match_end = end_iter; + + // Get the major and minor types + // First the major type + if (!FindInReadable(u"type="_ns, match_start, match_end)) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && *match_end != '/') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMajorTypeStart = match_start; + aMajorTypeEnd = match_end; + + // now the minor type + if (++match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && !nsCRT::IsAsciiSpace(*match_end) && + *match_end != ';') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMinorTypeStart = match_start; + aMinorTypeEnd = match_end; + + // ignore everything up to the end of the mime type from here on + start_iter = match_end; + + // get the extensions + match_start = match_end; + match_end = end_iter; + if (FindInReadable(u"exts="_ns, match_start, match_end)) { + nsAString::const_iterator extStart, extEnd; + + if (match_end == end_iter || + (*match_end == '"' && ++match_end == end_iter)) { + return NS_ERROR_FAILURE; + } + + extStart = match_end; + match_start = extStart; + match_end = end_iter; + if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { + // exts= before desc=, so we have to find the actual end of the extensions + extEnd = match_start; + if (extEnd == extStart) { + return NS_ERROR_FAILURE; + } + + do { + --extEnd; + } while (extEnd != extStart && nsCRT::IsAsciiSpace(*extEnd)); + + if (extEnd != extStart && *extEnd == '"') { + --extEnd; + } + } else { + // desc= before exts=, so we can use end_iter as the end of the extensions + extEnd = end_iter; + } + aExtensions = Substring(extStart, extEnd); + } else { + // no extensions + aExtensions.Truncate(); + } + + // get the description + match_start = start_iter; + match_end = end_iter; + if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { + aDescriptionStart = match_end; + match_start = aDescriptionStart; + match_end = end_iter; + if (FindInReadable(u"exts="_ns, match_start, match_end)) { + // exts= after desc=, so have to find actual end of description + aDescriptionEnd = match_start; + if (aDescriptionEnd == aDescriptionStart) { + return NS_ERROR_FAILURE; + } + + do { + --aDescriptionEnd; + } while (aDescriptionEnd != aDescriptionStart && + nsCRT::IsAsciiSpace(*aDescriptionEnd)); + } else { + // desc= after exts=, so use end_iter for the description end + aDescriptionEnd = end_iter; + } + } else { + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + } + + return NS_OK; +} + +/* + * This parses a normal format mime.types entry. The format is: + * + * major/minor ext1 ext2 ext3 + */ +// static +nsresult nsOSHelperAppService::ParseNormalMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG(("-- ParseNormalMIMETypesEntry\n")); + NS_ASSERTION(!aEntry.IsEmpty(), + "Empty Normal MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, iter; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + + // skip leading whitespace + while (start_iter != end_iter && nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + if (start_iter == end_iter) { + return NS_ERROR_FAILURE; + } + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); + + ++end_iter; // point to first whitespace char (or to end of string) + iter = start_iter; + + // get the major type + if (!FindCharInReadable('/', iter, end_iter)) return NS_ERROR_FAILURE; + + nsAString::const_iterator equals_sign_iter(start_iter); + if (FindCharInReadable('=', equals_sign_iter, iter)) + return NS_ERROR_FAILURE; // see bug 136670 + + aMajorTypeStart = start_iter; + aMajorTypeEnd = iter; + + // get the minor type + if (++iter == end_iter) { + return NS_ERROR_FAILURE; + } + start_iter = iter; + + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aMinorTypeStart = start_iter; + aMinorTypeEnd = iter; + + // get the extensions + aExtensions.Truncate(); + while (iter != end_iter) { + while (iter != end_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + start_iter = iter; + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aExtensions.Append(Substring(start_iter, iter)); + if (iter != end_iter) { // not the last extension + aExtensions.Append(char16_t(',')); + } + } + + return NS_OK; +} + +// static +nsresult nsOSHelperAppService::LookUpHandlerAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags) { + // The mailcap lookup is two-pass to handle the case of mailcap files + // that have something like: + // + // text/*; emacs %s + // text/rtf; soffice %s + // + // in that order. We want to pick up "soffice" for text/rtf in such cases + nsresult rv = DoLookUpHandlerAndDescription( + aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags, true); + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, aMinorType, aHandler, + aDescription, aMozillaFlags, false); + } + + // maybe we have an entry for "aMajorType/*"? + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, + aDescription, aMozillaFlags, true); + } + + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, + aDescription, aMozillaFlags, false); + } + + return rv; +} + +// static +nsresult nsOSHelperAppService::DoLookUpHandlerAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags, + bool aUserData) { + LOG(("-- LookUpHandlerAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsAutoString mailcapFileName; + + const char* filenamePref = aUserData ? "helpers.private_mailcap_file" + : "helpers.global_mailcap_file"; + const char* filenameEnvVar = aUserData ? "PERSONAL_MAILCAP" : "MAILCAP"; + + nsresult rv = GetFileLocation(filenamePref, filenameEnvVar, mailcapFileName); + if (NS_SUCCEEDED(rv) && !mailcapFileName.IsEmpty()) { + rv = GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, aMajorType, + aMinorType, aHandler, + aDescription, aMozillaFlags); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +// static +nsresult nsOSHelperAppService::GetHandlerAndDescriptionFromMailcapFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, + nsAString& aMozillaFlags) { + LOG(("-- GetHandlerAndDescriptionFromMailcapFile\n")); + LOG(("Getting handler and description from mailcap file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + + nsresult rv = NS_OK; + bool more = false; + + nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = file->InitWithPath(aFilename); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFileInputStream> mailcapFile( + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = mailcapFile->Init(file, -1, -1, false); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsILineInputStream> mailcap(do_QueryInterface(mailcapFile, &rv)); + + if (NS_FAILED(rv)) { + LOG(("Interface trouble in stream land!")); + return rv; + } + + nsAutoStringN<129> entry; + nsAutoStringN<81> buffer; + nsAutoCStringN<81> cBuffer; + rv = mailcap->ReadLine(cBuffer, &more); + if (NS_FAILED(rv)) { + mailcapFile->Close(); + return rv; + } + + do { // return on end-of-file in the loop + + CopyASCIItoUTF16(cBuffer, buffer); + if (!buffer.IsEmpty() && buffer.First() != '#') { + entry.Append(buffer); + if (entry.Last() == '\\') { // entry continues on next line + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry in entry. Check it for the type + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + + nsAString::const_iterator semicolon_iter, start_iter, end_iter, + majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd; + entry.BeginReading(start_iter); + entry.EndReading(end_iter); + semicolon_iter = start_iter; + FindSemicolon(semicolon_iter, end_iter); + if (semicolon_iter != + end_iter) { // we have something resembling a valid entry + rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, semicolon_iter); + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, majorTypeEnd) + .Equals(aMajorType, nsCaseInsensitiveStringComparator) && + Substring(minorTypeStart, minorTypeEnd) + .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { + // we have a match + bool match = true; + ++semicolon_iter; // point at the first char past the semicolon + start_iter = semicolon_iter; // handler string starts here + FindSemicolon(semicolon_iter, end_iter); + while (start_iter != semicolon_iter && + nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + + LOG(("The real handler is: '%s'\n", + NS_LossyConvertUTF16toASCII( + Substring(start_iter, semicolon_iter)) + .get())); + + // XXX ugly hack. Just grab the executable name + nsAString::const_iterator end_handler_iter = semicolon_iter; + nsAString::const_iterator end_executable_iter = start_iter; + while (end_executable_iter != end_handler_iter && + !nsCRT::IsAsciiSpace(*end_executable_iter)) { + ++end_executable_iter; + } + // XXX End ugly hack + + aHandler = Substring(start_iter, end_executable_iter); + + nsAString::const_iterator start_option_iter, end_optionname_iter, + equal_sign_iter; + bool equalSignFound; + while (match && semicolon_iter != end_iter && + ++semicolon_iter != + end_iter) { // there are options left and we still match + start_option_iter = semicolon_iter; + // skip over leading whitespace + while (start_option_iter != end_iter && + nsCRT::IsAsciiSpace(*start_option_iter)) { + ++start_option_iter; + } + if (start_option_iter == end_iter) { // nothing actually here + break; + } + semicolon_iter = start_option_iter; + FindSemicolon(semicolon_iter, end_iter); + equal_sign_iter = start_option_iter; + equalSignFound = false; + while (equal_sign_iter != semicolon_iter && !equalSignFound) { + switch (*equal_sign_iter) { + case '\\': + equal_sign_iter.advance(2); + break; + case '=': + equalSignFound = true; + break; + default: + ++equal_sign_iter; + break; + } + } + end_optionname_iter = start_option_iter; + // find end of option name + while (end_optionname_iter != equal_sign_iter && + !nsCRT::IsAsciiSpace(*end_optionname_iter)) { + ++end_optionname_iter; + } + nsDependentSubstring optionName(start_option_iter, + end_optionname_iter); + if (equalSignFound) { + // This is an option that has a name and value + if (optionName.EqualsLiteral("description")) { + aDescription = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("x-mozilla-flags")) { + aMozillaFlags = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("test")) { + nsAutoCString testCommand; + rv = UnescapeCommand( + Substring(++equal_sign_iter, semicolon_iter), aMajorType, + aMinorType, testCommand); + if (NS_FAILED(rv)) continue; + nsCOMPtr<nsIProcess> process = + do_CreateInstance(NS_PROCESS_CONTRACTID, &rv); + if (NS_FAILED(rv)) continue; + nsCOMPtr<nsIFile> file( + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) continue; + rv = file->InitWithNativePath("/bin/sh"_ns); + if (NS_FAILED(rv)) continue; + rv = process->Init(file); + if (NS_FAILED(rv)) continue; + const char* args[] = {"-c", testCommand.get()}; + LOG(("Running Test: %s\n", testCommand.get())); + rv = process->Run(true, args, 2); + if (NS_FAILED(rv)) continue; + int32_t exitValue; + rv = process->GetExitValue(&exitValue); + if (NS_FAILED(rv)) continue; + LOG(("Exit code: %d\n", exitValue)); + if (exitValue) { + match = false; + } + } + } else { + // This is an option that just has a name but no value (eg + // "copiousoutput") + if (optionName.EqualsLiteral("needsterminal")) { + match = false; + } + } + } + + if (match) { // we did not fail any test clauses; all is good + // get out of here + mailcapFile->Close(); + return NS_OK; + } + // pretend that this match never happened + aDescription.Truncate(); + aMozillaFlags.Truncate(); + aHandler.Truncate(); + } + } + // zero out the entry for the next cycle + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + rv = mailcap->ReadLine(cBuffer, &more); + } while (NS_SUCCEEDED(rv)); + mailcapFile->Close(); + return rv; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsresult rv = NS_OK; + + if (!XRE_IsContentProcess()) { +#ifdef MOZ_WIDGET_GTK + // Check the GNOME registry for a protocol handler + *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme); +#else + *aHandlerExists = false; +#endif + } else { + *aHandlerExists = false; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && handlerSvc) { + rv = handlerSvc->ExistsForProtocolOS(nsCString(aProtocolScheme), + aHandlerExists); + } + } + + return rv; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription( + const nsACString& aScheme, nsAString& _retval) { +#ifdef MOZ_WIDGET_GTK + nsGNOMERegistry::GetAppDescForScheme(aScheme, _retval); + return _retval.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK; +#else + return NS_ERROR_NOT_AVAILABLE; +#endif +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + *_retval = false; + return NS_OK; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath( + const char16_t* platformAppPath, nsIFile** aFile) { + LOG(("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n", + NS_LossyConvertUTF16toASCII(platformAppPath).get())); + if (!*platformAppPath) { // empty filename--return error + NS_WARNING("Empty filename passed in."); + return NS_ERROR_INVALID_ARG; + } + + // first check if the base class implementation finds anything + nsresult rv = + nsExternalHelperAppService::GetFileTokenForPath(platformAppPath, aFile); + if (NS_SUCCEEDED(rv)) return rv; + // If the reason for failure was that the file doesn't exist, return too + // (because it means the path was absolute, and so that we shouldn't search in + // the path) + if (rv == NS_ERROR_FILE_NOT_FOUND) return rv; + + // If we get here, we really should have a relative path. + NS_ASSERTION(*platformAppPath != char16_t('/'), "Unexpected absolute path"); + + nsCOMPtr<nsIFile> localFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID)); + + if (!localFile) return NS_ERROR_NOT_INITIALIZED; + + bool exists = false; + // ugly hack. Walk the PATH variable... + char* unixpath = PR_GetEnv("PATH"); + nsAutoCString path(unixpath); + + const char* start_iter = path.BeginReading(start_iter); + const char* colon_iter = start_iter; + const char* end_iter = path.EndReading(end_iter); + + while (start_iter != end_iter && !exists) { + while (colon_iter != end_iter && *colon_iter != ':') { + ++colon_iter; + } + localFile->InitWithNativePath(Substring(start_iter, colon_iter)); + rv = localFile->AppendRelativePath(nsDependentString(platformAppPath)); + // Failing AppendRelativePath is a bad thing - it should basically always + // succeed given a relative path. Show a warning if it does fail. + // To prevent infinite loops when it does fail, return at this point. + NS_ENSURE_SUCCESS(rv, rv); + localFile->Exists(&exists); + if (!exists) { + if (colon_iter == end_iter) { + break; + } + ++colon_iter; + start_iter = colon_iter; + } + } + + if (exists) { + rv = NS_OK; + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + *aFile = localFile; + NS_IF_ADDREF(*aFile); + + return rv; +} + +already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromExtension( + const nsCString& aFileExt) { + // if the extension is empty, return immediately + if (aFileExt.IsEmpty()) return nullptr; + + LOG(("Here we do an extension lookup for '%s'\n", aFileExt.get())); + + nsAutoString majorType, minorType, mime_types_description, + mailcap_description, handler, mozillaFlags; + + nsresult rv = + LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, + minorType, mime_types_description, true); + + if (NS_FAILED(rv) || majorType.IsEmpty()) { +#ifdef MOZ_WIDGET_GTK + LOG(("Looking in GNOME registry\n")); + RefPtr<nsMIMEInfoBase> gnomeInfo = + nsGNOMERegistry::GetFromExtension(aFileExt); + if (gnomeInfo) { + LOG(("Got MIMEInfo from GNOME registry\n")); + return gnomeInfo.forget(); + } +#endif + + rv = LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, + minorType, mime_types_description, false); + } + + if (NS_FAILED(rv)) return nullptr; + + NS_LossyConvertUTF16toASCII asciiMajorType(majorType); + NS_LossyConvertUTF16toASCII asciiMinorType(minorType); + + LOG( + ("Type/Description results: majorType='%s', minorType='%s', " + "description='%s'\n", + asciiMajorType.get(), asciiMinorType.get(), + NS_LossyConvertUTF16toASCII(mime_types_description).get())); + + if (majorType.IsEmpty() && minorType.IsEmpty()) { + // we didn't get a type mapping, so we can't do anything useful + return nullptr; + } + + nsAutoCString mimeType(asciiMajorType + "/"_ns + asciiMinorType); + RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(mimeType); + + mimeInfo->AppendExtension(aFileExt); + rv = LookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags); + LOG( + ("Handler/Description results: handler='%s', description='%s', " + "mozillaFlags='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get(), + NS_LossyConvertUTF16toASCII(mozillaFlags).get())); + mailcap_description.Trim(" \t\""); + mozillaFlags.Trim(" \t"); + if (!mime_types_description.IsEmpty()) { + mimeInfo->SetDescription(mime_types_description); + } else { + mimeInfo->SetDescription(mailcap_description); + } + + if (NS_SUCCEEDED(rv) && handler.IsEmpty()) { + rv = NS_ERROR_NOT_AVAILABLE; + } + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIFile> handlerFile; + rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); + + if (NS_SUCCEEDED(rv)) { + mimeInfo->SetDefaultApplication(handlerFile); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + mimeInfo->SetDefaultDescription(handler); + } + } + + if (NS_FAILED(rv)) { + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + return mimeInfo.forget(); +} + +already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromType( + const nsCString& aMIMEType) { + // if the type is empty, return immediately + if (aMIMEType.IsEmpty()) return nullptr; + + LOG(("Here we do a mimetype lookup for '%s'\n", aMIMEType.get())); + + // extract the major and minor types + NS_ConvertASCIItoUTF16 mimeType(aMIMEType); + nsAString::const_iterator start_iter, end_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd; + + mimeType.BeginReading(start_iter); + mimeType.EndReading(end_iter); + + // XXX FIXME: add typeOptions parsing in here + nsresult rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, end_iter); + + if (NS_FAILED(rv)) { + return nullptr; + } + + nsDependentSubstring majorType(majorTypeStart, majorTypeEnd); + nsDependentSubstring minorType(minorTypeStart, minorTypeEnd); + + // First check the user's private mailcap file + nsAutoString mailcap_description, handler, mozillaFlags; + DoLookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags, true); + + LOG(("Private Handler/Description results: handler='%s', description='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get())); + + // Now look up our extensions + nsAutoString extensions, mime_types_description; + LookUpExtensionsAndDescription(majorType, minorType, extensions, + mime_types_description); + +#ifdef MOZ_WIDGET_GTK + if (handler.IsEmpty()) { + RefPtr<nsMIMEInfoBase> gnomeInfo = nsGNOMERegistry::GetFromType(aMIMEType); + if (gnomeInfo) { + LOG( + ("Got MIMEInfo from GNOME registry without extensions; setting them " + "to %s\n", + NS_LossyConvertUTF16toASCII(extensions).get())); + + NS_ASSERTION(!gnomeInfo->HasExtensions(), "How'd that happen?"); + gnomeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); + return gnomeInfo.forget(); + } + } +#endif + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags, false); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, + mailcap_description, mozillaFlags, true); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, + mailcap_description, mozillaFlags, false); + } + + LOG( + ("Handler/Description results: handler='%s', description='%s', " + "mozillaFlags='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get(), + NS_LossyConvertUTF16toASCII(mozillaFlags).get())); + + mailcap_description.Trim(" \t\""); + mozillaFlags.Trim(" \t"); + + if (handler.IsEmpty() && extensions.IsEmpty() && + mailcap_description.IsEmpty() && mime_types_description.IsEmpty()) { + // No real useful info + return nullptr; + } + + RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(aMIMEType); + + mimeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); + if (!mime_types_description.IsEmpty()) { + mimeInfo->SetDescription(mime_types_description); + } else { + mimeInfo->SetDescription(mailcap_description); + } + + rv = NS_ERROR_NOT_AVAILABLE; + nsCOMPtr<nsIFile> handlerFile; + if (!handler.IsEmpty()) { + rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); + } + + if (NS_SUCCEEDED(rv)) { + mimeInfo->SetDefaultApplication(handlerFile); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + mimeInfo->SetDefaultDescription(handler); + } else { + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + return mimeInfo.forget(); +} + +nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType, + const nsACString& aFileExt, + bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aFound = true; + RefPtr<nsMIMEInfoBase> retval; + // Fallback to lookup by extension when generic 'application/octet-stream' + // content type is received. + if (!aType.EqualsLiteral(APPLICATION_OCTET_STREAM)) { + retval = GetFromType(PromiseFlatCString(aType)); + } + bool hasDefault = false; + if (retval) retval->GetHasDefaultHandler(&hasDefault); + if (!retval || !hasDefault) { + RefPtr<nsMIMEInfoBase> miByExt = + GetFromExtension(PromiseFlatCString(aFileExt)); + // If we had no extension match, but a type match, use that + if (!miByExt && retval) { + retval.forget(aMIMEInfo); + return NS_OK; + } + // If we had an extension match but no type match, set the mimetype and use + // it + if (!retval && miByExt) { + if (!aType.IsEmpty()) miByExt->SetMIMEType(aType); + miByExt.swap(retval); + + retval.forget(aMIMEInfo); + return NS_OK; + } + // If we got nothing, make a new mimeinfo + if (!retval) { + *aFound = false; + retval = new nsMIMEInfoUnix(aType); + if (retval) { + if (!aFileExt.IsEmpty()) retval->AppendExtension(aFileExt); + } + + retval.forget(aMIMEInfo); + return NS_OK; + } + + // Copy the attributes of retval (mimeinfo from type) onto miByExt, to + // return it + // but reset to just collected mDefaultAppDescription (from ext) + nsAutoString byExtDefault; + miByExt->GetDefaultDescription(byExtDefault); + retval->SetDefaultDescription(byExtDefault); + retval->CopyBasicDataTo(miByExt); + + miByExt.swap(retval); + } + retval.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = + OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoUnix* handlerInfo = + new nsMIMEInfoUnix(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.h b/uriloader/exthandler/unix/nsOSHelperAppService.h new file mode 100644 index 0000000000..ffa83f6a23 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.h @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsIFileInputStream; +class nsILineInputStream; +class nsMIMEInfoBase; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + virtual ~nsOSHelperAppService(); + + // method overrides for mime.types and mime.info look up steps + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMimeType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + // override nsIExternalProtocolService methods + nsresult OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + protected: + already_AddRefed<nsMIMEInfoBase> GetFromType(const nsCString& aMimeType); + already_AddRefed<nsMIMEInfoBase> GetFromExtension(const nsCString& aFileExt); + + private: + // Helper methods which have to access static members + static nsresult UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand); + static nsresult GetFileLocation(const char* aPrefName, + const char* aEnvVarName, + nsAString& aFileLocation); + static nsresult LookUpTypeAndDescription(const nsAString& aFileExtension, + nsAString& aMajorType, + nsAString& aMinorType, + nsAString& aDescription, + bool aUserData); + static nsresult CreateInputStream(const nsAString& aFilename, + nsIFileInputStream** aFileInputStream, + nsILineInputStream** aLineInputStream, + nsACString& aBuffer, bool* aNetscapeFormat, + bool* aMore); + + static nsresult GetTypeAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aFileExtension, + nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription); + + static nsresult LookUpExtensionsAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult GetExtensionsAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult ParseNetscapeMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult ParseNormalMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult LookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags); + + static nsresult DoLookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags, + bool aUserData); + + static nsresult GetHandlerAndDescriptionFromMailcapFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, + nsAString& aMozillaFlags); +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.cpp b/uriloader/exthandler/win/nsMIMEInfoWin.cpp new file mode 100644 index 0000000000..9f36a97ff9 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp @@ -0,0 +1,900 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsArrayEnumerator.h" +#include "nsCOMArray.h" +#include "nsLocalFile.h" +#include "nsMIMEInfoWin.h" +#include "nsNetUtil.h" +#include <windows.h> +#include <shellapi.h> +#include "nsIMutableArray.h" +#include "nsTArray.h" +#include "shlobj.h" +#include "windows.h" +#include "nsIWindowsRegKey.h" +#include "nsUnicharUtils.h" +#include "nsITextToSubURI.h" +#include "nsVariant.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/ShellHeaderOnlyUtils.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/UrlmonHeaderOnlyUtils.h" +#include "mozilla/UniquePtrExtensions.h" + +#define RUNDLL32_EXE L"\\rundll32.exe" + +NS_IMPL_ISUPPORTS_INHERITED(nsMIMEInfoWin, nsMIMEInfoBase, nsIPropertyBag) + +nsMIMEInfoWin::~nsMIMEInfoWin() {} + +nsresult nsMIMEInfoWin::LaunchDefaultWithFile(nsIFile* aFile) { + // Launch the file, unless it is an executable. + bool executable = true; + aFile->IsExecutable(&executable); + if (executable) return NS_ERROR_FAILURE; + + return aFile->Launch(); +} + +nsresult nsMIMEInfoWin::ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc, + const wchar_t** aArgv) { + nsresult rv; + + NS_ASSERTION(aArgc >= 1, "aArgc must be at least 1"); + + nsAutoString execPath; + rv = aExecutable->GetTarget(execPath); + if (NS_FAILED(rv) || execPath.IsEmpty()) { + rv = aExecutable->GetPath(execPath); + } + if (NS_FAILED(rv)) { + return rv; + } + + auto assembledArgs = mozilla::MakeCommandLine(aArgc, aArgv); + if (!assembledArgs) { + return NS_ERROR_FILE_EXECUTION_FAILED; + } + + _bstr_t execPathBStr(execPath.get()); + // Pass VT_ERROR/DISP_E_PARAMNOTFOUND to omit an optional RPC parameter + // to execute a file with the default verb. + _variant_t verbDefault(DISP_E_PARAMNOTFOUND, VT_ERROR); + _variant_t workingDir; + _variant_t showCmd(SW_SHOWNORMAL); + + // Ask Explorer to ShellExecute on our behalf, as some applications such as + // Skype for Business do not start correctly when inheriting our process's + // migitation policies. + // It does not work in a special environment such as Citrix. In such a case + // we fall back to launching an application as a child process. We need to + // find a way to handle the combination of these interop issues. + mozilla::LauncherVoidResult shellExecuteOk = mozilla::ShellExecuteByExplorer( + execPathBStr, assembledArgs.get(), verbDefault, workingDir, showCmd); + if (shellExecuteOk.isErr()) { + // No need to pass assembledArgs to LaunchWithIProcess. aArgv will be + // processed in nsProcess::RunProcess. + return LaunchWithIProcess(aExecutable, aArgc, + reinterpret_cast<const char16_t**>(aArgv)); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::LaunchWithFile(nsIFile* aFile) { + nsresult rv; + + // it doesn't make any sense to call this on protocol handlers + NS_ASSERTION(mClass == eMIMEInfo, + "nsMIMEInfoBase should have mClass == eMIMEInfo"); + + if (mPreferredAction == useSystemDefault) { + if (mDefaultApplication && + StaticPrefs::browser_pdf_launchDefaultEdgeAsApp()) { + // Since Edgium is the default handler for PDF and other kinds of files, + // if we're using the OS default and it's Edgium prefer its app mode so it + // operates as a viewer (without browser toolbars). Bug 1632277. + nsAutoCString defaultAppExecutable; + rv = mDefaultApplication->GetNativeLeafName(defaultAppExecutable); + if (NS_SUCCEEDED(rv) && + defaultAppExecutable.LowerCaseEqualsLiteral("msedge.exe")) { + nsAutoString path; + rv = aFile->GetPath(path); + if (NS_SUCCEEDED(rv)) { + // If the --app flag doesn't work we'll want to fallback to a + // regular path. Send two args so we call `msedge.exe --app={path} + // {path}`. + nsAutoString appArg; + appArg.AppendLiteral("--app="); + appArg.Append(path); + const wchar_t* argv[] = {appArg.get(), path.get()}; + + return ShellExecuteWithIFile(mDefaultApplication, + mozilla::ArrayLength(argv), argv); + } + } + } + return LaunchDefaultWithFile(aFile); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND; + + // at the moment, we only know how to hand files off to local handlers + nsCOMPtr<nsILocalHandlerApp> localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + // Deal with local dll based handlers + nsCString filename; + executable->GetNativeLeafName(filename); + if (filename.Length() > 4) { + nsCString extension(Substring(filename, filename.Length() - 4, 4)); + + if (extension.LowerCaseEqualsLiteral(".dll")) { + nsAutoString args; + + // executable is rundll32, everything else is a list of parameters, + // including the dll handler. + if (!GetDllLaunchInfo(executable, aFile, args, false)) + return NS_ERROR_INVALID_ARG; + + WCHAR rundll32Path[MAX_PATH + sizeof(RUNDLL32_EXE) / sizeof(WCHAR) + + 1] = {L'\0'}; + if (!GetSystemDirectoryW(rundll32Path, MAX_PATH)) { + return NS_ERROR_FILE_NOT_FOUND; + } + lstrcatW(rundll32Path, RUNDLL32_EXE); + + SHELLEXECUTEINFOW seinfo; + memset(&seinfo, 0, sizeof(seinfo)); + seinfo.cbSize = sizeof(SHELLEXECUTEINFOW); + seinfo.fMask = 0; + seinfo.hwnd = nullptr; + seinfo.lpVerb = nullptr; + seinfo.lpFile = rundll32Path; + seinfo.lpParameters = args.get(); + seinfo.lpDirectory = nullptr; + seinfo.nShow = SW_SHOWNORMAL; + if (ShellExecuteExW(&seinfo)) return NS_OK; + + switch ((LONG_PTR)seinfo.hInstApp) { + case 0: + case SE_ERR_OOM: + return NS_ERROR_OUT_OF_MEMORY; + case SE_ERR_ACCESSDENIED: + return NS_ERROR_FILE_ACCESS_DENIED; + case SE_ERR_ASSOCINCOMPLETE: + case SE_ERR_NOASSOC: + return NS_ERROR_UNEXPECTED; + case SE_ERR_DDEBUSY: + case SE_ERR_DDEFAIL: + case SE_ERR_DDETIMEOUT: + return NS_ERROR_NOT_AVAILABLE; + case SE_ERR_DLLNOTFOUND: + return NS_ERROR_FAILURE; + case SE_ERR_SHARE: + return NS_ERROR_FILE_IS_LOCKED; + default: + switch (GetLastError()) { + case ERROR_FILE_NOT_FOUND: + return NS_ERROR_FILE_NOT_FOUND; + case ERROR_PATH_NOT_FOUND: + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + case ERROR_BAD_FORMAT: + return NS_ERROR_FILE_CORRUPTED; + } + } + return NS_ERROR_FILE_EXECUTION_FAILED; + } + } + nsAutoString path; + aFile->GetPath(path); + const wchar_t* argv[] = {path.get()}; + return ShellExecuteWithIFile(executable, mozilla::ArrayLength(argv), argv); + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetHasDefaultHandler(bool* _retval) { + // We have a default application if we have a description + // We can ShellExecute anything; however, callers are probably interested if + // there is really an application associated with this type of file + *_retval = !mDefaultAppDescription.IsEmpty(); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetEnumerator(nsISimpleEnumerator** _retval) { + nsCOMArray<nsIVariant> properties; + + nsCOMPtr<nsIVariant> variant; + GetProperty(u"defaultApplicationIconURL"_ns, getter_AddRefs(variant)); + if (variant) properties.AppendObject(variant); + + GetProperty(u"customApplicationIconURL"_ns, getter_AddRefs(variant)); + if (variant) properties.AppendObject(variant); + + return NS_NewArrayEnumerator(_retval, properties, NS_GET_IID(nsIVariant)); +} + +static nsresult GetIconURLVariant(nsIFile* aApplication, nsIVariant** _retval) { + nsAutoCString fileURLSpec; + NS_GetURLSpecFromFile(aApplication, fileURLSpec); + nsAutoCString iconURLSpec; + iconURLSpec.AssignLiteral("moz-icon://"); + iconURLSpec += fileURLSpec; + RefPtr<nsVariant> writable(new nsVariant()); + writable->SetAsAUTF8String(iconURLSpec); + writable.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetProperty(const nsAString& aName, nsIVariant** _retval) { + nsresult rv; + if (mDefaultApplication && + aName.EqualsLiteral(PROPERTY_DEFAULT_APP_ICON_URL)) { + rv = GetIconURLVariant(mDefaultApplication, _retval); + NS_ENSURE_SUCCESS(rv, rv); + } else if (mPreferredApplication && + aName.EqualsLiteral(PROPERTY_CUSTOM_APP_ICON_URL)) { + nsCOMPtr<nsILocalHandlerApp> localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetIconURLVariant(executable, _retval); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// this implementation was pretty much copied verbatime from +// Tony Robinson's code in nsExternalProtocolWin.cpp +nsresult nsMIMEInfoWin::LoadUriInternal(nsIURI* aURL) { + nsresult rv = NS_OK; + + // 1. Find the default app for this protocol + // 2. Set up the command line + // 3. Launch the app. + + // For now, we'll just cheat essentially, check for the command line + // then just call ShellExecute()! + + if (aURL) { + // extract the url spec from the url + nsAutoCString urlSpec; + aURL->GetAsciiSpec(urlSpec); + + // Unescape non-ASCII characters in the URL + nsAutoString utf16Spec; + + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_FAILED(textToSubURI->UnEscapeNonAsciiURI("UTF-8"_ns, urlSpec, + utf16Spec))) { + CopyASCIItoUTF16(urlSpec, utf16Spec); + } + + // Ask the shell/urlmon to parse |utf16Spec| to avoid malformed URLs. + // Failure is indicative of a potential security issue so we should + // bail out if so. + LauncherResult<_bstr_t> validatedUri = UrlmonValidateUri(utf16Spec.get()); + if (validatedUri.isErr()) { + return NS_ERROR_FAILURE; + } + + _variant_t args; + _variant_t verb(L"open"); + _variant_t workingDir; + _variant_t showCmd(SW_SHOWNORMAL); + + // To open a uri, we first try ShellExecuteByExplorer, which starts a new + // process as a child process of explorer.exe, because applications may not + // support the mitigation policies inherited from our process. If it fails, + // we fall back to ShellExecuteExW. + // + // For Thunderbird, however, there is a known issue that + // ShellExecuteByExplorer succeeds but explorer.exe shows an error popup + // if a uri to open includes credentials. This does not happen in Firefox + // because Firefox does not have to launch a process to open a uri. + // + // Since Thunderbird does not use mitigation policies which could cause + // compatibility issues, we get no benefit from using + // ShellExecuteByExplorer. Thus we skip it and go straight to + // ShellExecuteExW for Thunderbird. +#ifndef MOZ_THUNDERBIRD + mozilla::LauncherVoidResult shellExecuteOk = + mozilla::ShellExecuteByExplorer(validatedUri.inspect(), args, verb, + workingDir, showCmd); + if (shellExecuteOk.isOk()) { + return NS_OK; + } +#endif // MOZ_THUNDERBIRD + + SHELLEXECUTEINFOW sinfo = {sizeof(sinfo)}; + sinfo.fMask = SEE_MASK_NOASYNC; + sinfo.lpVerb = V_BSTR(&verb); + sinfo.nShow = showCmd; + sinfo.lpFile = validatedUri.inspect(); + + BOOL result = ShellExecuteExW(&sinfo); + if (!result || reinterpret_cast<LONG_PTR>(sinfo.hInstApp) < 32) { + rv = NS_ERROR_FAILURE; + } + } + + return rv; +} + +// Given a path to a local file, return its nsILocalHandlerApp instance. +bool nsMIMEInfoWin::GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr<nsILocalHandlerApp>& aApp) { + nsCOMPtr<nsIFile> locfile; + nsresult rv = NS_NewLocalFile(aCommandHandler, true, getter_AddRefs(locfile)); + if (NS_FAILED(rv)) return false; + + aApp = do_CreateInstance("@mozilla.org/uriloader/local-handler-app;1"); + if (!aApp) return false; + + aApp->SetExecutable(locfile); + return true; +} + +// Return the cleaned up file path associated with a command verb +// located in root/Applications. +bool nsMIMEInfoWin::GetAppsVerbCommandHandler(const nsAString& appExeName, + nsAString& applicationPath, + bool edit) { + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe + nsAutoString applicationsPath; + applicationsPath.AppendLiteral("Applications\\"); + applicationsPath.Append(appExeName); + + nsresult rv = + appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + // Check for the NoOpenWith flag, if it exists + uint32_t value; + if (NS_SUCCEEDED(appKey->ReadIntValue(u"NoOpenWith"_ns, &value)) && + value == 1) + return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Return a fully populated command string based on +// passing information. Used in launchWithFile to trace +// back to the full handler path based on the dll. +// (dll, targetfile, return args, open/edit) +bool nsMIMEInfoWin::GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile, + nsAString& args, bool edit) { + if (!aDll || !aFile) return false; + + nsString appExeName; + aDll->GetLeafName(appExeName); + + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe + nsAutoString applicationsPath; + applicationsPath.AppendLiteral("Applications\\"); + applicationsPath.Append(appExeName); + + nsresult rv = + appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + // Check for the NoOpenWith flag, if it exists + uint32_t value; + rv = appKey->ReadIntValue(u"NoOpenWith"_ns, &value); + if (NS_SUCCEEDED(rv) && value == 1) return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Replace embedded environment variables. + uint32_t bufLength = + ::ExpandEnvironmentStringsW(appFilesystemCommand.get(), nullptr, 0); + if (bufLength == 0) // Error + return false; + + auto destination = mozilla::MakeUniqueFallible<wchar_t[]>(bufLength); + if (!destination) return false; + if (!::ExpandEnvironmentStringsW(appFilesystemCommand.get(), + destination.get(), bufLength)) + return false; + + appFilesystemCommand.Assign(destination.get()); + + // C:\Windows\System32\rundll32.exe "C:\Program Files\Windows + // Photo Gallery\PhotoViewer.dll", ImageView_Fullscreen %1 + nsAutoString params; + constexpr auto rundllSegment = u"rundll32.exe "_ns; + int32_t index = appFilesystemCommand.Find(rundllSegment); + if (index > kNotFound) { + params.Append( + Substring(appFilesystemCommand, index + rundllSegment.Length())); + } else { + params.Append(appFilesystemCommand); + } + + // check to make sure we have a %1 and fill it + constexpr auto percentOneParam = u"%1"_ns; + index = params.Find(percentOneParam); + if (index == kNotFound) // no parameter + return false; + + nsString target; + aFile->GetTarget(target); + params.Replace(index, 2, target); + + args = params; + + return true; + } + return false; +} + +// Return the cleaned up file path associated with a progid command +// verb located in root. +bool nsMIMEInfoWin::GetProgIDVerbCommandHandler(const nsAString& appProgIDName, + nsAString& applicationPath, + bool edit) { + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + nsAutoString appProgId(appProgIDName); + + // HKEY_CLASSES_ROOT\Windows.XPSReachViewer\shell\open\command + if (!edit) + appProgId.AppendLiteral("\\shell\\open\\command"); + else + appProgId.AppendLiteral("\\shell\\edit\\command"); + + nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, appProgId, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Helper routine used in tracking app lists. Converts path +// entries to lower case and stores them in the trackList array. +void nsMIMEInfoWin::ProcessPath(nsCOMPtr<nsIMutableArray>& appList, + nsTArray<nsString>& trackList, + const nsAString& appFilesystemCommand) { + nsAutoString lower(appFilesystemCommand); + ToLowerCase(lower); + + // Don't include firefox.exe in the list + WCHAR exe[MAX_PATH + 1]; + uint32_t len = GetModuleFileNameW(nullptr, exe, MAX_PATH); + if (len < MAX_PATH && len != 0) { + int32_t index = lower.Find(exe); + if (index != -1) return; + } + + nsCOMPtr<nsILocalHandlerApp> aApp; + if (!GetLocalHandlerApp(appFilesystemCommand, aApp)) return; + + // Save in our main tracking arrays + appList->AppendElement(aApp); + trackList.AppendElement(lower); +} + +// Helper routine that handles a compare between a path +// and an array of paths. +static bool IsPathInList(nsAString& appPath, nsTArray<nsString>& trackList) { + // trackList data is always lowercase, see ProcessPath + // above. + nsAutoString tmp(appPath); + ToLowerCase(tmp); + + for (uint32_t i = 0; i < trackList.Length(); i++) { + if (tmp.Equals(trackList[i])) return true; + } + return false; +} + +/** + * Returns a list of nsILocalHandlerApp objects containing local + * handlers associated with this mimeinfo. Implemented per + * platform using information in this object to generate the + * best list. Typically used for an "open with" style user + * option. + * + * @return nsIArray of nsILocalHandlerApp + */ +NS_IMETHODIMP +nsMIMEInfoWin::GetPossibleLocalHandlers(nsIArray** _retval) { + nsresult rv; + + *_retval = nullptr; + + nsCOMPtr<nsIMutableArray> appList = do_CreateInstance("@mozilla.org/array;1"); + + if (!appList) return NS_ERROR_FAILURE; + + nsTArray<nsString> trackList; + + nsAutoCString fileExt; + GetPrimaryExtension(fileExt); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_FAILURE; + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return NS_ERROR_FAILURE; + + nsAutoString workingRegistryPath; + + bool extKnown = false; + if (fileExt.IsEmpty()) { + extKnown = true; + // Mime type discovery is possible in some cases, through + // HKEY_CLASSES_ROOT\MIME\Database\Content Type, however, a number + // of file extensions related to mime type are simply not defined, + // (application/rss+xml & application/atom+xml are good examples) + // in which case we can only provide a generic list. + nsAutoCString mimeType; + GetMIMEType(mimeType); + if (!mimeType.IsEmpty()) { + workingRegistryPath.AppendLiteral("MIME\\Database\\Content Type\\"); + workingRegistryPath.Append(NS_ConvertASCIItoUTF16(mimeType)); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString mimeFileExt; + if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, mimeFileExt))) { + CopyUTF16toUTF8(mimeFileExt, fileExt); + extKnown = false; + } + } + } + } + + nsAutoString fileExtToUse; + if (!fileExt.IsEmpty() && fileExt.First() != '.') { + fileExtToUse = char16_t('.'); + } + fileExtToUse.Append(NS_ConvertUTF8toUTF16(fileExt)); + + // Note, the order in which these occur has an effect on the + // validity of the resulting display list. + + if (!extKnown) { + // 1) Get the default handler if it exists + workingRegistryPath = fileExtToUse; + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString appProgId; + if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, appProgId))) { + // Bug 358297 - ignore the embedded internet explorer handler + if (appProgId != u"XPSViewer.Document"_ns) { + nsAutoString appFilesystemCommand; + if (GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) && + !IsPathInList(appFilesystemCommand, trackList)) { + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + regKey->Close(); + } + + // 2) list HKEY_CLASSES_ROOT\.ext\OpenWithList + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 3) List HKEY_CLASSES_ROOT\.ext\OpenWithProgids, with the + // different step of resolving the progids for the command handler. + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + // HKEY_CLASSES_ROOT\.ext\OpenWithProgids\Windows.XPSReachViewer + nsAutoString appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 4) Add any non configured applications located in the MRU list + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithList + workingRegistryPath = nsLiteralString( + u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName, appValue; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + if (appName.EqualsLiteral("MRUList")) continue; + if (NS_FAILED(regKey->ReadStringValue(appName, appValue))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appValue, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + // 5) Add any non configured progids in the MRU list, with the + // different step of resolving the progids for the command handler. + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithProgids + workingRegistryPath = nsLiteralString( + u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appIndex, appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 6) Check the perceived type value, and use this to lookup the + // perceivedtype open with list. + // http://msdn2.microsoft.com/en-us/library/aa969373.aspx + + workingRegistryPath = fileExtToUse; + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString perceivedType; + rv = regKey->ReadStringValue(u"PerceivedType"_ns, perceivedType); + if (NS_SUCCEEDED(rv)) { + nsAutoString openWithListPath(u"SystemFileAssociations\\"_ns); + openWithListPath.Append(perceivedType); // no period + openWithListPath.AppendLiteral("\\OpenWithList"); + + nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + openWithListPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + } + } + } // extKnown == false + + // 7) list global HKEY_CLASSES_ROOT\*\OpenWithList + // Listing general purpose handlers, not specific to a mime type or file + // extension + + workingRegistryPath = u"*\\OpenWithList"_ns; + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 8) General application's list - not file extension specific on windows + workingRegistryPath = u"Applications"_ns; + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath, + nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS | + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetChildCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetChildName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + // Return to the caller + *_retval = appList; + NS_ADDREF(*_retval); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::IsCurrentAppOSDefault(bool* _retval) { + *_retval = false; + if (mDefaultApplication) { + // Determine if the default executable is our executable. + nsCOMPtr<nsIFile> ourBinary; + XRE_GetBinaryPath(getter_AddRefs(ourBinary)); + bool isSame = false; + nsresult rv = mDefaultApplication->Equals(ourBinary, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + } + return NS_OK; +} diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.h b/uriloader/exthandler/win/nsMIMEInfoWin.h new file mode 100644 index 0000000000..1de1f152ad --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.h @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoWin_h_ +#define nsMIMEInfoWin_h_ + +#include "nsMIMEInfoImpl.h" +#include "nsIPropertyBag.h" +#include "nsIMutableArray.h" +#include "nsTArray.h" + +class nsMIMEInfoWin : public nsMIMEInfoBase, public nsIPropertyBag { + virtual ~nsMIMEInfoWin(); + + public: + explicit nsMIMEInfoWin(const char* aType = "") : nsMIMEInfoBase(aType) {} + explicit nsMIMEInfoWin(const nsACString& aMIMEType) + : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoWin(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoBase(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + NS_IMETHOD GetPossibleLocalHandlers(nsIArray** _retval) override; + NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROPERTYBAG + + void SetDefaultApplicationHandler(nsIFile* aDefaultApplication) { + mDefaultApplication = aDefaultApplication; + } + + protected: + virtual nsresult LoadUriInternal(nsIURI* aURI); + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile); + + private: + nsCOMPtr<nsIFile> mDefaultApplication; + + // Given a path to a local handler, return its + // nsILocalHandlerApp instance. + bool GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr<nsILocalHandlerApp>& aApp); + + // Return the cleaned up file path associated + // with a command verb located in root/Applications. + bool GetAppsVerbCommandHandler(const nsAString& appExeName, + nsAString& applicationPath, bool bEdit); + + // Return the cleaned up file path associated + // with a progid command verb located in root. + bool GetProgIDVerbCommandHandler(const nsAString& appProgIDName, + nsAString& applicationPath, bool bEdit); + + // Lookup a rundll command handler and return + // a populated command template for use with rundll32.exe. + bool GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile, nsAString& args, + bool bEdit); + + // Helper routine used in tracking app lists + void ProcessPath(nsCOMPtr<nsIMutableArray>& appList, + nsTArray<nsString>& trackList, + const nsAString& appFilesystemCommand); + + // Helper routine to call mozilla::ShellExecuteByExplorer + nsresult ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc, + const wchar_t** aArgv); +}; + +#endif diff --git a/uriloader/exthandler/win/nsOSHelperAppService.cpp b/uriloader/exthandler/win/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..48c5ec64d8 --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp @@ -0,0 +1,589 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsComponentManagerUtils.h" +#include "nsOSHelperAppService.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsIMIMEInfo.h" +#include "nsMIMEInfoWin.h" +#include "nsMimeTypes.h" +#include "plstr.h" +#include "nsNativeCharsetUtils.h" +#include "nsLocalFile.h" +#include "nsIWindowsRegKey.h" +#include "nsXULAppAPI.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WindowsVersion.h" + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include <shellapi.h> +#include <shlwapi.h> + +#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args) + +// helper methods: forward declarations... +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension); + +nsOSHelperAppService::nsOSHelperAppService() + : nsExternalHelperAppService(), mAppAssoc(nullptr) { + CoInitialize(nullptr); + CoCreateInstance(CLSID_ApplicationAssociationRegistration, nullptr, + CLSCTX_INPROC, IID_IApplicationAssociationRegistration, + (void**)&mAppAssoc); +} + +nsOSHelperAppService::~nsOSHelperAppService() { + if (mAppAssoc) mAppAssoc->Release(); + mAppAssoc = nullptr; + CoUninitialize(); +} + +// The windows registry provides a mime database key which lists a set of mime +// types and corresponding "Extension" values. we can use this to look up our +// mime type to see if there is a preferred extension for the mime type. +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension) { + nsAutoString mimeDatabaseKey; + mimeDatabaseKey.AssignLiteral("MIME\\Database\\Content Type\\"); + + AppendASCIItoUTF16(aMimeType, mimeDatabaseKey); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, mimeDatabaseKey, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + + if (NS_SUCCEEDED(rv)) + regKey->ReadStringValue(u"Extension"_ns, aFileExtension); + + return NS_OK; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + // look up the protocol scheme in the windows registry....if we find a match + // then we have a handler for it... + *aHandlerExists = false; + if (aProtocolScheme && *aProtocolScheme) { + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + wchar_t* pResult = nullptr; + NS_ConvertASCIItoUTF16 scheme(aProtocolScheme); + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(scheme.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + CoTaskMemFree(pResult); + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + nsDependentString(scheme.get()), + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + // Open will fail if the registry key path doesn't exist. + return NS_OK; + } + + bool hasValue; + rv = regKey->HasValue(u"URL Protocol"_ns, &hasValue); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + if (!hasValue) { + return NS_OK; + } + + *aHandlerExists = true; + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription( + const nsACString& aScheme, nsAString& _retval) { + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + NS_ConvertASCIItoUTF16 buf(aScheme); + + if (mozilla::IsWin8OrLater()) { + wchar_t result[1024]; + DWORD resultSize = 1024; + HRESULT hr = AssocQueryString(0x1000 /* ASSOCF_IS_PROTOCOL */, + ASSOCSTR_FRIENDLYAPPNAME, buf.get(), NULL, + result, &resultSize); + if (SUCCEEDED(hr)) { + _retval = result; + return NS_OK; + } + } + + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + wchar_t* pResult = nullptr; + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + nsCOMPtr<nsIFile> app; + nsAutoString appInfo(pResult); + CoTaskMemFree(pResult); + if (NS_SUCCEEDED(GetDefaultAppInfo(appInfo, _retval, getter_AddRefs(app)))) + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + *_retval = false; + + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + + NS_ConvertASCIItoUTF16 buf(aScheme); + + // Find the progID + wchar_t* pResult = nullptr; + HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (FAILED(hr)) { + return NS_ERROR_FAILURE; + } + nsAutoString progID(pResult); + // We are responsible for freeing returned strings. + CoTaskMemFree(pResult); + + // Find the default executable. + nsAutoString description; + nsCOMPtr<nsIFile> appFile; + nsresult rv = GetDefaultAppInfo(progID, description, getter_AddRefs(appFile)); + if (NS_FAILED(rv)) { + return rv; + } + // Determine if the default executable is our executable. + nsCOMPtr<nsIFile> ourBinary; + XRE_GetBinaryPath(getter_AddRefs(ourBinary)); + bool isSame = false; + rv = appFile->Equals(ourBinary, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + return NS_OK; +} + +// GetMIMEInfoFromRegistry: This function obtains the values of some of the +// nsIMIMEInfo attributes for the mimeType/extension associated with the input +// registry key. The default entry for that key is the name of a registry key +// under HKEY_CLASSES_ROOT. The default value for *that* key is the descriptive +// name of the type. The EditFlags value is a binary value; the low order bit +// of the third byte of which indicates that the user does not need to be +// prompted. +// +// This function sets only the Description attribute of the input nsIMIMEInfo. +/* static */ +nsresult nsOSHelperAppService::GetMIMEInfoFromRegistry(const nsString& fileType, + nsIMIMEInfo* pInfo) { + nsresult rv = NS_OK; + + NS_ENSURE_ARG(pInfo); + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileType, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + // OK, the default value here is the description of the type. + nsAutoString description; + rv = regKey->ReadStringValue(u""_ns, description); + if (NS_SUCCEEDED(rv)) pInfo->SetDescription(description); + + return NS_OK; +} + +///////////////////////////////////////////////////////////////////////////////////////////////// +// method overrides used to gather information from the windows registry for +// various mime types. +//////////////////////////////////////////////////////////////////////////////////////////////// + +/// Looks up the type for the extension aExt and compares it to aType +/* static */ +bool nsOSHelperAppService::typeFromExtEquals(const char16_t* aExt, + const char* aType) { + if (!aType) return false; + nsAutoString fileExtToUse; + if (aExt[0] != char16_t('.')) fileExtToUse = char16_t('.'); + + fileExtToUse.Append(aExt); + + bool eq = false; + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return eq; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return eq; + + nsAutoString type; + rv = regKey->ReadStringValue(u"Content Type"_ns, type); + if (NS_SUCCEEDED(rv)) eq = type.LowerCaseEqualsASCII(aType); + + return eq; +} + +// The "real" name of a given helper app (as specified by the path to the +// executable file held in various registry keys) is stored n the VERSIONINFO +// block in the file's resources. We need to find the path to the executable +// and then retrieve the "FileDescription" field value from the file. +nsresult nsOSHelperAppService::GetDefaultAppInfo( + const nsAString& aAppInfo, nsAString& aDefaultDescription, + nsIFile** aDefaultApplication) { + nsAutoString handlerCommand; + + // If all else fails, use the file type key name, which will be + // something like "pngfile" for .pngs, "WMVFile" for .wmvs, etc. + aDefaultDescription = aAppInfo; + *aDefaultApplication = nullptr; + + if (aAppInfo.IsEmpty()) return NS_ERROR_FAILURE; + + // aAppInfo may be a file, file path, program id, or + // Applications reference - + // c:\dir\app.exe + // Applications\appfile.exe/dll (shell\open...) + // ProgID.progid (shell\open...) + + nsAutoString handlerKeyName(aAppInfo); + + nsCOMPtr<nsIWindowsRegKey> chkKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!chkKey) return NS_ERROR_FAILURE; + + nsresult rv = + chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + // It's a file system path to a handler + handlerCommand.Assign(aAppInfo); + } else { + handlerKeyName.AppendLiteral("\\shell\\open\\command"); + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_FAILURE; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + // OK, the default value here is the description of the type. + rv = regKey->ReadStringValue(u""_ns, handlerCommand); + if (NS_FAILED(rv)) { + // Check if there is a DelegateExecute string + nsAutoString delegateExecute; + rv = regKey->ReadStringValue(u"DelegateExecute"_ns, delegateExecute); + NS_ENSURE_SUCCESS(rv, rv); + + // Look for InProcServer32 + nsAutoString delegateExecuteRegPath; + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\InProcServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + rv = chkKey->ReadStringValue(u""_ns, handlerCommand); + } + + if (NS_FAILED(rv)) { + // Look for LocalServer32 + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\LocalServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + rv = chkKey->ReadStringValue(u""_ns, handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + // XXX FIXME: If this fails, the UI will display the full command + // string. + // There are some rare cases this can happen - ["url.dll" -foo] + // for example won't resolve correctly to the system dir. The + // subsequent launch of the helper app will work though. + nsCOMPtr<nsILocalFileWin> lf = new nsLocalFile(); + rv = lf->InitWithCommandLine(handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + lf.forget(aDefaultApplication); + + wchar_t friendlyName[1024]; + DWORD friendlyNameSize = 1024; + HRESULT hr = AssocQueryString(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, + PromiseFlatString(aAppInfo).get(), NULL, + friendlyName, &friendlyNameSize); + if (SUCCEEDED(hr) && friendlyNameSize > 1) { + aDefaultDescription.Assign(friendlyName, friendlyNameSize - 1); + } + + return NS_OK; +} + +already_AddRefed<nsMIMEInfoWin> nsOSHelperAppService::GetByExtension( + const nsString& aFileExt, const char* aTypeHint) { + if (aFileExt.IsEmpty()) return nullptr; + + // Determine the mime type. + nsAutoCString typeToUse; + if (aTypeHint && *aTypeHint) { + typeToUse.Assign(aTypeHint); + } else if (!GetMIMETypeFromOSForExtension(NS_ConvertUTF16toUTF8(aFileExt), + typeToUse)) { + return nullptr; + } + + RefPtr<nsMIMEInfoWin> mimeInfo = new nsMIMEInfoWin(typeToUse); + + // windows registry assumes your file extension is going to include the '.', + // but our APIs expect it to not be there, so make sure we normalize that bit. + nsAutoString fileExtToUse; + if (aFileExt.First() != char16_t('.')) fileExtToUse = char16_t('.'); + + fileExtToUse.Append(aFileExt); + + // don't append the '.' for our APIs. + nsAutoCString lowerFileExt = + NS_ConvertUTF16toUTF8(Substring(fileExtToUse, 1)); + ToLowerCase(lowerFileExt); + mimeInfo->AppendExtension(lowerFileExt); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + + nsAutoString appInfo; + bool found; + + // Retrieve the default application for this extension + NS_ENSURE_TRUE(mAppAssoc, nullptr); + nsString assocType(fileExtToUse); + wchar_t* pResult = nullptr; + HRESULT hr = mAppAssoc->QueryCurrentDefault(assocType.get(), AT_FILEEXTENSION, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + found = true; + appInfo.Assign(pResult); + CoTaskMemFree(pResult); + } else { + found = false; + } + + // Bug 358297 - ignore the default handler, force the user to choose app + if (appInfo.EqualsLiteral("XPSViewer.Document")) found = false; + + if (!found) { + return nullptr; + } + + // Get other nsIMIMEInfo fields from registry, if possible. + nsAutoString defaultDescription; + nsCOMPtr<nsIFile> defaultApplication; + + if (NS_FAILED(GetDefaultAppInfo(appInfo, defaultDescription, + getter_AddRefs(defaultApplication)))) { + return nullptr; + } + + mimeInfo->SetDefaultDescription(defaultDescription); + mimeInfo->SetDefaultApplicationHandler(defaultApplication); + + // Grab the general description + GetMIMEInfoFromRegistry(appInfo, mimeInfo); + + return mimeInfo.forget(); +} + +NS_IMETHODIMP +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound, nsIMIMEInfo** aMIMEInfo) { + *aFound = false; + + const nsCString& flatType = PromiseFlatCString(aMIMEType); + nsAutoString fileExtension; + CopyUTF8toUTF16(aFileExt, fileExtension); + + /* XXX The octet-stream check is a gross hack to wallpaper over the most + * common Win32 extension issues caused by the fix for bug 116938. See bug + * 120327, comment 271 for why this is needed. Not even sure we + * want to remove this once we have fixed all this stuff to work + * right; any info we get from the OS on this type is pretty much + * useless.... + */ + bool haveMeaningfulMimeType = + !aMIMEType.IsEmpty() && + !aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM); + LOG(("Extension lookup on '%s' with mimetype '%s'%s\n", fileExtension.get(), + flatType.get(), + haveMeaningfulMimeType ? " (treated as meaningful)" : "")); + + RefPtr<nsMIMEInfoWin> mi; + + // We should have *something* to go on here. + nsAutoString extensionFromMimeType; + if (haveMeaningfulMimeType) { + GetExtensionFromWindowsMimeDatabase(aMIMEType, extensionFromMimeType); + } + if (fileExtension.IsEmpty() && extensionFromMimeType.IsEmpty()) { + // Without an extension from the mimetype or the file, we can't + // do anything here. + mi = new nsMIMEInfoWin(flatType.get()); + mi.forget(aMIMEInfo); + return NS_OK; + } + + // Either fileExtension or extensionFromMimeType must now be non-empty. + + *aFound = true; + + // On Windows, we prefer the file extension for lookups over the mimetype, + // because that's how windows does things. + // If we have no file extension or it doesn't match the mimetype, use the + // mime type's default file extension instead. + bool usedMimeTypeExtensionForLookup = false; + if (fileExtension.IsEmpty() || + (!extensionFromMimeType.IsEmpty() && + !typeFromExtEquals(fileExtension.get(), flatType.get()))) { + usedMimeTypeExtensionForLookup = true; + fileExtension = extensionFromMimeType; + LOG(("Now using '%s' mimetype's default file extension '%s' for lookup\n", + flatType.get(), fileExtension.get())); + } + + // If we have an extension, use it for lookup: + mi = GetByExtension(fileExtension, flatType.get()); + LOG(("Extension lookup on '%s' found: 0x%p\n", fileExtension.get(), + mi.get())); + + if (mi) { + bool hasDefault = false; + mi->GetHasDefaultHandler(&hasDefault); + // If we don't find a default handler description, see if we can find one + // using the mimetype. + if (!hasDefault && !usedMimeTypeExtensionForLookup) { + RefPtr<nsMIMEInfoWin> miFromMimeType = + GetByExtension(extensionFromMimeType, flatType.get()); + LOG(("Mime-based ext. lookup for '%s' found 0x%p\n", + extensionFromMimeType.get(), miFromMimeType.get())); + if (miFromMimeType) { + nsAutoString desc; + miFromMimeType->GetDefaultDescription(desc); + mi->SetDefaultDescription(desc); + } + } + mi.forget(aMIMEInfo); + return NS_OK; + } + + // The extension didn't work. Try the extension from the mimetype if + // different: + if (!extensionFromMimeType.IsEmpty() && !usedMimeTypeExtensionForLookup) { + mi = GetByExtension(extensionFromMimeType, flatType.get()); + LOG(("Mime-based ext. lookup for '%s' found 0x%p\n", + extensionFromMimeType.get(), mi.get())); + } + if (mi) { + mi.forget(aMIMEInfo); + return NS_OK; + } + // This didn't work either, so just return an empty dummy mimeinfo. + *aFound = false; + mi = new nsMIMEInfoWin(flatType.get()); + // If we didn't resort to the mime type's extension, we must have had a + // valid extension, so stick its lowercase version on the mime info. + if (!usedMimeTypeExtensionForLookup) { + nsAutoCString lowerFileExt; + ToLowerCase(aFileExt, lowerFileExt); + mi->AppendExtension(lowerFileExt); + } + mi.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = + OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoWin* handlerInfo = + new nsMIMEInfoWin(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} + +bool nsOSHelperAppService::GetMIMETypeFromOSForExtension( + const nsACString& aExtension, nsACString& aMIMEType) { + if (aExtension.IsEmpty()) return false; + + // windows registry assumes your file extension is going to include the '.'. + // so make sure it's there... + nsAutoString fileExtToUse; + if (aExtension.First() != '.') fileExtToUse = char16_t('.'); + + AppendUTF8toUTF16(aExtension, fileExtToUse); + + // Try to get an entry from the windows registry. + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return false; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString mimeType; + if (NS_FAILED(regKey->ReadStringValue(u"Content Type"_ns, mimeType)) || + mimeType.IsEmpty()) { + return false; + } + // Content-Type is always in ASCII + aMIMEType.Truncate(); + LossyAppendUTF16toASCII(mimeType, aMIMEType); + return true; +} diff --git a/uriloader/exthandler/win/nsOSHelperAppService.h b/uriloader/exthandler/win/nsOSHelperAppService.h new file mode 100644 index 0000000000..0b5cbc21e5 --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" +#include <windows.h> + +#ifdef _WIN32_WINNT +# undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 +#include <shlobj.h> + +class nsMIMEInfoWin; +class nsIMIMEInfo; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + nsresult LoadUriInternal(nsIURI* aURL); + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + // method overrides for windows registry look up steps.... + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval); + virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension, + nsACString& aMIMEType) override; + + /** Get the string value of a registry value and store it in result. + * @return true on success, false on failure + */ + static bool GetValueString(HKEY hKey, const char16_t* pValueName, + nsAString& result); + + protected: + nsresult GetDefaultAppInfo(const nsAString& aTypeName, + nsAString& aDefaultDescription, + nsIFile** aDefaultApplication); + // Lookup a mime info by extension, using an optional type hint + already_AddRefed<nsMIMEInfoWin> GetByExtension( + const nsString& aFileExt, const char* aTypeHint = nullptr); + nsresult FindOSMimeInfoForType(const char* aMimeContentType, nsIURI* aURI, + char** aFileExtension, + nsIMIMEInfo** aMIMEInfo); + + static nsresult GetMIMEInfoFromRegistry(const nsString& fileType, + nsIMIMEInfo* pInfo); + /// Looks up the type for the extension aExt and compares it to aType + static bool typeFromExtEquals(const char16_t* aExt, const char* aType); + + private: + IApplicationAssociationRegistration* mAppAssoc; +}; + +#endif // nsOSHelperAppService_h__ |