diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /uriloader/exthandler | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'uriloader/exthandler')
100 files changed, 17123 insertions, 0 deletions
diff --git a/uriloader/exthandler/ContentHandlerService.cpp b/uriloader/exthandler/ContentHandlerService.cpp new file mode 100644 index 000000000..75575a730 --- /dev/null +++ b/uriloader/exthandler/ContentHandlerService.cpp @@ -0,0 +1,169 @@ +#include "ContentHandlerService.h" +#include "HandlerServiceChild.h" +#include "ContentChild.h" +#include "nsIMutableArray.h" +#include "nsIMIMEInfo.h" + +using mozilla::dom::ContentChild; +using mozilla::dom::PHandlerServiceChild; +using mozilla::dom::HandlerInfo; + +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 = static_cast<HandlerServiceChild*>(cpc->SendPHandlerServiceConstructor()); + 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)); + } + } + nsHandlerInfoAction action; + aInfo->GetPreferredAction(&action); + HandlerInfo info(type, isMIMEInfo, description, alwaysAskBeforeHandling, 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, nsIInterfaceRequestor *aWindowContext) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMPL_ISUPPORTS(RemoteHandlerApp, nsIHandlerApp) + +static inline void CopyHanderInfoTonsIHandlerInfo(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, false); +} +ContentHandlerService::~ContentHandlerService() +{ +} + +NS_IMETHODIMP ContentHandlerService::Enumerate(nsISimpleEnumerator * *_retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ContentHandlerService::FillHandlerInfo(nsIHandlerInfo *aHandlerInfo, const nsACString & aOverrideType) +{ + HandlerInfo info; + nsIHandlerInfoToHandlerInfo(aHandlerInfo, &info); + mHandlerServiceChild->SendFillHandlerInfo(info, nsCString(aOverrideType), &info); + CopyHanderInfoTonsIHandlerInfo(info, 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::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; +} + +} +} diff --git a/uriloader/exthandler/ContentHandlerService.h b/uriloader/exthandler/ContentHandlerService.h new file mode 100644 index 000000000..e39e89bc9 --- /dev/null +++ b/uriloader/exthandler/ContentHandlerService.h @@ -0,0 +1,52 @@ +#ifndef ContentHandlerService_h +#define ContentHandlerService_h + +#include "nsIHandlerService.h" +#include "nsClassHashtable.h" +#include "HandlerServiceChild.h" +#include "nsIMIMEInfo.h" + +#define NS_CONTENTHANDLERSERVICE_CID \ + {0xc4b6fb7c, 0xbfb1, 0x49dc, {0xa6, 0x5f, 0x03, 0x57, 0x96, 0x52, 0x4b, 0x53}} + +namespace mozilla { +namespace dom { + +class PHandlerServiceChild; + +class ContentHandlerService : public nsIHandlerService +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERSERVICE + + ContentHandlerService(); + MOZ_MUST_USE 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; +}; + + +} +} +#endif diff --git a/uriloader/exthandler/ExternalHelperAppChild.cpp b/uriloader/exthandler/ExternalHelperAppChild.cpp new file mode 100644 index 000000000..584e596b9 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppChild.cpp @@ -0,0 +1,127 @@ +/* -*- 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/net/ChannelDiverterChild.h" +#include "nsIDivertableChannel.h" +#include "nsIInputStream.h" +#include "nsIFTPChannel.h" +#include "nsIRequest.h" +#include "nsIResumableChannel.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, + nsISupports *ctx, + nsIInputStream *input, + uint64_t offset, + uint32_t count) +{ + if (NS_FAILED(mStatus)) + return mStatus; + + nsCString data; + nsresult rv = NS_ReadInputStreamToString(input, data, count); + if (NS_FAILED(rv)) + return rv; + + if (!SendOnDataAvailable(data, offset, count)) + return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////// +// nsIRequestObserver +////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +ExternalHelperAppChild::OnStartRequest(nsIRequest *request, nsISupports *ctx) +{ + nsCOMPtr<nsIDivertableChannel> divertable = do_QueryInterface(request); + if (divertable) { + return DivertToParent(divertable, request); + } + + nsresult rv = mHandler->OnStartRequest(request, ctx); + 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, + nsISupports *ctx, + nsresult status) +{ + // mHandler can be null if we diverted the request to the parent + if (mHandler) { + nsresult rv = mHandler->OnStopRequest(request, ctx, status); + SendOnStopRequest(status); + NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); + } + + return NS_OK; +} + +nsresult +ExternalHelperAppChild::DivertToParent(nsIDivertableChannel *divertable, + nsIRequest *request) +{ + // nsIDivertable must know about content conversions before being diverted. + MOZ_ASSERT(mHandler); + mHandler->MaybeApplyDecodingForExtension(request); + + mozilla::net::ChannelDiverterChild *diverter = nullptr; + nsresult rv = divertable->DivertToParent(&diverter); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(diverter); + + if (SendDivertToParentUsing(diverter)) { + mHandler->DidDivertRequest(request); + mHandler = nullptr; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +bool +ExternalHelperAppChild::RecvCancel(const nsresult& aStatus) +{ + mStatus = aStatus; + return true; +} + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/ExternalHelperAppChild.h b/uriloader/exthandler/ExternalHelperAppChild.h new file mode 100644 index 000000000..eecca01e6 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppChild.h @@ -0,0 +1,45 @@ +/* -*- 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 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; } + + virtual bool RecvCancel(const nsresult& aStatus) override; +private: + virtual ~ExternalHelperAppChild(); + MOZ_MUST_USE nsresult DivertToParent(nsIDivertableChannel *divertable, nsIRequest *request); + + 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 000000000..a8ddc54c8 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppParent.cpp @@ -0,0 +1,512 @@ +/* -*- 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/TabParent.h" +#include "nsIBrowserDOMWindow.h" +#include "nsStringStream.h" +#include "mozilla/ipc/URIUtils.h" +#include "nsNetUtil.h" +#include "nsIDocument.h" +#include "mozilla/net/ChannelDiverterParent.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( + const OptionalURIParams& uri, + const int64_t& aContentLength, + const bool& aWasFileChannel) + : mURI(DeserializeURI(uri)) + , mPending(false) +#ifdef DEBUG + , mDiverted(false) +#endif + , mIPCClosed(false) + , mLoadFlags(0) + , mStatus(NS_OK) + , mContentLength(aContentLength) + , mWasFileChannel(aWasFileChannel) +{ +} + +void +ExternalHelperAppParent::Init(ContentParent *parent, + const nsCString& aMimeContentType, + const nsCString& aContentDispositionHeader, + const uint32_t& aContentDispositionHint, + const nsString& aContentDispositionFilename, + const bool& aForceSave, + const OptionalURIParams& aReferrer, + PBrowserParent* aBrowser) +{ + nsCOMPtr<nsIExternalHelperAppService> helperAppService = + do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID); + NS_ASSERTION(helperAppService, "No Helper App Service!"); + + nsCOMPtr<nsIURI> referrer = DeserializeURI(aReferrer); + if (referrer) + SetPropertyAsInterface(NS_LITERAL_STRING("docshell.internalReferrer"), referrer); + + mContentDispositionHeader = aContentDispositionHeader; + if (!mContentDispositionHeader.IsEmpty()) { + NS_GetFilenameFromDisposition(mContentDispositionFilename, + mContentDispositionHeader, + mURI); + mContentDisposition = + NS_GetContentDispositionFromHeader(mContentDispositionHeader, this); + } + else { + mContentDisposition = aContentDispositionHint; + mContentDispositionFilename = aContentDispositionFilename; + } + + nsCOMPtr<nsIInterfaceRequestor> window; + if (aBrowser) { + TabParent* tabParent = TabParent::GetFrom(aBrowser); + if (tabParent->GetOwnerElement()) + window = do_QueryInterface(tabParent->GetOwnerElement()->OwnerDoc()->GetWindow()); + + bool isPrivate = false; + nsCOMPtr<nsILoadContext> loadContext = tabParent->GetLoadContext(); + loadContext->GetUsePrivateBrowsing(&isPrivate); + SetPrivate(isPrivate); + } + + helperAppService->DoContent(aMimeContentType, this, window, + aForceSave, nullptr, + getter_AddRefs(mListener)); +} + +void +ExternalHelperAppParent::ActorDestroy(ActorDestroyReason why) +{ + mIPCClosed = true; +} + +void +ExternalHelperAppParent::Delete() +{ + if (!mIPCClosed) { + Unused << Send__delete__(this); + } +} + +bool +ExternalHelperAppParent::RecvOnStartRequest(const nsCString& entityID) +{ + MOZ_ASSERT(!mDiverted, "child forwarding callbacks after request was diverted"); + + mEntityID = entityID; + mPending = true; + mStatus = mListener->OnStartRequest(this, nullptr); + return true; +} + +bool +ExternalHelperAppParent::RecvOnDataAvailable(const nsCString& data, + const uint64_t& offset, + const uint32_t& count) +{ + if (NS_FAILED(mStatus)) + return true; + + MOZ_ASSERT(!mDiverted, "child forwarding callbacks after request was diverted"); + MOZ_ASSERT(mPending, "must be pending!"); + + nsCOMPtr<nsIInputStream> stringStream; + DebugOnly<nsresult> rv = NS_NewByteInputStream(getter_AddRefs(stringStream), data.get(), count, NS_ASSIGNMENT_DEPEND); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create dependent string!"); + mStatus = mListener->OnDataAvailable(this, nullptr, stringStream, offset, count); + + return true; +} + +bool +ExternalHelperAppParent::RecvOnStopRequest(const nsresult& code) +{ + MOZ_ASSERT(!mDiverted, "child forwarding callbacks after request was diverted"); + + mPending = false; + mListener->OnStopRequest(this, nullptr, + (NS_SUCCEEDED(code) && NS_FAILED(mStatus)) ? mStatus : code); + Delete(); + return true; +} + +bool +ExternalHelperAppParent::RecvDivertToParentUsing(PChannelDiverterParent* diverter) +{ + MOZ_ASSERT(diverter); + auto p = static_cast<mozilla::net::ChannelDiverterParent*>(diverter); + p->DivertTo(this); +#ifdef DEBUG + mDiverted = true; +#endif + Unused << p->Send__delete__(p); + return true; +} + +// +// nsIStreamListener +// + +NS_IMETHODIMP +ExternalHelperAppParent::OnDataAvailable(nsIRequest *request, + nsISupports *ctx, + nsIInputStream *input, + uint64_t offset, + uint32_t count) +{ + MOZ_ASSERT(mDiverted); + return mListener->OnDataAvailable(request, ctx, input, offset, count); +} + +NS_IMETHODIMP +ExternalHelperAppParent::OnStartRequest(nsIRequest *request, nsISupports *ctx) +{ + MOZ_ASSERT(mDiverted); + return mListener->OnStartRequest(request, ctx); +} + +NS_IMETHODIMP +ExternalHelperAppParent::OnStopRequest(nsIRequest *request, + nsISupports *ctx, + nsresult status) +{ + MOZ_ASSERT(mDiverted); + nsresult rv = mListener->OnStopRequest(request, ctx, 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) +{ + mStatus = aStatus; + Unused << SendCancel(aStatus); + 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::Open2(nsIInputStream** aStream) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::AsyncOpen(nsIStreamListener *aListener, + nsISupports *aContext) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::AsyncOpen2(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::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) +{ + *aLoadInfo = nullptr; + 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 000000000..752da996d --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppParent.h @@ -0,0 +1,119 @@ +/* -*- 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 "nsIChannel.h" +#include "nsIMultiPartChannel.h" +#include "nsIResumableChannel.h" +#include "nsIStreamListener.h" +#include "nsHashPropertyBag.h" +#include "PrivateBrowsingChannel.h" + +namespace IPC { +class URI; +} // namespace IPC + +namespace mozilla { + +namespace ipc { +class OptionalURIParams; +} // namespace ipc + +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 +{ + typedef mozilla::ipc::OptionalURIParams OptionalURIParams; + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIMULTIPARTCHANNEL + NS_DECL_NSIRESUMABLECHANNEL + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + bool RecvOnStartRequest(const nsCString& entityID) override; + bool RecvOnDataAvailable(const nsCString& data, + const uint64_t& offset, + const uint32_t& count) override; + bool RecvOnStopRequest(const nsresult& code) override; + + bool RecvDivertToParentUsing(PChannelDiverterParent* diverter) override; + + bool WasFileChannel() override { + return mWasFileChannel; + } + + ExternalHelperAppParent(const OptionalURIParams& uri, const int64_t& contentLength, + const bool& wasFileChannel); + void Init(ContentParent *parent, + const nsCString& aMimeContentType, + const nsCString& aContentDisposition, + const uint32_t& aContentDispositionHint, + const nsString& aContentDispositionFilename, + const bool& aForceSave, + const OptionalURIParams& aReferrer, + PBrowserParent* aBrowser); + +protected: + virtual ~ExternalHelperAppParent(); + + virtual void ActorDestroy(ActorDestroyReason why) override; + void Delete(); + +private: + nsCOMPtr<nsIStreamListener> mListener; + nsCOMPtr<nsIURI> mURI; + bool mPending; +#ifdef DEBUG + bool mDiverted; +#endif + bool mIPCClosed; + nsLoadFlags mLoadFlags; + nsresult mStatus; + int64_t mContentLength; + bool mWasFileChannel; + uint32_t mContentDisposition; + nsString mContentDispositionFilename; + nsCString mContentDispositionHeader; + nsCString mEntityID; +}; + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/HandlerServiceChild.h b/uriloader/exthandler/HandlerServiceChild.h new file mode 100644 index 000000000..6ef6833a8 --- /dev/null +++ b/uriloader/exthandler/HandlerServiceChild.h @@ -0,0 +1,15 @@ +#ifndef handler_service_child_h +#define handler_service_child_h + +#include "mozilla/dom/PHandlerServiceChild.h" + +class HandlerServiceChild final : public mozilla::dom::PHandlerServiceChild +{ + public: + NS_INLINE_DECL_REFCOUNTING(HandlerServiceChild) + HandlerServiceChild() {} + private: + virtual ~HandlerServiceChild() {} +}; + +#endif diff --git a/uriloader/exthandler/HandlerServiceParent.cpp b/uriloader/exthandler/HandlerServiceParent.cpp new file mode 100644 index 000000000..3f9de04a8 --- /dev/null +++ b/uriloader/exthandler/HandlerServiceParent.cpp @@ -0,0 +1,270 @@ +#include "HandlerServiceParent.h" +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "ContentHandlerService.h" + +using mozilla::dom::HandlerInfo; +using mozilla::dom::HandlerApp; +using mozilla::dom::ContentHandlerService; +using mozilla::dom::RemoteHandlerApp; + +namespace { + +class ProxyHandlerInfo final : public nsIHandlerInfo { +public: + explicit ProxyHandlerInfo(const HandlerInfo& aHandlerInfo); + NS_DECL_ISUPPORTS; + NS_DECL_NSIHANDLERINFO; +protected: + ~ProxyHandlerInfo() {} + HandlerInfo mHandlerInfo; + nsHandlerInfoAction mPrefAction; + nsCOMPtr<nsIMutableArray> mPossibleApps; +}; + +NS_IMPL_ISUPPORTS(ProxyHandlerInfo, nsIHandlerInfo) + +ProxyHandlerInfo::ProxyHandlerInfo(const HandlerInfo& aHandlerInfo) : mHandlerInfo(aHandlerInfo), mPossibleApps(do_CreateInstance(NS_ARRAY_CONTRACTID)) +{ + for (auto& happ : aHandlerInfo.possibleApplicationHandlers()) { + mPossibleApps->AppendElement(new RemoteHandlerApp(happ), false); + } +} + +/* 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); + } + HandlerApp happ(name, detailedDescription); + mHandlerInfo = HandlerInfo(mHandlerInfo.type(), + mHandlerInfo.isMIMEInfo(), + mHandlerInfo.description(), + mHandlerInfo.alwaysAskBeforeHandling(), + happ, + mHandlerInfo.possibleApplicationHandlers(), + mHandlerInfo.preferredAction()); + 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 nsIInterfaceRequestor aWindowContext); */ +NS_IMETHODIMP ProxyHandlerInfo::LaunchWithURI(nsIURI *aURI, nsIInterfaceRequestor *aWindowContext) +{ + 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 = HandlerInfo(mHandlerInfo.type(), + mHandlerInfo.isMIMEInfo(), + mHandlerInfo.description(), + mHandlerInfo.alwaysAskBeforeHandling(), + mHandlerInfo.preferredApplicationHandler(), + mHandlerInfo.possibleApplicationHandlers(), + 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 = HandlerInfo(mHandlerInfo.type(), + mHandlerInfo.isMIMEInfo(), + mHandlerInfo.description(), + aAlwaysAskBeforeHandling, + mHandlerInfo.preferredApplicationHandler(), + mHandlerInfo.possibleApplicationHandlers(), + mHandlerInfo.preferredAction()); + return NS_OK; +} + + +class ProxyMIMEInfo : public nsIMIMEInfo +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMIMEINFO + NS_FORWARD_NSIHANDLERINFO(mProxyHandlerInfo->); + + explicit ProxyMIMEInfo(HandlerInfo aHandlerInfo) : mProxyHandlerInfo(new ProxyHandlerInfo(aHandlerInfo)) {} + +private: + virtual ~ProxyMIMEInfo() {} + nsCOMPtr<nsIHandlerInfo> mProxyHandlerInfo; + +protected: + /* additional members */ +}; + +NS_IMPL_ISUPPORTS(ProxyMIMEInfo, nsIMIMEInfo, nsIHandlerInfo) + +/* nsIUTF8StringEnumerator getFileExtensions (); */ +NS_IMETHODIMP ProxyMIMEInfo::GetFileExtensions(nsIUTF8StringEnumerator * *_retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* 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) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void appendExtension (in AUTF8String aExtension); */ +NS_IMETHODIMP ProxyMIMEInfo::AppendExtension(const nsACString & aExtension) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* attribute AUTF8String primaryExtension; */ +NS_IMETHODIMP ProxyMIMEInfo::GetPrimaryExtension(nsACString & aPrimaryExtension) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +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; +} + +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() +{ +} + +bool 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 true; +} + +bool 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 true; +} + +bool HandlerServiceParent::RecvGetTypeFromExtension(const nsCString& aFileExtension, + nsCString* type) +{ + nsCOMPtr<nsIHandlerService> handlerSvc = do_GetService(NS_HANDLERSERVICE_CONTRACTID); + handlerSvc->GetTypeFromExtension(aFileExtension, *type); + return true; +} + +void HandlerServiceParent::ActorDestroy(ActorDestroyReason aWhy) +{ +} diff --git a/uriloader/exthandler/HandlerServiceParent.h b/uriloader/exthandler/HandlerServiceParent.h new file mode 100644 index 000000000..37f586018 --- /dev/null +++ b/uriloader/exthandler/HandlerServiceParent.h @@ -0,0 +1,31 @@ +#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) + + private: + virtual ~HandlerServiceParent(); + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + + virtual bool RecvFillHandlerInfo(const HandlerInfo& aHandlerInfoData, + const nsCString& aOverrideType, + HandlerInfo* handlerInfoData) override; + virtual bool RecvExists(const HandlerInfo& aHandlerInfo, + bool* exits) override; + + virtual bool RecvGetTypeFromExtension(const nsCString& aFileExtension, + nsCString* type) override; + +}; + +#endif diff --git a/uriloader/exthandler/PExternalHelperApp.ipdl b/uriloader/exthandler/PExternalHelperApp.ipdl new file mode 100644 index 000000000..c7d526c19 --- /dev/null +++ b/uriloader/exthandler/PExternalHelperApp.ipdl @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; +include protocol PChannelDiverter; + +namespace mozilla { +namespace dom { + +protocol PExternalHelperApp +{ + manager PContent; + +parent: + async OnStartRequest(nsCString entityID); + async OnDataAvailable(nsCString data, uint64_t offset, uint32_t count); + async OnStopRequest(nsresult code); + + async DivertToParentUsing(PChannelDiverter diverter); + +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 000000000..bcaab60ac --- /dev/null +++ b/uriloader/exthandler/PHandlerService.ipdl @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; + HandlerApp preferredApplicationHandler; + HandlerApp[] possibleApplicationHandlers; + long preferredAction; +}; + +sync protocol PHandlerService +{ + manager PContent; + +parent: + sync FillHandlerInfo(HandlerInfo aHandlerInfoData, + nsCString aOverrideType) + returns (HandlerInfo handlerInfoData); + sync Exists(HandlerInfo aHandlerInfo) + returns (bool exists); + sync GetTypeFromExtension(nsCString aFileExtension) + returns (nsCString type); + async __delete__(); +}; + + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.cpp b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp new file mode 100644 index 000000000..4c7ffff48 --- /dev/null +++ b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp @@ -0,0 +1,91 @@ +/* -*- 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 "nsAndroidHandlerApp.h" +#include "GeneratedJNIWrappers.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; +} + +// XXX Workaround for bug 986975 to maintain the existing broken semantics +template<> +struct nsISharingHandlerApp::COMTypeInfo<nsAndroidHandlerApp, void> { + static const nsIID kIID; +}; +const nsIID nsISharingHandlerApp::COMTypeInfo<nsAndroidHandlerApp, void>::kIID = NS_IHANDLERAPP_IID; + +NS_IMETHODIMP +nsAndroidHandlerApp::Equals(nsIHandlerApp *aHandlerApp, bool *aRetval) +{ + nsCOMPtr<nsAndroidHandlerApp> aApp = do_QueryInterface(aHandlerApp); + *aRetval = aApp && aApp->mName.Equals(mName) && + aApp->mDescription.Equals(mDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::LaunchWithURI(nsIURI *aURI, nsIInterfaceRequestor *aWindowContext) +{ + nsCString uriSpec; + aURI->GetSpec(uriSpec); + return java::GeckoAppShell::OpenUriExternal( + uriSpec, mMimeType, mPackageName, mClassName, + mAction, EmptyString()) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::Share(const nsAString & data, const nsAString & title) +{ + return java::GeckoAppShell::OpenUriExternal( + data, mMimeType, mPackageName, mClassName, + mAction, EmptyString()) ? NS_OK : NS_ERROR_FAILURE; +} + diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.h b/uriloader/exthandler/android/nsAndroidHandlerApp.h new file mode 100644 index 000000000..bb3872463 --- /dev/null +++ b/uriloader/exthandler/android/nsAndroidHandlerApp.h @@ -0,0 +1,33 @@ +/* -*- Mode: c++; c-basic-offset: 4; 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 "nsIExternalSharingAppService.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/nsExternalSharingAppService.cpp b/uriloader/exthandler/android/nsExternalSharingAppService.cpp new file mode 100644 index 000000000..f4f8a7013 --- /dev/null +++ b/uriloader/exthandler/android/nsExternalSharingAppService.cpp @@ -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 "nsExternalSharingAppService.h" + +#include "mozilla/ModuleUtils.h" +#include "nsIClassInfoImpl.h" + +#include "AndroidBridge.h" +#include "nsArrayUtils.h" +#include "nsISupportsUtils.h" +#include "nsComponentManagerUtils.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsExternalSharingAppService, nsIExternalSharingAppService) + +nsExternalSharingAppService::nsExternalSharingAppService() +{ +} + +nsExternalSharingAppService::~nsExternalSharingAppService() +{ +} + +NS_IMETHODIMP +nsExternalSharingAppService::ShareWithDefault(const nsAString & data, + const nsAString & mime, + const nsAString & title) +{ + NS_NAMED_LITERAL_STRING(sendAction, "android.intent.action.SEND"); + const nsString emptyString = EmptyString(); + return java::GeckoAppShell::OpenUriExternal(data, + mime, emptyString, emptyString, sendAction, title) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsExternalSharingAppService::GetSharingApps(const nsAString & aMIMEType, + uint32_t *aLen, + nsISharingHandlerApp ***aHandlers) +{ + nsresult rv; + NS_NAMED_LITERAL_STRING(sendAction, "android.intent.action.SEND"); + nsCOMPtr<nsIMutableArray> array = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (!AndroidBridge::Bridge()) + return NS_OK; + AndroidBridge::Bridge()->GetHandlersForMimeType(aMIMEType, array, + nullptr, sendAction); + array->GetLength(aLen); + *aHandlers = + static_cast<nsISharingHandlerApp**>(moz_xmalloc(sizeof(nsISharingHandlerApp*) + * *aLen)); + for (uint32_t i = 0; i < *aLen; i++) { + rv = array->QueryElementAt(i, NS_GET_IID(nsISharingHandlerApp), + (void**)(*aHandlers + i)); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} diff --git a/uriloader/exthandler/android/nsExternalSharingAppService.h b/uriloader/exthandler/android/nsExternalSharingAppService.h new file mode 100644 index 000000000..a1e2e4363 --- /dev/null +++ b/uriloader/exthandler/android/nsExternalSharingAppService.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 NS_EXTERNAL_SHARING_APP_SERVICE_H +#define NS_EXTERNAL_SHARING_APP_SERVICE_H +#include "nsIExternalSharingAppService.h" + + +#define NS_EXTERNALSHARINGAPPSERVICE_CID \ + {0x93e2c46e, 0x0011, 0x434b, \ + {0x81, 0x2e, 0xb6, 0xf3, 0xa8, 0x1e, 0x2a, 0x58}} + +class nsExternalSharingAppService final + : public nsIExternalSharingAppService +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALSHARINGAPPSERVICE + + nsExternalSharingAppService(); + +private: + ~nsExternalSharingAppService(); + +}; + +#endif /*NS_EXTERNAL_SHARING_APP_SERVICE_H */ diff --git a/uriloader/exthandler/android/nsExternalURLHandlerService.cpp b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp new file mode 100644 index 000000000..f417b5b9f --- /dev/null +++ b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp @@ -0,0 +1,27 @@ +/* -*- 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) +{ + nsCString uriSpec; + aURL->GetSpec(uriSpec); + return nsMIMEInfoAndroid::GetMimeInfoForURL(uriSpec, found, info); +} diff --git a/uriloader/exthandler/android/nsExternalURLHandlerService.h b/uriloader/exthandler/android/nsExternalURLHandlerService.h new file mode 100644 index 000000000..f2618c7e6 --- /dev/null +++ b/uriloader/exthandler/android/nsExternalURLHandlerService.h @@ -0,0 +1,27 @@ +/* -*- Mode: c++; c-basic-offset: 4; 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" + +// {4BF1F8EF-D947-4BA3-9CD3-8C9A54A63A1C} +#define NS_EXTERNALURLHANDLERSERVICE_CID \ + {0x4bf1f8ef, 0xd947, 0x4ba3, {0x9c, 0xd3, 0x8c, 0x9a, 0x54, 0xa6, 0x3a, 0x1c}} + +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 000000000..ee9bc8570 --- /dev/null +++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp @@ -0,0 +1,427 @@ +/* -*- 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" + +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 = EmptyString(); + } else { + mimeType = NS_ConvertUTF8toUTF16(mType); + } + + if (java::GeckoAppShell::OpenUriExternal( + NS_ConvertUTF8toUTF16(uriSpec), mimeType, EmptyString(), + EmptyString(), EmptyString(), EmptyString())) { + 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, true)) + 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(EmptyString()); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::LaunchWithURI(nsIURI* aURI, nsIInterfaceRequestor* req) +{ + return mPrefApp->LaunchWithURI(aURI, req); +} + +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(); + nsCString extList(aExtensions); + + int32_t breakLocation = -1; + while ( (breakLocation = extList.FindChar(',')) != -1) + { + mExtensions.AppendElement(Substring(extList.get(), extList.get() + breakLocation)); + extList.Cut(0, breakLocation + 1); + } + if (!extList.IsEmpty()) + mExtensions.AppendElement(extList); + 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; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::AppendExtension(const nsACString & aExtension) +{ + mExtensions.AppendElement(aExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPrimaryExtension(nsACString & aPrimaryExtension) +{ + if (!mExtensions.Length()) + return NS_ERROR_NOT_INITIALIZED; + + aPrimaryExtension = mExtensions[0]; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetPrimaryExtension(const nsACString & aExtension) +{ + uint32_t extCount = mExtensions.Length(); + uint8_t i; + bool found = false; + for (i=0; i < extCount; i++) { + const nsCString& ext = mExtensions[i]; + if (ext.Equals(aExtension, nsCaseInsensitiveCStringComparator())) { + found = true; + break; + } + } + if (found) { + 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); +} + +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, false); +} + +NS_IMPL_ISUPPORTS(nsMIMEInfoAndroid::SystemChooser, nsIHandlerApp) + + +nsresult nsMIMEInfoAndroid::SystemChooser::GetName(nsAString & aName) { + aName.AssignLiteral(u"Android chooser"); + return NS_OK; +} + +nsresult +nsMIMEInfoAndroid::SystemChooser::SetName(const nsAString&) { + return NS_OK; +} + +nsresult +nsMIMEInfoAndroid::SystemChooser::GetDetailedDescription(nsAString & aDesc) { + aDesc.AssignLiteral(u"Android's default handler application chooser"); + return NS_OK; +} + +nsresult +nsMIMEInfoAndroid::SystemChooser::SetDetailedDescription(const nsAString&) { + return NS_OK; +} + +// XXX Workaround for bug 986975 to maintain the existing broken semantics +template<> +struct nsIHandlerApp::COMTypeInfo<nsMIMEInfoAndroid::SystemChooser, void> { + static const nsIID kIID; +}; +const nsIID nsIHandlerApp::COMTypeInfo<nsMIMEInfoAndroid::SystemChooser, void>::kIID = NS_IHANDLERAPP_IID; + +nsresult +nsMIMEInfoAndroid::SystemChooser::Equals(nsIHandlerApp *aHandlerApp, bool *aRetVal) { + nsCOMPtr<nsMIMEInfoAndroid::SystemChooser> info = do_QueryInterface(aHandlerApp); + if (info) + return mOuter->Equals(info->mOuter, aRetVal); + *aRetVal = false; + return NS_OK; +} + +nsresult +nsMIMEInfoAndroid::SystemChooser::LaunchWithURI(nsIURI* aURI, nsIInterfaceRequestor*) +{ + return mOuter->LoadUriInternal(aURI); +} diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.h b/uriloader/exthandler/android/nsMIMEInfoAndroid.h new file mode 100644 index 000000000..569d715bd --- /dev/null +++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.h @@ -0,0 +1,60 @@ +/* -*- 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: + static MOZ_MUST_USE bool + GetMimeInfoForMimeType(const nsACString& aMimeType, + nsMIMEInfoAndroid** aMimeInfo); + static MOZ_MUST_USE bool + GetMimeInfoForFileExt(const nsACString& aFileExt, + nsMIMEInfoAndroid** aMimeInfo); + + static MOZ_MUST_USE nsresult + GetMimeInfoForURL(const nsACString &aURL, bool *found, + nsIHandlerInfo **info); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMIMEINFO + NS_DECL_NSIHANDLERINFO + + nsMIMEInfoAndroid(const nsACString& aMIMEType); + +private: + ~nsMIMEInfoAndroid() {} + + virtual MOZ_MUST_USE nsresult LaunchDefaultWithFile(nsIFile* aFile); + virtual MOZ_MUST_USE 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 + 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 000000000..3a170dcf6 --- /dev/null +++ b/uriloader/exthandler/android/nsOSHelperAppService.cpp @@ -0,0 +1,67 @@ +/* -*- Mode: c++; c-basic-offset: 4; 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() +{ +} + +already_AddRefed<nsIMIMEInfo> +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound) +{ + 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); + + return mimeInfo.forget(); +} + +nsresult +nsOSHelperAppService::OSProtocolHandlerExists(const char* aScheme, + bool* aExists) +{ + *aExists = mozilla::AndroidBridge::Bridge()->GetHandlersForURL(NS_ConvertUTF8toUTF16(aScheme)); + return NS_OK; +} + +nsresult nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **info) +{ + return nsMIMEInfoAndroid::GetMimeInfoForURL(aScheme, found, info); +} + +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 000000000..4f3623894 --- /dev/null +++ b/uriloader/exthandler/android/nsOSHelperAppService.h @@ -0,0 +1,40 @@ +/* -*- Mode: c++; c-basic-offset: 4; 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(); + + virtual already_AddRefed<nsIMIMEInfo> + GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound); + + virtual MOZ_MUST_USE nsresult + OSProtocolHandlerExists(const char* aScheme, + bool* aExists); + + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **_retval); + + static nsIHandlerApp* + CreateAndroidHandlerApp(const nsAString& aName, + const nsAString& aDescription, + const nsAString& aPackageName, + const nsAString& aClassName, + const nsACString& aMimeType, + const nsAString& aAction = EmptyString()); +}; + +#endif /* nsOSHelperAppService_h */ diff --git a/uriloader/exthandler/gonk/nsOSHelperAppService.cpp b/uriloader/exthandler/gonk/nsOSHelperAppService.cpp new file mode 100644 index 000000000..d1342ec18 --- /dev/null +++ b/uriloader/exthandler/gonk/nsOSHelperAppService.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "nsOSHelperAppService.h" +#include "nsMIMEInfoImpl.h" + +class nsGonkMIMEInfo : public nsMIMEInfoImpl { +public: + nsGonkMIMEInfo(const nsACString& aMIMEType) : nsMIMEInfoImpl(aMIMEType) { } + +protected: + virtual nsresult LoadUriInternal(nsIURI *aURI) { + return NS_ERROR_NOT_IMPLEMENTED; + } +}; + +nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() +{ +} + +nsOSHelperAppService::~nsOSHelperAppService() +{ +} + +already_AddRefed<nsIMIMEInfo> +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound) +{ + *aFound = false; + // Even if we return false for aFound, we need to return a working + // nsIMIMEInfo implementation that will be used by the caller. + RefPtr<nsGonkMIMEInfo> mimeInfo = new nsGonkMIMEInfo(aMIMEType); + return mimeInfo.forget(); +} + +nsresult +nsOSHelperAppService::OSProtocolHandlerExists(const char* aScheme, + bool* aExists) +{ + *aExists = false; + return NS_OK; +} diff --git a/uriloader/exthandler/gonk/nsOSHelperAppService.h b/uriloader/exthandler/gonk/nsOSHelperAppService.h new file mode 100644 index 000000000..99a280bfc --- /dev/null +++ b/uriloader/exthandler/gonk/nsOSHelperAppService.h @@ -0,0 +1,39 @@ +/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef nsOSHelperAppService_h +#define nsOSHelperAppService_h + +#include "nsCExternalHandlerService.h" +#include "nsExternalHelperAppService.h" + +class nsOSHelperAppService : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + virtual already_AddRefed<nsIMIMEInfo> + GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound); + + virtual MOZ_MUST_USE nsresult + OSProtocolHandlerExists(const char* aScheme, + bool* aExists); +}; + +#endif /* nsOSHelperAppService_h */ diff --git a/uriloader/exthandler/mac/nsDecodeAppleFile.cpp b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp new file mode 100644 index 000000000..1a428a62d --- /dev/null +++ b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp @@ -0,0 +1,389 @@ +/* -*- 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 "nsDecodeAppleFile.h" +#include "prmem.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_THREADSAFE + +nsDecodeAppleFile::nsDecodeAppleFile() +{ + m_state = parseHeaders; + m_dataBufferLength = 0; + m_dataBuffer = (unsigned char*) PR_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() +{ + + PR_FREEIF(m_dataBuffer); + 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 000000000..cea2d701e --- /dev/null +++ b/uriloader/exthandler/mac/nsDecodeAppleFile.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDecodeAppleFile_h__ +#define nsDecodeAppleFile_h__ + +#include "nscore.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsILocalFileMac.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(); + + MOZ_MUST_USE 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 000000000..402ec5295 --- /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, nsIInterfaceRequestor* aWindowContext); + NS_IMETHOD GetName(nsAString& aName); +}; + +#endif /*NSLOCALHANDLERAPPMAC_H_*/ diff --git a/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm new file mode 100644 index 000000000..fd633ab72 --- /dev/null +++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#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, + nsIInterfaceRequestor *aWindowContext) +{ + 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 000000000..298357f75 --- /dev/null +++ b/uriloader/exthandler/mac/nsMIMEInfoMac.h @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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); + protected: + virtual MOZ_MUST_USE nsresult LoadUriInternal(nsIURI *aURI); +#ifdef DEBUG + virtual MOZ_MUST_USE nsresult LaunchDefaultWithFile(nsIFile* aFile) { + NS_NOTREACHED("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif + static MOZ_MUST_USE nsresult OpenApplicationWithURI(nsIFile *aApplication, + const nsCString& aURI); + + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription); + +}; + + +#endif diff --git a/uriloader/exthandler/mac/nsMIMEInfoMac.mm b/uriloader/exthandler/mac/nsMIMEInfoMac.mm new file mode 100644 index 000000000..64d3a82de --- /dev/null +++ b/uriloader/exthandler/mac/nsMIMEInfoMac.mm @@ -0,0 +1,114 @@ +/* -*- 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 "nsObjCExceptions.h" +#include "nsMIMEInfoMac.h" +#include "nsILocalFileMac.h" +#include "nsIFileURL.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 000000000..7371e1f42 --- /dev/null +++ b/uriloader/exthandler/mac/nsOSHelperAppService.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 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 "nsMIMEInfoImpl.h" +#include "nsCOMPtr.h" + +class nsOSHelperAppService : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, nsAString& _retval); + + // method overrides --> used to hook the mime service into internet config.... + NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType, const nsACString& aFileExt, nsIMIMEInfo ** aMIMEInfo); + already_AddRefed<nsIMIMEInfo> GetMIMEInfoFromOS(const nsACString& aMIMEType, const nsACString& aFileExt, bool * aFound); + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **_retval); + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got out of the + // rdf data source. This can be a mac file spec, a unix path or a windows path depending on the platform + // aFile --> an nsIFile representation of that platform application path. + virtual MOZ_MUST_USE nsresult GetFileTokenForPath(const char16_t * platformAppPath, nsIFile ** aFile); + + MOZ_MUST_USE nsresult OSProtocolHandlerExists(const char * aScheme, + bool * aHandlerExists); + +private: + uint32_t mPermissions; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/mac/nsOSHelperAppService.mm b/uriloader/exthandler/mac/nsOSHelperAppService.mm new file mode 100644 index 000000000..00f12f544 --- /dev/null +++ b/uriloader/exthandler/mac/nsOSHelperAppService.mm @@ -0,0 +1,569 @@ +/* -*- 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 "nsObjCExceptions.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsXPIDLString.h" +#include "nsIURL.h" +#include "nsIFile.h" +#include "nsILocalFileMac.h" +#include "nsMimeTypes.h" +#include "nsIStringBundle.h" +#include "nsIPromptService.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" + +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() : nsExternalHelperAppService() +{ + mode_t mask = umask(0777); + umask(mask); + mPermissions = 0666 & ~mask; +} + +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 wierd (like "ÿÿ~" + // aka "\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; + + 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) { + CFURLRef appCFURL = NULL; + OSStatus theErr = ::LSGetApplicationForURL(lookupCFURL, kLSRolesAll, NULL, &appCFURL); + + if (theErr == noErr) { + CFBundleRef handlerBundle = ::CFBundleCreate(NULL, appCFURL); + + if (handlerBundle) { + // Get the human-readable name of the default handler 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(appCFURL); + } + + ::CFRelease(lookupCFURL); + } + + ::CFRelease(lookupCFString); + } + + ::CFRelease(schemeCFString); + } + + 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; +} + +NS_IMETHODIMP nsOSHelperAppService::GetFromTypeAndExtension(const nsACString& aType, const nsACString& aFileExt, nsIMIMEInfo ** aMIMEInfo) +{ + return nsExternalHelperAppService::GetFromTypeAndExtension(aType, aFileExt, aMIMEInfo); +} + +// 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; +} + +// aMIMEType and aFileExt might not match, If they don't we set *aFound to +// false and return a minimal nsIMIMEInfo structure. +already_AddRefed<nsIMIMEInfo> +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool * aFound) +{ + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSNULL; + + *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 typeAppIsDefault = false; + bool extAppIsDefault = false; + FSRef typeAppFSRef; + FSRef extAppFSRef; + + CFStringRef cfMIMEType = NULL; + + if (!aMIMEType.IsEmpty()) { + 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 "ÿÿ~" + // aka "\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 wierd (like "ÿÿ~" + // aka "\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; + } + } else if (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()) { + 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(NS_LITERAL_CSTRING(APPLICATION_OCTET_STREAM)); + } + } + + if (typeAppIsDefault || extAppIsDefault) { + if (haveAppForExt) + mimeInfoMac->AppendExtension(aFileExt); + + nsCOMPtr<nsILocalFileMac> app(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID)); + if (!app) { + [localPool release]; + return nullptr; + } + + 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 preferred extension is included + // in our list. + NSURLFileTypeMappings *map = [NSURLFileTypeMappings sharedMappings]; + NSString *typeStr = [NSString stringWithCString:mimeType.get() encoding:NSASCIIStringEncoding]; + NSString *extStr = map ? [map preferredExtensionForMIMEType:typeStr] : NULL; + if (extStr) { + nsAutoCString preferredExt; + preferredExt.Assign((char *)[extStr cStringUsingEncoding:NSASCIIStringEncoding]); + mimeInfoMac->AppendExtension(preferredExt); + } + + 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]; + return mimeInfoMac.forget(); + + NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL; +} + +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; + } + + 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 000000000..6a3ca08af --- /dev/null +++ b/uriloader/exthandler/moz.build @@ -0,0 +1,139 @@ +# -*- 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/. + +TEST_DIRS += ['tests'] + +XPIDL_SOURCES += [ + 'nsCExternalHandlerService.idl', + 'nsIContentDispatchChooser.idl', + 'nsIExternalHelperAppService.idl', + 'nsIExternalProtocolService.idl', + 'nsIExternalSharingAppService.idl', + 'nsIExternalURLHandlerService.idl', + 'nsIHandlerService.idl', + 'nsIHelperAppLauncherDialog.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', 'gonk', 'uikit'): + osdir = CONFIG['MOZ_WIDGET_TOOLKIT'] +else: + osdir = 'unix' + +EXPORTS += [ + osdir + '/nsOSHelperAppService.h' +] + +EXPORTS += [ + 'ContentHandlerService.h', + 'nsExternalHelperAppService.h', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android': + EXPORTS += [ '%s/%s' % (osdir, f) for f in [ + 'nsExternalSharingAppService.h', + 'nsExternalURLHandlerService.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', +] + +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['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] + +if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']: + UNIFIED_SOURCES += [ + 'unix/nsGNOMERegistry.cpp', + 'unix/nsMIMEInfoUnix.cpp', + ] +elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android': + UNIFIED_SOURCES += [ + 'android/nsAndroidHandlerApp.cpp', + 'android/nsExternalSharingAppService.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', + ] + +if CONFIG['MOZ_ENABLE_CONTENTACTION']: + UNIFIED_SOURCES += [ + 'nsContentHandlerApp.cpp', + ] + +EXTRA_COMPONENTS += [ + 'nsHandlerService.js', + 'nsHandlerService.manifest', + 'nsWebHandlerApp.js', + 'nsWebHandlerApp.manifest', +] + +IPDL_SOURCES += [ + 'PExternalHelperApp.ipdl', + 'PHandlerService.ipdl', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +LOCAL_INCLUDES += [ + '/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'] in ('gtk2', 'gtk3'): + 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 000000000..6aa25a01d --- /dev/null +++ b/uriloader/exthandler/nsCExternalHandlerService.idl @@ -0,0 +1,58 @@ +/* -*- 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++ + +/* A7F800E0-4306-11d4-98D0-001083010E9B */ +#define NS_EXTERNALHELPERAPPSERVICE_CID \ + { 0xa7f800e0, 0x4306, 0x11d4, { 0x98, 0xd0, 0x0, 0x10, 0x83, 0x1, 0xe, 0x9b } } + +#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_EXTERNALPROTOCOLHANDLER_CID \ +{ 0xbd6390c8, 0xfbea, 0x11d4, {0x98, 0xf6, 0x0, 0x10, 0x83, 0x1, 0xe, 0x9b } } + +/* 9fa83ce7-d0ab-4ed3-938e-afafee435670 */ +#define NS_BLOCKEDEXTERNALPROTOCOLHANDLER_CID \ +{ 0x9fa83ce7, 0xd0ab, 0x4ed3, {0x93, 0x8e, 0xaf, 0xaf, 0xee, 0x43, 0x56, 0x70 } } + +/* bc0017e3-2438-47be-a567-41db58f17627 */ +#define NS_LOCALHANDLERAPP_CID \ +{ 0xbc0017e3, 0x2438, 0x47be, {0xa5, 0x67, 0x41, 0xdb, 0x58, 0xf1, 0x76, 0x27 } } + +/*6c3c274b-4cbf-4bb5-a635-05ad2cbb6535*/ +#define NS_DBUSHANDLERAPP_CID \ +{ 0x6c3c274b, 0x4cbf, 0x4bb5, {0xa6, 0x35, 0x05, 0xad, 0x2c, 0xbb, 0x65, 0x35 } } + +#define NS_DBUSHANDLERAPP_CONTRACTID \ +"@mozilla.org/uriloader/dbus-handler-app;1" + +#define NS_LOCALHANDLERAPP_CONTRACTID \ +"@mozilla.org/uriloader/local-handler-app;1" + +#define NS_WEBHANDLERAPP_CONTRACTID \ +"@mozilla.org/uriloader/web-handler-app;1" + +%} + diff --git a/uriloader/exthandler/nsContentHandlerApp.cpp b/uriloader/exthandler/nsContentHandlerApp.cpp new file mode 100644 index 000000000..ce3f7b90f --- /dev/null +++ b/uriloader/exthandler/nsContentHandlerApp.cpp @@ -0,0 +1,79 @@ +/* -*- 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 "nsContentHandlerApp.h" +#include "nsIURI.h" +#include "nsIClassInfoImpl.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +#define NS_CONTENTHANDLER_CID \ +{ 0x43ec2c82, 0xb9db, 0x4835, {0x80, 0x3f, 0x64, 0xc9, 0x72, 0x5a, 0x70, 0x28 } } + +NS_IMPL_CLASSINFO(nsContentHandlerApp, nullptr, 0, NS_CONTENTHANDLER_CID) +NS_IMPL_ISUPPORTS_CI(nsContentHandlerApp, nsIHandlerApp) + +nsContentHandlerApp::nsContentHandlerApp(nsString aName, nsCString aType, + ContentAction::Action& aAction) : + mName(aName), + mType(aType), + mAction(aAction) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIHandlerInfo + +NS_IMETHODIMP nsContentHandlerApp::GetName(nsAString& aName) +{ + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP nsContentHandlerApp::SetName(const nsAString& aName) +{ + mName.Assign(aName); + return NS_OK; +} + +NS_IMETHODIMP nsContentHandlerApp::Equals(nsIHandlerApp *aHandlerApp, bool *_retval) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsContentHandlerApp::GetDetailedDescription(nsAString& aDetailedDescription) +{ + aDetailedDescription.Assign(mDetailedDescription); + return NS_OK; +} + +NS_IMETHODIMP nsContentHandlerApp::SetDetailedDescription(const nsAString& aDetailedDescription) +{ + mDetailedDescription.Assign(aDetailedDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsContentHandlerApp::LaunchWithURI(nsIURI *aURI, + nsIInterfaceRequestor *aWindowContext) +{ + nsAutoCString spec; + nsresult rv = aURI->GetAsciiSpec(spec); + NS_ENSURE_SUCCESS(rv,rv); + const char* url = spec.get(); + + QList<ContentAction::Action> actions = + ContentAction::Action::actionsForFile(QUrl(url), QString(mType.get())); + for (int i = 0; i < actions.size(); ++i) { + if (actions[i].name() == QString((QChar*)mName.get(), mName.Length())) { + actions[i].trigger(); + break; + } + } + + return NS_OK; +} + diff --git a/uriloader/exthandler/nsContentHandlerApp.h b/uriloader/exthandler/nsContentHandlerApp.h new file mode 100644 index 000000000..8ace9a2cd --- /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 000000000..bed2ecd98 --- /dev/null +++ b/uriloader/exthandler/nsDBusHandlerApp.cpp @@ -0,0 +1,177 @@ +/* -*- 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/ipc/DBusConnectionDelete.h" +#include "mozilla/ipc/DBusMessageRefPtr.h" +#include "nsDBusHandlerApp.h" +#include "nsIURI.h" +#include "nsIClassInfoImpl.h" +#include "nsCOMPtr.h" +#include "nsCExternalHandlerService.h" + +// XXX why does nsMIMEInfoImpl have a threadsafe nsISupports? do we need one +// here too? +NS_IMPL_CLASSINFO(nsDBusHandlerApp, nullptr, 0, NS_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, + nsIInterfaceRequestor *aWindowContext) +{ + 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 000000000..a2835d98f --- /dev/null +++ b/uriloader/exthandler/nsDBusHandlerApp.h @@ -0,0 +1,33 @@ +/* -*- 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 000000000..51a7ee0f6 --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -0,0 +1,2926 @@ +/* -*- 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/dom/ContentChild.h" +#include "mozilla/dom/TabChild.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 "nsIDirectoryService.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsICategoryManager.h" +#include "nsDependentSubstring.h" +#include "nsXPIDLString.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 "nsAutoPtr.h" +#include "nsIMutableArray.h" + +// used to access our datastore of user-configured helper applications +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "nsIRefreshURI.h" // XXX needed to redirect according to Refresh: URI +#include "nsIDocumentLoader.h" // XXX needed to get orig. channel and assoc. refresh uri +#include "nsIHelperAppLauncherDialog.h" +#include "nsIContentDispatchChooser.h" +#include "nsNetUtil.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIIOService.h" +#include "nsNetCID.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 "nsIPluginHost.h" // XXX needed for ext->type mapping (bug 233289) +#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 "nsIMIMEHeaderParam.h" + +#include "nsIWindowWatcher.h" + +#include "nsIDownloadHistory.h" // to mark downloads as visited +#include "nsDocShellCID.h" + +#include "nsCRT.h" +#include "nsLocalHandlerApp.h" + +#include "nsIRandomGenerator.h" + +#include "ContentChild.h" +#include "nsXULAppAPI.h" +#include "nsPIDOMWindow.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIDocShellTreeItem.h" +#include "ExternalHelperAppChild.h" + +#ifdef XP_WIN +#include "nsWindowsHelpers.h" +#endif + +#ifdef MOZ_WIDGET_ANDROID +#include "FennecJNIWrappers.h" +#endif + +#include "mozilla/Preferences.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla; +using namespace mozilla::ipc; + +// 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) +{ + // First, we need a charset + nsAutoCString originCharset; + nsresult rv = aURI->GetOriginCharset(originCharset); + NS_ENSURE_SUCCESS(rv, rv); + + // Now, we need the unescaper + nsCOMPtr<nsITextToSubURI> textToSubURI = do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return textToSubURI->UnEscapeURIForUI(originCharset, 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); + 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()) + { + rv = UnescapeFragment(leafName, url, aFileName); + if (NS_FAILED(rv)) + { + CopyUTF8toUTF16(leafName, aFileName); // use escaped name + } + } + } + + // Extract Extension, if we have a filename; otherwise, + // truncate the string + if (aExtension.IsEmpty()) { + if (!aFileName.IsEmpty()) + { + // Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + aFileName.Trim(".", false); + + // 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 nsDownloadManager.cpp + * + * 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) + // We ask Java for the temporary download directory. The directory will be + // different depending on whether we have the permission to write to the + // public download directory or not. + // In the case where we do not have the permission we will start the + // download to the app cache directory and later move it to the final + // destination after prompting for the permission. + jni::String::LocalRef downloadDir; + if (jni::IsFennec()) { + downloadDir = java::DownloadsIntegration::GetTemporaryDownloadDirectory(); + } + + nsresult rv; + if (downloadDir) { + nsCOMPtr<nsIFile> ldir; + rv = NS_NewNativeLocalFile(downloadDir->ToCString(), + true, getter_AddRefs(ldir)); + + NS_ENSURE_SUCCESS(rv, rv); + dir = do_QueryInterface(ldir); + + // If we're not checking for availability we're done. + if (aSkipChecks) { + dir.forget(_directory); + return NS_OK; + } + } + else { + 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" }, + { TEXT_XUL, "xul" }, + { 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" }, + { VIDEO_OGG, "ogv" }, + { VIDEO_OGG, "ogg" }, + { APPLICATION_OGG, "ogg" }, + { AUDIO_OGG, "oga" }, + { AUDIO_OGG, "opus" }, + { APPLICATION_PDF, "pdf" }, + { VIDEO_WEBM, "webm" }, + { AUDIO_WEBM, "webm" }, +#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; +}; + +#ifdef XP_MACOSX +#define MAC_TYPE(x) x +#else +#define MAC_TYPE(x) 0 +#endif + +/** + * 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_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", "Javascript Module Source File" }, +#ifdef MOZ_WIDGET_ANDROID + { "application/vnd.android.package-archive", "apk", "Android Package" }, +#endif + { IMAGE_ART, "art", "ART Image" }, + { IMAGE_BMP, "bmp", "BMP Image" }, + { IMAGE_GIF, "gif", "GIF Image" }, + { IMAGE_ICO, "ico,cur", "ICO Image" }, + { IMAGE_JPEG, "jpeg,jpg,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" }, + { MESSAGE_RFC822, "eml", "RFC-822 data" }, + { TEXT_PLAIN, "txt,text", "Text File" }, + { 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_XUL, "xul", "XML-Based User Interface Language" }, + { TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language" }, + { TEXT_CSS, "css", "Style Sheet" }, + { TEXT_VCARD, "vcf,vcard", "Contact Information" }, + { 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" }, +#ifdef MOZ_WIDGET_GONK + { AUDIO_AMR, "amr", "Adaptive Multi-Rate Audio" }, + { AUDIO_FLAC, "flac", "FLAC Audio" }, + { VIDEO_AVI, "avi", "Audio Video Interleave" }, + { VIDEO_AVI, "divx", "Audio Video Interleave" }, + { VIDEO_MPEG_TS, "ts", "MPEG Transport Stream" }, + { VIDEO_MPEG_TS, "m2ts", "MPEG-2 Transport Stream" }, + { VIDEO_MATROSKA, "mkv", "MATROSKA VIDEO" }, + { AUDIO_MATROSKA, "mka", "MATROSKA AUDIO" }, +#endif + { 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" }, +#ifdef MOZ_WIDGET_GONK + // The AUDIO_3GPP has to come after the VIDEO_3GPP entry because the Gallery + // app on Firefox OS depends on the "3gp" extension mapping to the + // "video/3gpp" MIME type. + { AUDIO_3GPP, "3gpp,3gp", "3GPP Audio" }, + { AUDIO_3GPP2, "3g2", "3GPP2 Audio" }, +#endif + { AUDIO_MIDI, "mid", "Standard MIDI Audio" } +}; + +#undef MAC_TYPE + +/** + * 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" } +}; + +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, + nsIInterfaceRequestor *aContentContext, + bool aForceSave, + nsIInterfaceRequestor *aWindowContext, + nsIStreamListener ** aStreamListener) +{ + nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(aContentContext); + NS_ENSURE_STATE(window); + + // 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<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + channel->GetContentDispositionHeader(disp); + + nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest)); + wasFileChannel = fileChan != nullptr; + } + + + nsCOMPtr<nsIURI> referrer; + NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer)); + + OptionalURIParams uriParams, referrerParams; + SerializeURI(uri, uriParams); + SerializeURI(referrer, referrerParams); + + // 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. + mozilla::dom::PExternalHelperAppChild *pc = + child->SendPExternalHelperAppConstructor(uriParams, + nsCString(aMimeContentType), + disp, contentDisposition, + fileName, aForceSave, + contentLength, wasFileChannel, + referrerParams, + mozilla::dom::TabChild::GetFrom(window)); + ExternalHelperAppChild *childListener = static_cast<ExternalHelperAppChild *>(pc); + + NS_ADDREF(*aStreamListener = childListener); + + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + + RefPtr<nsExternalAppHandler> handler = + new nsExternalAppHandler(nullptr, EmptyCString(), aContentContext, aWindowContext, this, + fileName, reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + childListener->SetHandler(handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::DoContent(const nsACString& aMimeContentType, + nsIRequest *aRequest, + nsIInterfaceRequestor *aContentContext, + bool aForceSave, + nsIInterfaceRequestor *aWindowContext, + nsIStreamListener ** aStreamListener) +{ + if (XRE_IsContentProcess()) { + return DoContentContentProcessHelper(aMimeContentType, aRequest, aContentContext, + aForceSave, aWindowContext, aStreamListener); + } + + 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 = true; + nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(channel); + if (httpChan) { + nsAutoCString requestMethod; + httpChan->GetRequestMethod(requestMethod); + allowURLExt = !requestMethod.EqualsLiteral("POST"); + } + + // 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 + nsresult rv; + bool isHTTP, isHTTPS; + rv = uri->SchemeIs("http", &isHTTP); + if (NS_FAILED(rv)) { + isHTTP = false; + } + rv = uri->SchemeIs("https", &isHTTPS); + if (NS_FAILED(rv)) { + isHTTPS = false; + } + if (isHTTP || isHTTPS) { + 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(EmptyCString(), 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(NS_LITERAL_CSTRING(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); + + 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::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)); + NS_ENSURE_SUCCESS(rv, 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; +} + +NS_IMETHODIMP nsExternalHelperAppService::LoadUrl(nsIURI * aURL) +{ + return LoadURI(aURL, nullptr); +} + +static const char kExternalProtocolPrefPrefix[] = "network.protocol-handler.external."; +static const char kExternalProtocolDefaultPref[] = "network.protocol-handler.external-default"; + +NS_IMETHODIMP +nsExternalHelperAppService::LoadURI(nsIURI *aURI, + nsIInterfaceRequestor *aWindowContext) +{ + NS_ENSURE_ARG_POINTER(aURI); + + if (XRE_IsContentProcess()) { + URIParams uri; + SerializeURI(aURI, uri); + + nsCOMPtr<nsITabChild> tabChild(do_GetInterface(aWindowContext)); + mozilla::dom::ContentChild::GetSingleton()-> + SendLoadURIExternal(uri, static_cast<dom::TabChild*>(tabChild.get())); + 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 + } + + nsCOMPtr<nsIHandlerInfo> handler; + rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + + nsHandlerInfoAction preferredAction; + handler->GetPreferredAction(&preferredAction); + bool alwaysAsk = true; + handler->GetAlwaysAskBeforeHandling(&alwaysAsk); + + // if we are not supposed to ask, and the preferred action is to use + // a helper app or the system default, we just launch the URI. + if (!alwaysAsk && (preferredAction == nsIHandlerInfo::useHelperApp || + preferredAction == nsIHandlerInfo::useSystemDefault)) + return handler->LaunchWithURI(uri, aWindowContext); + + nsCOMPtr<nsIContentDispatchChooser> chooser = + do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return chooser->Ask(handler, aWindowContext, uri, + nsIContentDispatchChooser::REASON_CANNOT_HANDLE); +} + +NS_IMETHODIMP nsExternalHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) +{ + // this method should only be implemented by each OS specific implementation of this service. + return NS_ERROR_NOT_IMPLEMENTED; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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, EmptyCString()); + if (NS_SUCCEEDED(rv)) + return NS_OK; + } + } + + return SetProtocolHandlerDefaults(*aHandlerInfo, exists); +} + +NS_IMETHODIMP +nsExternalHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **aHandlerInfo) +{ + // intended to be implemented by the subclass + return NS_ERROR_NOT_IMPLEMENTED; +} + +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(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver) +NS_INTERFACE_MAP_END_THREADSAFE + +nsExternalAppHandler::nsExternalAppHandler(nsIMIMEInfo * aMIMEInfo, + const nsCSubstring& aTempFileExtension, + nsIInterfaceRequestor* aContentContext, + nsIInterfaceRequestor* aWindowContext, + nsExternalHelperAppService *aExtProtSvc, + const nsAString& aSuggestedFilename, + uint32_t aReason, bool aForceSave) +: mMimeInfo(aMIMEInfo) +, mContentContext(aContentContext) +, mWindowContext(aWindowContext) +, mWindowToClose(nullptr) +, mSuggestedFileName(aSuggestedFilename) +, mForceSave(aForceSave) +, mCanceled(false) +, mShouldCloseWindow(false) +, mStopRequestIssued(false) +, mReason(aReason) +, 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); + + // replace platform specific path separator and illegal characters to avoid any confusion + mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS 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, '_'); + + // Make sure extension is correct. + EnsureSuggestedFileName(); + + mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096); +} + +nsExternalAppHandler::~nsExternalAppHandler() +{ + MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted"); +} + +void +nsExternalAppHandler::DidDivertRequest(nsIRequest *request) +{ + MOZ_ASSERT(XRE_IsContentProcess(), "in child process"); + // Remove our request from the child loadGroup + RetargetLoadNotifications(request); + MaybeCloseWindow(); +} + +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; +} + +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; + + // we need to store off the original (pre redirect!) channel that initiated the load. We do + // this so later on, we can pass any refresh urls associated with the original channel back to the + // window context which started the whole process. More comments about that are listed below.... + // HACK ALERT: it's pretty bogus that we are getting the document channel from the doc loader. + // ideally we should be able to just use mChannel (the channel we are extracting content from) or + // the default load channel associated with the original load group. Unfortunately because + // a redirect may have occurred, the doc loader is the only one with a ptr to the original channel + // which is what we really want.... + + // Note that we need to do this before removing aChannel from the loadgroup, + // since that would mess with the original channel on the loader. + nsCOMPtr<nsIDocumentLoader> origContextLoader = + do_GetInterface(mContentContext); + if (origContextLoader) { + origContextLoader->GetDocumentChannel(getter_AddRefs(mOriginalChannel)); + } + + 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); + } +} + +/** + * Make mTempFileExtension contain an extension exactly when its previous value + * is different from mSuggestedFileName's extension, so that it can be appended + * to mSuggestedFileName and form a valid, useful leaf name. + * This is required so that the (renamed) temporary file has the correct extension + * after downloading to make sure the OS will launch the application corresponding + * to the MIME type (which was used to calculate mTempFileExtension). This prevents + * a cgi-script named foobar.exe that returns application/zip from being named + * foobar.exe and executed as an executable file. It also blocks content that + * a web site might provide with a content-disposition header indicating + * filename="foobar.exe" from being downloaded to a file with extension .exe + * and executed. + */ +void nsExternalAppHandler::EnsureSuggestedFileName() +{ + // Make sure there is a mTempFileExtension (not "" or "."). + // Remember that mTempFileExtension will always have the leading "." + // (the check for empty is just to be safe). + if (mTempFileExtension.Length() > 1) + { + // Get mSuggestedFileName's current extension. + nsAutoString fileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos != kNotFound) + mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos); + + // Now, compare fileExt to mTempFileExtension. + if (fileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator())) + { + // Matches -> mTempFileExtension can be empty + mTempFileExtension.Truncate(); + } + } +} + +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, NS_LITERAL_STRING(".part")), + 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 ); + return; +} + +NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest *request, nsISupports * aCtxt) +{ + NS_PRECONDITION(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; + + 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); + } + + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv)); + // Determine whether a new window was opened specifically for this request + if (props) { + bool tmp = false; + props->GetPropertyAsBool(NS_LITERAL_STRING("docshell.newWindowTarget"), + &tmp); + mShouldCloseWindow = tmp; + } + + // Now get the URI + if (aChannel) { + aChannel->GetURI(getter_AddRefs(mSourceUrl)); + } + + // retarget all load notifications to our docloader instead of the original window's docloader... + RetargetLoadNotifications(request); + + // Check to see if there is a refresh header on the original channel. + if (mOriginalChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mOriginalChannel)); + if (httpChannel) { + nsAutoCString refreshHeader; + httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("refresh"), + refreshHeader); + if (!refreshHeader.IsEmpty()) { + mShouldCloseWindow = false; + } + } + } + + // Close the underlying DOMWindow if there is no refresh header + // and it was opened specifically for the download + 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(aChannel && NS_UsePrivateBrowsing(aChannel)); + 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 prevent caching. + nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel); + if (httpInternal) { + httpInternal->SetChannelIsForDownload(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) { + nsAutoCString MIMEType; + mMimeInfo->GetMIMEType(MIMEType); + 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; + } + } + } + + 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 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. + rv = mDialog->Show(this, GetDialogParent(), 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(nullptr, false); + } else { + rv = SaveToDisk(nullptr, false); + } + } + + 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 nsAFlatString &path) +{ + nsAutoString msgId; + switch (rv) { + case NS_ERROR_OUT_OF_MEMORY: + // No memory + msgId.AssignLiteral("noMemory"); + break; + + case NS_ERROR_FILE_DISK_FULL: + case NS_ERROR_FILE_NO_DEVICE_SPACE: + // Out of space on target volume. + msgId.AssignLiteral("diskFull"); + break; + + case NS_ERROR_FILE_READ_ONLY: + // Attempt to write to read/only file. + msgId.AssignLiteral("readOnly"); + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + if (type == kWriteError) { + // Attempt to write without sufficient permissions. +#if defined(ANDROID) + // On Android (and Gonk), this means the SD card is present but + // unavailable (read-only). + msgId.AssignLiteral("SDAccessErrorCardReadOnly"); +#else + msgId.AssignLiteral("accessError"); +#endif + } else { + msgId.AssignLiteral("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.AssignLiteral("helperAppNotFound"); + break; + } +#if defined(ANDROID) + else if (type == kWriteError) { + // On Android (and Gonk), this means the SD card is missing (not in + // SD slot). + msgId.AssignLiteral("SDAccessErrorCardMissing"); + break; + } +#endif + MOZ_FALLTHROUGH; + + default: + // Generic read/write/launch error message. + switch (type) { + case kReadError: + msgId.AssignLiteral("readError"); + break; + case kWriteError: + msgId.AssignLiteral("writeError"); + break; + case kLaunchError: + msgId.AssignLiteral("launchError"); + break; + } + break; + } + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08X\n", + NS_LossyConvertUTF16toASCII(msgId).get(), type, mDialogProgressListener.get(), mTransfer.get(), 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)))) { + nsXPIDLString msgText; + const char16_t *strings[] = { path.get() }; + if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId.get(), strings, 1, + getter_Copies(msgText)))) { + if (mDialogProgressListener) { + // We have a listener, let it handle the error. + mDialogProgressListener->OnStatusChange(nullptr, (type == kReadError) ? aRequest : nullptr, rv, msgText); + } else if (mTransfer) { + mTransfer->OnStatusChange(nullptr, (type == kReadError) ? aRequest : nullptr, rv, msgText); + } else if (XRE_IsParentProcess()) { + // We don't have a listener. Simply show the alert ourselves. + nsresult qiRv; + nsCOMPtr<nsIPrompt> prompter(do_GetInterface(GetDialogParent(), &qiRv)); + nsXPIDLString title; + bundle->FormatStringFromName(u"title", + strings, + 1, + getter_Copies(title)); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug, + ("mContentContext=0x%p, prompter=0x%p, qi rv=0x%08X, title='%s', msg='%s'", + mContentContext.get(), + prompter.get(), + 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(GetDialogParent())); + if (!window || !window->GetDocShell()) { + return; + } + + prompter = do_GetInterface(window->GetDocShell(), &qiRv); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug, + ("No prompter from mContentContext, using DocShell, " \ + "window=0x%p, docShell=0x%p, " \ + "prompter=0x%p, qi rv=0x%08X", + window.get(), + window->GetDocShell(), + prompter.get(), + 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, msgText); + } + } + } + } +} + +NS_IMETHODIMP +nsExternalAppHandler::OnDataAvailable(nsIRequest *request, nsISupports * aCtxt, + 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, aCtxt, 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, nsISupports *aCtxt, + nsresult aStatus) +{ + LOG(("nsExternalAppHandler::OnStopRequest\n" + " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08X\n", + mCanceled, mTransfer.get(), 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%08X, mCanceled=%d, mTransfer=0x%p\n", + aSaver, aStatus, mCanceled, mTransfer.get())); + + if (!mCanceled) { + // Save the hash and signature information + (void)mSaver->GetSha256Hash(mHash); + (void)mSaver->GetSignatureInfo(getter_AddRefs(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->GetLoadInfo(); + if (loadInfo) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMutableArray> redirectChain = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("nsExternalAppHandler: Got %u redirects\n", loadInfo->RedirectChain().Length())); + for (nsIPrincipal* principal : loadInfo->RedirectChain()) { + redirectChain->AppendElement(principal, false); + } + 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(channel && NS_UsePrivateBrowsing(channel)); + } + + 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"); + } + 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); + + rv = transfer->Init(mSourceUrl, target, EmptyString(), + mMimeInfo, mTimeDownloadStarted, mTempFile, this, + channel && NS_UsePrivateBrowsing(channel)); + NS_ENSURE_SUCCESS(rv, rv); + + // Now let's add the download to history + nsCOMPtr<nsIDownloadHistory> dh(do_GetService(NS_DOWNLOADHISTORY_CONTRACTID)); + if (dh) { + if (channel && !NS_UsePrivateBrowsing(channel)) { + nsCOMPtr<nsIURI> referrer; + NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer)); + + dh->AddDownload(mSourceUrl, referrer, mTimeDownloadStarted, target); + } + } + + // 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(bool aIsPrivateBrowsing) +{ + 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); + + rv = transfer->Init(mSourceUrl, pseudoTarget, EmptyString(), + mMimeInfo, mTimeDownloadStarted, nullptr, this, + aIsPrivateBrowsing); + NS_ENSURE_SUCCESS(rv, rv); + + // Our failed transfer is ready. + mTransfer = transfer.forget(); + + return NS_OK; +} + +nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile * aFile) +{ + if (aFile) + ContinueSave(aFile); + else + Cancel(NS_BINDING_ABORTED); + + return NS_OK; +} + +void nsExternalAppHandler::RequestSaveDestination(const nsAFlatString &aDefaultFile, const nsAFlatString &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); + + rv = dlg->PromptForSaveToFileAsync(this, + GetDialogParent(), + aDefaultFile.get(), + aFileExtension.get(), + mForceSave); + if (NS_FAILED(rv)) { + Cancel(NS_BINDING_ABORTED); + } +} + +// SaveToDisk should only be called by the helper app dialog which allows +// the user to say launch with application or save to disk. It doesn't actually +// perform the save, it just prompts for the destination file name. +NS_IMETHODIMP nsExternalAppHandler::SaveToDisk(nsIFile * aNewFileLocation, bool aRememberThisPreference) +{ + if (mCanceled) + return NS_OK; + + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + + if (!aNewFileLocation) { + 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); + } + } else { + ContinueSave(aNewFileLocation); + } + + return NS_OK; +} +nsresult nsExternalAppHandler::ContinueSave(nsIFile * aNewFileLocation) +{ + if (mCanceled) + return NS_OK; + + NS_PRECONDITION(aNewFileLocation, "Must be called with a non-null file"); + + nsresult rv = NS_OK; + nsCOMPtr<nsIFile> fileToUse = do_QueryInterface(aNewFileLocation); + mFinalFileDestination = do_QueryInterface(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; + } + + // now that the user has chosen the file location to save to, it's okay to fire the refresh tag + // if there is one. We don't want to do this before the save as dialog goes away because this dialog + // is modal and we do bad things if you try to load a web page in the underlying window while a modal + // dialog is still up. + ProcessAnyRefreshTags(); + + 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. It doesn't +// actually perform launch with application. +NS_IMETHODIMP nsExternalAppHandler::LaunchWithApplication(nsIFile * aApplication, bool aRememberThisPreference) +{ + if (mCanceled) + return NS_OK; + + // user has chosen to launch using an application, fire any refresh tags now... + ProcessAnyRefreshTags(); + + if (mMimeInfo && aApplication) { + PlatformLocalHandlerApp_t *handlerApp = + new PlatformLocalHandlerApp_t(EmptyString(), aApplication); + mMimeInfo->SetPreferredApplicationHandler(handlerApp); + } + + // 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 nsDownload can rename 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 = do_QueryInterface(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; +} + +void nsExternalAppHandler::ProcessAnyRefreshTags() +{ + // one last thing, try to see if the original window context supports a refresh interface... + // Sometimes, when you download content that requires an external handler, there is + // a refresh header associated with the download. This refresh header points to a page + // the content provider wants the user to see after they download the content. How do we + // pass this refresh information back to the caller? For now, try to get the refresh URI + // interface. If the window context where the request originated came from supports this + // then we can force it to process the refresh information (if there is any) from this channel. + if (mContentContext && mOriginalChannel) { + nsCOMPtr<nsIRefreshURI> refreshHandler (do_GetInterface(mContentContext)); + if (refreshHandler) { + refreshHandler->SetupRefreshURI(mOriginalChannel); + } + mOriginalChannel = nullptr; + } +} + +bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char * prefName, const char * aContentType) +{ + // Search the obsolete pref strings. + nsAdoptingCString prefCString = Preferences::GetCString(prefName); + 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); +} + +nsresult nsExternalAppHandler::MaybeCloseWindow() +{ + nsCOMPtr<nsPIDOMWindowOuter> window = do_GetInterface(mContentContext); + NS_ENSURE_STATE(window); + + if (mShouldCloseWindow) { + // Reset the window context to the opener window so that the dependent + // dialogs have a parent + nsCOMPtr<nsPIDOMWindowOuter> opener = window->GetOpener(); + + if (opener && !opener->Closed()) { + mContentContext = do_GetInterface(opener); + + // Now close the old window. Do it on a timer so that we don't run + // into issues trying to close the window before it has fully opened. + NS_ASSERTION(!mTimer, "mTimer was already initialized once!"); + mTimer = do_CreateInstance("@mozilla.org/timer;1"); + if (!mTimer) { + return NS_ERROR_FAILURE; + } + + mTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT); + mWindowToClose = window; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsExternalAppHandler::Notify(nsITimer* timer) +{ + NS_ASSERTION(mWindowToClose, "No window to close after timer fired"); + + mWindowToClose->Close(); + mWindowToClose = nullptr; + mTimer = nullptr; + + 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) +{ + NS_PRECONDITION(!aMIMEType.IsEmpty() || + !aFileExt.IsEmpty(), + "Give me something to work with"); + 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); + + // (1) Ask the OS for a mime info + bool found; + *_retval = GetMIMEInfoFromOS(typeToUse, aFileExt, &found).take(); + 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; + + // (2) 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) + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void) handlerSvc->Exists(*_retval, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*_retval, EmptyCString()); + LOG(("Data source: Via type: retval 0x%08x\n", rv)); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + found = found || NS_SUCCEEDED(rv); + + if (!found || NS_FAILED(rv)) { + // No type match, try extension match + if (!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%08x\n", rv)); + found = found || NS_SUCCEEDED(rv); + } + } + } + } + + // (3) No match yet. Ask extras. + if (!found) { + rv = NS_ERROR_FAILURE; +#ifdef XP_WIN + /* XXX 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 extras on this type is pretty much + * useless.... + */ + if (!typeToUse.Equals(APPLICATION_OCTET_STREAM, nsCaseInsensitiveCStringComparator())) +#endif + rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, *_retval); + LOG(("Searched extras (by type), rv 0x%08X\n", rv)); + // If that didn't work out, try file extension from extras + if (NS_FAILED(rv) && !aFileExt.IsEmpty()) { + rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval); + LOG(("Searched extras (by ext), rv 0x%08X\n", rv)); + } + // If that still didn't work, set the file description to "ext File" + if (NS_FAILED(rv) && !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")); + } + } + + // 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) + (*_retval)->SetPrimaryExtension(aFileExt); + } + + if (LOG_ENABLED()) { + nsAutoCString type; + (*_retval)->GetMIMEType(type); + + nsAutoCString ext; + (*_retval)->GetPrimaryExtension(ext); + LOG(("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(), ext.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 + nsXPIDLCString type; + nsresult rv = catMan->GetCategoryEntry("ext-to-type-mapping", + lowercaseFileExt.get(), + getter_Copies(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(fileName.get() + i + 1, fileExt); + break; + } + } + } + + if (fileExt.IsEmpty()) + return NS_ERROR_FAILURE; + + return GetTypeFromExtension(fileExt, aContentType); +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras( + const nsACString& aContentType, nsIMIMEInfo * aMIMEInfo) +{ + NS_ENSURE_ARG( aMIMEInfo ); + + NS_ENSURE_ARG( !aContentType.IsEmpty() ); + + // Look for default entry with matching mime type. + nsAutoCString MIMEType(aContentType); + ToLowerCase(MIMEType); + int32_t numEntries = ArrayLength(extraMimeEntries); + for (int32_t index = 0; index < numEntries; index++) + { + if ( MIMEType.Equals(extraMimeEntries[index].mMimeType) ) + { + // This is the one. Set attributes appropriately. + aMIMEInfo->SetFileExtensions(nsDependentCString(extraMimeEntries[index].mFileExtensions)); + aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(extraMimeEntries[index].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, aMIMEInfo); +} + +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 = GetMIMEInfoFromOS(EmptyCString(), aExtension, &found); + return found && mimeInfo && NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType)); +} diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h new file mode 100644 index 000000000..ceec66661 --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.h @@ -0,0 +1,498 @@ +/* -*- 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 "nsIStreamListener.h" +#include "nsIFile.h" +#include "nsIFileStreams.h" +#include "nsIOutputStream.h" +#include "nsString.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIChannel.h" +#include "nsITimer.h" +#include "nsIBackgroundFileSaver.h" + +#include "nsIHandlerService.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsCOMArray.h" +#include "nsWeakReference.h" +#include "nsIPrompt.h" +#include "nsAutoPtr.h" +#include "mozilla/Attributes.h" + +class nsExternalAppHandler; +class nsIMIMEInfo; +class nsITransfer; +class nsPIDOMWindowOuter; + +/** + * The helper app service. Responsible for handling content that Mozilla + * itself can not handle + */ +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_NSIEXTERNALPROTOCOLSERVICE + NS_DECL_NSIMIMESERVICE + NS_DECL_NSIOBSERVER + + nsExternalHelperAppService(); + + /** + * Initializes internal state. Will be called automatically when + * this service is first instantiated. + */ + MOZ_MUST_USE nsresult Init(); + + /** + * Given a mimetype and an extension, looks up a mime info from the OS. + * The mime type is given preference. This function follows the same rules + * as nsIMIMEService::GetFromTypeAndExtension. + * This is supposed to be overridden by the platform-specific + * nsOSHelperAppService! + * @param aFileExt The file extension; may be empty. UTF-8 encoded. + * @param [out] aFound + * Should be set to true if the os has a mapping, to + * false otherwise. Must not be null. + * @return A MIMEInfo. This function must return a MIMEInfo object if it + * can allocate one. The only justifiable reason for not + * returning one is an out-of-memory error. + * If null, the value of aFound is unspecified. + */ + virtual already_AddRefed<nsIMIMEInfo> GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool * aFound) = 0; + + /** + * 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); + + virtual nsresult 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); + +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 aMIMEInfo [inout] The mime info, if found + */ + nsresult FillMIMEInfoForMimeTypeFromExtras( + const nsACString& aContentType, 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); + + /** + * 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, + nsIInterfaceRequestor *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 nsITimerCallback, + public nsIBackgroundFileSaverObserver +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIHELPERAPPLAUNCHER + NS_DECL_NSICANCELABLE + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIBACKGROUNDFILESAVEROBSERVER + + /** + * @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 nsCSubstring& aFileExtension, + nsIInterfaceRequestor * aContentContext, + 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); + +protected: + ~nsExternalAppHandler(); + + nsIInterfaceRequestor* GetDialogParent() { + return mWindowContext ? mWindowContext : mContentContext; + } + + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsIURI> mSourceUrl; + nsString mTempFileExtension; + nsString mTempLeafName; + + /** + * The MIME Info for this load. Will never be null. + */ + nsCOMPtr<nsIMIMEInfo> mMimeInfo; + + /** + * The dom window associated with this request to handle content. + */ + nsCOMPtr<nsIInterfaceRequestor> mContentContext; + + /** + * 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. + */ + nsCOMPtr<nsPIDOMWindowOuter> mWindowToClose; + nsCOMPtr<nsITimer> mTimer; + + /** + * 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; + + /** + * This is set based on whether the channel indicates that a new window + * was opened specifically for this download. If so, then we + * close it. + */ + bool mShouldCloseWindow; + + /** + * True if a stop request has been issued. + */ + bool mStopRequestIssued; + + bool mIsFileChannel; + + /** + * One of the REASON_ constants from nsIHelperAppLauncherDialog. Indicates the + * reason the dialog was shown (unknown content type, server requested it, + * etc). + */ + uint32_t mReason; + + /** + * 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 nsIArray of + * nsIX509CertList of nsIX509Cert. If the file is unsigned this will be + * empty. + */ + nsCOMPtr<nsIArray> mSignatureInfo; + /** + * Stores the redirect information associated with the channel. + */ + nsCOMPtr<nsIArray> mRedirects; + /** + * 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(bool aIsPrivateBrowsing); + + /* + * 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 nsAFlatString &aDefaultFile, + const nsAFlatString &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); + + /** + * After we're done prompting the user for any information, if the original + * channel had a refresh url associated with it (which might point to a + * "thank you for downloading" kind of page, then process that....It is safe + * to invoke this method multiple times. We'll clear mOriginalChannel after + * it's called and this ensures we won't call it again.... + */ + void ProcessAnyRefreshTags(); + + /** + * Notify our nsITransfer object that we are done with the download. This is + * always called after the target file has been closed. + * + * @param aStatus + * NS_OK for success, or a failure code if the download failed. + * A partially downloaded file may still be available in this case. + */ + void NotifyTransfer(nsresult aStatus); + + /** + * Helper routine that searches a pref string for a given mime type + */ + bool GetNeverAskFlagFromPref(const char * prefName, const char * aContentType); + + /** + * Helper routine to ensure mSuggestedFileName is "correct"; + * this ensures that mTempFileExtension only contains an extension when it + * is different from mSuggestedFileName's extension. + */ + void EnsureSuggestedFileName(); + + 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 nsAFlatString &path); + + /** + * Closes the window context if it does not have a refresh header + * and it never displayed content before the external helper app + * service was invoked. + */ + nsresult MaybeCloseWindow(); + + /** + * 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<nsIChannel> mOriginalChannel; /**< in the case of a redirect, this will be the pre-redirect channel. */ + nsCOMPtr<nsIHelperAppLauncherDialog> mDialog; + + /** + * Keep request alive in case when helper non-modal dialog shown. + * Thus in OnStopRequest the mRequest will not be set to null (it will be set to null further). + */ + bool mKeepRequestAlive; + + /** + * 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; +}; + +#endif // nsExternalHelperAppService_h__ diff --git a/uriloader/exthandler/nsExternalProtocolHandler.cpp b/uriloader/exthandler/nsExternalProtocolHandler.cpp new file mode 100644 index 000000000..5b3d07b4c --- /dev/null +++ b/uriloader/exthandler/nsExternalProtocolHandler.cpp @@ -0,0 +1,561 @@ +/* -*- 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 "nsIURI.h" +#include "nsIURL.h" +#include "nsExternalProtocolHandler.h" +#include "nsXPIDLString.h" +#include "nsReadableUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIServiceManager.h" +#include "nsServiceManagerUtils.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIStringBundle.h" +#include "nsIPrefService.h" +#include "nsIPrompt.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; + // 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; +}; + +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_THREADSAFE + +nsExtProtocolChannel::nsExtProtocolChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo) + : mUrl(aURI) + , mOriginalURI(aURI) + , mStatus(NS_OK) + , mLoadFlags(nsIRequest::LOAD_NORMAL) + , mWasOpened(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 + + nsCOMPtr<nsIInterfaceRequestor> aggCallbacks; + rv = NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(aggCallbacks)); + if (NS_FAILED(rv)) { + goto finish; + } + + rv = extProtService->LoadURI(mUrl, aggCallbacks); + if (NS_SUCCEEDED(rv)) { + // despite success, we need to abort this channel, at the very least + // to make it clear to the caller that no on{Start,Stop}Request + // should be expected. + rv = NS_ERROR_NO_CONTENT; + } + } + +finish: + mCallbacks = nullptr; + return rv; +} + +NS_IMETHODIMP nsExtProtocolChannel::Open(nsIInputStream **_retval) +{ + return OpenURL(); +} + +NS_IMETHODIMP nsExtProtocolChannel::Open2(nsIInputStream** aStream) +{ + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return Open(aStream); +} + +NS_IMETHODIMP nsExtProtocolChannel::AsyncOpen(nsIStreamListener *listener, nsISupports *ctxt) +{ + if (mConnectedParent) { + return NS_OK; + } + + MOZ_ASSERT(!mLoadInfo || + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL && + nsContentUtils::IsSystemPrincipal(mLoadInfo->LoadingPrincipal())), + "security flags in loadInfo but asyncOpen2() not called"); + + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); + + mWasOpened = true; + + return OpenURL(); +} + +NS_IMETHODIMP nsExtProtocolChannel::AsyncOpen2(nsIStreamListener *aListener) +{ + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return AsyncOpen(listener, nullptr); +} + +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::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) +{ + NS_NOTREACHED("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) +{ + 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) +{ + mStatus = status; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Suspend() +{ + NS_NOTREACHED("Suspend"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::Resume() +{ + NS_NOTREACHED("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, + nsISupports *context) +{ + // 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, context); +} + +/////////////////////////////////////////////////////////////////////// +// From nsIParentChannel (derives from nsIStreamListener) +////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsExtProtocolChannel::SetParentListener(HttpChannelParentListener* 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::NotifyTrackingProtectionDisabled() +{ + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Delete() +{ + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + // no data is expected + MOZ_CRASH("No data expected from external protocol channel"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatusCode) +{ + // no data is expected + MOZ_CRASH("No data expected from external protocol channel"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + 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_THREADSAFE + +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::NewURI(const nsACString &aSpec, + const char *aCharset, // ignore charset info + nsIURI *aBaseURI, + nsIURI **_retval) +{ + nsresult rv; + nsCOMPtr<nsIURI> uri = do_CreateInstance(NS_SIMPLEURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = uri->SetSpec(aSpec); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*_retval = uri); + return NS_OK; +} + +NS_IMETHODIMP +nsExternalProtocolHandler::NewChannel2(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; +} + +NS_IMETHODIMP nsExternalProtocolHandler::NewChannel(nsIURI *aURI, nsIChannel **_retval) +{ + return NewChannel2(aURI, nullptr, _retval); +} + +/////////////////////////////////////////////////////////////////////// +// 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 000000000..426c98da1 --- /dev/null +++ b/uriloader/exthandler/nsExternalProtocolHandler.h @@ -0,0 +1,38 @@ +/* -*- 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 "nsIExternalProtocolService.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/nsHandlerService.js b/uriloader/exthandler/nsHandlerService.js new file mode 100644 index 000000000..c932f9f5d --- /dev/null +++ b/uriloader/exthandler/nsHandlerService.js @@ -0,0 +1,1429 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const Cr = Components.results; + + +const CLASS_MIMEINFO = "mimetype"; +const CLASS_PROTOCOLINFO = "scheme"; + + +// namespace prefix +const NC_NS = "http://home.netscape.com/NC-rdf#"; + +// the most recent default handlers that have been injected. Note that +// this is used to construct an RDF resource, which needs to have NC_NS +// prepended, since that hasn't been done yet +const DEFAULT_HANDLERS_VERSION = "defaultHandlersVersion"; + +// type list properties + +const NC_MIME_TYPES = NC_NS + "MIME-types"; +const NC_PROTOCOL_SCHEMES = NC_NS + "Protocol-Schemes"; + +// content type ("type") properties + +// nsIHandlerInfo::type +const NC_VALUE = NC_NS + "value"; +const NC_DESCRIPTION = NC_NS + "description"; + +// additional extensions +const NC_FILE_EXTENSIONS = NC_NS + "fileExtensions"; + +// references nsIHandlerInfo record +const NC_HANDLER_INFO = NC_NS + "handlerProp"; + +// handler info ("info") properties + +// nsIHandlerInfo::preferredAction +const NC_SAVE_TO_DISK = NC_NS + "saveToDisk"; +const NC_HANDLE_INTERNALLY = NC_NS + "handleInternal"; +const NC_USE_SYSTEM_DEFAULT = NC_NS + "useSystemDefault"; + +// nsIHandlerInfo::alwaysAskBeforeHandling +const NC_ALWAYS_ASK = NC_NS + "alwaysAsk"; + +// references nsIHandlerApp records +const NC_PREFERRED_APP = NC_NS + "externalApplication"; +const NC_POSSIBLE_APP = NC_NS + "possibleApplication"; + +// handler app ("handler") properties + +// nsIHandlerApp::name +const NC_PRETTY_NAME = NC_NS + "prettyName"; + +// nsILocalHandlerApp::executable +const NC_PATH = NC_NS + "path"; + +// nsIWebHandlerApp::uriTemplate +const NC_URI_TEMPLATE = NC_NS + "uriTemplate"; + +// nsIDBusHandlerApp::service +const NC_SERVICE = NC_NS + "service"; + +// nsIDBusHandlerApp::method +const NC_METHOD = NC_NS + "method"; + +// nsIDBusHandlerApp::objectPath +const NC_OBJPATH = NC_NS + "objectPath"; + +// nsIDBusHandlerApp::dbusInterface +const NC_INTERFACE = NC_NS + "dBusInterface"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + + +function HandlerService() { + this._init(); +} + +const HandlerServiceFactory = { + _instance: null, + createInstance: function (outer, iid) { + if (this._instance) + return this._instance; + + let processType = Cc["@mozilla.org/xre/runtime;1"]. + getService(Ci.nsIXULRuntime).processType; + if (processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) + return Cr.NS_ERROR_NOT_IMPLEMENTED; + + return (this._instance = new HandlerService()); + } +}; + +HandlerService.prototype = { + //**************************************************************************// + // XPCOM Plumbing + + classID: Components.ID("{32314cc8-22f7-4f7f-a645-1a45453ba6a6}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHandlerService]), + _xpcom_factory: HandlerServiceFactory, + + //**************************************************************************// + // Initialization & Destruction + + _init: function HS__init() { + // Observe profile-before-change so we can switch to the datasource + // in the new profile when the user changes profiles. + this._observerSvc.addObserver(this, "profile-before-change", false); + + // Observe xpcom-shutdown so we can remove these observers + // when the application shuts down. + this._observerSvc.addObserver(this, "xpcom-shutdown", false); + + // Observe profile-do-change so that non-default profiles get upgraded too + this._observerSvc.addObserver(this, "profile-do-change", false); + + // do any necessary updating of the datastore + this._updateDB(); + }, + + _updateDB: function HS__updateDB() { + try { + var defaultHandlersVersion = this._datastoreDefaultHandlersVersion; + } catch(ex) { + // accessing the datastore failed, we can't update anything + return; + } + + try { + // if we don't have the current version of the default prefs for + // this locale, inject any new default handers into the datastore + if (defaultHandlersVersion < this._prefsDefaultHandlersVersion) { + + // set the new version first so that if we recurse we don't + // call _injectNewDefaults several times + this._datastoreDefaultHandlersVersion = + this._prefsDefaultHandlersVersion; + this._injectNewDefaults(); + } + } catch (ex) { + // if injecting the defaults failed, set the version back to the + // previous value + this._datastoreDefaultHandlersVersion = defaultHandlersVersion; + } + }, + + get _currentLocale() { + var chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]. + getService(Ci.nsIXULChromeRegistry); + var currentLocale = chromeRegistry.getSelectedLocale("global"); + return currentLocale; + }, + + _destroy: function HS__destroy() { + this._observerSvc.removeObserver(this, "profile-before-change"); + this._observerSvc.removeObserver(this, "xpcom-shutdown"); + this._observerSvc.removeObserver(this, "profile-do-change"); + + // XXX Should we also null references to all the services that get stored + // by our memoizing getters in the Convenience Getters section? + }, + + _onProfileChange: function HS__onProfileChange() { + // Lose our reference to the datasource so we reacquire it + // from the new profile the next time we need it. + this.__ds = null; + }, + + _isInHandlerArray: function HS__isInHandlerArray(aArray, aHandler) { + var enumerator = aArray.enumerate(); + while (enumerator.hasMoreElements()) { + let handler = enumerator.getNext(); + handler.QueryInterface(Ci.nsIHandlerApp); + if (handler.equals(aHandler)) + return true; + } + + return false; + }, + + // note that this applies to the current locale only + get _datastoreDefaultHandlersVersion() { + var version = this._getValue("urn:root", NC_NS + this._currentLocale + + "_" + DEFAULT_HANDLERS_VERSION); + + return version ? version : -1; + }, + + set _datastoreDefaultHandlersVersion(aNewVersion) { + return this._setLiteral("urn:root", NC_NS + this._currentLocale + "_" + + DEFAULT_HANDLERS_VERSION, aNewVersion); + }, + + get _prefsDefaultHandlersVersion() { + // get handler service pref branch + var prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + var handlerSvcBranch = prefSvc.getBranch("gecko.handlerService."); + + // get the version of the preferences for this locale + return Number(handlerSvcBranch. + getComplexValue("defaultHandlersVersion", + Ci.nsIPrefLocalizedString).data); + }, + + _injectNewDefaults: function HS__injectNewDefaults() { + // get handler service pref branch + var prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + + let schemesPrefBranch = prefSvc.getBranch("gecko.handlerService.schemes."); + let schemePrefList = schemesPrefBranch.getChildList(""); + + var schemes = {}; + + // read all the scheme prefs into a hash + for (var schemePrefName of schemePrefList) { + + let [scheme, handlerNumber, attribute] = schemePrefName.split("."); + + try { + var 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) {} + } + + let protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + for (var scheme in schemes) { + + // This clause is essentially a reimplementation of + // nsIExternalProtocolHandlerService.getProtocolHandlerInfo(). + // Necessary because calling that from here would make XPConnect barf + // when getService tried to re-enter the constructor for this + // service. + let osDefaultHandlerFound = {}; + let protoInfo = protoSvc.getProtocolHandlerInfoFromOS(scheme, + osDefaultHandlerFound); + + if (this.exists(protoInfo)) + this.fillHandlerInfo(protoInfo, null); + else + protoSvc.setProtocolHandlerDefaults(protoInfo, + osDefaultHandlerFound.value); + + // cache the possible handlers to avoid extra xpconnect traversals. + let possibleHandlers = protoInfo.possibleApplicationHandlers; + + for (let handlerNumber in schemes[scheme]) { + let handlerPrefs = schemes[scheme][handlerNumber]; + let handlerApp = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + + handlerApp.uriTemplate = handlerPrefs.uriTemplate; + handlerApp.name = handlerPrefs.name; + + if (!this._isInHandlerArray(possibleHandlers, handlerApp)) { + possibleHandlers.appendElement(handlerApp, false); + } + } + + this.store(protoInfo); + } + }, + + //**************************************************************************// + // nsIObserver + + observe: function HS__observe(subject, topic, data) { + switch(topic) { + case "profile-before-change": + this._onProfileChange(); + break; + case "xpcom-shutdown": + this._destroy(); + break; + case "profile-do-change": + this._updateDB(); + break; + } + }, + + + //**************************************************************************// + // nsIHandlerService + + enumerate: function HS_enumerate() { + var handlers = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + this._appendHandlers(handlers, CLASS_MIMEINFO); + this._appendHandlers(handlers, CLASS_PROTOCOLINFO); + return handlers.enumerate(); + }, + + fillHandlerInfo: function HS_fillHandlerInfo(aHandlerInfo, aOverrideType) { + var type = aOverrideType || aHandlerInfo.type; + var typeID = this._getTypeID(this._getClass(aHandlerInfo), type); + + // Determine whether or not information about this handler is available + // in the datastore by looking for its "value" property, which stores its + // type and should always be present. + if (!this._hasValue(typeID, NC_VALUE)) + throw Cr.NS_ERROR_NOT_AVAILABLE; + + // Retrieve the human-readable description of the type. + if (this._hasValue(typeID, NC_DESCRIPTION)) + aHandlerInfo.description = this._getValue(typeID, NC_DESCRIPTION); + + // Note: for historical reasons, we don't actually check that the type + // record has a "handlerProp" property referencing the info record. It's + // unclear whether or not we should start doing this check; perhaps some + // legacy datasources don't have such references. + var infoID = this._getInfoID(this._getClass(aHandlerInfo), type); + + aHandlerInfo.preferredAction = this._retrievePreferredAction(infoID); + + var preferredHandlerID = + this._getPreferredHandlerID(this._getClass(aHandlerInfo), type); + + // Retrieve the preferred handler. + // Note: for historical reasons, we don't actually check that the info + // record has an "externalApplication" property referencing the preferred + // handler record. It's unclear whether or not we should start doing + // this check; perhaps some legacy datasources don't have such references. + aHandlerInfo.preferredApplicationHandler = + this._retrieveHandlerApp(preferredHandlerID); + + // Fill the array of possible handlers with the ones in the datastore. + this._fillPossibleHandlers(infoID, + aHandlerInfo.possibleApplicationHandlers, + aHandlerInfo.preferredApplicationHandler); + + // If we have an "always ask" flag stored in the RDF, always use its + // value. Otherwise, use the default value stored in the pref service. + var alwaysAsk; + if (this._hasValue(infoID, NC_ALWAYS_ASK)) { + alwaysAsk = (this._getValue(infoID, NC_ALWAYS_ASK) != "false"); + } else { + var prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + var prefBranch = prefSvc.getBranch("network.protocol-handler."); + try { + alwaysAsk = prefBranch.getBoolPref("warn-external." + type); + } catch (e) { + // will throw if pref didn't exist. + try { + alwaysAsk = prefBranch.getBoolPref("warn-external-default"); + } catch (e) { + // Nothing to tell us what to do, so be paranoid and prompt. + alwaysAsk = true; + } + } + } + aHandlerInfo.alwaysAskBeforeHandling = alwaysAsk; + + // If the object represents a MIME type handler, then also retrieve + // any file extensions. + if (aHandlerInfo instanceof Ci.nsIMIMEInfo) + for (let fileExtension of this._retrieveFileExtensions(typeID)) + aHandlerInfo.appendExtension(fileExtension); + }, + + store: function HS_store(aHandlerInfo) { + // FIXME: when we switch from RDF to something with transactions (like + // SQLite), enclose the following changes in a transaction so they all + // get rolled back if any of them fail and we don't leave the datastore + // in an inconsistent state. + + this._ensureRecordsForType(aHandlerInfo); + this._storePreferredAction(aHandlerInfo); + this._storePreferredHandler(aHandlerInfo); + this._storePossibleHandlers(aHandlerInfo); + this._storeAlwaysAsk(aHandlerInfo); + this._storeExtensions(aHandlerInfo); + + // Write the changes to the database immediately so we don't lose them + // if the application crashes. + if (this._ds instanceof Ci.nsIRDFRemoteDataSource) + this._ds.Flush(); + }, + + exists: function HS_exists(aHandlerInfo) { + var found; + + try { + var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); + found = this._hasLiteralAssertion(typeID, NC_VALUE, aHandlerInfo.type); + } catch (e) { + // If the RDF threw (eg, corrupt file), treat as nonexistent. + found = false; + } + + return found; + }, + + remove: function HS_remove(aHandlerInfo) { + var preferredHandlerID = + this._getPreferredHandlerID(this._getClass(aHandlerInfo), aHandlerInfo.type); + this._removeAssertions(preferredHandlerID); + + var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); + + // Get a list of possible handlers. After we have removed the info record, + // we'll check if any other info records reference these handlers, and we'll + // remove the handler records that aren't referenced by other info records. + var possibleHandlerIDs = []; + var possibleHandlerTargets = this._getTargets(infoID, NC_POSSIBLE_APP); + while (possibleHandlerTargets.hasMoreElements()) { + let possibleHandlerTarget = possibleHandlerTargets.getNext(); + // Note: possibleHandlerTarget should always be an nsIRDFResource. + // The conditional is just here in case of a corrupt RDF datasource. + if (possibleHandlerTarget instanceof Ci.nsIRDFResource) + possibleHandlerIDs.push(possibleHandlerTarget.ValueUTF8); + } + + // Remove the info record. + this._removeAssertions(infoID); + + // Now that we've removed the info record, remove any possible handlers + // that aren't referenced by other info records. + for (let possibleHandlerID of possibleHandlerIDs) + if (!this._existsResourceTarget(NC_POSSIBLE_APP, possibleHandlerID)) + this._removeAssertions(possibleHandlerID); + + var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); + this._removeAssertions(typeID); + + // Now that there's no longer a handler for this type, remove the type + // from the list of types for which there are known handlers. + var typeList = this._ensureAndGetTypeList(this._getClass(aHandlerInfo)); + var type = this._rdf.GetResource(typeID); + var typeIndex = typeList.IndexOf(type); + if (typeIndex != -1) + typeList.RemoveElementAt(typeIndex, true); + + // Write the changes to the database immediately so we don't lose them + // if the application crashes. + // XXX If we're removing a bunch of handlers at once, will flushing + // after every removal cause a significant performance hit? + if (this._ds instanceof Ci.nsIRDFRemoteDataSource) + this._ds.Flush(); + }, + + getTypeFromExtension: function HS_getTypeFromExtension(aFileExtension) { + var fileExtension = aFileExtension.toLowerCase(); + var typeID; + + // See bug 1100069 for why we want to fail gracefully and silently here. + try { + this._ds; + } catch (ex) { + Components.returnCode = Cr.NS_ERROR_NOT_AVAILABLE; + return; + } + + if (this._existsLiteralTarget(NC_FILE_EXTENSIONS, fileExtension)) + typeID = this._getSourceForLiteral(NC_FILE_EXTENSIONS, fileExtension); + + if (typeID && this._hasValue(typeID, NC_VALUE)) { + let type = this._getValue(typeID, NC_VALUE); + if (type == "") + throw Cr.NS_ERROR_FAILURE; + return type; + } + + return ""; + }, + + + //**************************************************************************// + // Retrieval Methods + + /** + * Retrieve the preferred action for the info record with the given ID. + * + * @param aInfoID {string} the info record ID + * + * @returns {integer} the preferred action enumeration value + */ + _retrievePreferredAction: function HS__retrievePreferredAction(aInfoID) { + if (this._getValue(aInfoID, NC_SAVE_TO_DISK) == "true") + return Ci.nsIHandlerInfo.saveToDisk; + + if (this._getValue(aInfoID, NC_USE_SYSTEM_DEFAULT) == "true") + return Ci.nsIHandlerInfo.useSystemDefault; + + if (this._getValue(aInfoID, NC_HANDLE_INTERNALLY) == "true") + return Ci.nsIHandlerInfo.handleInternally; + + return Ci.nsIHandlerInfo.useHelperApp; + }, + + /** + * Fill an array of possible handlers with the handlers for the given info ID. + * + * @param aInfoID {string} the ID of the info record + * @param aPossibleHandlers {nsIMutableArray} the array of possible handlers + * @param aPreferredHandler {nsIHandlerApp} the preferred handler, if any + */ + _fillPossibleHandlers: function HS__fillPossibleHandlers(aInfoID, + aPossibleHandlers, + aPreferredHandler) { + // The set of possible handlers should include the preferred handler, + // but legacy datastores (from before we added possible handlers) won't + // include the preferred handler, so check if it's included as we build + // the list of handlers, and, if it's not included, add it to the list. + if (aPreferredHandler) + aPossibleHandlers.appendElement(aPreferredHandler, false); + + var possibleHandlerTargets = this._getTargets(aInfoID, NC_POSSIBLE_APP); + + while (possibleHandlerTargets.hasMoreElements()) { + let possibleHandlerTarget = possibleHandlerTargets.getNext(); + if (!(possibleHandlerTarget instanceof Ci.nsIRDFResource)) + continue; + + let possibleHandlerID = possibleHandlerTarget.ValueUTF8; + let possibleHandler = this._retrieveHandlerApp(possibleHandlerID); + if (possibleHandler && (!aPreferredHandler || + !possibleHandler.equals(aPreferredHandler))) + aPossibleHandlers.appendElement(possibleHandler, false); + } + }, + + /** + * Retrieve the handler app object with the given ID. + * + * @param aHandlerAppID {string} the ID of the handler app to retrieve + * + * @returns {nsIHandlerApp} the handler app, if any; otherwise null + */ + _retrieveHandlerApp: function HS__retrieveHandlerApp(aHandlerAppID) { + var handlerApp; + + // If it has a path, it's a local handler; otherwise, it's a web handler. + if (this._hasValue(aHandlerAppID, NC_PATH)) { + let executable = + this._getFileWithPath(this._getValue(aHandlerAppID, NC_PATH)); + if (!executable) + return null; + + handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = executable; + } + else if (this._hasValue(aHandlerAppID, NC_URI_TEMPLATE)) { + let uriTemplate = this._getValue(aHandlerAppID, NC_URI_TEMPLATE); + if (!uriTemplate) + return null; + + handlerApp = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + handlerApp.uriTemplate = uriTemplate; + } + else if (this._hasValue(aHandlerAppID, NC_SERVICE)) { + let service = this._getValue(aHandlerAppID, NC_SERVICE); + if (!service) + return null; + + let method = this._getValue(aHandlerAppID, NC_METHOD); + if (!method) + return null; + + let objpath = this._getValue(aHandlerAppID, NC_OBJPATH); + if (!objpath) + return null; + + let iface = this._getValue(aHandlerAppID, NC_INTERFACE); + if (!iface) + return null; + + handlerApp = Cc["@mozilla.org/uriloader/dbus-handler-app;1"]. + createInstance(Ci.nsIDBusHandlerApp); + handlerApp.service = service; + handlerApp.method = method; + handlerApp.objectPath = objpath; + handlerApp.dBusInterface = iface; + + } + else + return null; + + handlerApp.name = this._getValue(aHandlerAppID, NC_PRETTY_NAME); + + return handlerApp; + }, + + /* + * Retrieve file extensions, if any, for the MIME type with the given type ID. + * + * @param aTypeID {string} the type record ID + */ + _retrieveFileExtensions: function HS__retrieveFileExtensions(aTypeID) { + var fileExtensions = []; + + var fileExtensionTargets = this._getTargets(aTypeID, NC_FILE_EXTENSIONS); + + while (fileExtensionTargets.hasMoreElements()) { + let fileExtensionTarget = fileExtensionTargets.getNext(); + if (fileExtensionTarget instanceof Ci.nsIRDFLiteral && + fileExtensionTarget.Value != "") + fileExtensions.push(fileExtensionTarget.Value); + } + + return fileExtensions; + }, + + /** + * Get the file with the given path. This is not as simple as merely + * initializing a local file object with the path, because the path might be + * relative to the current process directory, in which case we have to + * construct a path starting from that directory. + * + * @param aPath {string} a path to a file + * + * @returns {nsILocalFile} the file, or null if the file does not exist + */ + _getFileWithPath: function HS__getFileWithPath(aPath) { + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); + + try { + file.initWithPath(aPath); + + if (file.exists()) + return file; + } + catch(ex) { + // Note: for historical reasons, we don't actually check to see + // if the exception is NS_ERROR_FILE_UNRECOGNIZED_PATH, which is what + // nsILocalFile::initWithPath throws when a path is relative. + + file = this._dirSvc.get("XCurProcD", Ci.nsIFile); + + try { + file.append(aPath); + if (file.exists()) + return file; + } + catch(ex) {} + } + + return null; + }, + + + //**************************************************************************// + // Storage Methods + + _storePreferredAction: function HS__storePreferredAction(aHandlerInfo) { + var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); + + switch(aHandlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + this._setLiteral(infoID, NC_SAVE_TO_DISK, "true"); + this._removeTarget(infoID, NC_HANDLE_INTERNALLY); + this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT); + break; + + case Ci.nsIHandlerInfo.handleInternally: + this._setLiteral(infoID, NC_HANDLE_INTERNALLY, "true"); + this._removeTarget(infoID, NC_SAVE_TO_DISK); + this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT); + break; + + case Ci.nsIHandlerInfo.useSystemDefault: + this._setLiteral(infoID, NC_USE_SYSTEM_DEFAULT, "true"); + this._removeTarget(infoID, NC_SAVE_TO_DISK); + this._removeTarget(infoID, NC_HANDLE_INTERNALLY); + break; + + // This value is indicated in the datastore either by the absence of + // the three properties or by setting them all "false". Of these two + // options, the former seems preferable, because it reduces the size + // of the RDF file and thus the amount of stuff we have to parse. + case Ci.nsIHandlerInfo.useHelperApp: + default: + this._removeTarget(infoID, NC_SAVE_TO_DISK); + this._removeTarget(infoID, NC_HANDLE_INTERNALLY); + this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT); + break; + } + }, + + _storePreferredHandler: function HS__storePreferredHandler(aHandlerInfo) { + var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); + var handlerID = + this._getPreferredHandlerID(this._getClass(aHandlerInfo), aHandlerInfo.type); + + var handler = aHandlerInfo.preferredApplicationHandler; + + if (handler) { + this._storeHandlerApp(handlerID, handler); + + // Make this app be the preferred app for the handler info. + // + // Note: nsExternalHelperAppService::FillContentHandlerProperties ignores + // this setting and instead identifies the preferred app as the resource + // whose URI follows the pattern urn:<class>:externalApplication:<type>. + // But the old downloadactions.js code used to set this property, so just + // in case there is still some code somewhere that relies on its presence, + // we set it here. + this._setResource(infoID, NC_PREFERRED_APP, handlerID); + } + else { + // There isn't a preferred handler. Remove the existing record for it, + // if any. + this._removeTarget(infoID, NC_PREFERRED_APP); + this._removeAssertions(handlerID); + } + }, + + /** + * Store the list of possible handler apps for the content type represented + * by the given handler info object. + * + * @param aHandlerInfo {nsIHandlerInfo} the handler info object + */ + _storePossibleHandlers: function HS__storePossibleHandlers(aHandlerInfo) { + var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); + + // First, retrieve the set of handler apps currently stored for the type, + // keeping track of their IDs in a hash that we'll use to determine which + // ones are no longer valid and should be removed. + var currentHandlerApps = {}; + var currentHandlerTargets = this._getTargets(infoID, NC_POSSIBLE_APP); + while (currentHandlerTargets.hasMoreElements()) { + let handlerApp = currentHandlerTargets.getNext(); + if (handlerApp instanceof Ci.nsIRDFResource) { + let handlerAppID = handlerApp.ValueUTF8; + currentHandlerApps[handlerAppID] = true; + } + } + + // Next, store any new handler apps. + var newHandlerApps = + aHandlerInfo.possibleApplicationHandlers.enumerate(); + while (newHandlerApps.hasMoreElements()) { + let handlerApp = + newHandlerApps.getNext().QueryInterface(Ci.nsIHandlerApp); + let handlerAppID = this._getPossibleHandlerAppID(handlerApp); + if (!this._hasResourceAssertion(infoID, NC_POSSIBLE_APP, handlerAppID)) { + this._storeHandlerApp(handlerAppID, handlerApp); + this._addResourceAssertion(infoID, NC_POSSIBLE_APP, handlerAppID); + } + delete currentHandlerApps[handlerAppID]; + } + + // Finally, remove any old handler apps that aren't being used anymore, + // and if those handler apps aren't being used by any other type either, + // then completely remove their record from the datastore so we don't + // leave it clogged up with information about handler apps we don't care + // about anymore. + for (let handlerAppID in currentHandlerApps) { + this._removeResourceAssertion(infoID, NC_POSSIBLE_APP, handlerAppID); + if (!this._existsResourceTarget(NC_POSSIBLE_APP, handlerAppID)) + this._removeAssertions(handlerAppID); + } + }, + + /** + * Store the given handler app. + * + * Note: the reason this method takes the ID of the handler app in a param + * is that the ID is different than it usually is when the handler app + * in question is a preferred handler app, so this method can't just derive + * the ID of the handler app by calling _getPossibleHandlerAppID, its callers + * have to do that for it. + * + * @param aHandlerAppID {string} the ID of the handler app to store + * @param aHandlerApp {nsIHandlerApp} the handler app to store + */ + _storeHandlerApp: function HS__storeHandlerApp(aHandlerAppID, aHandlerApp) { + aHandlerApp.QueryInterface(Ci.nsIHandlerApp); + this._setLiteral(aHandlerAppID, NC_PRETTY_NAME, aHandlerApp.name); + + // In the case of the preferred handler, the handler ID could have been + // used to refer to a different kind of handler in the past (i.e. either + // a local hander or a web handler), so if the new handler is a local + // handler, then we remove any web handler properties and vice versa. + // This is unnecessary but harmless for possible handlers. + + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) { + this._setLiteral(aHandlerAppID, NC_PATH, aHandlerApp.executable.path); + this._removeTarget(aHandlerAppID, NC_URI_TEMPLATE); + this._removeTarget(aHandlerAppID, NC_METHOD); + this._removeTarget(aHandlerAppID, NC_SERVICE); + this._removeTarget(aHandlerAppID, NC_OBJPATH); + this._removeTarget(aHandlerAppID, NC_INTERFACE); + } + else if(aHandlerApp instanceof Ci.nsIWebHandlerApp){ + aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp); + this._setLiteral(aHandlerAppID, NC_URI_TEMPLATE, aHandlerApp.uriTemplate); + this._removeTarget(aHandlerAppID, NC_PATH); + this._removeTarget(aHandlerAppID, NC_METHOD); + this._removeTarget(aHandlerAppID, NC_SERVICE); + this._removeTarget(aHandlerAppID, NC_OBJPATH); + this._removeTarget(aHandlerAppID, NC_INTERFACE); + } + else if(aHandlerApp instanceof Ci.nsIDBusHandlerApp){ + aHandlerApp.QueryInterface(Ci.nsIDBusHandlerApp); + this._setLiteral(aHandlerAppID, NC_SERVICE, aHandlerApp.service); + this._setLiteral(aHandlerAppID, NC_METHOD, aHandlerApp.method); + this._setLiteral(aHandlerAppID, NC_OBJPATH, aHandlerApp.objectPath); + this._setLiteral(aHandlerAppID, NC_INTERFACE, aHandlerApp.dBusInterface); + this._removeTarget(aHandlerAppID, NC_PATH); + this._removeTarget(aHandlerAppID, NC_URI_TEMPLATE); + } + else { + throw "unknown handler type"; + } + + }, + + _storeAlwaysAsk: function HS__storeAlwaysAsk(aHandlerInfo) { + var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); + this._setLiteral(infoID, + NC_ALWAYS_ASK, + aHandlerInfo.alwaysAskBeforeHandling ? "true" : "false"); + }, + + _storeExtensions: function HS__storeExtensions(aHandlerInfo) { + if (aHandlerInfo instanceof Ci.nsIMIMEInfo) { + var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); + var extEnum = aHandlerInfo.getFileExtensions(); + while (extEnum.hasMore()) { + let ext = extEnum.getNext().toLowerCase(); + if (!this._hasLiteralAssertion(typeID, NC_FILE_EXTENSIONS, ext)) { + this._setLiteral(typeID, NC_FILE_EXTENSIONS, ext); + } + } + } + }, + + + //**************************************************************************// + // Convenience Getters + + // Observer Service + __observerSvc: null, + get _observerSvc() { + if (!this.__observerSvc) + this.__observerSvc = + Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + return this.__observerSvc; + }, + + // Directory Service + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = + Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties); + return this.__dirSvc; + }, + + // MIME Service + __mimeSvc: null, + get _mimeSvc() { + if (!this.__mimeSvc) + this.__mimeSvc = + Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService); + return this.__mimeSvc; + }, + + // Protocol Service + __protocolSvc: null, + get _protocolSvc() { + if (!this.__protocolSvc) + this.__protocolSvc = + Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + return this.__protocolSvc; + }, + + // RDF Service + __rdf: null, + get _rdf() { + if (!this.__rdf) + this.__rdf = Cc["@mozilla.org/rdf/rdf-service;1"]. + getService(Ci.nsIRDFService); + return this.__rdf; + }, + + // RDF Container Utils + __containerUtils: null, + get _containerUtils() { + if (!this.__containerUtils) + this.__containerUtils = Cc["@mozilla.org/rdf/container-utils;1"]. + getService(Ci.nsIRDFContainerUtils); + return this.__containerUtils; + }, + + // RDF datasource containing content handling config (i.e. mimeTypes.rdf) + __ds: null, + get _ds() { + if (!this.__ds) { + var file = this._dirSvc.get("UMimTyp", Ci.nsIFile); + // FIXME: make this a memoizing getter if we use it anywhere else. + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var fileHandler = ioService.getProtocolHandler("file"). + QueryInterface(Ci.nsIFileProtocolHandler); + this.__ds = + this._rdf.GetDataSourceBlocking(fileHandler.getURLSpecFromFile(file)); + } + + return this.__ds; + }, + + + //**************************************************************************// + // Datastore Utils + + /** + * Get the string identifying whether this is a MIME or a protocol handler. + * This string is used in the URI IDs of various RDF properties. + * + * @param aHandlerInfo {nsIHandlerInfo} the handler for which to get the class + * + * @returns {string} the class + */ + _getClass: function HS__getClass(aHandlerInfo) { + if (aHandlerInfo instanceof Ci.nsIMIMEInfo) + return CLASS_MIMEINFO; + else + return CLASS_PROTOCOLINFO; + }, + + /** + * Return the unique identifier for a content type record, which stores + * the value field plus a reference to the content type's handler info record. + * + * |urn:<class>:<type>| + * + * XXX: should this be a property of nsIHandlerInfo? + * + * @param aClass {string} the class (CLASS_MIMEINFO or CLASS_PROTOCOLINFO) + * @param aType {string} the type (a MIME type or protocol scheme) + * + * @returns {string} the ID + */ + _getTypeID: function HS__getTypeID(aClass, aType) { + return "urn:" + aClass + ":" + aType; + }, + + /** + * Return the unique identifier for a handler info record, which stores + * the preferredAction and alwaysAsk fields plus a reference to the preferred + * handler app. Roughly equivalent to the nsIHandlerInfo interface. + * + * |urn:<class>:handler:<type>| + * + * FIXME: the type info record should be merged into the type record, + * since there's a one to one relationship between them, and this record + * merely stores additional attributes of a content type. + * + * @param aClass {string} the class (CLASS_MIMEINFO or CLASS_PROTOCOLINFO) + * @param aType {string} the type (a MIME type or protocol scheme) + * + * @returns {string} the ID + */ + _getInfoID: function HS__getInfoID(aClass, aType) { + return "urn:" + aClass + ":handler:" + aType; + }, + + /** + * Return the unique identifier for a preferred handler record, which stores + * information about the preferred handler for a given content type, including + * its human-readable name and the path to its executable (for a local app) + * or its URI template (for a web app). + * + * |urn:<class>:externalApplication:<type>| + * + * XXX: should this be a property of nsIHandlerApp? + * + * FIXME: this should be an arbitrary ID, and we should retrieve it from + * the datastore for a given content type via the NC:ExternalApplication + * property rather than looking for a specific ID, so a handler doesn't + * have to change IDs when it goes from being a possible handler to being + * the preferred one (once we support possible handlers). + * + * @param aClass {string} the class (CLASS_MIMEINFO or CLASS_PROTOCOLINFO) + * @param aType {string} the type (a MIME type or protocol scheme) + * + * @returns {string} the ID + */ + _getPreferredHandlerID: function HS__getPreferredHandlerID(aClass, aType) { + return "urn:" + aClass + ":externalApplication:" + aType; + }, + + /** + * Return the unique identifier for a handler app record, which stores + * information about a possible handler for one or more content types, + * including its human-readable name and the path to its executable (for a + * local app) or its URI template (for a web app). + * + * Note: handler app IDs for preferred handlers are different. For those, + * see the _getPreferredHandlerID method. + * + * @param aHandlerApp {nsIHandlerApp} the handler app object + */ + _getPossibleHandlerAppID: function HS__getPossibleHandlerAppID(aHandlerApp) { + var handlerAppID = "urn:handler:"; + + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) + handlerAppID += "local:" + aHandlerApp.executable.path; + else if(aHandlerApp instanceof Ci.nsIWebHandlerApp){ + aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp); + handlerAppID += "web:" + aHandlerApp.uriTemplate; + } + else if(aHandlerApp instanceof Ci.nsIDBusHandlerApp){ + aHandlerApp.QueryInterface(Ci.nsIDBusHandlerApp); + handlerAppID += "dbus:" + aHandlerApp.service + " " + aHandlerApp.method + " " + aHandlerApp.uriTemplate; + }else{ + throw "unknown handler type"; + } + + return handlerAppID; + }, + + /** + * Get the list of types for the given class, creating the list if it doesn't + * already exist. The class can be either CLASS_MIMEINFO or CLASS_PROTOCOLINFO + * (i.e. the result of a call to _getClass). + * + * |urn:<class>s| + * |urn:<class>s:root| + * + * @param aClass {string} the class for which to retrieve a list of types + * + * @returns {nsIRDFContainer} the list of types + */ + _ensureAndGetTypeList: function HS__ensureAndGetTypeList(aClass) { + var source = this._rdf.GetResource("urn:" + aClass + "s"); + var property = + this._rdf.GetResource(aClass == CLASS_MIMEINFO ? NC_MIME_TYPES + : NC_PROTOCOL_SCHEMES); + var target = this._rdf.GetResource("urn:" + aClass + "s:root"); + + // Make sure we have an arc from the source to the target. + if (!this._ds.HasAssertion(source, property, target, true)) + this._ds.Assert(source, property, target, true); + + // Make sure the target is a container. + if (!this._containerUtils.IsContainer(this._ds, target)) + this._containerUtils.MakeSeq(this._ds, target); + + // Get the type list as an RDF container. + var typeList = Cc["@mozilla.org/rdf/container;1"]. + createInstance(Ci.nsIRDFContainer); + typeList.Init(this._ds, target); + + return typeList; + }, + + /** + * Make sure there are records in the datasource for the given content type + * by creating them if they don't already exist. We have to do this before + * storing any specific data, because we can't assume the presence + * of the records (the nsIHandlerInfo object might have been created + * from the OS), and the records have to all be there in order for the helper + * app service to properly construct an nsIHandlerInfo object for the type. + * + * Based on old downloadactions.js::_ensureMIMERegistryEntry. + * + * @param aHandlerInfo {nsIHandlerInfo} the type to make sure has a record + */ + _ensureRecordsForType: function HS__ensureRecordsForType(aHandlerInfo) { + // Get the list of types. + var typeList = this._ensureAndGetTypeList(this._getClass(aHandlerInfo)); + + // If there's already a record in the datastore for this type, then we + // don't need to do anything more. + var typeID = this._getTypeID(this._getClass(aHandlerInfo), aHandlerInfo.type); + var type = this._rdf.GetResource(typeID); + if (typeList.IndexOf(type) != -1) + return; + + // Create a basic type record for this type. + typeList.AppendElement(type); + this._setLiteral(typeID, NC_VALUE, aHandlerInfo.type); + + // Create a basic info record for this type. + var infoID = this._getInfoID(this._getClass(aHandlerInfo), aHandlerInfo.type); + this._setLiteral(infoID, NC_ALWAYS_ASK, "false"); + this._setResource(typeID, NC_HANDLER_INFO, infoID); + // XXX Shouldn't we set preferredAction to useSystemDefault? + // That's what it is if there's no record in the datastore; why should it + // change to useHelperApp just because we add a record to the datastore? + + // Create a basic preferred handler record for this type. + // XXX Not sure this is necessary, since preferred handlers are optional, + // and nsExternalHelperAppService::FillHandlerInfoForTypeFromDS doesn't seem + // to require the record , but downloadactions.js::_ensureMIMERegistryEntry + // used to create it, so we'll do the same. + var preferredHandlerID = + this._getPreferredHandlerID(this._getClass(aHandlerInfo), aHandlerInfo.type); + this._setLiteral(preferredHandlerID, NC_PATH, ""); + this._setResource(infoID, NC_PREFERRED_APP, preferredHandlerID); + }, + + /** + * Append known handlers of the given class to the given array. The class + * can be either CLASS_MIMEINFO or CLASS_PROTOCOLINFO. + * + * @param aHandlers {array} the array of handlers to append to + * @param aClass {string} the class for which to append handlers + */ + _appendHandlers: function HS__appendHandlers(aHandlers, aClass) { + var typeList = this._ensureAndGetTypeList(aClass); + var enumerator = typeList.GetElements(); + + while (enumerator.hasMoreElements()) { + var element = enumerator.getNext(); + + // This should never happen. If it does, that means our datasource + // is corrupted with type list entries that point to literal values + // instead of resources. If it does happen, let's just do our best + // to recover by ignoring this entry and moving on to the next one. + if (!(element instanceof Ci.nsIRDFResource)) + continue; + + // Get the value of the element's NC:value property, which contains + // the MIME type or scheme for which we're retrieving a handler info. + var type = this._getValue(element.ValueUTF8, NC_VALUE); + if (!type) + continue; + + var handler; + if (typeList.Resource.ValueUTF8 == "urn:mimetypes:root") + handler = this._mimeSvc.getFromTypeAndExtension(type, null); + else + handler = this._protocolSvc.getProtocolHandlerInfo(type); + + aHandlers.appendElement(handler, false); + } + }, + + /** + * Whether or not a property of an RDF source has a value. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @returns {boolean} whether or not the property has a value + */ + _hasValue: function HS__hasValue(sourceURI, propertyURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + return this._ds.hasArcOut(source, property); + }, + + /** + * Get the value of a property of an RDF source. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @returns {string} the value of the property + */ + _getValue: function HS__getValue(sourceURI, propertyURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + + var target = this._ds.GetTarget(source, property, true); + + if (!target) + return null; + + if (target instanceof Ci.nsIRDFResource) + return target.ValueUTF8; + + if (target instanceof Ci.nsIRDFLiteral) + return target.Value; + + return null; + }, + + /** + * Get all targets for the property of an RDF source. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * + * @returns {nsISimpleEnumerator} an enumerator of targets + */ + _getTargets: function HS__getTargets(sourceURI, propertyURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + + return this._ds.GetTargets(source, property, true); + }, + + /** + * Set a property of an RDF source to a literal value. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @param value {string} the literal value + */ + _setLiteral: function HS__setLiteral(sourceURI, propertyURI, value) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetLiteral(value); + + this._setTarget(source, property, target); + }, + + /** + * Set a property of an RDF source to a resource target. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @param targetURI {string} the URI of the target + */ + _setResource: function HS__setResource(sourceURI, propertyURI, targetURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetResource(targetURI); + + this._setTarget(source, property, target); + }, + + /** + * Assert an arc into the RDF datasource if there is no arc with the given + * source and property; otherwise, if there is already an existing arc, + * change it to point to the given target. _setLiteral and _setResource + * call this after converting their string arguments into resources + * and literals, and most callers should call one of those two methods + * instead of this one. + * + * @param source {nsIRDFResource} the source + * @param property {nsIRDFResource} the property + * @param target {nsIRDFNode} the target + */ + _setTarget: function HS__setTarget(source, property, target) { + if (this._ds.hasArcOut(source, property)) { + var oldTarget = this._ds.GetTarget(source, property, true); + this._ds.Change(source, property, oldTarget, target); + } + else + this._ds.Assert(source, property, target, true); + }, + + /** + * Assert that a property of an RDF source has a resource target. + * + * The difference between this method and _setResource is that this one adds + * an assertion even if one already exists, which allows its callers to make + * sets of assertions (i.e. to set a property to multiple targets). + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @param targetURI {string} the URI of the target + */ + _addResourceAssertion: function HS__addResourceAssertion(sourceURI, + propertyURI, + targetURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetResource(targetURI); + + this._ds.Assert(source, property, target, true); + }, + + /** + * Remove an assertion with a resource target. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @param targetURI {string} the URI of the target + */ + _removeResourceAssertion: function HS__removeResourceAssertion(sourceURI, + propertyURI, + targetURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetResource(targetURI); + + this._ds.Unassert(source, property, target); + }, + + /** + * Whether or not a property of an RDF source has a given resource target. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @param targetURI {string} the URI of the target + * + * @returns {boolean} whether or not there is such an assertion + */ + _hasResourceAssertion: function HS__hasResourceAssertion(sourceURI, + propertyURI, + targetURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetResource(targetURI); + + return this._ds.HasAssertion(source, property, target, true); + }, + + /** + * Whether or not a property of an RDF source has a given literal value. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + * @param value {string} the literal value + * + * @returns {boolean} whether or not there is such an assertion + */ + _hasLiteralAssertion: function HS__hasLiteralAssertion(sourceURI, + propertyURI, + value) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetLiteral(value); + + return this._ds.HasAssertion(source, property, target, true); + }, + + /** + * Whether or not there is an RDF source that has the given property set to + * the given literal value. + * + * @param propertyURI {string} the URI of the property + * @param value {string} the literal value + * + * @returns {boolean} whether or not there is a source + */ + _existsLiteralTarget: function HS__existsLiteralTarget(propertyURI, value) { + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetLiteral(value); + + return this._ds.hasArcIn(target, property); + }, + + /** + * Get the source for a property set to a given literal value. + * + * @param propertyURI {string} the URI of the property + * @param value {string} the literal value + */ + _getSourceForLiteral: function HS__getSourceForLiteral(propertyURI, value) { + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetLiteral(value); + + var source = this._ds.GetSource(property, target, true); + if (source) + return source.ValueUTF8; + + return null; + }, + + /** + * Whether or not there is an RDF source that has the given property set to + * the given resource target. + * + * @param propertyURI {string} the URI of the property + * @param targetURI {string} the URI of the target + * + * @returns {boolean} whether or not there is a source + */ + _existsResourceTarget: function HS__existsResourceTarget(propertyURI, + targetURI) { + var property = this._rdf.GetResource(propertyURI); + var target = this._rdf.GetResource(targetURI); + + return this._ds.hasArcIn(target, property); + }, + + /** + * Remove a property of an RDF source. + * + * @param sourceURI {string} the URI of the source + * @param propertyURI {string} the URI of the property + */ + _removeTarget: function HS__removeTarget(sourceURI, propertyURI) { + var source = this._rdf.GetResource(sourceURI); + var property = this._rdf.GetResource(propertyURI); + + if (this._ds.hasArcOut(source, property)) { + var target = this._ds.GetTarget(source, property, true); + this._ds.Unassert(source, property, target); + } + }, + + /** + * Remove all assertions about a given RDF source. + * + * Note: not recursive. If some assertions point to other resources, + * and you want to remove assertions about those resources too, you need + * to do so manually. + * + * @param sourceURI {string} the URI of the source + */ + _removeAssertions: function HS__removeAssertions(sourceURI) { + var source = this._rdf.GetResource(sourceURI); + var properties = this._ds.ArcLabelsOut(source); + + while (properties.hasMoreElements()) { + let property = properties.getNext(); + let targets = this._ds.GetTargets(source, property, true); + while (targets.hasMoreElements()) { + let target = targets.getNext(); + this._ds.Unassert(source, property, target); + } + } + } + +}; + +//****************************************************************************// +// More XPCOM Plumbing + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HandlerService]); diff --git a/uriloader/exthandler/nsHandlerService.manifest b/uriloader/exthandler/nsHandlerService.manifest new file mode 100644 index 000000000..5ed86c798 --- /dev/null +++ b/uriloader/exthandler/nsHandlerService.manifest @@ -0,0 +1,2 @@ +component {32314cc8-22f7-4f7f-a645-1a45453ba6a6} nsHandlerService.js +contract @mozilla.org/uriloader/handler-service;1 {32314cc8-22f7-4f7f-a645-1a45453ba6a6} process=main
\ No newline at end of file diff --git a/uriloader/exthandler/nsIContentDispatchChooser.idl b/uriloader/exthandler/nsIContentDispatchChooser.idl new file mode 100644 index 000000000..7c186deee --- /dev/null +++ b/uriloader/exthandler/nsIContentDispatchChooser.idl @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsIHelperAppLauncher; +interface nsIURI; +interface nsIInterfaceRequestor; + +/** + * 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 { + /** + * This request is passed to the helper app dialog because Gecko can not + * handle content of this type. + */ + const unsigned long REASON_CANNOT_HANDLE = 0; + + /** + * Asks the user what to do with the content. + * + * @param aHander + * The interface describing the details of how this content should or + * can be handled. + * @param aWindowContext + * The parent window context to show this chooser. This can be null, + * and some implementations may not care about it. Generally, you'll + * want to pass an nsIDOMWindow in so the chooser can be properly + * parented when opened. + * @param aURI + * The URI of the resource that we are asking about. + * @param aReason + * The reason why we are asking (see above). + */ + void ask(in nsIHandlerInfo aHandler, + in nsIInterfaceRequestor aWindowContext, + in nsIURI aURI, + in unsigned long aReason); +}; + diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl new file mode 100644 index 000000000..bfdfff5ce --- /dev/null +++ b/uriloader/exthandler/nsIExternalHelperAppService.idl @@ -0,0 +1,154 @@ +/* -*- 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; + +/** + * 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. Note in e10s land + * this is likely a CPOW that points to a window in the child process. + * @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); + + /** + * 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. Does not actually perform the + * save. + * NOTE: This will release the reference to the + * nsIHelperAppLauncherDialog. + */ + void saveToDisk(in nsIFile aNewFileLocation, in boolean aRememberThisPreference); + + /** + * Remembers that aApplication should be used to launch this content. Does + * not actually launch the application. + * NOTE: This will release the reference to the nsIHelperAppLauncherDialog. + * @param aApplication nsIFile corresponding to the location of the application to use. + * @param aRememberThisPreference TRUE if we should remember this choice. + */ + void launchWithApplication(in nsIFile aApplication, in boolean aRememberThisPreference); + + /** + * 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; +}; diff --git a/uriloader/exthandler/nsIExternalProtocolService.idl b/uriloader/exthandler/nsIExternalProtocolService.idl new file mode 100644 index 000000000..44d756030 --- /dev/null +++ b/uriloader/exthandler/nsIExternalProtocolService.idl @@ -0,0 +1,135 @@ +/* -*- 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 nsIInterfaceRequestor; +interface nsIHandlerInfo; + +/** + * 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 url via an external protocol handler (if one exists) + * + * @param aURL The url to load + * + * @deprecated Use LoadURI instead (See Bug 389565 for removal) + */ + [deprecated] void loadUrl(in nsIURI aURL); + + /** + * 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 aWindowContext + * The window 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 nsIInterfaceRequestor aWindowContext); + + /** + * 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); +}; diff --git a/uriloader/exthandler/nsIExternalSharingAppService.idl b/uriloader/exthandler/nsIExternalSharingAppService.idl new file mode 100644 index 000000000..f58f6981d --- /dev/null +++ b/uriloader/exthandler/nsIExternalSharingAppService.idl @@ -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/. */ + +#include "nsIMIMEInfo.idl" + +%{C++ +#define NS_EXTERNALSHARINGAPPSERVICE_CONTRACTID "@mozilla.org/uriloader/external-sharing-app-service;1" +%} + + +[scriptable, uuid(7111f769-53ec-41fd-b314-613661d5b6ba)] +interface nsISharingHandlerApp : nsIHandlerApp +{ + void share(in AString data, [optional] in AString title); +}; + +[scriptable, uuid(cf7d04e5-3892-482e-81bb-073dc1c83f76)] +interface nsIExternalSharingAppService : nsISupports { + void shareWithDefault(in AString data, in AString mime, + [optional] in AString title); + + void getSharingApps(in AString aMIMEType, + [optional] out unsigned long aLen, + [array, size_is(aLen), retval] out nsISharingHandlerApp handlerApps); +}; + + diff --git a/uriloader/exthandler/nsIExternalURLHandlerService.idl b/uriloader/exthandler/nsIExternalURLHandlerService.idl new file mode 100644 index 000000000..357349759 --- /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 000000000..efea43937 --- /dev/null +++ b/uriloader/exthandler/nsIHandlerService.idl @@ -0,0 +1,117 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 +{ + /** + * 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); +}; diff --git a/uriloader/exthandler/nsIHelperAppLauncherDialog.idl b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl new file mode 100644 index 000000000..f8190e744 --- /dev/null +++ b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl @@ -0,0 +1,89 @@ +/* -*- 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; + +/** + * 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 nsISupports 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 nsISupports aWindowContext, + in wstring aDefaultFileName, + in wstring aSuggestedFileExtension, + in boolean aForcePrompt); +}; + + +%{C++ +#define NS_HELPERAPPLAUNCHERDLG_CONTRACTID "@mozilla.org/helperapplauncherdialog;1" +%} diff --git a/uriloader/exthandler/nsLocalHandlerApp.cpp b/uriloader/exthandler/nsLocalHandlerApp.cpp new file mode 100644 index 000000000..f1b65dca2 --- /dev/null +++ b/uriloader/exthandler/nsLocalHandlerApp.cpp @@ -0,0 +1,179 @@ +/* -*- 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, + nsIInterfaceRequestor *aWindowContext) +{ + // 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 000000000..30b410ce3 --- /dev/null +++ b/uriloader/exthandler/nsLocalHandlerApp.h @@ -0,0 +1,60 @@ +/* -*- 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/nsMIMEInfoImpl.cpp b/uriloader/exthandler/nsMIMEInfoImpl.cpp new file mode 100644 index 000000000..59886e465 --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoImpl.cpp @@ -0,0 +1,435 @@ +/* -*- 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 "nsXPIDLString.h" +#include "nsReadableUtils.h" +#include "nsStringEnumerator.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsEscape.h" +#include "nsIURILoader.h" +#include "nsCURILoader.h" + +// 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_THREADSAFE + +// 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) +{ + NS_ASSERTION(!aExtension.IsEmpty(), "no extension"); + bool found = false; + uint32_t extCount = mExtensions.Length(); + if (extCount < 1) return NS_OK; + + for (uint8_t i=0; i < extCount; i++) { + const nsCString& ext = mExtensions[i]; + if (ext.Equals(aExtension, nsCaseInsensitiveCStringComparator())) { + found = true; + break; + } + } + + *_retval = found; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPrimaryExtension(nsACString& _retval) +{ + if (!mExtensions.Length()) + return NS_ERROR_NOT_INITIALIZED; + + _retval = mExtensions[0]; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetPrimaryExtension(const nsACString& aExtension) +{ + NS_ASSERTION(!aExtension.IsEmpty(), "no extension"); + uint32_t extCount = mExtensions.Length(); + uint8_t i; + bool found = false; + for (i=0; i < extCount; i++) { + const nsCString& ext = mExtensions[i]; + if (ext.Equals(aExtension, nsCaseInsensitiveCStringComparator())) { + found = true; + break; + } + } + if (found) { + mExtensions.RemoveElementAt(i); + } + + mExtensions.InsertElementAt(0, aExtension); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::AppendExtension(const nsACString& aExtension) +{ + mExtensions.AppendElement(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(); + nsCString extList( aExtensions ); + + int32_t breakLocation = -1; + while ( (breakLocation= extList.FindChar(',') )!= -1) + { + mExtensions.AppendElement(Substring(extList.get(), extList.get() + breakLocation)); + extList.Cut(0, breakLocation+1 ); + } + if ( !extList.IsEmpty() ) + mExtensions.AppendElement( extList ); + 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); + + nsAutoCString path; + aFile->GetNativePath(path); + return LaunchWithIProcess(executable, path); + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMIMEInfoBase::LaunchWithURI(nsIURI* aURI, + nsIInterfaceRequestor* aWindowContext) +{ + // for now, this is only being called with protocol handlers; that + // will change once we get to more general registerContentHandler + // support + NS_ASSERTION(mClass == eProtocolInfo, + "nsMIMEInfoBase should be a protocol handler"); + + if (mPreferredAction == useSystemDefault) { + return LoadUriInternal(aURI); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) + return NS_ERROR_FILE_NOT_FOUND; + + return mPreferredApplication->LaunchWithURI(aURI, aWindowContext); + } + + return NS_ERROR_INVALID_ARG; +} + +void +nsMIMEInfoBase::CopyBasicDataTo(nsMIMEInfoBase* aOther) +{ + aOther->mSchemeOrType = mSchemeOrType; + aOther->mDefaultAppDescription = mDefaultAppDescription; + aOther->mExtensions = mExtensions; +} + +/* 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); +} + +// 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; +} + +nsresult +nsMIMEInfoImpl::LaunchDefaultWithFile(nsIFile* aFile) +{ + if (!mDefaultApplication) + return NS_ERROR_FILE_NOT_FOUND; + + nsAutoCString nativePath; + aFile->GetNativePath(nativePath); + + return LaunchWithIProcess(mDefaultApplication, 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 000000000..34f244242 --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoImpl.h @@ -0,0 +1,196 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 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 "nsIAtom.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIMutableArray.h" +#include "nsIFile.h" +#include "nsCOMPtr.h" +#include "nsIURI.h" +#include "nsIProcess.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, + nsIInterfaceRequestor *aWindowContext) 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); + + /** + * 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); + + // 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); + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription); + + // 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); + + /** + * Loads the URI with the OS default app. This should be overridden by each + * OS's implementation. + */ + virtual nsresult LoadUriInternal(nsIURI *aURI) = 0; + + nsCOMPtr<nsIFile> mDefaultApplication; ///< default application associated with this type. +}; + +#endif //__nsmimeinfoimpl_h___ diff --git a/uriloader/exthandler/nsWebHandlerApp.js b/uriloader/exthandler/nsWebHandlerApp.js new file mode 100644 index 000000000..65d600f55 --- /dev/null +++ b/uriloader/exthandler/nsWebHandlerApp.js @@ -0,0 +1,172 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//////////////////////////////////////////////////////////////////////////////// +//// Constants + +const Ci = Components.interfaces; +const Cr = Components.results; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +//////////////////////////////////////////////////////////////////////////////// +//// nsWebHandler class + +function nsWebHandlerApp() {} + +nsWebHandlerApp.prototype = { + ////////////////////////////////////////////////////////////////////////////// + //// nsWebHandler + + classDescription: "A web handler for protocols and content", + classID: Components.ID("8b1ae382-51a9-4972-b930-56977a57919d"), + contractID: "@mozilla.org/uriloader/web-handler-app;1", + + _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: function(aHandlerApp) { + if (!aHandlerApp) + throw Cr.NS_ERROR_NULL_POINTER; + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp && + aHandlerApp.uriTemplate && + this.uriTemplate && + aHandlerApp.uriTemplate == this.uriTemplate) + return true; + + return false; + }, + + launchWithURI: function nWHA__launchWithURI(aURI, aWindowContext) { + + // 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 ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var uriToSend = ioService.newURI(uriSpecToSend, null, null); + + // if we have a window context, use the URI loader to load there + if (aWindowContext) { + try { + // getInterface throws if the object doesn't implement the given + // interface, so this try/catch statement is more of an if. + // If aWindowContext refers to a remote docshell, send the load + // request to the correct process. + aWindowContext.getInterface(Ci.nsIRemoteWindowContext) + .openURI(uriToSend); + return; + } catch (e) { + if (e.result != Cr.NS_NOINTERFACE) { + throw e; + } + } + + // create a channel from this URI + var channel = NetUtil.newChannel({ + uri: uriToSend, + loadUsingSystemPrincipal: true + }); + channel.loadFlags = Ci.nsIChannel.LOAD_DOCUMENT_URI; + + // load the channel + var uriLoader = Cc["@mozilla.org/uriloader;1"]. + getService(Ci.nsIURILoader); + // XXX ideally, whether to pass the IS_CONTENT_PREFERRED flag should be + // passed in from above. Practically, the flag is probably a reasonable + // default since browsers don't care much, and link click is likely to be + // the more interesting case for non-browser apps. See + // <https://bugzilla.mozilla.org/show_bug.cgi?id=392957#c9> for details. + uriLoader.openURI(channel, Ci.nsIURILoader.IS_CONTENT_PREFERRED, + aWindowContext); + return; + } + + // since we don't have a window context, hand it off to a browser + var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + + // get browser dom window + var browserDOMWin = windowMediator.getMostRecentWindow("navigator:browser") + .QueryInterface(Ci.nsIDOMChromeWindow) + .browserDOMWindow; + + // if we got 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 + browserDOMWin.openURI(uriToSend, + null, // no window.opener + Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, + Ci.nsIBrowserDOMWindow.OPEN_NEW); + + return; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsIWebHandlerApp + + get uriTemplate() { + return this._uriTemplate; + }, + + set uriTemplate(aURITemplate) { + this._uriTemplate = aURITemplate; + }, + + ////////////////////////////////////////////////////////////////////////////// + //// nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebHandlerApp, Ci.nsIHandlerApp]) +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Module + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsWebHandlerApp]); + diff --git a/uriloader/exthandler/nsWebHandlerApp.manifest b/uriloader/exthandler/nsWebHandlerApp.manifest new file mode 100644 index 000000000..02b1d02af --- /dev/null +++ b/uriloader/exthandler/nsWebHandlerApp.manifest @@ -0,0 +1,2 @@ +component {8b1ae382-51a9-4972-b930-56977a57919d} nsWebHandlerApp.js +contract @mozilla.org/uriloader/web-handler-app;1 {8b1ae382-51a9-4972-b930-56977a57919d} diff --git a/uriloader/exthandler/tests/Makefile.in b/uriloader/exthandler/tests/Makefile.in new file mode 100644 index 000000000..13250c8ab --- /dev/null +++ b/uriloader/exthandler/tests/Makefile.in @@ -0,0 +1,11 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include $(topsrcdir)/config/rules.mk + +# need the executable for running the xpcshell unit tests +ifneq (,$(SIMPLE_PROGRAMS)) +libs:: + $(INSTALL) $(SIMPLE_PROGRAMS) $(DEPTH)/_tests/xpcshell/$(relativesrcdir)/unit +endif diff --git a/uriloader/exthandler/tests/WriteArgument.cpp b/uriloader/exthandler/tests/WriteArgument.cpp new file mode 100644 index 000000000..2efbed906 --- /dev/null +++ b/uriloader/exthandler/tests/WriteArgument.cpp @@ -0,0 +1,24 @@ +#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/browser.ini b/uriloader/exthandler/tests/mochitest/browser.ini new file mode 100644 index 000000000..9647dcf8c --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = head.js +support-files = + protocolHandler.html + +[browser_download_always_ask_preferred_app.js] +[browser_remember_download_option.js] +[browser_web_protocol_handlers.js] 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 000000000..40f7ef71e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js @@ -0,0 +1,19 @@ +add_task(function*() { + // Create mocked objects for test + let launcher = createMockedObjects(false); + // Open helper app dialog with mocked launcher + let dlg = yield* 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."); + yield 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.documentElement.cancelDialog(); + yield helperAppDialogHiddenPromise; +}); 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 000000000..996e5ad69 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js @@ -0,0 +1,49 @@ +add_task(function*() { + // create mocked objects + let launcher = createMockedObjects(true); + + // open helper app dialog with mocked launcher + let dlg = yield* openHelperAppDialog(launcher); + + let doc = dlg.document; + + // 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://dxr.mozilla.org/mozilla-central/source/toolkit/components/prompts/src/SharedPromptUtils.jsm#53 + doc.documentElement.getButton("accept").disabled = false; + doc.documentElement.acceptDialog(); + yield 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."); + var handlerInfos = gHandlerSvc.enumerate(); + while (handlerInfos.hasMoreElements()) { + let handlerInfo = handlerInfos.getNext().QueryInterface(Ci.nsIHandlerInfo); + 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 000000000..ac3e66258 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js @@ -0,0 +1,76 @@ +let testURL = "http://example.com/browser/" + + "uriloader/exthandler/tests/mochitest/protocolHandler.html"; + +add_task(function*() { + // Load a page registering a protocol handler. + let browser = gBrowser.selectedBrowser; + browser.loadURI(testURL); + yield BrowserTestUtils.browserLoaded(browser, testURL); + + // Register the protocol handler by clicking the notificationbar button. + let notificationValue = "Protocol Registration: testprotocol"; + let getNotification = () => + gBrowser.getNotificationBox().getNotificationWithValue(notificationValue); + yield BrowserTestUtils.waitForCondition(getNotification); + let notification = getNotification(); + let button = + notification.getElementsByClassName("notification-button-default")[0]; + 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("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 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); + + // Middle-click a testprotocol link and check the new tab is correct + let link = "#link"; + const expectedURL = "https://example.com/foobar?uri=testprotocol%3Atest"; + + let promiseTabOpened = + BrowserTestUtils.waitForNewTab(gBrowser, expectedURL); + yield BrowserTestUtils.synthesizeMouseAtCenter(link, {button: 1}, browser); + let tab = yield promiseTabOpened; + gBrowser.selectedTab = tab; + is(gURLBar.value, expectedURL, + "the expected URL is displayed in the location bar"); + yield BrowserTestUtils.removeTab(tab); + + // Shift-click the testprotocol link and check the new window. + let newWindowPromise = BrowserTestUtils.waitForNewWindow(); + yield BrowserTestUtils.synthesizeMouseAtCenter(link, {shiftKey: true}, + browser); + let win = yield newWindowPromise; + yield BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + yield BrowserTestUtils.waitForCondition(() => win.gBrowser.currentURI.spec == expectedURL); + is(win.gURLBar.value, expectedURL, + "the expected URL is displayed in the location bar"); + yield BrowserTestUtils.closeWindow(win); + + // Click the testprotocol link and check the url in the current tab. + let loadPromise = BrowserTestUtils.browserLoaded(browser); + yield BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser); + yield loadPromise; + yield 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/handlerApp.xhtml b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml new file mode 100644 index 000000000..83ba0d1a5 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml @@ -0,0 +1,30 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Pseudo Web Handler App</title> + <script type="text/javascript" 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 000000000..597f9442d --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/handlerApps.js @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 = + "http://mochi.test:8888/tests/uriloader/exthandler/tests/mochitest/" + + "handlerApp.xhtml?uri=%s"; + + // set up the uri to test with + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(SpecialPowers.Ci.nsIIOService); + var uri = ioService.newURI(testURI, null, null); + + // create a window, and launch the handler in it + var newWindow = window.open("", "handlerWindow", "height=300,width=300"); + var windowContext = + SpecialPowers.wrap(newWindow).QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsIWebNavigation). + QueryInterface(SpecialPowers.Ci.nsIDocShell); + + 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 + 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.nsILocalFile); + 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.nsILocalFile); + exe.initWithPath("/bin/echo"); + } + } else { + // assume a generic UNIX variant + exe = Cc["@mozilla.org/file/local;1"]. + createInstance(SpecialPowers.Ci.nsILocalFile); + exe.initWithPath("/bin/echo"); + } + + localHandler.executable = exe; + localHandler.launchWithURI(ioService.newURI(testURI, null, null)); + + // 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.nsILocalFile); + 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 000000000..dad0493f8 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/head.js @@ -0,0 +1,105 @@ +Components.utils.import("resource://gre/modules/FileUtils.jsm"); +Components.utils.import("resource://gre/modules/Task.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, false); + internalMockedMIME.preferredApplicationHandler = mockedHandlerApp; + } + + // Proxy for the mocked MIME info for faking the read-only attributes + let mockedMIME = new Proxy(internalMockedMIME, { + get: function (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/", null, null), + 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: XPCOMUtils.generateQI([Ci.nsICancelable, Ci.nsIHelperAppLauncher]) + }; + + registerCleanupFunction(function() { + // remove the mocked mime info from database. + let mockHandlerInfo = gMimeSvc.getFromTypeAndExtension("text/x-test-handler", null); + if (gHandlerSvc.exists(mockHandlerInfo)) { + gHandlerSvc.remove(mockHandlerInfo); + } + }); + + return mockedLauncher; +} + +function* openHelperAppDialog(launcher) { + let helperAppDialog = Cc["@mozilla.org/helperapplauncherdialog;1"]. + createInstance(Ci.nsIHelperAppLauncherDialog); + + let helperAppDialogShownPromise = BrowserTestUtils.domWindowOpened(); + try { + helperAppDialog.show(launcher, window, "foopy"); + } catch (ex) { + ok(false, "Trying to show unknownContentType.xul failed with exception: " + ex); + Cu.reportError(ex); + } + let dlg = yield helperAppDialogShownPromise; + + yield BrowserTestUtils.waitForEvent(dlg, "load", false); + + is(dlg.location.href, "chrome://mozapps/content/downloads/unknownContentType.xul", + "Got correct dialog"); + + return dlg; +} diff --git a/uriloader/exthandler/tests/mochitest/mochitest.ini b/uriloader/exthandler/tests/mochitest/mochitest.ini new file mode 100644 index 000000000..ae191dc7b --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/mochitest.ini @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = + handlerApp.xhtml + handlerApps.js + unsafeBidi_chromeScript.js + unsafeBidiFileName.sjs + +[test_handlerApps.xhtml] +skip-if = (toolkit == 'android' || os == 'mac') || e10s # OS X: bug 786938 +[test_unsafeBidiChars.xhtml] diff --git a/uriloader/exthandler/tests/mochitest/protocolHandler.html b/uriloader/exthandler/tests/mochitest/protocolHandler.html new file mode 100644 index 000000000..5a929ba99 --- /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("testprotocol", + "https://example.com/foobar?uri=%s", + "Test Protocol"); + </script> + <a id="link" href="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 000000000..e5d73a232 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml @@ -0,0 +1,12 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handler Apps </title> + <script type="text/javascript" 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_unsafeBidiChars.xhtml b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml new file mode 100644 index 000000000..fafe0b4c5 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml @@ -0,0 +1,77 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handling of unsafe bidi chars</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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"> +<![CDATA[ + +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(function* () { + let url = SimpleTest.getTestFileURL("unsafeBidi_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((yield 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"); + yield 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 000000000..48301be5b --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) + return; + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader("Content-Type", "application/octet-stream; name=\"" + name + "\""); + response.setHeader("Content-Disposition", "inline; filename=\"" + name + "\""); +} diff --git a/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js b/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js new file mode 100644 index 000000000..16c82d818 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js @@ -0,0 +1,28 @@ +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1"; +const HELPERAPP_DIALOG_CID = + Components.ID(Cc[HELPERAPP_DIALOG_CONTRACT].number); + +const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator).generateUUID(); + +function HelperAppLauncherDialog() {} +HelperAppLauncherDialog.prototype = { + show: function(aLauncher, aWindowContext, aReason) { + sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]) +}; + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory(FAKE_CID, "", HELPERAPP_DIALOG_CONTRACT, + XPCOMUtils._getFactory(HelperAppLauncherDialog)); + +addMessageListener("unregister", function() { + registrar.registerFactory(HELPERAPP_DIALOG_CID, "", + HELPERAPP_DIALOG_CONTRACT, null); + sendAsyncMessage("unregistered"); +}); diff --git a/uriloader/exthandler/tests/moz.build b/uriloader/exthandler/tests/moz.build new file mode 100644 index 000000000..6aadfdc52 --- /dev/null +++ b/uriloader/exthandler/tests/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini'] + +XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini'] + +BROWSER_CHROME_MANIFESTS += ['mochitest/browser.ini'] + +# The encoding test is already implemented in the Downloads API by a set of +# test cases with the string "content_encoding" in their names. +if not CONFIG['MOZ_JSDOWNLOADS']: + XPCSHELL_TESTS_MANIFESTS += ['unit_ipc/xpcshell.ini'] + +GeckoSimplePrograms([ + 'WriteArgument', +], linkage=None) + +USE_LIBS += [ + 'nspr', +] diff --git a/uriloader/exthandler/tests/unit/head_handlerService.js b/uriloader/exthandler/tests/unit/head_handlerService.js new file mode 100644 index 000000000..8b6803d24 --- /dev/null +++ b/uriloader/exthandler/tests/unit/head_handlerService.js @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Inspired by the Places infrastructure in head_bookmarks.js + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; +var Cu = Components.utils; + +var HandlerServiceTest = { + //**************************************************************************// + // Convenience Getters + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + QueryInterface(Ci.nsIDirectoryService); + return this.__dirSvc; + }, + + __consoleSvc: null, + get _consoleSvc() { + if (!this.__consoleSvc) + this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__consoleSvc; + }, + + + //**************************************************************************// + // nsISupports + + interfaces: [Ci.nsIDirectoryServiceProvider, Ci.nsISupports], + + QueryInterface: function HandlerServiceTest_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + + + //**************************************************************************// + // Initialization & Destruction + + init: function HandlerServiceTest_init() { + // Register ourselves as a directory provider for the datasource file + // if there isn't one registered already. + try { + this._dirSvc.get("UMimTyp", Ci.nsIFile); + } catch (ex) { + this._dirSvc.registerProvider(this); + this._providerRegistered = true; + } + + // Delete the existing datasource file, if any, so we start from scratch. + // We also do this after finishing the tests, so there shouldn't be an old + // file lying around, but just in case we delete it here as well. + this._deleteDatasourceFile(); + + // Turn on logging so we can troubleshoot problems with the tests. + var prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefBranch.setBoolPref("browser.contentHandling.log", true); + }, + + destroy: function HandlerServiceTest_destroy() { + // Delete the existing datasource file, if any, so we don't leave test files + // lying around and we start from scratch the next time. + this._deleteDatasourceFile(); + // Unregister the directory service provider + if (this._providerRegistered) + this._dirSvc.unregisterProvider(this); + }, + + + //**************************************************************************// + // nsIDirectoryServiceProvider + + getFile: function HandlerServiceTest_getFile(property, persistent) { + this.log("getFile: requesting " + property); + + persistent.value = true; + + if (property == "UMimTyp") { + var datasourceFile = this._dirSvc.get("CurProcD", Ci.nsIFile); + datasourceFile.append("mimeTypes.rdf"); + return datasourceFile; + } + + // This causes extraneous errors to show up in the log when the directory + // service asks us first for CurProcD and MozBinD. I wish there was a way + // to suppress those errors. + this.log("the following NS_ERROR_FAILURE exception in " + + "nsIDirectoryServiceProvider::getFile is expected, " + + "as we don't provide the '" + property + "' file"); + throw Cr.NS_ERROR_FAILURE; + }, + + + //**************************************************************************// + // Utilities + + /** + * Delete the datasource file. + */ + _deleteDatasourceFile: function HandlerServiceTest__deleteDatasourceFile() { + var file = this._dirSvc.get("UMimTyp", Ci.nsIFile); + if (file.exists()) + file.remove(false); + }, + + /** + * Get the contents of the datasource as a serialized string. Useful for + * debugging problems with test failures, i.e.: + * + * HandlerServiceTest.log(HandlerServiceTest.getDatasourceContents()); + * + * @returns {string} the serialized datasource + */ + getDatasourceContents: function HandlerServiceTest_getDatasourceContents() { + var rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); + + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var fileHandler = ioService.getProtocolHandler("file"). + QueryInterface(Ci.nsIFileProtocolHandler); + var fileURL = fileHandler.getURLSpecFromFile(this.getDatasourceFile()); + var ds = rdf.GetDataSourceBlocking(fileURL); + + var outputStream = { + data: "", + close: function() {}, + flush: function() {}, + write: function (buffer,count) { + this.data += buffer; + return count; + }, + writeFrom: function (stream,count) {}, + isNonBlocking: false + }; + + ds.QueryInterface(Components.interfaces.nsIRDFXMLSource); + ds.Serialize(outputStream); + + return outputStream.data; + }, + + /** + * Log a message to the console and the test log. + */ + log: function HandlerServiceTest_log(message) { + message = "*** HandlerServiceTest: " + message; + this._consoleSvc.logStringMessage(message); + print(message); + } + +}; + +HandlerServiceTest.init(); diff --git a/uriloader/exthandler/tests/unit/mailcap b/uriloader/exthandler/tests/unit/mailcap new file mode 100644 index 000000000..dc93ef804 --- /dev/null +++ b/uriloader/exthandler/tests/unit/mailcap @@ -0,0 +1,2 @@ +text/plain; cat '%s'; needsterminal +text/plain; sed '%s' diff --git a/uriloader/exthandler/tests/unit/tail_handlerService.js b/uriloader/exthandler/tests/unit/tail_handlerService.js new file mode 100644 index 000000000..1d1989127 --- /dev/null +++ b/uriloader/exthandler/tests/unit/tail_handlerService.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +HandlerServiceTest.destroy(); diff --git a/uriloader/exthandler/tests/unit/test_badMIMEType.js b/uriloader/exthandler/tests/unit/test_badMIMEType.js new file mode 100644 index 000000000..df1202a0f --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_badMIMEType.js @@ -0,0 +1,26 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + // "text/plain" has an 0xFF character appended to it. This means it's an + // invalid string, which is tricky to enter using a text editor (I used + // emacs' hexl-mode). It also means an ordinary text editor might drop it + // or convert it to something that *is* valid (in UTF8). So we measure + // its length to make sure this hasn't happened. + var badMimeType = "text/plainÿ"; + do_check_eq(badMimeType.length, 11); + + try { + var type = Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService). + getFromTypeAndExtension(badMimeType, "txt"); + } catch (e if (e instanceof Ci.nsIException && + e.result == Cr.NS_ERROR_NOT_AVAILABLE)) { + // This is an expected exception, thrown if the type can't be determined + } finally { + } + // Not crashing is good enough + do_check_eq(true, true); +} diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js new file mode 100644 index 000000000..d83c486bb --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 508030 <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 = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + + // --- Test procedure --- + + const kTestExtension = "testextension"; + const kTestExtensionMixedCase = "testExtensIon"; + const kTestMimeType = "application/x-testextension"; + + // Ensure that the test extension is not initially recognized by the operating + // system or the "ext-to-type-mapping" category. + try { + // Try and get the MIME type associated with the extension. + mimeService.getTypeFromExtension(kTestExtension); + // The line above should have thrown an exception. + do_throw("nsIMIMEService.getTypeFromExtension succeeded unexpectedly"); + } catch (e if (e instanceof Ci.nsIException && + e.result == Cr.NS_ERROR_NOT_AVAILABLE)) { + // This is an expected exception, thrown if the type can't be determined. + // Any other exception would cause the test to fail. + } + + // Add a temporary category entry mapping the extension to the MIME type. + categoryManager.addCategoryEntry("ext-to-type-mapping", kTestExtension, + kTestMimeType, false, true); + + // Check that the mapping is recognized in the simple case. + var type = mimeService.getTypeFromExtension(kTestExtension); + do_check_eq(type, kTestMimeType); + + // Check that the mapping is recognized even if the extension has mixed case. + type = mimeService.getTypeFromExtension(kTestExtensionMixedCase); + do_check_eq(type, kTestMimeType); + + // Clean up after ourselves. + categoryManager.deleteCategoryEntry("ext-to-type-mapping", kTestExtension, false); +} diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js new file mode 100644 index 000000000..1ae2a6fcf --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js @@ -0,0 +1,186 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 484579 <https://bugzilla.mozilla.org/show_bug.cgi?id=484579>: + * nsIMIMEService.getTypeFromExtension may fail unexpectedly on Windows when + * "Content Type" is empty in the registry. + */ +function run_test() { + // --- Preliminary platform check --- + + // If this test is not running on the Windows platform, stop now, before + // calling XPCOMUtils.generateQI during the MockWindowsRegKey declaration. + if (mozinfo.os != "win") + return; + + // --- Modified nsIWindowsRegKey implementation --- + + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + + /** + * Constructs a new mock registry key by wrapping the provided object. + * + * This mock implementation is tailored for this test, and forces consumers + * of the readStringValue method to believe that the "Content Type" value of + * the ".txt" key under HKEY_CLASSES_ROOT is an empty string. + * + * The same value read from "HKEY_LOCAL_MACHINE\SOFTWARE\Classes" is not + * affected. + * + * @param aWrappedObject An actual nsIWindowsRegKey implementation. + */ + function MockWindowsRegKey(aWrappedObject) { + this._wrappedObject = aWrappedObject; + + // This function creates a forwarding function for wrappedObject + function makeForwardingFunction(functionName) { + return function() { + return aWrappedObject[functionName].apply(aWrappedObject, arguments); + } + } + + // Forward all the functions that are not explicitly overridden + for (var propertyName in aWrappedObject) { + if (!(propertyName in this)) { + if (typeof aWrappedObject[propertyName] == "function") { + this[propertyName] = makeForwardingFunction(propertyName); + } else { + this[propertyName] = aWrappedObject[propertyName]; + } + } + } + } + + MockWindowsRegKey.prototype = { + // --- Overridden nsISupports interface functions --- + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowsRegKey]), + + // --- Overridden nsIWindowsRegKey interface functions --- + + open: function(aRootKey, aRelPath, aMode) { + // Remember the provided root key and path + this._rootKey = aRootKey; + this._relPath = aRelPath; + + // Create the actual registry key + return this._wrappedObject.open(aRootKey, aRelPath, aMode); + }, + + openChild: function(aRelPath, aMode) { + // Open the child key and wrap it + var innerKey = this._wrappedObject.openChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + createChild: function(aRelPath, aMode) { + // Create the child key and wrap it + var innerKey = this._wrappedObject.createChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + get childCount() { + return this._wrappedObject.childCount; + }, + + get valueCount() { + return this._wrappedObject.valueCount; + }, + + readStringValue: function(aName) { + // If this is the key under test, return a fake value + if (this._rootKey == Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT && + this._relPath.toLowerCase() == ".txt" && + aName.toLowerCase() == "content type") { + return ""; + } + + // Return the real value in the registry + return this._wrappedObject.readStringValue(aName); + } + }; + + // --- Mock nsIWindowsRegKey factory --- + + var componentRegistrar = Components.manager. + QueryInterface(Ci.nsIComponentRegistrar); + + var originalWindowsRegKeyCID; + var mockWindowsRegKeyFactory; + + const kMockCID = Components.ID("{9b23dfe9-296b-4740-ba1c-d39c9a16e55e}"); + const kWindowsRegKeyContractID = "@mozilla.org/windows-registry-key;1"; + const kWindowsRegKeyClassName = "nsWindowsRegKey"; + + function registerMockWindowsRegKeyFactory() { + mockWindowsRegKeyFactory = { + createInstance: function(aOuter, aIid) { + if (aOuter != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + + var innerKey = originalWindowsRegKeyFactory.createInstance(null, aIid); + var key = new MockWindowsRegKey(innerKey); + + return key.QueryInterface(aIid); + } + }; + + // Preserve the original factory + originalWindowsRegKeyCID = Cc[kWindowsRegKeyContractID].number; + + // Register the mock factory + componentRegistrar.registerFactory( + kMockCID, + "Mock Windows Registry Key Implementation", + kWindowsRegKeyContractID, + mockWindowsRegKeyFactory + ); + } + + function unregisterMockWindowsRegKeyFactory() { + // Free references to the mock factory + componentRegistrar.unregisterFactory( + kMockCID, + mockWindowsRegKeyFactory + ); + + // Restore the original factory + componentRegistrar.registerFactory( + Components.ID(originalWindowsRegKeyCID), + "", + kWindowsRegKeyContractID, + null + ); + } + + // --- Test procedure --- + + // Activate the override of the ".txt" file association data in the registry + registerMockWindowsRegKeyFactory(); + try { + // Try and get the MIME type associated with the extension. If this + // operation does not throw an unexpected exception, the test succeeds. + var type = Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService). + getTypeFromExtension(".txt"); + } catch (e if (e instanceof Ci.nsIException && + e.result == Cr.NS_ERROR_NOT_AVAILABLE)) { + // This is an expected exception, thrown if the type can't be determined + } finally { + // Ensure we restore the original factory when the test is finished + unregisterMockWindowsRegKeyFactory(); + } +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService.js b/uriloader/exthandler/tests/unit/test_handlerService.js new file mode 100644 index 000000000..3facc63ae --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService.js @@ -0,0 +1,470 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + //**************************************************************************// + // Constants + + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + + const mimeSvc = Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService); + + const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + + const prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + + const ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + const env = Cc["@mozilla.org/process/environment;1"]. + getService(Components.interfaces.nsIEnvironment); + + const rootPrefBranch = prefSvc.getBranch(""); + + let noMailto = false; + if (mozinfo.os == "win") { + // Check mailto handler from registry. + // If registry entry is nothing, no mailto handler + let regSvc = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + try { + regSvc.open(regSvc.ROOT_KEY_CLASSES_ROOT, + "mailto", + regSvc.ACCESS_READ); + noMailto = false; + } catch (ex) { + noMailto = true; + } + regSvc.close(); + } + + if (mozinfo.os == "linux") { + // Check mailto handler from GIO + // If there isn't one, then we have no mailto handler + let gIOSvc = Cc["@mozilla.org/gio-service;1"]. + createInstance(Ci.nsIGIOService); + try { + gIOSvc.getAppForURIScheme("mailto"); + noMailto = false; + } catch (ex) { + noMailto = true; + } + } + + //**************************************************************************// + // Sample Data + + // It doesn't matter whether or not this nsIFile is actually executable, + // only that it has a path and exists. Since we don't know any executable + // that exists on all platforms (except possibly the application being + // tested, but there doesn't seem to be a way to get a reference to that + // from the directory service), we use the temporary directory itself. + var executable = HandlerServiceTest._dirSvc.get("TmpD", Ci.nsIFile); + // XXX We could, of course, create an actual executable in the directory: + //executable.append("localhandler"); + //if (!executable.exists()) + // executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + + var localHandler = { + name: "Local Handler", + executable: executable, + interfaces: [Ci.nsIHandlerApp, Ci.nsILocalHandlerApp, Ci.nsISupports], + QueryInterface: function(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + } + }; + + var webHandler = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + webHandler.name = "Web Handler"; + webHandler.uriTemplate = "http://www.example.com/?%s"; + + // FIXME: these tests create and manipulate enough variables that it would + // make sense to move each test into its own scope so we don't run the risk + // of one test stomping on another's data. + + + //**************************************************************************// + // Test Default Properties + + // Get a handler info for a MIME type that neither the application nor + // the OS knows about and make sure its properties are set to the proper + // default values. + + var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + // Make sure it's also an nsIHandlerInfo. + do_check_true(handlerInfo instanceof Ci.nsIHandlerInfo); + + do_check_eq(handlerInfo.type, "nonexistent/type"); + + // Deprecated property, but we should still make sure it's set correctly. + do_check_eq(handlerInfo.MIMEType, "nonexistent/type"); + + // These properties are the ones the handler service knows how to store. + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk); + do_check_eq(handlerInfo.preferredApplicationHandler, null); + do_check_eq(handlerInfo.possibleApplicationHandlers.length, 0); + do_check_true(handlerInfo.alwaysAskBeforeHandling); + + // These properties are initialized to default values by the service, + // so we might as well make sure they're initialized to the right defaults. + do_check_eq(handlerInfo.description, ""); + do_check_eq(handlerInfo.hasDefaultHandler, false); + do_check_eq(handlerInfo.defaultDescription, ""); + + // test some default protocol info properties + var haveDefaultHandlersVersion = false; + try { + // If we have a defaultHandlersVersion pref, then assume that we're in the + // firefox tree and that we'll also have default handlers. + // Bug 395131 has been filed to make this test work more generically + // by providing our own prefs for this test rather than this icky + // special casing. + rootPrefBranch.getCharPref("gecko.handlerService.defaultHandlersVersion"); + haveDefaultHandlersVersion = true; + } catch (ex) {} + + const kExternalWarningDefault = + "network.protocol-handler.warn-external-default"; + prefSvc.setBoolPref(kExternalWarningDefault, true); + + // XXX add more thorough protocol info property checking + + // no OS default handler exists + var protoInfo = protoSvc.getProtocolHandlerInfo("x-moz-rheet"); + do_check_eq(protoInfo.preferredAction, protoInfo.alwaysAsk); + do_check_true(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: false + const kExternalWarningPrefPrefix = "network.protocol-handler.warn-external."; + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", false); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_false(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", true); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + // OS handler isn't included in possibleApplicationHandlers, so length is 0 + // Once they become instances of nsILocalHandlerApp, this number will need + // to change. + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_true(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: false + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + else + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + + // Win7+ or Linux's GIO might not have a default mailto: handler + if (noMailto) + do_check_true(protoInfo.alwaysAskBeforeHandling); + else + do_check_false(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", true); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) { + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + // Win7+ or Linux's GIO may have no default mailto: handler. Otherwise + // alwaysAskBeforeHandling is expected to be false here, because although + // the pref is true, the value in RDF is false. The injected mailto handler + // carried over the default pref value, and so when we set the pref above + // to true it's ignored. + if (noMailto) + do_check_true(protoInfo.alwaysAskBeforeHandling); + else + do_check_false(protoInfo.alwaysAskBeforeHandling); + + } else { + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_true(protoInfo.alwaysAskBeforeHandling); + } + + if (haveDefaultHandlersVersion) { + // Now set the value stored in RDF to true, and the pref to false, to make + // sure we still get the right value. (Basically, same thing as above but + // with the values reversed.) + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo.alwaysAskBeforeHandling = true; + handlerSvc.store(protoInfo); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + do_check_true(protoInfo.alwaysAskBeforeHandling); + } + + + //**************************************************************************// + // Test Round-Trip Data Integrity + + // Test round-trip data integrity by setting the properties of the handler + // info object to different values, telling the handler service to store the + // object, and then retrieving a new info object for the same type and making + // sure its properties are identical. + + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.preferredApplicationHandler = localHandler; + handlerInfo.alwaysAskBeforeHandling = false; + + handlerSvc.store(handlerInfo); + + handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + + do_check_neq(handlerInfo.preferredApplicationHandler, null); + var preferredHandler = handlerInfo.preferredApplicationHandler; + do_check_eq(typeof preferredHandler, "object"); + do_check_eq(preferredHandler.name, "Local Handler"); + do_check_true(preferredHandler instanceof Ci.nsILocalHandlerApp); + preferredHandler.QueryInterface(Ci.nsILocalHandlerApp); + do_check_eq(preferredHandler.executable.path, localHandler.executable.path); + + do_check_false(handlerInfo.alwaysAskBeforeHandling); + + // Make sure the handler service's enumerate method lists all known handlers. + var handlerInfo2 = mimeSvc.getFromTypeAndExtension("nonexistent/type2", null); + handlerSvc.store(handlerInfo2); + var handlerTypes = ["nonexistent/type", "nonexistent/type2"]; + if (haveDefaultHandlersVersion) { + handlerTypes.push("webcal"); + handlerTypes.push("mailto"); + handlerTypes.push("irc"); + handlerTypes.push("ircs"); + } + var handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) { + var handler = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo); + do_check_neq(handlerTypes.indexOf(handler.type), -1); + handlerTypes.splice(handlerTypes.indexOf(handler.type), 1); + } + do_check_eq(handlerTypes.length, 0); + + // Make sure the handler service's remove method removes a handler record. + handlerSvc.remove(handlerInfo2); + handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) + do_check_neq(handlers.getNext().QueryInterface(Ci.nsIHandlerInfo).type, + handlerInfo2.type); + + // Make sure we can store and retrieve a handler info object with no preferred + // handler. + var noPreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/no-preferred-handler", null); + handlerSvc.store(noPreferredHandlerInfo); + noPreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/no-preferred-handler", null); + do_check_eq(noPreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure that the handler service removes an existing handler record + // if we store a handler info object with no preferred handler. + var removePreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null); + removePreferredHandlerInfo.preferredApplicationHandler = localHandler; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null); + removePreferredHandlerInfo.preferredApplicationHandler = null; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null); + do_check_eq(removePreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure we can store and retrieve a handler info object with possible + // handlers. We test both adding and removing handlers. + + // Get a handler info and make sure it has no possible handlers. + var possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Store and re-retrieve the handler and make sure it still has no possible + // handlers. + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Add two handlers, store the object, re-retrieve it, and make sure it has + // two handlers. + possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler, + false); + possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler, + false); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 2); + + // Figure out which is the local and which is the web handler and the index + // in the array of the local handler, which is the one we're going to remove + // to test removal of a handler. + var handler1 = possibleHandlersInfo.possibleApplicationHandlers. + queryElementAt(0, Ci.nsIHandlerApp); + var handler2 = possibleHandlersInfo.possibleApplicationHandlers. + queryElementAt(1, Ci.nsIHandlerApp); + var localPossibleHandler, webPossibleHandler, localIndex; + if (handler1 instanceof Ci.nsILocalHandlerApp) + [localPossibleHandler, webPossibleHandler, localIndex] = [handler1, + handler2, + 0]; + else + [localPossibleHandler, webPossibleHandler, localIndex] = [handler2, + handler1, + 1]; + localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp); + webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp); + + // Make sure the two handlers are the ones we stored. + do_check_eq(localPossibleHandler.name, localHandler.name); + do_check_true(localPossibleHandler.equals(localHandler)); + do_check_eq(webPossibleHandler.name, webHandler.name); + do_check_true(webPossibleHandler.equals(webHandler)); + + // Remove a handler, store the object, re-retrieve it, and make sure + // it only has one handler. + possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 1); + + // Make sure the handler is the one we didn't remove. + webPossibleHandler = possibleHandlersInfo.possibleApplicationHandlers. + queryElementAt(0, Ci.nsIWebHandlerApp); + do_check_eq(webPossibleHandler.name, webHandler.name); + do_check_true(webPossibleHandler.equals(webHandler)); + + ////////////////////////////////////////////////////// + // handler info command line parameters and equality + var localApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + var handlerApp = localApp.QueryInterface(Ci.nsIHandlerApp); + + do_check_true(handlerApp.equals(localApp)); + + localApp.executable = executable; + + do_check_eq(0, localApp.parameterCount); + localApp.appendParameter("-test1"); + do_check_eq(1, localApp.parameterCount); + localApp.appendParameter("-test2"); + do_check_eq(2, localApp.parameterCount); + do_check_true(localApp.parameterExists("-test1")); + do_check_true(localApp.parameterExists("-test2")); + do_check_false(localApp.parameterExists("-false")); + localApp.clearParameters(); + do_check_eq(0, localApp.parameterCount); + + var localApp2 = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + + localApp2.executable = executable; + + localApp.clearParameters(); + do_check_true(localApp.equals(localApp2)); + + // equal: + // cut -d 1 -f 2 + // cut -d 1 -f 2 + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test3"); + do_check_true(localApp.equals(localApp2)); + + // not equal: + // cut -d 1 -f 2 + // cut -f 1 -d 2 + + localApp.clearParameters(); + localApp2.clearParameters(); + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test3"); + do_check_false(localApp2.equals(localApp)); + + var str; + str = localApp.getParameter(0) + do_check_eq(str, "-test1"); + str = localApp.getParameter(1) + do_check_eq(str, "-test2"); + str = localApp.getParameter(2) + do_check_eq(str, "-test3"); + + // FIXME: test round trip integrity for a protocol. + // FIXME: test round trip integrity for a handler info with a web handler. + + //**************************************************************************// + // getTypeFromExtension tests + + // test nonexistent extension + var lolType = handlerSvc.getTypeFromExtension("lolcat"); + do_check_eq(lolType, ""); + + + // add a handler for the extension + var lolHandler = mimeSvc.getFromTypeAndExtension("application/lolcat", null); + + do_check_false(lolHandler.extensionExists("lolcat")); + lolHandler.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + lolHandler.preferredApplicationHandler = localHandler; + lolHandler.alwaysAskBeforeHandling = false; + + // store the handler + do_check_false(handlerSvc.exists(lolHandler)); + handlerSvc.store(lolHandler); + do_check_true(handlerSvc.exists(lolHandler)); + + // Get a file:// string pointing to mimeTypes.rdf + var rdfFile = HandlerServiceTest._dirSvc.get("UMimTyp", Ci.nsIFile); + var fileHandler = ioService.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler); + var rdfFileURI = fileHandler.getURLSpecFromFile(rdfFile); + + // Assign a file extenstion to the handler. handlerSvc.store() doesn't + // actually store any file extensions added with setFileExtensions(), you + // have to wade into RDF muck to do so. + + // Based on toolkit/mozapps/downloads/content/helperApps.js :: addExtension() + var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); + var mimeSource = gRDF.GetUnicodeResource("urn:mimetype:application/lolcat"); + var valueProperty = gRDF.GetUnicodeResource("http://home.netscape.com/NC-rdf#fileExtensions"); + var mimeLiteral = gRDF.GetLiteral("lolcat"); + + var DS = gRDF.GetDataSourceBlocking(rdfFileURI); + DS.Assert(mimeSource, valueProperty, mimeLiteral, true); + + + // test now-existent extension + lolType = handlerSvc.getTypeFromExtension("lolcat"); + do_check_eq(lolType, "application/lolcat"); + + // test mailcap entries with needsterminal are ignored on non-Windows non-Mac. + if (mozinfo.os != "win" && mozinfo.os != "mac") { + env.set('PERSONAL_MAILCAP', do_get_file('mailcap').path); + handlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", null); + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useSystemDefault); + do_check_eq(handlerInfo.defaultDescription, "sed"); + } +} diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js new file mode 100644 index 000000000..38622c840 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Encoded test URI to work on all platforms/independent of file encoding +const kTestURI = "http://\u65e5\u672c\u8a93.jp/"; +const kExpectedURI = "http://xn--wgv71a309e.jp/"; +const kOutputFile = "result.txt"; + +// Try several times in case the box we're running on is slow. +const kMaxCheckExistAttempts = 30; // seconds +var gCheckExistsAttempts = 0; + +const tempDir = do_get_tempdir(); + +function checkFile() { + // This is where we expect the output + var tempFile = tempDir.clone(); + tempFile.append(kOutputFile); + + if (!tempFile.exists()) { + if (gCheckExistsAttempts >= kMaxCheckExistAttempts) { + do_throw("Expected File " + tempFile.path + " does not exist after " + + kMaxCheckExistAttempts + " seconds"); + } + else { + ++gCheckExistsAttempts; + // Wait a bit longer then try again + do_timeout(1000, checkFile); + return; + } + } + + // Now read it + var fstream = + Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + var sstream = + Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.nsIScriptableInputStream); + fstream.init(tempFile, -1, 0, 0); + sstream.init(fstream); + + // Read the first line only as that's the one we expect WriteArguments + // to be writing the argument to. + var data = sstream.read(4096); + + sstream.close(); + fstream.close(); + + // Now remove the old file + tempFile.remove(false); + + // This currently fails on Mac with an argument like -psn_0_nnnnnn + // This seems to be to do with how the executable is called, but I couldn't + // find a way around it. + // Additionally the lack of OS detection in xpcshell tests sucks, so we'll + // have to check for the argument mac gives us. + if (data.substring(0, 7) != "-psn_0_") + do_check_eq(data, kExpectedURI); + + do_test_finished(); +} + +function run_test() { + if (mozinfo.os == "mac") { + dump("INFO | test_punycodeURIs.js | Skipping test on mac, bug 599475") + return; + } + + // set up the uri to test with + var ioService = + Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + // set up the local handler object + var localHandler = + Components.classes["@mozilla.org/uriloader/local-handler-app;1"] + .createInstance(Components.interfaces.nsILocalHandlerApp); + localHandler.name = "Test Local Handler App"; + + // WriteArgument will just dump its arguments to a file for us. + var processDir = do_get_cwd(); + var exe = processDir.clone(); + exe.append("WriteArgument"); + + if (!exe.exists()) { + // Maybe we are on windows + exe.leafName = "WriteArgument.exe"; + if (!exe.exists()) + do_throw("Could not locate the WriteArgument tests executable\n"); + } + + var outFile = tempDir.clone(); + outFile.append(kOutputFile); + + // Set an environment variable for WriteArgument to pick up + var envSvc = + Components.classes["@mozilla.org/process/environment;1"] + .getService(Components.interfaces.nsIEnvironment); + + // The Write Argument file needs to know where its libraries are, so + // just force the path variable + // For mac + var greDir = HandlerServiceTest._dirSvc.get("GreD", Components.interfaces.nsIFile); + + envSvc.set("DYLD_LIBRARY_PATH", greDir.path); + // For Linux + envSvc.set("LD_LIBRARY_PATH", greDir.path); + //XXX: handle windows + + // Now tell it where we want the file. + envSvc.set("WRITE_ARGUMENT_FILE", outFile.path); + + var uri = ioService.newURI(kTestURI, null, null); + + // Just check we've got these matching, if we haven't there's a problem + // with ascii spec or our test case. + do_check_eq(uri.asciiSpec, kExpectedURI); + + localHandler.executable = exe; + localHandler.launchWithURI(uri); + + do_test_pending(); + do_timeout(1000, checkFile); +} diff --git a/uriloader/exthandler/tests/unit/xpcshell.ini b/uriloader/exthandler/tests/unit/xpcshell.ini new file mode 100644 index 000000000..e268ff9c3 --- /dev/null +++ b/uriloader/exthandler/tests/unit/xpcshell.ini @@ -0,0 +1,15 @@ +[DEFAULT] +head = head_handlerService.js +tail = tail_handlerService.js +run-sequentially = Bug 912235 - Intermittent failures + +[test_getTypeFromExtension_ext_to_type_mapping.js] +[test_getTypeFromExtension_with_empty_Content_Type.js] +[test_badMIMEType.js] +[test_handlerService.js] +support-files = mailcap +# Bug 676997: test consistently fails on Android +fail-if = os == "android" +[test_punycodeURIs.js] +# Bug 676997: test consistently fails on Android +fail-if = os == "android" diff --git a/uriloader/exthandler/tests/unit_ipc/test_encoding.js b/uriloader/exthandler/tests/unit_ipc/test_encoding.js new file mode 100644 index 000000000..a2a00937a --- /dev/null +++ b/uriloader/exthandler/tests/unit_ipc/test_encoding.js @@ -0,0 +1,231 @@ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://testing-common/MockRegistrar.js"); + +do_get_profile(); + +var DownloadListener = { + init: function () { + let obs = Services.obs; + obs.addObserver(this, "dl-done", true); + }, + + observe: function (subject, topic, data) { + this.onFinished(subject, topic, data); + }, + + QueryInterface: function (iid) { + if (iid.equals(Ci.nsIObserver) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +} +DownloadListener.init(); + +function HelperAppDlg() { } +HelperAppDlg.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), + show: function (launcher, ctx, reason, usePrivateUI) { + launcher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToFile; + launcher.launchWithApplication(null, false); + } +} + +// Override the download-manager-ui to prevent anyone from trying to open +// a window. +function DownloadMgrUI() { } +DownloadMgrUI.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]), + show: function (ir, aID, reason) { }, + + visible: false, + + getAttention: function () { } +} + +function AlertsSVC() { } +AlertsSVC.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService]), + showAlertNotification: function (url, title, text, clickable, cookie, listener, name) { }, +} + +MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", + HelperAppDlg); +MockRegistrar.register("@mozilla.org/download-manager-ui;1", + DownloadMgrUI); +MockRegistrar.register("@mozilla.org/alerts-service;1", + AlertsSVC); + +function initChildTestEnv() +{ + sendCommand(' \ + const Cc = Components.classes; \ + const Ci = Components.interfaces; \ + const Cr = Components.results; \ + const Cu = Components.utils; \ + Cu.import("resource://gre/modules/Services.jsm"); \ + function WindowContext() { } \ + \ + WindowContext.prototype = { \ + getInterface: function (iid) { \ + if (iid.equals(Ci.nsIInterfaceRequestor) || \ + iid.equals(Ci.nsIURIContentListener) || \ + iid.equals(Ci.nsILoadGroup) || \ + iid.equals(Ci.nsIDocumentLoader) || \ + iid.equals(Ci.nsIDOMWindow)) \ + return this; \ + \ + throw Cr.NS_ERROR_NO_INTERFACE; \ + }, \ + \ + /* nsIURIContentListener */ \ + onStartURIOpen: function (uri) { }, \ + isPreferred: function (type, desiredtype) { return false; }, \ + \ + /* nsILoadGroup */ \ + addRequest: function (request, context) { }, \ + removeRequest: function (request, context, status) { } \ + }; \ + \ + var ioservice = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);\ + var uriloader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);\ + '); +} + +function testFinisher(endFunc) { + let ef = endFunc; + return function (file) { + ef(file); + runNextTest(); + } +} + +function runChildTestSet(set) +{ + DownloadListener.onFinished = testFinisher(set[2]); + sendCommand('\ + let uri = ioservice.newURI("http://localhost:4444' + set[0] + '", null, null); \ + let channel = NetUtil.newChannel({uri: uri, loadUsingSystemPrincipal: true}); \ + uriloader.openURI(channel, Ci.nsIURILoader.IS_CONTENT_PREFERRED, new WindowContext()); \ + '); +} + +var httpserver = null; +var currentTest = 0; +function runNextTest() +{ + if (currentTest == tests.length) { + httpserver.stop(do_test_finished); + return; + } + + let set = tests[currentTest++]; + runChildTestSet(set); +} + +const responseBody = [0x1f, 0x8b, 0x08, 0x00, 0x16, 0x5a, 0x8a, 0x48, 0x02, + 0x03, 0x2b, 0x49, 0x2d, 0x2e, 0xe1, 0x02, 0x00, 0xc6, + 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00]; + +/* + * First test: a file with Content-Type application/x-gzip and Content-Encoding gzip + * should not be decoded in a round-trip + */ +function testResponse1(metadata, response) { + response.setHeader("Content-Type", "application/x-gzip", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest1(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test1.gz") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + do_check_matches(str, responseBody); +} + +/* + * Second test: a file with Content-Type text/html and Content-Encoding gzip + * should not be decoded in a round-trip, if its filename ends in ".gz". + * We specify a Content-disposition header to force it to be saved as a file. + */ +function testResponse2(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest2(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test2.gz") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + do_check_matches(str, responseBody); +} + +function testResponse3(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest3(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test3.txt") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + let decodedBody = [ 116, 101, 115, 116, 10 ]; // 't','e','s','t','\n' + do_check_matches(str, decodedBody); +} + +var tests = [ + [ "/test1.gz", testResponse1, finishTest1 ], + [ "/test2.gz", testResponse2, finishTest2 ], + [ "/test3.txt", testResponse3, finishTest3 ], +]; + +function run_test() { +// do_load_child_test_harness(); + httpserver = new HttpServer(); + httpserver.start(4444); + do_test_pending(); + + initChildTestEnv(); + + for (let set of tests) + httpserver.registerPathHandler(set[0], set[1]); + + runNextTest(); +} diff --git a/uriloader/exthandler/tests/unit_ipc/xpcshell.ini b/uriloader/exthandler/tests/unit_ipc/xpcshell.ini new file mode 100644 index 000000000..962b93dec --- /dev/null +++ b/uriloader/exthandler/tests/unit_ipc/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = +tail = + +[test_encoding.js] +# Bug 676995: test hangs consistently on Android +# Bug 907732: thunderbird still uses legacy downloads manager. +skip-if = (os == "android" || buildapp == '../mail') diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h new file mode 100644 index 000000000..9d5ef31da --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nslocalhandlerappuikit_h_ +#define nslocalhandlerappuikit_h_ + +#include "nsLocalHandlerApp.h" + +class nsLocalHandlerAppUIKit final : public nsLocalHandlerApp +{ +public: + nsLocalHandlerAppUIKit() + {} + ~nsLocalHandlerAppUIKit() + {} + + nsLocalHandlerAppUIKit(const char16_t* aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) + {} + + nsLocalHandlerAppUIKit(const nsAString& aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) + {} + + + NS_IMETHOD LaunchWithURI(nsIURI* aURI, nsIInterfaceRequestor* aWindowContext); +}; + +#endif /* nslocalhandlerappuikit_h_ */ diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm new file mode 100644 index 000000000..afa600721 --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <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 000000000..3f6bc8b42 --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUIKit_h_ +#define nsMIMEInfoUIKit_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUIKit final : public nsMIMEInfoImpl +{ +public: + explicit nsMIMEInfoUIKit(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) + {} + nsMIMEInfoUIKit(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) + {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile); + +protected: + virtual nsresult LoadUriInternal(nsIURI* aURI); +#ifdef DEBUG + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) + { + NS_NOTREACHED("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif + static nsresult OpenApplicationWithURI(nsIFile* aApplication, + const nsCString& aURI); +}; + +#endif diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm new file mode 100644 index 000000000..4b950dffc --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUIKit.h" +#include "nsIFileURL.h" + +NS_IMETHODIMP +nsMIMEInfoUIKit::LaunchWithFile(nsIFile* aFile) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult +nsMIMEInfoUIKit::LoadUriInternal(nsIURI* aURI) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.h b/uriloader/exthandler/uikit/nsOSHelperAppService.h new file mode 100644 index 000000000..1f33be0fb --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and +// is implemented on each platform. It contains platform specific code for +// finding helper applications for a given mime type in addition to launching +// those applications. This is the UIKit version. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsOSHelperAppService final : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval); + + // method overrides --> used to hook the mime service into internet config.... + NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo); + already_AddRefed<nsIMIMEInfo> GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound); + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval); + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile); + + nsresult OSProtocolHandlerExists(const char* aScheme, bool* aHandlerExists); +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.mm b/uriloader/exthandler/uikit/nsOSHelperAppService.mm new file mode 100644 index 000000000..dfee24b2d --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" + +nsOSHelperAppService::nsOSHelperAppService() + : nsExternalHelperAppService() +{} + +nsOSHelperAppService::~nsOSHelperAppService() +{} + +nsresult +nsOSHelperAppService::OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) +{ + *aHandlerExists = false; + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult +nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath, + nsIFile** aFile) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo) +{ + return nsExternalHelperAppService::GetFromTypeAndExtension(aType, + aFileExt, + aMIMEInfo); +} + +already_AddRefed<nsIMIMEInfo> +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound) +{ + *aFound = false; + return nullptr; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) +{ + *found = false; + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsExternalSharingAppService.h b/uriloader/exthandler/unix/nsExternalSharingAppService.h new file mode 100644 index 000000000..3b1794b5e --- /dev/null +++ b/uriloader/exthandler/unix/nsExternalSharingAppService.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_EXTERNAL_SHARING_APP_SERVICE_H +#define NS_EXTERNAL_SHARING_APP_SERVICE_H + +#include "nsIExternalSharingAppService.h" +#include "nsAutoPtr.h" + +class ShareUiInterface; + +#define NS_EXTERNALSHARINGAPPSERVICE_CID \ + {0xea782c90, 0xc6ec, 0x11df, \ + {0xbd, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66, 0x74}} + +class nsExternalSharingAppService : public nsIExternalSharingAppService +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALSHARINGAPPSERVICE + + nsExternalSharingAppService(); + +private: + ~nsExternalSharingAppService(); + +protected: + nsAutoPtr<ShareUiInterface> mShareUi; +}; +#endif diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.cpp b/uriloader/exthandler/unix/nsGNOMERegistry.cpp new file mode 100644 index 000000000..bf71ae913 --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.cpp @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsGNOMERegistry.h" +#include "nsString.h" +#include "nsIComponentManager.h" +#include "nsMIMEInfoUnix.h" +#include "nsAutoPtr.h" +#include "nsIGIOService.h" + +/* static */ bool +nsGNOMERegistry::HandlerExists(const char *aProtocolScheme) +{ + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return false; + } + + nsCOMPtr<nsIGIOMimeApp> 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; + + nsAutoCString name; + nsCOMPtr<nsIGIOMimeApp> app; + if (NS_FAILED(giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(app)))) + return; + + app->GetName(name); + + CopyUTF8toUTF16(name, 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); + + nsAutoCString name; + nsAutoCString description; + + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + nsCOMPtr<nsIGIOMimeApp> gioHandlerApp; + if (NS_FAILED(giovfs->GetAppForMimeType(aMIMEType, getter_AddRefs(gioHandlerApp))) || + !gioHandlerApp) { + return nullptr; + } + gioHandlerApp->GetName(name); + giovfs->GetDescriptionForMimeType(aMIMEType, description); + + mimeInfo->SetDefaultDescription(NS_ConvertUTF8toUTF16(name)); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + mimeInfo->SetDescription(NS_ConvertUTF8toUTF16(description)); + + return mimeInfo.forget(); +} diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.h b/uriloader/exthandler/unix/nsGNOMERegistry.h new file mode 100644 index 000000000..fe42ee622 --- /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 000000000..e51660887 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUnix.h" +#include "nsGNOMERegistry.h" +#include "nsIGIOService.h" +#include "nsNetCID.h" +#include "nsIIOService.h" +#include "nsAutoPtr.h" +#ifdef MOZ_ENABLE_DBUS +#include "nsDBusHandlerApp.h" +#endif + +nsresult +nsMIMEInfoUnix::LoadUriInternal(nsIURI * aURI) +{ + return nsGNOMERegistry::LoadURL(aURI); +} + +NS_IMETHODIMP +nsMIMEInfoUnix::GetHasDefaultHandler(bool *_retval) +{ + // if mDefaultApplication is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (mDefaultApplication) + return nsMIMEInfoImpl::GetHasDefaultHandler(_retval); + + *_retval = false; + + if (mClass == eProtocolInfo) { + *_retval = nsGNOMERegistry::HandlerExists(mSchemeOrType.get()); + } else { + RefPtr<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; + +#if defined(MOZ_ENABLE_CONTENTACTION) + ContentAction::Action action = + ContentAction::Action::defaultActionForFile(QUrl(), QString(mSchemeOrType.get())); + if (action.isValid()) { + *_retval = true; + return NS_OK; + } +#endif + + return NS_OK; +} + +nsresult +nsMIMEInfoUnix::LaunchDefaultWithFile(nsIFile *aFile) +{ + // if mDefaultApplication is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (mDefaultApplication) + return nsMIMEInfoImpl::LaunchDefaultWithFile(aFile); + + nsAutoCString nativePath; + aFile->GetNativePath(nativePath); + +#if defined(MOZ_ENABLE_CONTENTACTION) + QUrl uri = QUrl::fromLocalFile(QString::fromUtf8(nativePath.get())); + ContentAction::Action action = + ContentAction::Action::defaultActionForFile(uri, QString(mSchemeOrType.get())); + if (action.isValid()) { + action.trigger(); + return NS_OK; + } + return NS_ERROR_FAILURE; +#endif + + nsCOMPtr<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); + nsAutoCString uriSpec; + uri->GetSpec(uriSpec); + + nsCOMPtr<nsIGIOMimeApp> app; + if (NS_FAILED(giovfs->GetAppForMimeType(mSchemeOrType, getter_AddRefs(app))) || !app) { + return NS_ERROR_FILE_NOT_FOUND; + } + + return app->Launch(uriSpec); +} + +#if defined(MOZ_ENABLE_CONTENTACTION) +NS_IMETHODIMP +nsMIMEInfoUnix::GetPossibleApplicationHandlers(nsIMutableArray ** aPossibleAppHandlers) +{ + if (!mPossibleApplications) { + mPossibleApplications = do_CreateInstance(NS_ARRAY_CONTRACTID); + + if (!mPossibleApplications) + return NS_ERROR_OUT_OF_MEMORY; + + QList<ContentAction::Action> actions = + ContentAction::Action::actionsForFile(QUrl(), QString(mSchemeOrType.get())); + + for (int i = 0; i < actions.size(); ++i) { + nsContentHandlerApp* app = + new nsContentHandlerApp(nsString((char16_t*)actions[i].name().data()), + mSchemeOrType, actions[i]); + mPossibleApplications->AppendElement(app, false); + } + } + + *aPossibleAppHandlers = mPossibleApplications; + NS_ADDREF(*aPossibleAppHandlers); + return NS_OK; +} +#endif + diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.h b/uriloader/exthandler/unix/nsMIMEInfoUnix.h new file mode 100644 index 000000000..65f6d996d --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUnix_h_ +#define nsMIMEInfoUnix_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUnix : public nsMIMEInfoImpl +{ +public: + explicit nsMIMEInfoUnix(const char *aMIMEType = "") : nsMIMEInfoImpl(aMIMEType) {} + explicit nsMIMEInfoUnix(const nsACString& aMIMEType) : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUnix(const nsACString& aType, HandlerClass aClass) : + nsMIMEInfoImpl(aType, aClass) {} + static bool HandlerExists(const char *aProtocolScheme); + +protected: + NS_IMETHOD GetHasDefaultHandler(bool *_retval); + + virtual nsresult LoadUriInternal(nsIURI *aURI); + + virtual nsresult LaunchDefaultWithFile(nsIFile *aFile); +#if defined(MOZ_ENABLE_CONTENTACTION) + NS_IMETHOD GetPossibleApplicationHandlers(nsIMutableArray * *aPossibleAppHandlers); +#endif +}; + +#endif // nsMIMEInfoUnix_h_ diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.cpp b/uriloader/exthandler/unix/nsOSHelperAppService.cpp new file mode 100644 index 000000000..84e1cf5b0 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp @@ -0,0 +1,1537 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <sys/stat.h> + +#if defined(MOZ_ENABLE_CONTENTACTION) +#include <contentaction/contentaction.h> +#include <QString> +#endif + +#include "nsOSHelperAppService.h" +#include "nsMIMEInfoUnix.h" +#ifdef MOZ_WIDGET_GTK +#include "nsGNOMERegistry.h" +#endif +#include "nsISupports.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsXPIDLString.h" +#include "nsIURL.h" +#include "nsIFileStreams.h" +#include "nsILineInputStream.h" +#include "nsIFile.h" +#include "nsIProcess.h" +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsISupportsPrimitives.h" +#include "nsCRT.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "prenv.h" // for PR_GetEnv() +#include "nsAutoPtr.h" +#include "mozilla/Preferences.h" + +using namespace mozilla; + +#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(mLog, mozilla::LogLevel::Debug) + +static nsresult +FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter); +static nsresult +ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter); + +inline bool +IsNetscapeFormat(const nsACString& aBuffer); + +nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() +{ + mode_t mask = umask(0777); + umask(mask); + mPermissions = 0666 & ~mask; +} + +nsOSHelperAppService::~nsOSHelperAppService() +{} + +/* + * Take a command with all the mailcap escapes in it and unescape it + * Ideally this needs the mime type, mime type options, and location of the + * temporary file, but this last can't be got from here + */ +// static +nsresult +nsOSHelperAppService::UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand) { + LOG(("-- UnescapeCommand")); + LOG(("Command to escape: '%s'\n", + NS_LossyConvertUTF16toASCII(aEscapedCommand).get())); + // XXX This function will need to get the mime type and various stuff like that being passed in to work properly + + LOG(("UnescapeCommand really needs some work -- it should actually do some unescaping\n")); + + CopyUTF16toUTF8(aEscapedCommand, aUnEscapedCommand); + LOG(("Escaped command: '%s'\n", + PromiseFlatCString(aUnEscapedCommand).get())); + return NS_OK; +} + +/* Put aSemicolon_iter at the first non-escaped semicolon after + * aStart_iter but before aEnd_iter + */ + +static nsresult +FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter) { + bool semicolonFound = false; + while (aSemicolon_iter != aEnd_iter && !semicolonFound) { + switch(*aSemicolon_iter) { + case '\\': + aSemicolon_iter.advance(2); + break; + case ';': + semicolonFound = true; + break; + default: + ++aSemicolon_iter; + break; + } + } + return NS_OK; +} + +static nsresult +ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter) { + nsAString::const_iterator iter(aStart_iter); + + // skip leading whitespace + while (iter != aEnd_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeStart = iter; + + // find major/minor separator ('/') + while (iter != aEnd_iter && *iter != '/') { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeEnd = iter; + + // skip '/' + ++iter; + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMinorTypeStart = iter; + + // find end of minor type, delimited by whitespace or ';' + while (iter != aEnd_iter && !nsCRT::IsAsciiSpace(*iter) && *iter != ';') { + ++iter; + } + + aMinorTypeEnd = iter; + + return NS_OK; +} + +// static +nsresult +nsOSHelperAppService::GetFileLocation(const char* aPrefName, + const char* aEnvVarName, + nsAString& aFileLocation) { + LOG(("-- GetFileLocation. Pref: '%s' EnvVar: '%s'\n", + aPrefName, + aEnvVarName)); + NS_PRECONDITION(aPrefName, "Null pref name passed; don't do that!"); + + aFileLocation.Truncate(); + /* The lookup order is: + 1) user pref + 2) env var + 3) pref + */ + NS_ENSURE_TRUE(Preferences::GetRootBranch(), NS_ERROR_FAILURE); + + /* + If we have an env var we should check whether the pref is a user + pref. If we do not, we don't care. + */ + if (Preferences::HasUserValue(aPrefName) && + NS_SUCCEEDED(Preferences::GetString(aPrefName, &aFileLocation))) { + return NS_OK; + } + + if (aEnvVarName && *aEnvVarName) { + char* prefValue = PR_GetEnv(aEnvVarName); + if (prefValue && *prefValue) { + // the pref is in the system charset and it's a filepath... The + // natural way to do the charset conversion is by just initing + // an nsIFile with the native path and asking it for the Unicode + // version. + nsresult rv; + nsCOMPtr<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())); + nsresult rv = NS_OK; + nsAutoString mimeFileName; + + const char* filenamePref = aUserData ? + "helpers.private_mime_types_file" : "helpers.global_mime_types_file"; + + rv = GetFileLocation(filenamePref, nullptr, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetTypeAndDescriptionFromMimetypesFile(mimeFileName, + aFileExtension, + aMajorType, + aMinorType, + aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +inline bool +IsNetscapeFormat(const nsACString& aBuffer) { + return StringBeginsWith(aBuffer, NS_LITERAL_CSTRING("#--Netscape Communications Corporation MIME Information")) || + StringBeginsWith(aBuffer, NS_LITERAL_CSTRING("#--MCOM MIME Information")); +} + +/* + * Create a file stream and line input stream for the filename. + * Leaves the first line of the file in aBuffer and sets the format to + * true for netscape files and false for normail ones + */ +// static +nsresult +nsOSHelperAppService::CreateInputStream(const nsAString& aFilename, + nsIFileInputStream ** aFileInputStream, + nsILineInputStream ** aLineInputStream, + nsACString& aBuffer, + bool * aNetscapeFormat, + bool * aMore) { + LOG(("-- CreateInputStream")); + nsresult rv = NS_OK; + + nsCOMPtr<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())); + nsresult rv = NS_OK; + nsCOMPtr<nsIFileInputStream> mimeFile; + nsCOMPtr<nsILineInputStream> mimeTypes; + bool netscapeFormat; + nsAutoString buf; + nsAutoCString cBuf; + bool more = false; + rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), getter_AddRefs(mimeTypes), + cBuf, &netscapeFormat, &more); + + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsString entry; + entry.SetCapacity(100); + nsAString::const_iterator majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t(' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + extensions, + descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG(("Bogus entry; trying 'normal' mode\n")); + rv = ParseNormalMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + extensions, + descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + extensions, + descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG(("Bogus entry; trying 'Netscape' mode\n")); + rv = ParseNetscapeMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + extensions, + descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv)) { // entry parses + nsAString::const_iterator start, end; + extensions.BeginReading(start); + extensions.EndReading(end); + nsAString::const_iterator iter(start); + + while (start != end) { + FindCharInReadable(',', iter, end); + if (Substring(start, iter).Equals(aFileExtension, + nsCaseInsensitiveStringComparator())) { + // it's a match. Assign the type and description and run + aMajorType.Assign(Substring(majorTypeStart, majorTypeEnd)); + aMinorType.Assign(Substring(minorTypeStart, minorTypeEnd)); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } + if (iter != end) { + ++iter; + } + start = iter; + } + } else { + LOG(("Failed to parse entry: %s\n", NS_LossyConvertUTF16toASCII(entry).get())); + } + // truncate the entry for the next iteration + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* Get the mime.types file names from prefs and look up info in them + based on mimetype */ +// static +nsresult +nsOSHelperAppService::LookUpExtensionsAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aFileExtensions, + nsAString& aDescription) { + LOG(("-- LookUpExtensionsAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsresult rv = NS_OK; + nsAutoString mimeFileName; + + rv = GetFileLocation("helpers.private_mime_types_file", nullptr, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile(mimeFileName, + aMajorType, + aMinorType, + aFileExtensions, + aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + if (NS_FAILED(rv) || aFileExtensions.IsEmpty()) { + rv = GetFileLocation("helpers.global_mime_types_file", + nullptr, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile(mimeFileName, + aMajorType, + aMinorType, + aFileExtensions, + aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + } + return rv; +} + +/* Open the file, read the first line, decide what type of file it is, + then get info based on extension */ +// static +nsresult +nsOSHelperAppService::GetExtensionsAndDescriptionFromMimetypesFile(const nsAString& aFilename, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aFileExtensions, + nsAString& aDescription) { + LOG(("-- GetExtensionsAndDescriptionFromMimetypesFile\n")); + LOG(("Getting extensions and description from types file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + + nsresult rv = NS_OK; + nsCOMPtr<nsIFileInputStream> mimeFile; + nsCOMPtr<nsILineInputStream> mimeTypes; + bool netscapeFormat; + nsAutoCString cBuf; + nsAutoString buf; + bool more = false; + rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), getter_AddRefs(mimeTypes), + cBuf, &netscapeFormat, &more); + + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsString entry; + entry.SetCapacity(100); + nsAString::const_iterator majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t(' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + extensions, + descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG(("Bogus entry; trying 'normal' mode\n")); + rv = ParseNormalMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + extensions, + descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, + minorTypeEnd, extensions, + descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG(("Bogus entry; trying 'Netscape' mode\n")); + rv = ParseNetscapeMIMETypesEntry(entry, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, + extensions, + descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, + majorTypeEnd).Equals(aMajorType, + nsCaseInsensitiveStringComparator())&& + Substring(minorTypeStart, + minorTypeEnd).Equals(aMinorType, + nsCaseInsensitiveStringComparator())) { + // it's a match + aFileExtensions.Assign(extensions); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } else if (NS_FAILED(rv)) { + LOG(("Failed to parse entry: %s\n", NS_LossyConvertUTF16toASCII(entry).get())); + } + + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* + * This parses a Netscape format mime.types entry. There are two + * possible formats: + * + * type=foo/bar; options exts="baz" description="Some type" + * + * and + * + * type=foo/bar; options description="Some type" exts="baz" + */ +// static +nsresult +nsOSHelperAppService::ParseNetscapeMIMETypesEntry(const nsAString& aEntry, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG(("-- ParseNetscapeMIMETypesEntry\n")); + NS_ASSERTION(!aEntry.IsEmpty(), "Empty Netscape MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, match_start, match_end; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && + nsCRT::IsAsciiSpace(*end_iter)); + // if we're pointing to a quote, don't advance -- we don't want to + // include the quote.... + if (*end_iter != '"') + ++end_iter; + match_start = start_iter; + match_end = end_iter; + + // Get the major and minor types + // First the major type + if (! FindInReadable(NS_LITERAL_STRING("type="), match_start, match_end)) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && + *match_end != '/') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMajorTypeStart = match_start; + aMajorTypeEnd = match_end; + + // now the minor type + if (++match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && + !nsCRT::IsAsciiSpace(*match_end) && + *match_end != ';') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMinorTypeStart = match_start; + aMinorTypeEnd = match_end; + + // ignore everything up to the end of the mime type from here on + start_iter = match_end; + + // get the extensions + match_start = match_end; + match_end = end_iter; + if (FindInReadable(NS_LITERAL_STRING("exts="), match_start, match_end)) { + nsAString::const_iterator extStart, extEnd; + + if (match_end == end_iter || + (*match_end == '"' && ++match_end == end_iter)) { + return NS_ERROR_FAILURE; + } + + extStart = match_end; + match_start = extStart; + match_end = end_iter; + if (FindInReadable(NS_LITERAL_STRING("desc=\""), match_start, match_end)) { + // exts= before desc=, so we have to find the actual end of the extensions + extEnd = match_start; + if (extEnd == extStart) { + return NS_ERROR_FAILURE; + } + + do { + --extEnd; + } while (extEnd != extStart && + nsCRT::IsAsciiSpace(*extEnd)); + + if (extEnd != extStart && *extEnd == '"') { + --extEnd; + } + } else { + // desc= before exts=, so we can use end_iter as the end of the extensions + extEnd = end_iter; + } + aExtensions = Substring(extStart, extEnd); + } else { + // no extensions + aExtensions.Truncate(); + } + + // get the description + match_start = start_iter; + match_end = end_iter; + if (FindInReadable(NS_LITERAL_STRING("desc=\""), match_start, match_end)) { + aDescriptionStart = match_end; + match_start = aDescriptionStart; + match_end = end_iter; + if (FindInReadable(NS_LITERAL_STRING("exts="), match_start, match_end)) { + // exts= after desc=, so have to find actual end of description + aDescriptionEnd = match_start; + if (aDescriptionEnd == aDescriptionStart) { + return NS_ERROR_FAILURE; + } + + do { + --aDescriptionEnd; + } while (aDescriptionEnd != aDescriptionStart && + nsCRT::IsAsciiSpace(*aDescriptionEnd)); + + if (aDescriptionStart != aDescriptionStart && *aDescriptionEnd == '"') { + --aDescriptionEnd; + } + } else { + // desc= after exts=, so use end_iter for the description end + aDescriptionEnd = end_iter; + } + } else { + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + } + + return NS_OK; +} + +/* + * This parses a normal format mime.types entry. The format is: + * + * major/minor ext1 ext2 ext3 + */ +// static +nsresult +nsOSHelperAppService::ParseNormalMIMETypesEntry(const nsAString& aEntry, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG(("-- ParseNormalMIMETypesEntry\n")); + NS_ASSERTION(!aEntry.IsEmpty(), "Empty Normal MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, iter; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + + // skip leading whitespace + while (start_iter != end_iter && nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + if (start_iter == end_iter) { + return NS_ERROR_FAILURE; + } + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); + + ++end_iter; // point to first whitespace char (or to end of string) + iter = start_iter; + + // get the major type + if (! FindCharInReadable('/', iter, end_iter)) + return NS_ERROR_FAILURE; + + nsAString::const_iterator equals_sign_iter(start_iter); + if (FindCharInReadable('=', equals_sign_iter, iter)) + return NS_ERROR_FAILURE; // see bug 136670 + + aMajorTypeStart = start_iter; + aMajorTypeEnd = iter; + + // get the minor type + if (++iter == end_iter) { + return NS_ERROR_FAILURE; + } + start_iter = iter; + + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aMinorTypeStart = start_iter; + aMinorTypeEnd = iter; + + // get the extensions + aExtensions.Truncate(); + while (iter != end_iter) { + while (iter != end_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + start_iter = iter; + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aExtensions.Append(Substring(start_iter, iter)); + if (iter != end_iter) { // not the last extension + aExtensions.Append(char16_t(',')); + } + } + + return NS_OK; +} + +// static +nsresult +nsOSHelperAppService::LookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags) { + + // The mailcap lookup is two-pass to handle the case of mailcap files + // that have something like: + // + // text/*; emacs %s + // text/rtf; soffice %s + // + // in that order. We want to pick up "soffice" for text/rtf in such cases + nsresult rv = DoLookUpHandlerAndDescription(aMajorType, + aMinorType, + aHandler, + aDescription, + aMozillaFlags, + true); + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, + aMinorType, + aHandler, + aDescription, + aMozillaFlags, + false); + } + + // maybe we have an entry for "aMajorType/*"? + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, + NS_LITERAL_STRING("*"), + aHandler, + aDescription, + aMozillaFlags, + true); + } + + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, + NS_LITERAL_STRING("*"), + aHandler, + aDescription, + aMozillaFlags, + false); + } + + return rv; +} + +// static +nsresult +nsOSHelperAppService::DoLookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags, + bool aUserData) { + LOG(("-- LookUpHandlerAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsresult rv = NS_OK; + nsAutoString mailcapFileName; + + const char * filenamePref = aUserData ? + "helpers.private_mailcap_file" : "helpers.global_mailcap_file"; + const char * filenameEnvVar = aUserData ? + "PERSONAL_MAILCAP" : "MAILCAP"; + + rv = GetFileLocation(filenamePref, filenameEnvVar, mailcapFileName); + if (NS_SUCCEEDED(rv) && !mailcapFileName.IsEmpty()) { + rv = GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, + aMajorType, + aMinorType, + aHandler, + aDescription, + aMozillaFlags); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +// static +nsresult +nsOSHelperAppService::GetHandlerAndDescriptionFromMailcapFile(const nsAString& aFilename, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags) { + + LOG(("-- GetHandlerAndDescriptionFromMailcapFile\n")); + LOG(("Getting handler and description from mailcap file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + + nsresult rv = NS_OK; + bool more = false; + + nsCOMPtr<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; + } + + nsString entry, buffer; + nsAutoCString cBuffer; + entry.SetCapacity(128); + cBuffer.SetCapacity(80); + rv = mailcap->ReadLine(cBuffer, &more); + if (NS_FAILED(rv)) { + mailcapFile->Close(); + return rv; + } + + do { // return on end-of-file in the loop + + CopyASCIItoUTF16(cBuffer, buffer); + if (!buffer.IsEmpty() && buffer.First() != '#') { + entry.Append(buffer); + if (entry.Last() == '\\') { // entry continues on next line + entry.Truncate(entry.Length()-1); + entry.Append(char16_t(' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry in entry. Check it for the type + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + + nsAString::const_iterator semicolon_iter, + start_iter, end_iter, + majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd; + entry.BeginReading(start_iter); + entry.EndReading(end_iter); + semicolon_iter = start_iter; + FindSemicolon(semicolon_iter, end_iter); + if (semicolon_iter != end_iter) { // we have something resembling a valid entry + rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, semicolon_iter); + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, + majorTypeEnd).Equals(aMajorType, + nsCaseInsensitiveStringComparator()) && + Substring(minorTypeStart, + minorTypeEnd).Equals(aMinorType, + nsCaseInsensitiveStringComparator())) { // we have a match + bool match = true; + ++semicolon_iter; // point at the first char past the semicolon + start_iter = semicolon_iter; // handler string starts here + FindSemicolon(semicolon_iter, end_iter); + while (start_iter != semicolon_iter && + nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + + LOG(("The real handler is: '%s'\n", + NS_LossyConvertUTF16toASCII(Substring(start_iter, + semicolon_iter)).get())); + + // XXX ugly hack. Just grab the executable name + nsAString::const_iterator end_handler_iter = semicolon_iter; + nsAString::const_iterator end_executable_iter = start_iter; + while (end_executable_iter != end_handler_iter && + !nsCRT::IsAsciiSpace(*end_executable_iter)) { + ++end_executable_iter; + } + // XXX End ugly hack + + aHandler = Substring(start_iter, end_executable_iter); + + nsAString::const_iterator start_option_iter, end_optionname_iter, equal_sign_iter; + bool equalSignFound; + while (match && + semicolon_iter != end_iter && + ++semicolon_iter != end_iter) { // there are options left and we still match + start_option_iter = semicolon_iter; + // skip over leading whitespace + while (start_option_iter != end_iter && + nsCRT::IsAsciiSpace(*start_option_iter)) { + ++start_option_iter; + } + if (start_option_iter == end_iter) { // nothing actually here + break; + } + semicolon_iter = start_option_iter; + FindSemicolon(semicolon_iter, end_iter); + equal_sign_iter = start_option_iter; + equalSignFound = false; + while (equal_sign_iter != semicolon_iter && !equalSignFound) { + switch(*equal_sign_iter) { + case '\\': + equal_sign_iter.advance(2); + break; + case '=': + equalSignFound = true; + break; + default: + ++equal_sign_iter; + break; + } + } + end_optionname_iter = start_option_iter; + // find end of option name + while (end_optionname_iter != equal_sign_iter && + !nsCRT::IsAsciiSpace(*end_optionname_iter)) { + ++end_optionname_iter; + } + nsDependentSubstring optionName(start_option_iter, end_optionname_iter); + if (equalSignFound) { + // This is an option that has a name and value + if (optionName.EqualsLiteral("description")) { + aDescription = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("x-mozilla-flags")) { + aMozillaFlags = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("test")) { + nsAutoCString testCommand; + rv = UnescapeCommand(Substring(++equal_sign_iter, semicolon_iter), + aMajorType, + aMinorType, + testCommand); + if (NS_FAILED(rv)) + continue; + nsCOMPtr<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(NS_LITERAL_CSTRING("/bin/sh")); + if (NS_FAILED(rv)) + continue; + rv = process->Init(file); + if (NS_FAILED(rv)) + continue; + const char *args[] = { "-c", testCommand.get() }; + LOG(("Running Test: %s\n", testCommand.get())); + rv = process->Run(true, args, 2); + if (NS_FAILED(rv)) + continue; + int32_t exitValue; + rv = process->GetExitValue(&exitValue); + if (NS_FAILED(rv)) + continue; + LOG(("Exit code: %d\n", exitValue)); + if (exitValue) { + match = false; + } + } + } else { + // This is an option that just has a name but no value (eg "copiousoutput") + if (optionName.EqualsLiteral("needsterminal")) { + match = false; + } + } + } + + + if (match) { // we did not fail any test clauses; all is good + // get out of here + mailcapFile->Close(); + return NS_OK; + } else { // pretend that this match never happened + aDescription.Truncate(); + aMozillaFlags.Truncate(); + aHandler.Truncate(); + } + } + } + // zero out the entry for the next cycle + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + rv = mailcap->ReadLine(cBuffer, &more); + } while (NS_SUCCEEDED(rv)); + mailcapFile->Close(); + return rv; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char * aProtocolScheme, bool * aHandlerExists) +{ + LOG(("-- nsOSHelperAppService::OSProtocolHandlerExists for '%s'\n", + aProtocolScheme)); + *aHandlerExists = false; + +#if defined(MOZ_ENABLE_CONTENTACTION) + // libcontentaction requires character ':' after scheme + ContentAction::Action action = + ContentAction::Action::defaultActionForScheme(QString(aProtocolScheme) + ':'); + + if (action.isValid()) + *aHandlerExists = true; +#endif + +#ifdef MOZ_WIDGET_GTK + // Check the GNOME registry for a protocol handler + *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme); +#endif + + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) +{ +#ifdef MOZ_WIDGET_GTK + nsGNOMERegistry::GetAppDescForScheme(aScheme, _retval); + return _retval.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK; +#else + return NS_ERROR_NOT_AVAILABLE; +#endif +} + +nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t * platformAppPath, nsIFile ** aFile) +{ + LOG(("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n", + NS_LossyConvertUTF16toASCII(platformAppPath).get())); + if (! *platformAppPath) { // empty filename--return error + NS_WARNING("Empty filename passed in."); + return NS_ERROR_INVALID_ARG; + } + + // first check if the base class implementation finds anything + nsresult rv = nsExternalHelperAppService::GetFileTokenForPath(platformAppPath, aFile); + if (NS_SUCCEEDED(rv)) + return rv; + // If the reason for failure was that the file doesn't exist, return too + // (because it means the path was absolute, and so that we shouldn't search in + // the path) + if (rv == NS_ERROR_FILE_NOT_FOUND) + return rv; + + // If we get here, we really should have a relative path. + NS_ASSERTION(*platformAppPath != char16_t('/'), "Unexpected absolute path"); + + nsCOMPtr<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_LITERAL_CSTRING("/") + 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, + NS_LITERAL_STRING("*"), + handler, + mailcap_description, + mozillaFlags, + true); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, + NS_LITERAL_STRING("*"), + handler, + mailcap_description, + mozillaFlags, + false); + } + + LOG(("Handler/Description results: handler='%s', description='%s', mozillaFlags='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get(), + NS_LossyConvertUTF16toASCII(mozillaFlags).get())); + + mailcap_description.Trim(" \t\""); + mozillaFlags.Trim(" \t"); + + if (handler.IsEmpty() && extensions.IsEmpty() && + mailcap_description.IsEmpty() && mime_types_description.IsEmpty()) { + // No real useful info + return nullptr; + } + + RefPtr<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(); +} + + +already_AddRefed<nsIMIMEInfo> +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType, + const nsACString& aFileExt, + bool *aFound) { + *aFound = true; + RefPtr<nsMIMEInfoBase> 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) + return retval.forget(); + // If we had an extension match but no type match, set the mimetype and use + // it + if (!retval && miByExt) { + if (!aType.IsEmpty()) + miByExt->SetMIMEType(aType); + miByExt.swap(retval); + + return retval.forget(); + } + // If we got nothing, make a new mimeinfo + if (!retval) { + *aFound = false; + retval = new nsMIMEInfoUnix(aType); + if (retval) { + if (!aFileExt.IsEmpty()) + retval->AppendExtension(aFileExt); + } + + return retval.forget(); + } + + // Copy the attributes of retval (mimeinfo from type) onto miByExt, to + // return it + // but reset to just collected mDefaultAppDescription (from ext) + nsAutoString byExtDefault; + miByExt->GetDefaultDescription(byExtDefault); + retval->SetDefaultDescription(byExtDefault); + retval->CopyBasicDataTo(miByExt); + + miByExt.swap(retval); + } + return retval.forget(); +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **_retval) +{ + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), + found); + if (NS_FAILED(rv)) + return rv; + + nsMIMEInfoUnix *handlerInfo = + new nsMIMEInfoUnix(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.h b/uriloader/exthandler/unix/nsOSHelperAppService.h new file mode 100644 index 000000000..33eb934ed --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.h @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is implemented on each +// platform. It contains platform specific code for finding helper applications for a given mime type +// in addition to launching those applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsMIMEInfoImpl.h" +#include "nsCOMPtr.h" + +class nsILineInputStream; + +class nsOSHelperAppService : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // method overrides for mime.types and mime.info look up steps + already_AddRefed<nsIMIMEInfo> GetMIMEInfoFromOS(const nsACString& aMimeType, + const nsACString& aFileExt, + bool *aFound); + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **_retval); + + // override nsIExternalProtocolService methods + nsresult OSProtocolHandlerExists(const char * aProtocolScheme, bool * aHandlerExists); + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, nsAString& _retval); + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got out of the + // rdf data source. This can be a mac file spec, a unix path or a windows path depending on the platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t * platformAppPath, nsIFile ** aFile); + +protected: + already_AddRefed<nsMIMEInfoBase> GetFromType(const nsCString& aMimeType); + already_AddRefed<nsMIMEInfoBase> GetFromExtension(const nsCString& aFileExt); + +private: + uint32_t mPermissions; + + // Helper methods which have to access static members + static nsresult UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand); + static nsresult GetFileLocation(const char* aPrefName, + const char* aEnvVarName, + nsAString& aFileLocation); + static nsresult LookUpTypeAndDescription(const nsAString& aFileExtension, + nsAString& aMajorType, + nsAString& aMinorType, + nsAString& aDescription, + bool aUserData); + static nsresult CreateInputStream(const nsAString& aFilename, + nsIFileInputStream ** aFileInputStream, + nsILineInputStream ** aLineInputStream, + nsACString& aBuffer, + bool * aNetscapeFormat, + bool * aMore); + + static nsresult GetTypeAndDescriptionFromMimetypesFile(const nsAString& aFilename, + const nsAString& aFileExtension, + nsAString& aMajorType, + nsAString& aMinorType, + nsAString& aDescription); + + static nsresult LookUpExtensionsAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult GetExtensionsAndDescriptionFromMimetypesFile(const nsAString& aFilename, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult ParseNetscapeMIMETypesEntry(const nsAString& aEntry, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult ParseNormalMIMETypesEntry(const nsAString& aEntry, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult LookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags); + + static nsresult DoLookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags, + bool aUserData); + + static nsresult GetHandlerAndDescriptionFromMailcapFile(const nsAString& aFilename, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags); +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.cpp b/uriloader/exthandler/win/nsMIMEInfoWin.cpp new file mode 100644 index 000000000..2c7171c87 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp @@ -0,0 +1,883 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsArrayEnumerator.h" +#include "nsCOMArray.h" +#include "nsLocalFile.h" +#include "nsMIMEInfoWin.h" +#include "nsNetUtil.h" +#include <windows.h> +#include <shellapi.h> +#include "nsAutoPtr.h" +#include "nsIMutableArray.h" +#include "nsTArray.h" +#include "shlobj.h" +#include "windows.h" +#include "nsIWindowsRegKey.h" +#include "nsIProcess.h" +#include "nsUnicharUtils.h" +#include "nsITextToSubURI.h" +#include "nsVariant.h" +#include "mozilla/UniquePtrExtensions.h" + +#define RUNDLL32_EXE L"\\rundll32.exe" + + +NS_IMPL_ISUPPORTS_INHERITED(nsMIMEInfoWin, nsMIMEInfoBase, nsIPropertyBag) + +nsMIMEInfoWin::~nsMIMEInfoWin() +{ +} + +nsresult +nsMIMEInfoWin::LaunchDefaultWithFile(nsIFile* aFile) +{ + // Launch the file, unless it is an executable. + bool executable = true; + aFile->IsExecutable(&executable); + if (executable) + return NS_ERROR_FAILURE; + + return aFile->Launch(); +} + +NS_IMETHODIMP +nsMIMEInfoWin::LaunchWithFile(nsIFile* aFile) +{ + nsresult rv; + + // it doesn't make any sense to call this on protocol handlers + NS_ASSERTION(mClass == eMIMEInfo, + "nsMIMEInfoBase should have mClass == eMIMEInfo"); + + if (mPreferredAction == useSystemDefault) { + return LaunchDefaultWithFile(aFile); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) + return NS_ERROR_FILE_NOT_FOUND; + + // at the moment, we only know how to hand files off to local handlers + nsCOMPtr<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); + + nsAutoString path; + aFile->GetPath(path); + + // Deal with local dll based handlers + nsCString filename; + executable->GetNativeLeafName(filename); + if (filename.Length() > 4) { + nsCString extension(Substring(filename, filename.Length() - 4, 4)); + + if (extension.LowerCaseEqualsLiteral(".dll")) { + nsAutoString args; + + // executable is rundll32, everything else is a list of parameters, + // including the dll handler. + if (!GetDllLaunchInfo(executable, aFile, args, false)) + return NS_ERROR_INVALID_ARG; + + WCHAR rundll32Path[MAX_PATH + sizeof(RUNDLL32_EXE) / sizeof(WCHAR) + 1] = {L'\0'}; + if (!GetSystemDirectoryW(rundll32Path, MAX_PATH)) { + return NS_ERROR_FILE_NOT_FOUND; + } + lstrcatW(rundll32Path, RUNDLL32_EXE); + + SHELLEXECUTEINFOW seinfo; + memset(&seinfo, 0, sizeof(seinfo)); + seinfo.cbSize = sizeof(SHELLEXECUTEINFOW); + seinfo.fMask = 0; + seinfo.hwnd = nullptr; + seinfo.lpVerb = nullptr; + seinfo.lpFile = rundll32Path; + seinfo.lpParameters = args.get(); + seinfo.lpDirectory = nullptr; + seinfo.nShow = SW_SHOWNORMAL; + if (ShellExecuteExW(&seinfo)) + return NS_OK; + + switch ((LONG_PTR)seinfo.hInstApp) { + case 0: + case SE_ERR_OOM: + return NS_ERROR_OUT_OF_MEMORY; + case SE_ERR_ACCESSDENIED: + return NS_ERROR_FILE_ACCESS_DENIED; + case SE_ERR_ASSOCINCOMPLETE: + case SE_ERR_NOASSOC: + return NS_ERROR_UNEXPECTED; + case SE_ERR_DDEBUSY: + case SE_ERR_DDEFAIL: + case SE_ERR_DDETIMEOUT: + return NS_ERROR_NOT_AVAILABLE; + case SE_ERR_DLLNOTFOUND: + return NS_ERROR_FAILURE; + case SE_ERR_SHARE: + return NS_ERROR_FILE_IS_LOCKED; + default: + switch(GetLastError()) { + case ERROR_FILE_NOT_FOUND: + return NS_ERROR_FILE_NOT_FOUND; + case ERROR_PATH_NOT_FOUND: + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + case ERROR_BAD_FORMAT: + return NS_ERROR_FILE_CORRUPTED; + } + + } + return NS_ERROR_FILE_EXECUTION_FAILED; + } + } + return LaunchWithIProcess(executable, path); + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetHasDefaultHandler(bool * _retval) +{ + // We have a default application if we have a description + // We can ShellExecute anything; however, callers are probably interested if + // there is really an application associated with this type of file + *_retval = !mDefaultAppDescription.IsEmpty(); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetEnumerator(nsISimpleEnumerator* *_retval) +{ + nsCOMArray<nsIVariant> properties; + + nsCOMPtr<nsIVariant> variant; + GetProperty(NS_LITERAL_STRING("defaultApplicationIconURL"), getter_AddRefs(variant)); + if (variant) + properties.AppendObject(variant); + + GetProperty(NS_LITERAL_STRING("customApplicationIconURL"), getter_AddRefs(variant)); + if (variant) + properties.AppendObject(variant); + + return NS_NewArrayEnumerator(_retval, properties); +} + +static nsresult GetIconURLVariant(nsIFile* aApplication, nsIVariant* *_retval) +{ + nsAutoCString fileURLSpec; + NS_GetURLSpecFromFile(aApplication, fileURLSpec); + nsAutoCString iconURLSpec; iconURLSpec.AssignLiteral("moz-icon://"); + iconURLSpec += fileURLSpec; + RefPtr<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 + nsAutoCString urlCharset; + nsAutoString utf16Spec; + rv = aURL->GetOriginCharset(urlCharset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsITextToSubURI> textToSubURI = do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_FAILED(textToSubURI->UnEscapeNonAsciiURI(urlCharset, urlSpec, utf16Spec))) { + CopyASCIItoUTF16(urlSpec, utf16Spec); + } + + static const wchar_t cmdVerb[] = L"open"; + SHELLEXECUTEINFOW sinfo; + memset(&sinfo, 0, sizeof(sinfo)); + sinfo.cbSize = sizeof(sinfo); + sinfo.fMask = SEE_MASK_FLAG_DDEWAIT | + SEE_MASK_FLAG_NO_UI; + sinfo.hwnd = nullptr; + sinfo.lpVerb = (LPWSTR)&cmdVerb; + sinfo.nShow = SW_SHOWNORMAL; + + LPITEMIDLIST pidl = nullptr; + SFGAOF sfgao; + + // Bug 394974 + if (SUCCEEDED(SHParseDisplayName(utf16Spec.get(), nullptr, + &pidl, 0, &sfgao))) { + sinfo.lpIDList = pidl; + sinfo.fMask |= SEE_MASK_INVOKEIDLIST; + } else { + // SHParseDisplayName failed. Bailing out as work around for + // Microsoft Security Bulletin MS07-061 + rv = NS_ERROR_FAILURE; + } + if (NS_SUCCEEDED(rv)) { + BOOL result = ShellExecuteExW(&sinfo); + if (!result || ((LONG_PTR)sinfo.hInstApp) < 32) + rv = NS_ERROR_FAILURE; + } + if (pidl) + CoTaskMemFree(pidl); + } + + return rv; +} + +// Given a path to a local file, return its nsILocalHandlerApp instance. +bool nsMIMEInfoWin::GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr<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( + NS_LITERAL_STRING("NoOpenWith"), &value)) && + value == 1) + return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue( + NS_LITERAL_STRING("NoOpenWith"), dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) + return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(EmptyString(), + appFilesystemCommand))) { + + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) + return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Return a fully populated command string based on +// passing information. Used in launchWithFile to trace +// back to the full handler path based on the dll. +// (dll, targetfile, return args, open/edit) +bool nsMIMEInfoWin::GetDllLaunchInfo(nsIFile * aDll, + nsIFile * aFile, + nsAString& args, + bool edit) +{ + if (!aDll || !aFile) + return false; + + nsString appExeName; + aDll->GetLeafName(appExeName); + + nsCOMPtr<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(NS_LITERAL_STRING("NoOpenWith"), &value); + if (NS_SUCCEEDED(rv) && value == 1) + return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(NS_LITERAL_STRING("NoOpenWith"), + dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) + return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(EmptyString(), + appFilesystemCommand))) { + // Replace embedded environment variables. + uint32_t bufLength = + ::ExpandEnvironmentStringsW(appFilesystemCommand.get(), + L"", 0); + if (bufLength == 0) // Error + return false; + + auto destination = mozilla::MakeUniqueFallible<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; + NS_NAMED_LITERAL_STRING(rundllSegment, "rundll32.exe "); + int32_t index = appFilesystemCommand.Find(rundllSegment); + if (index > kNotFound) { + params.Append(Substring(appFilesystemCommand, + index + rundllSegment.Length())); + } else { + params.Append(appFilesystemCommand); + } + + // check to make sure we have a %1 and fill it + NS_NAMED_LITERAL_STRING(percentOneParam, "%1"); + index = params.Find(percentOneParam); + if (index == kNotFound) // no parameter + return false; + + nsString target; + aFile->GetTarget(target); + params.Replace(index, 2, target); + + args = params; + + return true; + } + return false; +} + +// Return the cleaned up file path associated with a progid command +// verb located in root. +bool nsMIMEInfoWin::GetProgIDVerbCommandHandler(const nsAString& appProgIDName, + nsAString& applicationPath, + bool edit) +{ + nsCOMPtr<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(EmptyString(), appFilesystemCommand))) { + + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) + return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Helper routine used in tracking app lists. Converts path +// entries to lower case and stores them in the trackList array. +void nsMIMEInfoWin::ProcessPath(nsCOMPtr<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) { + uint32_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, false); + 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(EmptyString(), mimeFileExt))) { + CopyUTF16toUTF8(mimeFileExt, fileExt); + extKnown = false; + } + } + } + } + + nsAutoString fileExtToUse; + if (fileExt.First() != '.') + fileExtToUse = char16_t('.'); + fileExtToUse.Append(NS_ConvertUTF8toUTF16(fileExt)); + + // Note, the order in which these occur has an effect on the + // validity of the resulting display list. + + if (!extKnown) { + // 1) Get the default handler if it exists + workingRegistryPath = fileExtToUse; + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString appProgId; + if (NS_SUCCEEDED(regKey->ReadStringValue(EmptyString(), appProgId))) { + // Bug 358297 - ignore the embedded internet explorer handler + if (appProgId != NS_LITERAL_STRING("XPSViewer.Document")) { + nsAutoString appFilesystemCommand; + if (GetProgIDVerbCommandHandler(appProgId, + appFilesystemCommand, + false) && + !IsPathInList(appFilesystemCommand, trackList)) { + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + regKey->Close(); + } + + + // 2) list HKEY_CLASSES_ROOT\.ext\OpenWithList + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) + continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, + appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + + // 3) List HKEY_CLASSES_ROOT\.ext\OpenWithProgids, with the + // different step of resolving the progids for the command handler. + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + // HKEY_CLASSES_ROOT\.ext\OpenWithProgids\Windows.XPSReachViewer + nsAutoString appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) + continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, + appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + + // 4) Add any non configured applications located in the MRU list + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithList + workingRegistryPath = + NS_LITERAL_STRING("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName, appValue; + if (NS_FAILED(regKey->GetValueName(index, appName))) + continue; + if (appName.EqualsLiteral("MRUList")) + continue; + if (NS_FAILED(regKey->ReadStringValue(appName, appValue))) + continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appValue, + appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + + // 5) Add any non configured progids in the MRU list, with the + // different step of resolving the progids for the command handler. + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithProgids + workingRegistryPath = + NS_LITERAL_STRING("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appIndex, appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) + continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, + appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + + // 6) Check the perceived type value, and use this to lookup the perceivedtype + // open with list. + // http://msdn2.microsoft.com/en-us/library/aa969373.aspx + + workingRegistryPath = fileExtToUse; + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString perceivedType; + rv = regKey->ReadStringValue(NS_LITERAL_STRING("PerceivedType"), + perceivedType); + if (NS_SUCCEEDED(rv)) { + nsAutoString openWithListPath(NS_LITERAL_STRING("SystemFileAssociations\\")); + openWithListPath.Append(perceivedType); // no period + openWithListPath.AppendLiteral("\\OpenWithList"); + + nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + openWithListPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) + continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + } + } + } // extKnown == false + + + // 7) list global HKEY_CLASSES_ROOT\*\OpenWithList + // Listing general purpose handlers, not specific to a mime type or file extension + + workingRegistryPath = NS_LITERAL_STRING("*\\OpenWithList"); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) + continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + + // 8) General application's list - not file extension specific on windows + workingRegistryPath = NS_LITERAL_STRING("Applications"); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS| + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetChildCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetChildName(index, appName))) + continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + // Return to the caller + *_retval = appList; + NS_ADDREF(*_retval); + + return NS_OK; +} diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.h b/uriloader/exthandler/win/nsMIMEInfoWin.h new file mode 100644 index 000000000..0a5b678e8 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.h @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoWin_h_ +#define nsMIMEInfoWin_h_ + +#include "nsMIMEInfoImpl.h" +#include "nsIPropertyBag.h" +#include "nsIMutableArray.h" +#include "nsTArray.h" + +class nsMIMEInfoWin : public nsMIMEInfoBase, public nsIPropertyBag { + virtual ~nsMIMEInfoWin(); + + public: + nsMIMEInfoWin(const char* aType = "") : nsMIMEInfoBase(aType) {} + nsMIMEInfoWin(const nsACString& aMIMEType) : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoWin(const nsACString& aType, HandlerClass aClass) : + nsMIMEInfoBase(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile); + NS_IMETHOD GetHasDefaultHandler(bool * _retval); + NS_IMETHOD GetPossibleLocalHandlers(nsIArray **_retval); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROPERTYBAG + + void SetDefaultApplicationHandler(nsIFile* aDefaultApplication) + { + mDefaultApplication = aDefaultApplication; + } + + protected: + virtual nsresult LoadUriInternal(nsIURI *aURI); + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile); + + private: + nsCOMPtr<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); + +}; + +#endif diff --git a/uriloader/exthandler/win/nsOSHelperAppService.cpp b/uriloader/exthandler/win/nsOSHelperAppService.cpp new file mode 100644 index 000000000..f01f3b49b --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp @@ -0,0 +1,626 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsXPIDLString.h" +#include "nsIURL.h" +#include "nsIMIMEInfo.h" +#include "nsMIMEInfoWin.h" +#include "nsMimeTypes.h" +#include "nsIProcess.h" +#include "plstr.h" +#include "nsAutoPtr.h" +#include "nsNativeCharsetUtils.h" +#include "nsLocalFile.h" +#include "nsIWindowsRegKey.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WindowsVersion.h" + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include <shellapi.h> +#include <shlwapi.h> + +#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args) + +// helper methods: forward declarations... +static nsresult GetExtensionFrom4xRegistryInfo(const nsACString& aMimeType, + nsString& aFileExtension); +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension); + +nsOSHelperAppService::nsOSHelperAppService() : + nsExternalHelperAppService() + , mAppAssoc(nullptr) +{ + CoInitialize(nullptr); + CoCreateInstance(CLSID_ApplicationAssociationRegistration, nullptr, + CLSCTX_INPROC, IID_IApplicationAssociationRegistration, + (void**)&mAppAssoc); +} + +nsOSHelperAppService::~nsOSHelperAppService() +{ + if (mAppAssoc) + mAppAssoc->Release(); + mAppAssoc = nullptr; + CoUninitialize(); +} + +// The windows registry provides a mime database key which lists a set of mime types and corresponding "Extension" values. +// we can use this to look up our mime type to see if there is a preferred extension for the mime type. +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension) +{ + nsAutoString mimeDatabaseKey; + mimeDatabaseKey.AssignLiteral("MIME\\Database\\Content Type\\"); + + AppendASCIItoUTF16(aMimeType, mimeDatabaseKey); + + nsCOMPtr<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(NS_LITERAL_STRING("Extension"), aFileExtension); + + return NS_OK; +} + +// We have a serious problem!! I have this content type and the windows registry only gives me +// helper apps based on extension. Right now, we really don't have a good place to go for +// trying to figure out the extension for a particular mime type....One short term hack is to look +// this information in 4.x (it's stored in the windows regsitry). +static nsresult GetExtensionFrom4xRegistryInfo(const nsACString& aMimeType, + nsString& aFileExtension) +{ + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) + return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = regKey-> + Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + NS_LITERAL_STRING("Software\\Netscape\\Netscape Navigator\\Suffixes"), + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) + return NS_ERROR_NOT_AVAILABLE; + + rv = regKey->ReadStringValue(NS_ConvertASCIItoUTF16(aMimeType), + aFileExtension); + if (NS_FAILED(rv)) + return NS_OK; + + aFileExtension.Insert(char16_t('.'), 0); + + // this may be a comma separated list of extensions...just take the + // first one for now... + + int32_t pos = aFileExtension.FindChar(char16_t(',')); + if (pos > 0) { + // we have a comma separated list of types... + // truncate everything after the first comma (including the comma) + aFileExtension.Truncate(pos); + } + + return NS_OK; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char * aProtocolScheme, bool * aHandlerExists) +{ + // look up the protocol scheme in the windows registry....if we find a match then we have a handler for it... + *aHandlerExists = false; + if (aProtocolScheme && *aProtocolScheme) + { + // Vista: use new application association interface + if (mAppAssoc) { + wchar_t * pResult = nullptr; + NS_ConvertASCIItoUTF16 scheme(aProtocolScheme); + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(scheme.get(), + AT_URLPROTOCOL, AL_EFFECTIVE, + &pResult); + if (SUCCEEDED(hr)) { + CoTaskMemFree(pResult); + *aHandlerExists = true; + } + return NS_OK; + } + + HKEY hKey; + LONG err = ::RegOpenKeyExW(HKEY_CLASSES_ROOT, + NS_ConvertASCIItoUTF16(aProtocolScheme).get(), + 0, + KEY_QUERY_VALUE, + &hKey); + if (err == ERROR_SUCCESS) + { + err = ::RegQueryValueExW(hKey, L"URL Protocol", + nullptr, nullptr, nullptr, nullptr); + *aHandlerExists = (err == ERROR_SUCCESS); + // close the key + ::RegCloseKey(hKey); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) +{ + nsCOMPtr<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; + } + } + + if (mAppAssoc) { + // Vista: use new application association interface + wchar_t * pResult = nullptr; + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), + AT_URLPROTOCOL, AL_EFFECTIVE, + &pResult); + if (SUCCEEDED(hr)) { + nsCOMPtr<nsIFile> app; + nsAutoString appInfo(pResult); + CoTaskMemFree(pResult); + if (NS_SUCCEEDED(GetDefaultAppInfo(appInfo, _retval, getter_AddRefs(app)))) + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIFile> app; + GetDefaultAppInfo(buf, _retval, getter_AddRefs(app)); + + if (!_retval.Equals(buf)) + return NS_OK; + + // Fall back to full path + buf.AppendLiteral("\\shell\\open\\command"); + nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + buf, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) + return NS_ERROR_NOT_AVAILABLE; + + rv = regKey->ReadStringValue(EmptyString(), _retval); + + return NS_SUCCEEDED(rv) ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +// GetMIMEInfoFromRegistry: This function obtains the values of some of the nsIMIMEInfo +// attributes for the mimeType/extension associated with the input registry key. The default +// entry for that key is the name of a registry key under HKEY_CLASSES_ROOT. The default +// value for *that* key is the descriptive name of the type. The EditFlags value is a binary +// value; the low order bit of the third byte of which indicates that the user does not need +// to be prompted. +// +// This function sets only the Description attribute of the input nsIMIMEInfo. +/* static */ +nsresult nsOSHelperAppService::GetMIMEInfoFromRegistry(const nsAFlatString& fileType, nsIMIMEInfo *pInfo) +{ + nsresult rv = NS_OK; + + NS_ENSURE_ARG(pInfo); + nsCOMPtr<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(EmptyString(), description); + if (NS_SUCCEEDED(rv)) + pInfo->SetDescription(description); + + return NS_OK; +} + +///////////////////////////////////////////////////////////////////////////////////////////////// +// method overrides used to gather information from the windows registry for +// various mime types. +//////////////////////////////////////////////////////////////////////////////////////////////// + +/// Looks up the type for the extension aExt and compares it to aType +/* static */ bool +nsOSHelperAppService::typeFromExtEquals(const char16_t* aExt, const char *aType) +{ + if (!aType) + return false; + nsAutoString fileExtToUse; + if (aExt[0] != char16_t('.')) + fileExtToUse = char16_t('.'); + + fileExtToUse.Append(aExt); + + bool eq = false; + nsCOMPtr<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(NS_LITERAL_STRING("Content Type"), type); + if (NS_SUCCEEDED(rv)) + eq = type.EqualsASCII(aType); + + return eq; +} + +// The "real" name of a given helper app (as specified by the path to the +// executable file held in various registry keys) is stored n the VERSIONINFO +// block in the file's resources. We need to find the path to the executable +// and then retrieve the "FileDescription" field value from the file. +nsresult +nsOSHelperAppService::GetDefaultAppInfo(const nsAString& aAppInfo, + nsAString& aDefaultDescription, + nsIFile** aDefaultApplication) +{ + nsAutoString handlerCommand; + + // If all else fails, use the file type key name, which will be + // something like "pngfile" for .pngs, "WMVFile" for .wmvs, etc. + aDefaultDescription = aAppInfo; + *aDefaultApplication = nullptr; + + if (aAppInfo.IsEmpty()) + return NS_ERROR_FAILURE; + + // aAppInfo may be a file, file path, program id, or + // Applications reference - + // c:\dir\app.exe + // Applications\appfile.exe/dll (shell\open...) + // ProgID.progid (shell\open...) + + nsAutoString handlerKeyName(aAppInfo); + + nsCOMPtr<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(EmptyString(), handlerCommand); + if (NS_FAILED(rv)) { + + // Check if there is a DelegateExecute string + nsAutoString delegateExecute; + rv = regKey->ReadStringValue(NS_LITERAL_STRING("DelegateExecute"), delegateExecute); + NS_ENSURE_SUCCESS(rv, rv); + + // Look for InProcServer32 + nsAutoString delegateExecuteRegPath; + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\InProcServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + rv = chkKey->ReadStringValue(EmptyString(), handlerCommand); + } + + if (NS_FAILED(rv)) { + // Look for LocalServer32 + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\LocalServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + rv = chkKey->ReadStringValue(EmptyString(), handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + // XXX FIXME: If this fails, the UI will display the full command + // string. + // There are some rare cases this can happen - ["url.dll" -foo] + // for example won't resolve correctly to the system dir. The + // subsequent launch of the helper app will work though. + nsCOMPtr<nsILocalFileWin> lf = new nsLocalFile(); + rv = lf->InitWithCommandLine(handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + + // The "FileDescription" field contains the actual name of the application. + lf->GetVersionInfoField("FileDescription", aDefaultDescription); + lf.forget(aDefaultApplication); + + return NS_OK; +} + +already_AddRefed<nsMIMEInfoWin> nsOSHelperAppService::GetByExtension(const nsAFlatString& aFileExt, const char *aTypeHint) +{ + if (aFileExt.IsEmpty()) + return nullptr; + + // Determine the mime type. + nsAutoCString typeToUse; + if (aTypeHint && *aTypeHint) { + typeToUse.Assign(aTypeHint); + } else if (!GetMIMETypeFromOSForExtension(NS_ConvertUTF16toUTF8(aFileExt), typeToUse)) { + return nullptr; + } + + RefPtr<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. + mimeInfo->AppendExtension(NS_ConvertUTF16toUTF8(Substring(fileExtToUse, 1))); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + + nsAutoString appInfo; + bool found; + + // Retrieve the default application for this extension + if (mAppAssoc) { + // Vista: use the new application association COM interfaces + // for resolving helpers. + nsString assocType(fileExtToUse); + wchar_t * pResult = nullptr; + HRESULT hr = mAppAssoc->QueryCurrentDefault(assocType.get(), + AT_FILEEXTENSION, AL_EFFECTIVE, + &pResult); + if (SUCCEEDED(hr)) { + found = true; + appInfo.Assign(pResult); + CoTaskMemFree(pResult); + } + else { + found = false; + } + } + else + { + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) + return nullptr; + nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + found = NS_SUCCEEDED(regKey->ReadStringValue(EmptyString(), + appInfo)); + } + } + + // Bug 358297 - ignore the default handler, force the user to choose app + if (appInfo.EqualsLiteral("XPSViewer.Document")) + found = false; + + if (!found) { + return nullptr; + } + + // Get other nsIMIMEInfo fields from registry, if possible. + nsAutoString defaultDescription; + nsCOMPtr<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(); +} + +already_AddRefed<nsIMIMEInfo> nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, const nsACString& aFileExt, bool *aFound) +{ + *aFound = true; + + const nsCString& flatType = PromiseFlatCString(aMIMEType); + const nsCString& flatExt = PromiseFlatCString(aFileExt); + + nsAutoString fileExtension; + /* XXX The Equals is a gross hack to wallpaper over the most common Win32 + * extension issues caused by the fix for bug 116938. See bug + * 120327, comment 271 for why this is needed. Not even sure we + * want to remove this once we have fixed all this stuff to work + * right; any info we get from the OS on this type is pretty much + * useless.... + * We'll do extension-based lookup for this type later in this function. + */ + if (!aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM)) { + // (1) try to use the windows mime database to see if there is a mapping to a file extension + // (2) try to see if we have some left over 4.x registry info we can peek at... + GetExtensionFromWindowsMimeDatabase(aMIMEType, fileExtension); + LOG(("Windows mime database: extension '%s'\n", fileExtension.get())); + if (fileExtension.IsEmpty()) { + GetExtensionFrom4xRegistryInfo(aMIMEType, fileExtension); + LOG(("4.x Registry: extension '%s'\n", fileExtension.get())); + } + } + // If we found an extension for the type, do the lookup + RefPtr<nsMIMEInfoWin> mi; + if (!fileExtension.IsEmpty()) + mi = GetByExtension(fileExtension, flatType.get()); + LOG(("Extension lookup on '%s' found: 0x%p\n", fileExtension.get(), mi.get())); + + bool hasDefault = false; + if (mi) { + mi->GetHasDefaultHandler(&hasDefault); + // OK. We might have the case that |aFileExt| is a valid extension for the + // mimetype we were given. In that case, we do want to append aFileExt + // to the mimeinfo that we have. (E.g.: We are asked for video/mpeg and + // .mpg, but the primary extension for video/mpeg is .mpeg. But because + // .mpg is an extension for video/mpeg content, we want to append it) + if (!aFileExt.IsEmpty() && typeFromExtEquals(NS_ConvertUTF8toUTF16(flatExt).get(), flatType.get())) { + LOG(("Appending extension '%s' to mimeinfo, because its mimetype is '%s'\n", + flatExt.get(), flatType.get())); + bool extExist = false; + mi->ExtensionExists(aFileExt, &extExist); + if (!extExist) + mi->AppendExtension(aFileExt); + } + } + if (!mi || !hasDefault) { + RefPtr<nsMIMEInfoWin> miByExt = + GetByExtension(NS_ConvertUTF8toUTF16(aFileExt), flatType.get()); + LOG(("Ext. lookup for '%s' found 0x%p\n", flatExt.get(), miByExt.get())); + if (!miByExt && mi) + return mi.forget(); + if (miByExt && !mi) { + return miByExt.forget(); + } + if (!miByExt && !mi) { + *aFound = false; + mi = new nsMIMEInfoWin(flatType); + if (!aFileExt.IsEmpty()) { + mi->AppendExtension(aFileExt); + } + + return mi.forget(); + } + + // if we get here, mi has no default app. copy from extension lookup. + nsCOMPtr<nsIFile> defaultApp; + nsAutoString desc; + miByExt->GetDefaultDescription(desc); + + mi->SetDefaultDescription(desc); + } + return mi.forget(); +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **_retval) +{ + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), + found); + if (NS_FAILED(rv)) + return rv; + + nsMIMEInfoWin *handlerInfo = + new nsMIMEInfoWin(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} + +bool +nsOSHelperAppService::GetMIMETypeFromOSForExtension(const nsACString& aExtension, + nsACString& aMIMEType) +{ + if (aExtension.IsEmpty()) + return false; + + // windows registry assumes your file extension is going to include the '.'. + // so make sure it's there... + nsAutoString fileExtToUse; + if (aExtension.First() != '.') + fileExtToUse = char16_t('.'); + + AppendUTF8toUTF16(aExtension, fileExtToUse); + + // Try to get an entry from the windows registry. + nsCOMPtr<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(NS_LITERAL_STRING("Content Type"), + mimeType)) || mimeType.IsEmpty()) { + return false; + } + // Content-Type is always in ASCII + aMIMEType.Truncate(); + LossyAppendUTF16toASCII(mimeType, aMIMEType); + return true; +} diff --git a/uriloader/exthandler/win/nsOSHelperAppService.h b/uriloader/exthandler/win/nsOSHelperAppService.h new file mode 100644 index 000000000..b00529433 --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is implemented on each +// platform. It contains platform specific code for finding helper applications for a given mime type +// in addition to launching those applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsMIMEInfoImpl.h" +#include "nsCOMPtr.h" +#include <windows.h> + +#ifdef _WIN32_WINNT +#undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 +#include <shlobj.h> + +class nsMIMEInfoWin; + +class nsOSHelperAppService : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + nsresult OSProtocolHandlerExists(const char * aProtocolScheme, bool * aHandlerExists); + nsresult LoadUriInternal(nsIURI * aURL); + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, nsAString& _retval); + + // method overrides for windows registry look up steps.... + already_AddRefed<nsIMIMEInfo> GetMIMEInfoFromOS(const nsACString& aMIMEType, const nsACString& aFileExt, bool *aFound); + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString &aScheme, + bool *found, + nsIHandlerInfo **_retval); + virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension, + nsACString& aMIMEType) override; + + /** Get the string value of a registry value and store it in result. + * @return true on success, false on failure + */ + static bool GetValueString(HKEY hKey, const char16_t* pValueName, nsAString& result); + +protected: + nsresult GetDefaultAppInfo(const nsAString& aTypeName, nsAString& aDefaultDescription, nsIFile** aDefaultApplication); + // Lookup a mime info by extension, using an optional type hint + already_AddRefed<nsMIMEInfoWin> GetByExtension(const nsAFlatString& aFileExt, const char *aTypeHint = nullptr); + nsresult FindOSMimeInfoForType(const char * aMimeContentType, nsIURI * aURI, char ** aFileExtension, nsIMIMEInfo ** aMIMEInfo); + + static nsresult GetMIMEInfoFromRegistry(const nsAFlatString& fileType, nsIMIMEInfo *pInfo); + /// Looks up the type for the extension aExt and compares it to aType + static bool typeFromExtEquals(const char16_t* aExt, const char *aType); + +private: + IApplicationAssociationRegistration* mAppAssoc; +}; + +#endif // nsOSHelperAppService_h__ |