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 | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'uriloader')
131 files changed, 27574 insertions, 0 deletions
diff --git a/uriloader/base/moz.build b/uriloader/base/moz.build new file mode 100644 index 0000000000..0d0f9a1c67 --- /dev/null +++ b/uriloader/base/moz.build @@ -0,0 +1,33 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include('/ipc/chromium/chromium-config.mozbuild') + +XPIDL_SOURCES += [ + 'nsCURILoader.idl', + 'nsIContentHandler.idl', + 'nsIDocumentLoader.idl', + 'nsITransfer.idl', + 'nsIURIContentListener.idl', + 'nsIURILoader.idl', + 'nsIWebProgress.idl', + 'nsIWebProgressListener.idl', + 'nsIWebProgressListener2.idl', +] + +XPIDL_MODULE = 'uriloader' + +EXPORTS += [ + 'nsDocLoader.h', + 'nsURILoader.h', +] + +UNIFIED_SOURCES += [ + 'nsDocLoader.cpp', + 'nsURILoader.cpp', +] + +FINAL_LIBRARY = 'xul' diff --git a/uriloader/base/nsCURILoader.idl b/uriloader/base/nsCURILoader.idl new file mode 100644 index 0000000000..5d9d8f0131 --- /dev/null +++ b/uriloader/base/nsCURILoader.idl @@ -0,0 +1,49 @@ +/* -*- Mode: IDL; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIURILoader.idl" + +/* +nsCURILoader implements: +------------------------- +nsIURILoader +*/ + +%{ C++ +// {9F6D5D40-90E7-11d3-AF93-00A024FFC08C} - +#define NS_URI_LOADER_CID \ +{ 0x9f6d5d40, 0x90e7, 0x11d3, { 0xaf, 0x80, 0x00, 0xa0, 0x24, 0xff, 0xc0, 0x8c } } +#define NS_URI_LOADER_CONTRACTID \ +"@mozilla.org/uriloader;1" + +/* 057b04d0-0ccf-11d2-beba-00805f8a66dc */ +#define NS_DOCUMENTLOADER_SERVICE_CID \ + { 0x057b04d0, 0x0ccf, 0x11d2,{0xbe, 0xba, 0x00, 0x80, 0x5f, 0x8a, 0x66, 0xdc}} + +#define NS_DOCUMENTLOADER_SERVICE_CONTRACTID \ +"@mozilla.org/docloaderservice;1" + +#define NS_CONTENT_HANDLER_CONTRACTID "@mozilla.org/uriloader/content-handler;1" +#define NS_CONTENT_HANDLER_CONTRACTID_PREFIX NS_CONTENT_HANDLER_CONTRACTID "?type=" + +/** + * A category where content listeners can register. The name of the entry must + * be the content that this listener wants to handle, the value must be a + * contract ID for the listener. It will be created using createInstance (not + * getService). + * + * Listeners added this way are tried after the initial target of the load and + * after explicitly registered listeners (nsIURILoader::registerContentListener). + * + * These listeners must implement at least nsIURIContentListener (and + * nsISupports). + * + * @see nsICategoryManager + * @see nsIURIContentListener + */ +#define NS_CONTENT_LISTENER_CATEGORYMANAGER_ENTRY "external-uricontentlisteners" + +%} diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp new file mode 100644 index 0000000000..69885b93f8 --- /dev/null +++ b/uriloader/base/nsDocLoader.cpp @@ -0,0 +1,1516 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nspr.h" +#include "mozilla/Logging.h" + +#include "nsDocLoader.h" +#include "nsCURILoader.h" +#include "nsNetUtil.h" +#include "nsIHttpChannel.h" +#include "nsIWebProgressListener2.h" + +#include "nsIServiceManager.h" +#include "nsXPIDLString.h" + +#include "nsIURL.h" +#include "nsCOMPtr.h" +#include "nscore.h" +#include "nsWeakPtr.h" +#include "nsAutoPtr.h" +#include "nsQueryObject.h" + +#include "nsIDOMWindow.h" + +#include "nsIStringBundle.h" +#include "nsIScriptSecurityManager.h" + +#include "nsITransport.h" +#include "nsISocketTransport.h" +#include "nsIDocShell.h" +#include "nsIDOMDocument.h" +#include "nsIDocument.h" +#include "nsPresContext.h" +#include "nsIAsyncVerifyRedirectCallback.h" + +using mozilla::DebugOnly; +using mozilla::LogLevel; + +static NS_DEFINE_CID(kThisImplCID, NS_THIS_DOCLOADER_IMPL_CID); + +// +// Log module for nsIDocumentLoader logging... +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=DocLoader:5 +// set MOZ_LOG_FILE=debug.log +// +// this enables LogLevel::Debug level information and places all output in +// the file 'debug.log'. +// +mozilla::LazyLogModule gDocLoaderLog("DocLoader"); + + +#if defined(DEBUG) +void GetURIStringFromRequest(nsIRequest* request, nsACString &name) +{ + if (request) + request->GetName(name); + else + name.AssignLiteral("???"); +} +#endif /* DEBUG */ + + + +void +nsDocLoader::RequestInfoHashInitEntry(PLDHashEntryHdr* entry, + const void* key) +{ + // Initialize the entry with placement new + new (entry) nsRequestInfo(key); +} + +void +nsDocLoader::RequestInfoHashClearEntry(PLDHashTable* table, + PLDHashEntryHdr* entry) +{ + nsRequestInfo* info = static_cast<nsRequestInfo *>(entry); + info->~nsRequestInfo(); +} + +// this is used for mListenerInfoList.Contains() +template <> +class nsDefaultComparator <nsDocLoader::nsListenerInfo, nsIWebProgressListener*> { + public: + bool Equals(const nsDocLoader::nsListenerInfo& aInfo, + nsIWebProgressListener* const& aListener) const { + nsCOMPtr<nsIWebProgressListener> listener = + do_QueryReferent(aInfo.mWeakListener); + return aListener == listener; + } +}; + +/* static */ const PLDHashTableOps nsDocLoader::sRequestInfoHashOps = +{ + PLDHashTable::HashVoidPtrKeyStub, + PLDHashTable::MatchEntryStub, + PLDHashTable::MoveEntryStub, + nsDocLoader::RequestInfoHashClearEntry, + nsDocLoader::RequestInfoHashInitEntry +}; + +nsDocLoader::nsDocLoader() + : mParent(nullptr), + mCurrentSelfProgress(0), + mMaxSelfProgress(0), + mCurrentTotalProgress(0), + mMaxTotalProgress(0), + mRequestInfoHash(&sRequestInfoHashOps, sizeof(nsRequestInfo)), + mCompletedTotalProgress(0), + mIsLoadingDocument(false), + mIsRestoringDocument(false), + mDontFlushLayout(false), + mIsFlushingLayout(false) +{ + ClearInternalProgress(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: created.\n", this)); +} + +nsresult +nsDocLoader::SetDocLoaderParent(nsDocLoader *aParent) +{ + mParent = aParent; + return NS_OK; +} + +nsresult +nsDocLoader::Init() +{ + nsresult rv = NS_NewLoadGroup(getter_AddRefs(mLoadGroup), this); + if (NS_FAILED(rv)) return rv; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: load group %x.\n", this, mLoadGroup.get())); + + return NS_OK; +} + +nsDocLoader::~nsDocLoader() +{ + /* + |ClearWeakReferences()| here is intended to prevent people holding weak references + from re-entering this destructor since |QueryReferent()| will |AddRef()| me, and the + subsequent |Release()| will try to destroy me. At this point there should be only + weak references remaining (otherwise, we wouldn't be getting destroyed). + + An alternative would be incrementing our refcount (consider it a compressed flag + saying "Don't re-destroy."). I haven't yet decided which is better. [scc] + */ + // XXXbz now that NS_IMPL_RELEASE stabilizes by setting refcount to 1, is + // this needed? + ClearWeakReferences(); + + Destroy(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: deleted.\n", this)); +} + + +/* + * Implementation of ISupports methods... + */ +NS_IMPL_ADDREF(nsDocLoader) +NS_IMPL_RELEASE(nsDocLoader) + +NS_INTERFACE_MAP_BEGIN(nsDocLoader) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIDocumentLoader) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIWebProgress) + NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY(nsISecurityEventSink) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + if (aIID.Equals(kThisImplCID)) + foundInterface = static_cast<nsIDocumentLoader *>(this); + else +NS_INTERFACE_MAP_END + + +/* + * Implementation of nsIInterfaceRequestor methods... + */ +NS_IMETHODIMP nsDocLoader::GetInterface(const nsIID& aIID, void** aSink) +{ + nsresult rv = NS_ERROR_NO_INTERFACE; + + NS_ENSURE_ARG_POINTER(aSink); + + if(aIID.Equals(NS_GET_IID(nsILoadGroup))) { + *aSink = mLoadGroup; + NS_IF_ADDREF((nsISupports*)*aSink); + rv = NS_OK; + } else { + rv = QueryInterface(aIID, aSink); + } + + return rv; +} + +/* static */ +already_AddRefed<nsDocLoader> +nsDocLoader::GetAsDocLoader(nsISupports* aSupports) +{ + RefPtr<nsDocLoader> ret = do_QueryObject(aSupports); + return ret.forget(); +} + +/* static */ +nsresult +nsDocLoader::AddDocLoaderAsChildOfRoot(nsDocLoader* aDocLoader) +{ + nsresult rv; + nsCOMPtr<nsIDocumentLoader> docLoaderService = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsDocLoader> rootDocLoader = GetAsDocLoader(docLoaderService); + NS_ENSURE_TRUE(rootDocLoader, NS_ERROR_UNEXPECTED); + + return rootDocLoader->AddChildLoader(aDocLoader); +} + +NS_IMETHODIMP +nsDocLoader::Stop(void) +{ + nsresult rv = NS_OK; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Stop() called\n", this)); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, nsDocLoader, Stop, ()); + + if (mLoadGroup) + rv = mLoadGroup->Cancel(NS_BINDING_ABORTED); + + // Don't report that we're flushing layout so IsBusy returns false after a + // Stop call. + mIsFlushingLayout = false; + + // Clear out mChildrenInOnload. We want to make sure to fire our + // onload at this point, and there's no issue with mChildrenInOnload + // after this, since mDocumentRequest will be null after the + // DocLoaderIsEmpty() call. + mChildrenInOnload.Clear(); + + // Make sure to call DocLoaderIsEmpty now so that we reset mDocumentRequest, + // etc, as needed. We could be getting into here from a subframe onload, in + // which case the call to DocLoaderIsEmpty() is coming but hasn't quite + // happened yet, Canceling the loadgroup did nothing (because it was already + // empty), and we're about to start a new load (which is what triggered this + // Stop() call). + + // XXXbz If the child frame loadgroups were requests in mLoadgroup, I suspect + // we wouldn't need the call here.... + + NS_ASSERTION(!IsBusy(), "Shouldn't be busy here"); + DocLoaderIsEmpty(false); + + return rv; +} + + +bool +nsDocLoader::IsBusy() +{ + nsresult rv; + + // + // A document loader is busy if either: + // + // 1. One of its children is in the middle of an onload handler. Note that + // the handler may have already removed this child from mChildList! + // 2. It is currently loading a document and either has parts of it still + // loading, or has a busy child docloader. + // 3. It's currently flushing layout in DocLoaderIsEmpty(). + // + + if (mChildrenInOnload.Count() || mIsFlushingLayout) { + return true; + } + + /* Is this document loader busy? */ + if (!mIsLoadingDocument) { + return false; + } + + bool busy; + rv = mLoadGroup->IsPending(&busy); + if (NS_FAILED(rv)) { + return false; + } + if (busy) { + return true; + } + + /* check its child document loaders... */ + uint32_t count = mChildList.Length(); + for (uint32_t i=0; i < count; i++) { + nsIDocumentLoader* loader = ChildAt(i); + + // This is a safe cast, because we only put nsDocLoader objects into the + // array + if (loader && static_cast<nsDocLoader*>(loader)->IsBusy()) + return true; + } + + return false; +} + +NS_IMETHODIMP +nsDocLoader::GetContainer(nsISupports** aResult) +{ + NS_ADDREF(*aResult = static_cast<nsIDocumentLoader*>(this)); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetLoadGroup(nsILoadGroup** aResult) +{ + nsresult rv = NS_OK; + + if (nullptr == aResult) { + rv = NS_ERROR_NULL_POINTER; + } else { + *aResult = mLoadGroup; + NS_IF_ADDREF(*aResult); + } + return rv; +} + +void +nsDocLoader::Destroy() +{ + Stop(); + + // Remove the document loader from the parent list of loaders... + if (mParent) + { + DebugOnly<nsresult> rv = mParent->RemoveChildLoader(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RemoveChildLoader failed"); + } + + // Release all the information about network requests... + ClearRequestInfoHash(); + + mListenerInfoList.Clear(); + mListenerInfoList.Compact(); + + mDocumentRequest = nullptr; + + if (mLoadGroup) + mLoadGroup->SetGroupObserver(nullptr); + + DestroyChildren(); +} + +void +nsDocLoader::DestroyChildren() +{ + uint32_t count = mChildList.Length(); + // if the doc loader still has children...we need to enumerate the + // children and make them null out their back ptr to the parent doc + // loader + for (uint32_t i=0; i < count; i++) + { + nsIDocumentLoader* loader = ChildAt(i); + + if (loader) { + // This is a safe cast, as we only put nsDocLoader objects into the + // array + DebugOnly<nsresult> rv = + static_cast<nsDocLoader*>(loader)->SetDocLoaderParent(nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetDocLoaderParent failed"); + } + } + mChildList.Clear(); +} + +NS_IMETHODIMP +nsDocLoader::OnStartRequest(nsIRequest *request, nsISupports *aCtxt) +{ + // called each time a request is added to the group. + + if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) { + nsAutoCString name; + request->GetName(name); + + uint32_t count = 0; + if (mLoadGroup) + mLoadGroup->GetActiveCount(&count); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: OnStartRequest[%p](%s) mIsLoadingDocument=%s, %u active URLs", + this, request, name.get(), + (mIsLoadingDocument ? "true" : "false"), + count)); + } + + bool bJustStartedLoading = false; + + nsLoadFlags loadFlags = 0; + request->GetLoadFlags(&loadFlags); + + if (!mIsLoadingDocument && (loadFlags & nsIChannel::LOAD_DOCUMENT_URI)) { + bJustStartedLoading = true; + mIsLoadingDocument = true; + ClearInternalProgress(); // only clear our progress if we are starting a new load.... + } + + // + // Create a new nsRequestInfo for the request that is starting to + // load... + // + AddRequestInfo(request); + + // + // Only fire a doStartDocumentLoad(...) if the document loader + // has initiated a load... Otherwise, this notification has + // resulted from a request being added to the load group. + // + if (mIsLoadingDocument) { + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) { + // + // Make sure that the document channel is null at this point... + // (unless its been redirected) + // + NS_ASSERTION((loadFlags & nsIChannel::LOAD_REPLACE) || + !(mDocumentRequest.get()), + "Overwriting an existing document channel!"); + + // This request is associated with the entire document... + mDocumentRequest = request; + mLoadGroup->SetDefaultLoadRequest(request); + + // Only fire the start document load notification for the first + // document URI... Do not fire it again for redirections + // + if (bJustStartedLoading) { + // Update the progress status state + mProgressStateFlags = nsIWebProgressListener::STATE_START; + + // Fire the start document load notification + doStartDocumentLoad(); + return NS_OK; + } + } + } + + NS_ASSERTION(!mIsLoadingDocument || mDocumentRequest, + "mDocumentRequest MUST be set for the duration of a page load!"); + + doStartURLLoad(request); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::OnStopRequest(nsIRequest *aRequest, + nsISupports *aCtxt, + nsresult aStatus) +{ + nsresult rv = NS_OK; + + if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) { + nsAutoCString name; + aRequest->GetName(name); + + uint32_t count = 0; + if (mLoadGroup) + mLoadGroup->GetActiveCount(&count); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: OnStopRequest[%p](%s) status=%x mIsLoadingDocument=%s, %u active URLs", + this, aRequest, name.get(), + aStatus, (mIsLoadingDocument ? "true" : "false"), + count)); + } + + bool bFireTransferring = false; + + // + // Set the Maximum progress to the same value as the current progress. + // Since the URI has finished loading, all the data is there. Also, + // this will allow a more accurate estimation of the max progress (in case + // the old value was unknown ie. -1) + // + nsRequestInfo *info = GetRequestInfo(aRequest); + if (info) { + // Null out mLastStatus now so we don't find it when looking for + // status from now on. This destroys the nsStatusInfo and hence + // removes it from our list. + info->mLastStatus = nullptr; + + int64_t oldMax = info->mMaxProgress; + + info->mMaxProgress = info->mCurrentProgress; + + // + // If a request whose content-length was previously unknown has just + // finished loading, then use this new data to try to calculate a + // mMaxSelfProgress... + // + if ((oldMax < int64_t(0)) && (mMaxSelfProgress < int64_t(0))) { + mMaxSelfProgress = CalculateMaxProgress(); + } + + // As we know the total progress of this request now, save it to be part + // of CalculateMaxProgress() result. We need to remove the info from the + // hash, see bug 480713. + mCompletedTotalProgress += info->mMaxProgress; + + // + // Determine whether a STATE_TRANSFERRING notification should be + // 'synthesized'. + // + // If nsRequestInfo::mMaxProgress (as stored in oldMax) and + // nsRequestInfo::mCurrentProgress are both 0, then the + // STATE_TRANSFERRING notification has not been fired yet... + // + if ((oldMax == 0) && (info->mCurrentProgress == 0)) { + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + + // Only fire a TRANSFERRING notification if the request is also a + // channel -- data transfer requires a nsIChannel! + // + if (channel) { + if (NS_SUCCEEDED(aStatus)) { + bFireTransferring = true; + } + // + // If the request failed (for any reason other than being + // redirected or retargeted), the TRANSFERRING notification can + // still be fired if a HTTP connection was established to a server. + // + else if (aStatus != NS_BINDING_REDIRECTED && + aStatus != NS_BINDING_RETARGETED) { + // + // Only if the load has been targeted (see bug 268483)... + // + uint32_t lf; + channel->GetLoadFlags(&lf); + if (lf & nsIChannel::LOAD_TARGETED) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aRequest)); + if (httpChannel) { + uint32_t responseCode; + rv = httpChannel->GetResponseStatus(&responseCode); + if (NS_SUCCEEDED(rv)) { + // + // A valid server status indicates that a connection was + // established to the server... So, fire the notification + // even though a failure occurred later... + // + bFireTransferring = true; + } + } + } + } + } + } + } + + if (bFireTransferring) { + // Send a STATE_TRANSFERRING notification for the request. + int32_t flags; + + flags = nsIWebProgressListener::STATE_TRANSFERRING | + nsIWebProgressListener::STATE_IS_REQUEST; + // + // Move the WebProgress into the STATE_TRANSFERRING state if necessary... + // + if (mProgressStateFlags & nsIWebProgressListener::STATE_START) { + mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING; + + // Send STATE_TRANSFERRING for the document too... + flags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + } + + FireOnStateChange(this, aRequest, flags, NS_OK); + } + + // + // Fire the OnStateChange(...) notification for stop request + // + doStopURLLoad(aRequest, aStatus); + + // Clear this request out of the hash to avoid bypass of FireOnStateChange + // when address of the request is reused. + RemoveRequestInfo(aRequest); + + // + // Only fire the DocLoaderIsEmpty(...) if the document loader has initiated a + // load. This will handle removing the request from our hashtable as needed. + // + if (mIsLoadingDocument) { + nsCOMPtr<nsIDocShell> ds = do_QueryInterface(static_cast<nsIRequestObserver*>(this)); + bool doNotFlushLayout = false; + if (ds) { + // Don't do unexpected layout flushes while we're in process of restoring + // a document from the bfcache. + ds->GetRestoringDocument(&doNotFlushLayout); + } + DocLoaderIsEmpty(!doNotFlushLayout); + } + + return NS_OK; +} + + +nsresult nsDocLoader::RemoveChildLoader(nsDocLoader* aChild) +{ + nsresult rv = mChildList.RemoveElement(aChild) ? NS_OK : NS_ERROR_FAILURE; + if (NS_SUCCEEDED(rv)) { + rv = aChild->SetDocLoaderParent(nullptr); + } + return rv; +} + +nsresult nsDocLoader::AddChildLoader(nsDocLoader* aChild) +{ + nsresult rv = mChildList.AppendElement(aChild) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + if (NS_SUCCEEDED(rv)) { + rv = aChild->SetDocLoaderParent(this); + } + return rv; +} + +NS_IMETHODIMP nsDocLoader::GetDocumentChannel(nsIChannel ** aChannel) +{ + if (!mDocumentRequest) { + *aChannel = nullptr; + return NS_OK; + } + + return CallQueryInterface(mDocumentRequest, aChannel); +} + + +void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) +{ + if (mIsLoadingDocument) { + /* In the unimagineably rude circumstance that onload event handlers + triggered by this function actually kill the window ... ok, it's + not unimagineable; it's happened ... this deathgrip keeps this object + alive long enough to survive this function call. */ + nsCOMPtr<nsIDocumentLoader> kungFuDeathGrip(this); + + // Don't flush layout if we're still busy. + if (IsBusy()) { + return; + } + + NS_ASSERTION(!mIsFlushingLayout, "Someone screwed up"); + NS_ASSERTION(mDocumentRequest, "No Document Request!"); + + // The load group for this DocumentLoader is idle. Flush if we need to. + if (aFlushLayout && !mDontFlushLayout) { + nsCOMPtr<nsIDOMDocument> domDoc = do_GetInterface(GetAsSupports(this)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(domDoc); + if (doc) { + // We start loads from style resolution, so we need to flush out style + // no matter what. If we have user fonts, we also need to flush layout, + // since the reflow is what starts font loads. + mozFlushType flushType = Flush_Style; + nsIPresShell* shell = doc->GetShell(); + if (shell) { + // Be safe in case this presshell is in teardown now + nsPresContext* presContext = shell->GetPresContext(); + if (presContext && presContext->GetUserFontSet()) { + flushType = Flush_Layout; + } + } + mDontFlushLayout = mIsFlushingLayout = true; + doc->FlushPendingNotifications(flushType); + mDontFlushLayout = mIsFlushingLayout = false; + } + } + + // And now check whether we're really busy; that might have changed with + // the layout flush. + // Note, mDocumentRequest can be null if the flushing above re-entered this + // method. + if (!IsBusy() && mDocumentRequest) { + // Clear out our request info hash, now that our load really is done and + // we don't need it anymore to CalculateMaxProgress(). + ClearInternalProgress(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Is now idle...\n", this)); + + nsCOMPtr<nsIRequest> docRequest = mDocumentRequest; + + mDocumentRequest = nullptr; + mIsLoadingDocument = false; + + // Update the progress status state - the document is done + mProgressStateFlags = nsIWebProgressListener::STATE_STOP; + + + nsresult loadGroupStatus = NS_OK; + mLoadGroup->GetStatus(&loadGroupStatus); + + // + // New code to break the circular reference between + // the load group and the docloader... + // + mLoadGroup->SetDefaultLoadRequest(nullptr); + + // Take a ref to our parent now so that we can call DocLoaderIsEmpty() on + // it even if our onload handler removes us from the docloader tree. + RefPtr<nsDocLoader> parent = mParent; + + // Note that if calling ChildEnteringOnload() on the parent returns false + // then calling our onload handler is not safe. That can only happen on + // OOM, so that's ok. + if (!parent || parent->ChildEnteringOnload(this)) { + // Do nothing with our state after firing the + // OnEndDocumentLoad(...). The document loader may be loading a *new* + // document - if LoadDocument() was called from a handler! + // + doStopDocumentLoad(docRequest, loadGroupStatus); + + if (parent) { + parent->ChildDoneWithOnload(this); + } + } + } + } +} + +void nsDocLoader::doStartDocumentLoad(void) +{ + +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(mDocumentRequest, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for start document load (...)." + "\tURI: %s \n", + this, buffer.get())); +#endif /* DEBUG */ + + // Fire an OnStatus(...) notification STATE_START. This indicates + // that the document represented by mDocumentRequest has started to + // load... + FireOnStateChange(this, + mDocumentRequest, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_DOCUMENT | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); +} + +void nsDocLoader::doStartURLLoad(nsIRequest *request) +{ +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange start url load (...)." + "\tURI: %s\n", + this, buffer.get())); +#endif /* DEBUG */ + + FireOnStateChange(this, + request, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_REQUEST, + NS_OK); +} + +void nsDocLoader::doStopURLLoad(nsIRequest *request, nsresult aStatus) +{ +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for end url load (...)." + "\tURI: %s status=%x\n", + this, buffer.get(), aStatus)); +#endif /* DEBUG */ + + FireOnStateChange(this, + request, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_REQUEST, + aStatus); + + // Fire a status change message for the most recent unfinished + // request to make sure that the displayed status is not outdated. + if (!mStatusInfoList.isEmpty()) { + nsStatusInfo* statusInfo = mStatusInfoList.getFirst(); + FireOnStatusChange(this, statusInfo->mRequest, + statusInfo->mStatusCode, + statusInfo->mStatusMessage.get()); + } +} + +void nsDocLoader::doStopDocumentLoad(nsIRequest *request, + nsresult aStatus) +{ +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for end document load (...)." + "\tURI: %s Status=%x\n", + this, buffer.get(), aStatus)); +#endif /* DEBUG */ + + // Firing STATE_STOP|STATE_IS_DOCUMENT will fire onload handlers. + // Grab our parent chain before doing that so we can still dispatch + // STATE_STOP|STATE_IS_WINDW_STATE_IS_NETWORK to them all, even if + // the onload handlers rearrange the docshell tree. + WebProgressList list; + GatherAncestorWebProgresses(list); + + // + // Fire an OnStateChange(...) notification indicating the the + // current document has finished loading... + // + int32_t flags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_DOCUMENT; + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(this, request, flags, aStatus); + } + + // + // Fire a final OnStateChange(...) notification indicating the the + // current document has finished loading... + // + flags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK; + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(this, request, flags, aStatus); + } +} + +//////////////////////////////////////////////////////////////////////////////////// +// The following section contains support for nsIWebProgress and related stuff +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsDocLoader::AddProgressListener(nsIWebProgressListener *aListener, + uint32_t aNotifyMask) +{ + if (mListenerInfoList.Contains(aListener)) { + // The listener is already registered! + return NS_ERROR_FAILURE; + } + + nsWeakPtr listener = do_GetWeakReference(aListener); + if (!listener) { + return NS_ERROR_INVALID_ARG; + } + + return mListenerInfoList.AppendElement(nsListenerInfo(listener, aNotifyMask)) ? + NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP +nsDocLoader::RemoveProgressListener(nsIWebProgressListener *aListener) +{ + return mListenerInfoList.RemoveElement(aListener) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsDocLoader::GetDOMWindow(mozIDOMWindowProxy **aResult) +{ + return CallGetInterface(this, aResult); +} + +NS_IMETHODIMP +nsDocLoader::GetDOMWindowID(uint64_t *aResult) +{ + *aResult = 0; + + nsCOMPtr<mozIDOMWindowProxy> window; + nsresult rv = GetDOMWindow(getter_AddRefs(window)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsPIDOMWindowOuter> piwindow = nsPIDOMWindowOuter::From(window); + NS_ENSURE_STATE(piwindow); + + MOZ_ASSERT(piwindow->IsOuterWindow()); + *aResult = piwindow->WindowID(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetIsTopLevel(bool *aResult) +{ + *aResult = false; + + nsCOMPtr<mozIDOMWindowProxy> window; + GetDOMWindow(getter_AddRefs(window)); + if (window) { + nsCOMPtr<nsPIDOMWindowOuter> piwindow = nsPIDOMWindowOuter::From(window); + NS_ENSURE_STATE(piwindow); + + nsCOMPtr<nsPIDOMWindowOuter> topWindow = piwindow->GetTop(); + *aResult = piwindow == topWindow; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetIsLoadingDocument(bool *aIsLoadingDocument) +{ + *aIsLoadingDocument = mIsLoadingDocument; + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetLoadType(uint32_t *aLoadType) +{ + *aLoadType = 0; + + return NS_ERROR_NOT_IMPLEMENTED; +} + +int64_t nsDocLoader::GetMaxTotalProgress() +{ + int64_t newMaxTotal = 0; + + uint32_t count = mChildList.Length(); + for (uint32_t i=0; i < count; i++) + { + int64_t individualProgress = 0; + nsIDocumentLoader* docloader = ChildAt(i); + if (docloader) + { + // Cast is safe since all children are nsDocLoader too + individualProgress = ((nsDocLoader *) docloader)->GetMaxTotalProgress(); + } + if (individualProgress < int64_t(0)) // if one of the elements doesn't know it's size + // then none of them do + { + newMaxTotal = int64_t(-1); + break; + } + else + newMaxTotal += individualProgress; + } + + int64_t progress = -1; + if (mMaxSelfProgress >= int64_t(0) && newMaxTotal >= int64_t(0)) + progress = newMaxTotal + mMaxSelfProgress; + + return progress; +} + +//////////////////////////////////////////////////////////////////////////////////// +// The following section contains support for nsIProgressEventSink which is used to +// pass progress and status between the actual request and the doc loader. The doc loader +// then turns around and makes the right web progress calls based on this information. +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsDocLoader::OnProgress(nsIRequest *aRequest, nsISupports* ctxt, + int64_t aProgress, int64_t aProgressMax) +{ + int64_t progressDelta = 0; + + // + // Update the RequestInfo entry with the new progress data + // + if (nsRequestInfo* info = GetRequestInfo(aRequest)) { + // Update info->mCurrentProgress before we call FireOnStateChange, + // since that can make the "info" pointer invalid. + int64_t oldCurrentProgress = info->mCurrentProgress; + progressDelta = aProgress - oldCurrentProgress; + info->mCurrentProgress = aProgress; + + // suppress sending STATE_TRANSFERRING if this is upload progress (see bug 240053) + if (!info->mUploading && (int64_t(0) == oldCurrentProgress) && (int64_t(0) == info->mMaxProgress)) { + // + // If we receive an OnProgress event from a toplevel channel that the URI Loader + // has not yet targeted, then we must suppress the event. This is necessary to + // ensure that webprogresslisteners do not get confused when the channel is + // finally targeted. See bug 257308. + // + nsLoadFlags lf = 0; + aRequest->GetLoadFlags(&lf); + if ((lf & nsIChannel::LOAD_DOCUMENT_URI) && !(lf & nsIChannel::LOAD_TARGETED)) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p Ignoring OnProgress while load is not targeted\n", this)); + return NS_OK; + } + + // + // This is the first progress notification for the entry. If + // (aMaxProgress != -1) then the content-length of the data is known, + // so update mMaxSelfProgress... Otherwise, set it to -1 to indicate + // that the content-length is no longer known. + // + if (aProgressMax != -1) { + mMaxSelfProgress += aProgressMax; + info->mMaxProgress = aProgressMax; + } else { + mMaxSelfProgress = int64_t(-1); + info->mMaxProgress = int64_t(-1); + } + + // Send a STATE_TRANSFERRING notification for the request. + int32_t flags; + + flags = nsIWebProgressListener::STATE_TRANSFERRING | + nsIWebProgressListener::STATE_IS_REQUEST; + // + // Move the WebProgress into the STATE_TRANSFERRING state if necessary... + // + if (mProgressStateFlags & nsIWebProgressListener::STATE_START) { + mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING; + + // Send STATE_TRANSFERRING for the document too... + flags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + } + + FireOnStateChange(this, aRequest, flags, NS_OK); + } + + // Update our overall current progress count. + mCurrentSelfProgress += progressDelta; + } + // + // The request is not part of the load group, so ignore its progress + // information... + // + else { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(aRequest, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p OOPS - No Request Info for: %s\n", + this, buffer.get())); +#endif /* DEBUG */ + + return NS_OK; + } + + // + // Fire progress notifications out to any registered nsIWebProgressListeners + // + FireOnProgressChange(this, aRequest, aProgress, aProgressMax, progressDelta, + mCurrentTotalProgress, mMaxTotalProgress); + + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::OnStatus(nsIRequest* aRequest, nsISupports* ctxt, + nsresult aStatus, const char16_t* aStatusArg) +{ + // + // Fire progress notifications out to any registered nsIWebProgressListeners + // + if (aStatus != NS_OK) { + // Remember the current status for this request + nsRequestInfo *info; + info = GetRequestInfo(aRequest); + if (info) { + bool uploading = (aStatus == NS_NET_STATUS_WRITING || + aStatus == NS_NET_STATUS_SENDING_TO); + // If switching from uploading to downloading (or vice versa), then we + // need to reset our progress counts. This is designed with HTTP form + // submission in mind, where an upload is performed followed by download + // of possibly several documents. + if (info->mUploading != uploading) { + mCurrentSelfProgress = mMaxSelfProgress = 0; + mCurrentTotalProgress = mMaxTotalProgress = 0; + mCompletedTotalProgress = 0; + info->mUploading = uploading; + info->mCurrentProgress = 0; + info->mMaxProgress = 0; + } + } + + nsCOMPtr<nsIStringBundleService> sbs = + mozilla::services::GetStringBundleService(); + if (!sbs) + return NS_ERROR_FAILURE; + nsXPIDLString msg; + nsresult rv = sbs->FormatStatusMessage(aStatus, aStatusArg, + getter_Copies(msg)); + if (NS_FAILED(rv)) + return rv; + + // Keep around the message. In case a request finishes, we need to make sure + // to send the status message of another request to our user to that we + // don't display, for example, "Transferring" messages for requests that are + // already done. + if (info) { + if (!info->mLastStatus) { + info->mLastStatus = new nsStatusInfo(aRequest); + } else { + // We're going to move it to the front of the list, so remove + // it from wherever it is now. + info->mLastStatus->remove(); + } + info->mLastStatus->mStatusMessage = msg; + info->mLastStatus->mStatusCode = aStatus; + // Put the info at the front of the list + mStatusInfoList.insertFront(info->mLastStatus); + } + FireOnStatusChange(this, aRequest, aStatus, msg); + } + return NS_OK; +} + +void nsDocLoader::ClearInternalProgress() +{ + ClearRequestInfoHash(); + + mCurrentSelfProgress = mMaxSelfProgress = 0; + mCurrentTotalProgress = mMaxTotalProgress = 0; + mCompletedTotalProgress = 0; + + mProgressStateFlags = nsIWebProgressListener::STATE_STOP; +} + +/** + * |_code| is executed for every listener matching |_flag| + * |listener| should be used inside |_code| as the nsIWebProgressListener var. + */ +#define NOTIFY_LISTENERS(_flag, _code) \ +PR_BEGIN_MACRO \ + nsCOMPtr<nsIWebProgressListener> listener; \ + ListenerArray::BackwardIterator iter(mListenerInfoList); \ + while (iter.HasMore()) { \ + nsListenerInfo &info = iter.GetNext(); \ + if (!(info.mNotifyMask & (_flag))) { \ + continue; \ + } \ + listener = do_QueryReferent(info.mWeakListener); \ + if (!listener) { \ + iter.Remove(); \ + continue; \ + } \ + _code \ + } \ + mListenerInfoList.Compact(); \ +PR_END_MACRO + +void nsDocLoader::FireOnProgressChange(nsDocLoader *aLoadInitiator, + nsIRequest *request, + int64_t aProgress, + int64_t aProgressMax, + int64_t aProgressDelta, + int64_t aTotalProgress, + int64_t aMaxTotalProgress) +{ + if (mIsLoadingDocument) { + mCurrentTotalProgress += aProgressDelta; + mMaxTotalProgress = GetMaxTotalProgress(); + + aTotalProgress = mCurrentTotalProgress; + aMaxTotalProgress = mMaxTotalProgress; + } + +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Progress (%s): curSelf: %d maxSelf: %d curTotal: %d maxTotal %d\n", + this, buffer.get(), aProgress, aProgressMax, aTotalProgress, aMaxTotalProgress)); +#endif /* DEBUG */ + + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_PROGRESS, + // XXX truncates 64-bit to 32-bit + listener->OnProgressChange(aLoadInitiator,request, + int32_t(aProgress), int32_t(aProgressMax), + int32_t(aTotalProgress), int32_t(aMaxTotalProgress)); + ); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnProgressChange(aLoadInitiator, request, + aProgress, aProgressMax, + aProgressDelta, + aTotalProgress, aMaxTotalProgress); + } +} + +void nsDocLoader::GatherAncestorWebProgresses(WebProgressList& aList) +{ + for (nsDocLoader* loader = this; loader; loader = loader->mParent) { + aList.AppendElement(loader); + } +} + +void nsDocLoader::FireOnStateChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + int32_t aStateFlags, + nsresult aStatus) +{ + WebProgressList list; + GatherAncestorWebProgresses(list); + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(aProgress, aRequest, aStateFlags, aStatus); + } +} + +void nsDocLoader::DoFireOnStateChange(nsIWebProgress * const aProgress, + nsIRequest * const aRequest, + int32_t &aStateFlags, + const nsresult aStatus) +{ + // + // Remove the STATE_IS_NETWORK bit if necessary. + // + // The rule is to remove this bit, if the notification has been passed + // up from a child WebProgress, and the current WebProgress is already + // active... + // + if (mIsLoadingDocument && + (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) && + (this != aProgress)) { + aStateFlags &= ~nsIWebProgressListener::STATE_IS_NETWORK; + } + + // Add the STATE_RESTORING bit if necessary. + if (mIsRestoringDocument) + aStateFlags |= nsIWebProgressListener::STATE_RESTORING; + +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(aRequest, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Status (%s): code: %x\n", + this, buffer.get(), aStateFlags)); +#endif /* DEBUG */ + + NS_ASSERTION(aRequest, "Firing OnStateChange(...) notification with a NULL request!"); + + NOTIFY_LISTENERS(((aStateFlags >> 16) & nsIWebProgress::NOTIFY_STATE_ALL), + listener->OnStateChange(aProgress, aRequest, aStateFlags, aStatus); + ); +} + + + +void +nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI *aUri, + uint32_t aFlags) +{ + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_LOCATION, + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader [%p] calling %p->OnLocationChange", this, listener.get())); + listener->OnLocationChange(aWebProgress, aRequest, aUri, aFlags); + ); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnLocationChange(aWebProgress, aRequest, aUri, aFlags); + } +} + +void +nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) +{ + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_STATUS, + listener->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage); + ); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnStatusChange(aWebProgress, aRequest, aStatus, aMessage); + } +} + +bool +nsDocLoader::RefreshAttempted(nsIWebProgress* aWebProgress, + nsIURI *aURI, + int32_t aDelay, + bool aSameURI) +{ + /* + * Returns true if the refresh may proceed, + * false if the refresh should be blocked. + */ + bool allowRefresh = true; + + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_REFRESH, + nsCOMPtr<nsIWebProgressListener2> listener2 = + do_QueryReferent(info.mWeakListener); + if (!listener2) + continue; + + bool listenerAllowedRefresh; + nsresult listenerRV = listener2->OnRefreshAttempted( + aWebProgress, aURI, aDelay, aSameURI, &listenerAllowedRefresh); + if (NS_FAILED(listenerRV)) + continue; + + allowRefresh = allowRefresh && listenerAllowedRefresh; + ); + + // Pass the notification up to the parent... + if (mParent) { + allowRefresh = allowRefresh && + mParent->RefreshAttempted(aWebProgress, aURI, aDelay, aSameURI); + } + + return allowRefresh; +} + +nsresult nsDocLoader::AddRequestInfo(nsIRequest *aRequest) +{ + if (!mRequestInfoHash.Add(aRequest, mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void nsDocLoader::RemoveRequestInfo(nsIRequest *aRequest) +{ + mRequestInfoHash.Remove(aRequest); +} + +nsDocLoader::nsRequestInfo* nsDocLoader::GetRequestInfo(nsIRequest* aRequest) +{ + return static_cast<nsRequestInfo*>(mRequestInfoHash.Search(aRequest)); +} + +void nsDocLoader::ClearRequestInfoHash(void) +{ + mRequestInfoHash.Clear(); +} + +int64_t nsDocLoader::CalculateMaxProgress() +{ + int64_t max = mCompletedTotalProgress; + for (auto iter = mRequestInfoHash.Iter(); !iter.Done(); iter.Next()) { + auto info = static_cast<const nsRequestInfo*>(iter.Get()); + + if (info->mMaxProgress < info->mCurrentProgress) { + return int64_t(-1); + } + max += info->mMaxProgress; + } + return max; +} + +NS_IMETHODIMP nsDocLoader::AsyncOnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback *cb) +{ + if (aOldChannel) + { + nsLoadFlags loadFlags = 0; + int32_t stateFlags = nsIWebProgressListener::STATE_REDIRECTING | + nsIWebProgressListener::STATE_IS_REQUEST; + + aOldChannel->GetLoadFlags(&loadFlags); + // If the document channel is being redirected, then indicate that the + // document is being redirected in the notification... + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) + { + stateFlags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + +#if defined(DEBUG) + nsCOMPtr<nsIRequest> request(do_QueryInterface(aOldChannel)); + NS_ASSERTION(request == mDocumentRequest, "Wrong Document Channel"); +#endif /* DEBUG */ + } + + OnRedirectStateChange(aOldChannel, aNewChannel, aFlags, stateFlags); + FireOnStateChange(this, aOldChannel, stateFlags, NS_OK); + } + + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +/* + * Implementation of nsISecurityEventSink method... + */ + +NS_IMETHODIMP nsDocLoader::OnSecurityChange(nsISupports * aContext, + uint32_t aState) +{ + // + // Fire progress notifications out to any registered nsIWebProgressListeners. + // + + nsCOMPtr<nsIRequest> request = do_QueryInterface(aContext); + nsIWebProgress* webProgress = static_cast<nsIWebProgress*>(this); + + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_SECURITY, + listener->OnSecurityChange(webProgress, request, aState); + ); + + // Pass the notification up to the parent... + if (mParent) { + mParent->OnSecurityChange(aContext, aState); + } + return NS_OK; +} + +/* + * Implementation of nsISupportsPriority methods... + * + * The priority of the DocLoader _is_ the priority of its LoadGroup. + * + * XXX(darin): Once we start storing loadgroups in loadgroups, this code will + * go away. + */ + +NS_IMETHODIMP nsDocLoader::GetPriority(int32_t *aPriority) +{ + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup); + if (p) + return p->GetPriority(aPriority); + + *aPriority = 0; + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::SetPriority(int32_t aPriority) +{ + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: SetPriority(%d) called\n", this, aPriority)); + + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup); + if (p) + p->SetPriority(aPriority); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, nsDocLoader, + SetPriority, (aPriority)); + + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::AdjustPriority(int32_t aDelta) +{ + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: AdjustPriority(%d) called\n", this, aDelta)); + + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup); + if (p) + p->AdjustPriority(aDelta); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, nsDocLoader, + AdjustPriority, (aDelta)); + + return NS_OK; +} + + + + +#if 0 +void nsDocLoader::DumpChannelInfo() +{ + nsChannelInfo *info; + int32_t i, count; + int32_t current=0, max=0; + + + printf("==== DocLoader=%x\n", this); + + count = mChannelInfoList.Count(); + for(i=0; i<count; i++) { + info = (nsChannelInfo *)mChannelInfoList.ElementAt(i); + +#if defined(DEBUG) + nsAutoCString buffer; + nsresult rv = NS_OK; + if (info->mURI) { + rv = info->mURI->GetSpec(buffer); + } + + printf(" [%d] current=%d max=%d [%s]\n", i, + info->mCurrentProgress, + info->mMaxProgress, buffer.get()); +#endif /* DEBUG */ + + current += info->mCurrentProgress; + if (max >= 0) { + if (info->mMaxProgress < info->mCurrentProgress) { + max = -1; + } else { + max += info->mMaxProgress; + } + } + } + + printf("\nCurrent=%d Total=%d\n====\n", current, max); +} +#endif /* 0 */ diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h new file mode 100644 index 0000000000..481b1397bc --- /dev/null +++ b/uriloader/base/nsDocLoader.h @@ -0,0 +1,335 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* +*/ + +#ifndef nsDocLoader_h__ +#define nsDocLoader_h__ + +#include "nsIDocumentLoader.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsIRequestObserver.h" +#include "nsWeakReference.h" +#include "nsILoadGroup.h" +#include "nsCOMArray.h" +#include "nsTObserverArray.h" +#include "nsString.h" +#include "nsIChannel.h" +#include "nsIProgressEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIChannelEventSink.h" +#include "nsISecurityEventSink.h" +#include "nsISupportsPriority.h" +#include "nsCOMPtr.h" +#include "PLDHashTable.h" +#include "nsAutoPtr.h" + +#include "mozilla/LinkedList.h" + +/**************************************************************************** + * nsDocLoader implementation... + ****************************************************************************/ + +#define NS_THIS_DOCLOADER_IMPL_CID \ + { /* b4ec8387-98aa-4c08-93b6-6d23069c06f2 */ \ + 0xb4ec8387, \ + 0x98aa, \ + 0x4c08, \ + {0x93, 0xb6, 0x6d, 0x23, 0x06, 0x9c, 0x06, 0xf2} \ + } + +class nsDocLoader : public nsIDocumentLoader, + public nsIRequestObserver, + public nsSupportsWeakReference, + public nsIProgressEventSink, + public nsIWebProgress, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsISecurityEventSink, + public nsISupportsPriority +{ +public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_THIS_DOCLOADER_IMPL_CID) + + nsDocLoader(); + + virtual MOZ_MUST_USE nsresult Init(); + + static already_AddRefed<nsDocLoader> GetAsDocLoader(nsISupports* aSupports); + // Needed to deal with ambiguous inheritance from nsISupports... + static nsISupports* GetAsSupports(nsDocLoader* aDocLoader) { + return static_cast<nsIDocumentLoader*>(aDocLoader); + } + + // Add aDocLoader as a child to the docloader service. + static MOZ_MUST_USE nsresult AddDocLoaderAsChildOfRoot(nsDocLoader* aDocLoader); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOCUMENTLOADER + + // nsIProgressEventSink + NS_DECL_NSIPROGRESSEVENTSINK + + NS_DECL_NSISECURITYEVENTSINK + + // nsIRequestObserver methods: (for observing the load group) + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIWEBPROGRESS + + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSISUPPORTSPRIORITY + + // Implementation specific methods... + + // Remove aChild from our childlist. This nulls out the child's mParent + // pointer. + MOZ_MUST_USE nsresult RemoveChildLoader(nsDocLoader *aChild); + // Add aChild to our child list. This will set aChild's mParent pointer to + // |this|. + MOZ_MUST_USE nsresult AddChildLoader(nsDocLoader* aChild); + nsDocLoader* GetParent() const { return mParent; } + + struct nsListenerInfo { + nsListenerInfo(nsIWeakReference *aListener, unsigned long aNotifyMask) + : mWeakListener(aListener), + mNotifyMask(aNotifyMask) + { + } + + // Weak pointer for the nsIWebProgressListener... + nsWeakPtr mWeakListener; + + // Mask indicating which notifications the listener wants to receive. + unsigned long mNotifyMask; + }; + +protected: + virtual ~nsDocLoader(); + + virtual MOZ_MUST_USE nsresult SetDocLoaderParent(nsDocLoader * aLoader); + + bool IsBusy(); + + void Destroy(); + virtual void DestroyChildren(); + + nsIDocumentLoader* ChildAt(int32_t i) { + return mChildList.SafeElementAt(i, nullptr); + } + + void FireOnProgressChange(nsDocLoader* aLoadInitiator, + nsIRequest *request, + int64_t aProgress, + int64_t aProgressMax, + int64_t aProgressDelta, + int64_t aTotalProgress, + int64_t aMaxTotalProgress); + + // This should be at least 2 long since we'll generally always + // have the current page and the global docloader on the ancestor + // list. But to deal with frames it's better to make it a bit + // longer, and it's always a stack temporary so there's no real + // reason not to. + typedef AutoTArray<RefPtr<nsDocLoader>, 8> WebProgressList; + void GatherAncestorWebProgresses(WebProgressList& aList); + + void FireOnStateChange(nsIWebProgress *aProgress, + nsIRequest* request, + int32_t aStateFlags, + nsresult aStatus); + + // The guts of FireOnStateChange, but does not call itself on our ancestors. + // The arguments that are const are const so that we can detect cases when + // DoFireOnStateChange wants to propagate changes to the next web progress + // at compile time. The ones that are not, are references so that such + // changes can be propagated. + void DoFireOnStateChange(nsIWebProgress * const aProgress, + nsIRequest* const request, + int32_t &aStateFlags, + const nsresult aStatus); + + void FireOnStatusChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + nsresult aStatus, + const char16_t* aMessage); + + void FireOnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI *aUri, + uint32_t aFlags); + + MOZ_MUST_USE bool RefreshAttempted(nsIWebProgress* aWebProgress, + nsIURI *aURI, + int32_t aDelay, + bool aSameURI); + + // this function is overridden by the docshell, it is provided so that we + // can pass more information about redirect state (the normal OnStateChange + // doesn't get the new channel). + // @param aRedirectFlags The flags being sent to OnStateChange that + // indicate the type of redirect. + // @param aStateFlags The channel flags normally sent to OnStateChange. + virtual void OnRedirectStateChange(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aRedirectFlags, + uint32_t aStateFlags) {} + + void doStartDocumentLoad(); + void doStartURLLoad(nsIRequest *request); + void doStopURLLoad(nsIRequest *request, nsresult aStatus); + void doStopDocumentLoad(nsIRequest *request, nsresult aStatus); + + // Inform a parent docloader that aChild is about to call its onload + // handler. + MOZ_MUST_USE bool ChildEnteringOnload(nsIDocumentLoader* aChild) { + // It's ok if we're already in the list -- we'll just be in there twice + // and then the RemoveObject calls from ChildDoneWithOnload will remove + // us. + return mChildrenInOnload.AppendObject(aChild); + } + + // Inform a parent docloader that aChild is done calling its onload + // handler. + void ChildDoneWithOnload(nsIDocumentLoader* aChild) { + mChildrenInOnload.RemoveObject(aChild); + DocLoaderIsEmpty(true); + } + +protected: + struct nsStatusInfo : public mozilla::LinkedListElement<nsStatusInfo> + { + nsString mStatusMessage; + nsresult mStatusCode; + // Weak mRequest is ok; we'll be told if it decides to go away. + nsIRequest * const mRequest; + + explicit nsStatusInfo(nsIRequest* aRequest) : + mRequest(aRequest) + { + MOZ_COUNT_CTOR(nsStatusInfo); + } + ~nsStatusInfo() + { + MOZ_COUNT_DTOR(nsStatusInfo); + } + }; + + struct nsRequestInfo : public PLDHashEntryHdr + { + explicit nsRequestInfo(const void* key) + : mKey(key), mCurrentProgress(0), mMaxProgress(0), mUploading(false) + , mLastStatus(nullptr) + { + MOZ_COUNT_CTOR(nsRequestInfo); + } + + ~nsRequestInfo() + { + MOZ_COUNT_DTOR(nsRequestInfo); + } + + nsIRequest* Request() { + return static_cast<nsIRequest*>(const_cast<void*>(mKey)); + } + + const void* mKey; // Must be first for the PLDHashTable stubs to work + int64_t mCurrentProgress; + int64_t mMaxProgress; + bool mUploading; + + nsAutoPtr<nsStatusInfo> mLastStatus; + }; + + static void RequestInfoHashInitEntry(PLDHashEntryHdr* entry, const void* key); + static void RequestInfoHashClearEntry(PLDHashTable* table, PLDHashEntryHdr* entry); + + // IMPORTANT: The ownership implicit in the following member + // variables has been explicitly checked and set using nsCOMPtr + // for owning pointers and raw COM interface pointers for weak + // (ie, non owning) references. If you add any members to this + // class, please make the ownership explicit (pinkerton, scc). + + nsCOMPtr<nsIRequest> mDocumentRequest; // [OWNER] ???compare with document + + nsDocLoader* mParent; // [WEAK] + + typedef nsAutoTObserverArray<nsListenerInfo, 8> ListenerArray; + ListenerArray mListenerInfoList; + + nsCOMPtr<nsILoadGroup> mLoadGroup; + // We hold weak refs to all our kids + nsTObserverArray<nsDocLoader*> mChildList; + + // The following member variables are related to the new nsIWebProgress + // feedback interfaces that travis cooked up. + int32_t mProgressStateFlags; + + int64_t mCurrentSelfProgress; + int64_t mMaxSelfProgress; + + int64_t mCurrentTotalProgress; + int64_t mMaxTotalProgress; + + PLDHashTable mRequestInfoHash; + int64_t mCompletedTotalProgress; + + mozilla::LinkedList<nsStatusInfo> mStatusInfoList; + + /* + * This flag indicates that the loader is loading a document. It is set + * from the call to LoadDocument(...) until the OnConnectionsComplete(...) + * notification is fired... + */ + bool mIsLoadingDocument; + + /* Flag to indicate that we're in the process of restoring a document. */ + bool mIsRestoringDocument; + + /* Flag to indicate that we're in the process of flushing layout + under DocLoaderIsEmpty() and should not do another flush. */ + bool mDontFlushLayout; + + /* Flag to indicate whether we should consider ourselves as currently + flushing layout for the purposes of IsBusy. For example, if Stop has + been called then IsBusy should return false even if we are still + flushing. */ + bool mIsFlushingLayout; + +private: + static const PLDHashTableOps sRequestInfoHashOps; + + // A list of kids that are in the middle of their onload calls and will let + // us know once they're done. We don't want to fire onload for "normal" + // DocLoaderIsEmpty calls (those coming from requests finishing in our + // loadgroup) unless this is empty. + nsCOMArray<nsIDocumentLoader> mChildrenInOnload; + + // DocLoaderIsEmpty should be called whenever the docloader may be empty. + // This method is idempotent and does nothing if the docloader is not in + // fact empty. This method _does_ make sure that layout is flushed if our + // loadgroup has no active requests before checking for "real" emptiness if + // aFlushLayout is true. + void DocLoaderIsEmpty(bool aFlushLayout); + + int64_t GetMaxTotalProgress(); + + nsresult AddRequestInfo(nsIRequest* aRequest); + void RemoveRequestInfo(nsIRequest* aRequest); + nsRequestInfo *GetRequestInfo(nsIRequest* aRequest); + void ClearRequestInfoHash(); + int64_t CalculateMaxProgress(); +/// void DumpChannelInfo(void); + + // used to clear our internal progress state between loads... + void ClearInternalProgress(); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsDocLoader, NS_THIS_DOCLOADER_IMPL_CID) + +#endif /* nsDocLoader_h__ */ diff --git a/uriloader/base/nsIContentHandler.idl b/uriloader/base/nsIContentHandler.idl new file mode 100644 index 0000000000..31ef87a8ba --- /dev/null +++ b/uriloader/base/nsIContentHandler.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +interface nsIRequest; +interface nsIInterfaceRequestor; + +[scriptable, uuid(49439df2-b3d2-441c-bf62-866bdaf56fd2)] +interface nsIContentHandler : nsISupports +{ + /** + * Tells the content handler to take over handling the content. If this + * function succeeds, the URI Loader will leave this request alone, ignoring + * progress notifications. Failure of this method will cause the request to be + * cancelled, unless the error code is NS_ERROR_WONT_HANDLE_CONTENT (see + * below). + * + * @param aWindowContext + * Window context, used to get things like the current nsIDOMWindow + * for this request. May be null. + * @param aContentType + * The content type of aRequest + * @param aRequest + * A request whose content type is already known. + * + * @throw NS_ERROR_WONT_HANDLE_CONTENT Indicates that this handler does not + * want to handle this content. A different way for handling this + * content should be tried. + */ + void handleContent(in string aContentType, + in nsIInterfaceRequestor aWindowContext, + in nsIRequest aRequest); +}; diff --git a/uriloader/base/nsIDocumentLoader.idl b/uriloader/base/nsIDocumentLoader.idl new file mode 100644 index 0000000000..3bd960ac84 --- /dev/null +++ b/uriloader/base/nsIDocumentLoader.idl @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +interface nsILoadGroup; +interface nsIChannel; +interface nsIURI; +interface nsIWebProgress; +interface nsIRequest; + +/** + * An nsIDocumentLoader is an interface responsible for tracking groups of + * loads that belong together (images, external scripts, etc) and subdocuments + * (<iframe>, <frame>, etc). It is also responsible for sending + * nsIWebProgressListener notifications. + * XXXbz this interface should go away, we think... + */ +[scriptable, uuid(bbe961ee-59e9-42bb-be50-0331979bb79f)] +interface nsIDocumentLoader : nsISupports +{ + // Stop all loads in the loadgroup of this docloader + void stop(); + + // XXXbz is this needed? For embedding? What this does is does is not + // defined by this interface! + readonly attribute nsISupports container; + + // The loadgroup associated with this docloader + readonly attribute nsILoadGroup loadGroup; + + // The defaultLoadRequest of the loadgroup associated with this docloader + readonly attribute nsIChannel documentChannel; +}; + diff --git a/uriloader/base/nsITransfer.idl b/uriloader/base/nsITransfer.idl new file mode 100644 index 0000000000..da34d4ac49 --- /dev/null +++ b/uriloader/base/nsITransfer.idl @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIWebProgressListener2.idl" + +interface nsIArray; +interface nsIURI; +interface nsICancelable; +interface nsIMIMEInfo; +interface nsIFile; + +[scriptable, uuid(37ec75d3-97ad-4da8-afaa-eabe5b4afd73)] +interface nsITransfer : nsIWebProgressListener2 { + + /** + * Initializes the transfer with certain properties. This function must + * be called prior to accessing any properties on this interface. + * + * @param aSource The source URI of the transfer. Must not be null. + * + * @param aTarget The target URI of the transfer. Must not be null. + * + * @param aDisplayName The user-readable description of the transfer. + * Can be empty. + * + * @param aMIMEInfo The MIME info associated with the target, + * including MIME type and helper app when appropriate. + * This parameter is optional. + * + * @param startTime Time when the download started (ie, when the first + * response from the server was received) + * XXX presumably wbp and exthandler do this differently + * + * @param aTempFile The location of a temporary file; i.e. a file in which + * the received data will be stored, but which is not + * equal to the target file. (will be moved to the real + * target by the caller, when the download is finished) + * May be null. + * + * @param aCancelable An object that can be used to abort the download. + * Must not be null. + * Implementations are expected to hold a strong + * reference to this object until the download is + * finished, at which point they should release the + * reference. + * + * @param aIsPrivate Used to determine the privacy status of the new transfer. + * If true, indicates that the transfer was initiated from + * a source that desires privacy. + */ + void init(in nsIURI aSource, + in nsIURI aTarget, + in AString aDisplayName, + in nsIMIMEInfo aMIMEInfo, + in PRTime startTime, + in nsIFile aTempFile, + in nsICancelable aCancelable, + in boolean aIsPrivate); + + /* + * Used to notify the transfer object of the hash of the downloaded file. + * Must be called on the main thread, only after the download has finished + * successfully. + * @param aHash The SHA-256 hash in raw bytes of the downloaded file. + */ + void setSha256Hash(in ACString aHash); + + /* + * Used to notify the transfer object of the signature of the downloaded + * file. Must be called on the main thread, only after the download has + * finished successfully. + * @param aSignatureInfo The nsIArray of nsIX509CertList of nsIX509Cert + * certificates of the downloaded file. + */ + void setSignatureInfo(in nsIArray aSignatureInfo); + + /* + * Used to notify the transfer object of the redirects associated with the + * channel that terminated in the downloaded file. Must be called on the + * main thread, only after the download has finished successfully. + * @param aRedirects The nsIArray of nsIPrincipal of redirected URIs + * associated with the downloaded file. + */ + void setRedirects(in nsIArray aRedirects); +}; + +%{C++ +/** + * A component with this contract ID will be created each time a download is + * started, and nsITransfer::Init will be called on it and an observer will be set. + * + * Notifications of the download progress will happen via + * nsIWebProgressListener/nsIWebProgressListener2. + * + * INTERFACES THAT MUST BE IMPLEMENTED: + * nsITransfer + * nsIWebProgressListener + * nsIWebProgressListener2 + * + * XXX move this to nsEmbedCID.h once the interfaces (and the contract ID) are + * frozen. + */ +#define NS_TRANSFER_CONTRACTID "@mozilla.org/transfer;1" +%} diff --git a/uriloader/base/nsIURIContentListener.idl b/uriloader/base/nsIURIContentListener.idl new file mode 100644 index 0000000000..9008cb61e0 --- /dev/null +++ b/uriloader/base/nsIURIContentListener.idl @@ -0,0 +1,135 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIRequest; +interface nsIStreamListener; +interface nsIURI; + +/** + * nsIURIContentListener is an interface used by components which + * want to know (and have a chance to handle) a particular content type. + * Typical usage scenarios will include running applications which register + * a nsIURIContentListener for each of its content windows with the uri + * dispatcher service. + */ +[scriptable, uuid(10a28f38-32e8-4c63-8aa1-12eaaebc369a)] +interface nsIURIContentListener : nsISupports +{ + /** + * Gives the original content listener first crack at stopping a load before + * it happens. + * + * @param aURI URI that is being opened. + * + * @return <code>false</code> if the load can continue; + * <code>true</code> if the open should be aborted. + */ + boolean onStartURIOpen(in nsIURI aURI); + + /** + * Notifies the content listener to hook up an nsIStreamListener capable of + * consuming the data stream. + * + * @param aContentType Content type of the data. + * @param aIsContentPreferred Indicates whether the content should be + * preferred by this listener. + * @param aRequest Request that is providing the data. + * @param aContentHandler nsIStreamListener that will consume the data. + * This should be set to <code>nullptr</code> if + * this content listener can't handle the content + * type; in this case, doContent should also fail + * (i.e., return failure nsresult). + * + * @return <code>true</code> if the load should + * be aborted and consumer wants to + * handle the load completely by itself. This + * causes the URI Loader do nothing else... + * <code>false</code> if the URI Loader should + * continue handling the load and call the + * returned streamlistener's methods. + */ + boolean doContent(in ACString aContentType, + in boolean aIsContentPreferred, + in nsIRequest aRequest, + out nsIStreamListener aContentHandler); + + /** + * When given a uri to dispatch, if the URI is specified as 'preferred + * content' then the uri loader tries to find a preferred content handler + * for the content type. The thought is that many content listeners may + * be able to handle the same content type if they have to. i.e. the mail + * content window can handle text/html just like a browser window content + * listener. However, if the user clicks on a link with text/html content, + * then the browser window should handle that content and not the mail + * window where the user may have clicked the link. This is the difference + * between isPreferred and canHandleContent. + * + * @param aContentType Content type of the data. + * @param aDesiredContentType Indicates that aContentType must be converted + * to aDesiredContentType before processing the + * data. This causes a stream converted to be + * inserted into the nsIStreamListener chain. + * This argument can be <code>nullptr</code> if + * the content should be consumed directly as + * aContentType. + * + * @return <code>true</code> if this is a preferred + * content handler for aContentType; + * <code>false<code> otherwise. + */ + boolean isPreferred(in string aContentType, out string aDesiredContentType); + + /** + * When given a uri to dispatch, if the URI is not specified as 'preferred + * content' then the uri loader calls canHandleContent to see if the content + * listener is capable of handling the content. + * + * @param aContentType Content type of the data. + * @param aIsContentPreferred Indicates whether the content should be + * preferred by this listener. + * @param aDesiredContentType Indicates that aContentType must be converted + * to aDesiredContentType before processing the + * data. This causes a stream converted to be + * inserted into the nsIStreamListener chain. + * This argument can be <code>nullptr</code> if + * the content should be consumed directly as + * aContentType. + * + * @return <code>true</code> if the data can be consumed. + * <code>false</code> otherwise. + * + * Note: I really envision canHandleContent as a method implemented + * by the docshell as the implementation is generic to all doc + * shells. The isPreferred decision is a decision made by a top level + * application content listener that sits at the top of the docshell + * hierarchy. + */ + boolean canHandleContent(in string aContentType, + in boolean aIsContentPreferred, + out string aDesiredContentType); + + /** + * The load context associated with a particular content listener. + * The URI Loader stores and accesses this value as needed. + */ + attribute nsISupports loadCookie; + + /** + * The parent content listener if this particular listener is part of a chain + * of content listeners (i.e. a docshell!) + * + * @note If this attribute is set to an object that implements + * nsISupportsWeakReference, the implementation should get the + * nsIWeakReference and hold that. Otherwise, the implementation + * should not refcount this interface; it should assume that a non + * null value is always valid. In that case, the caller is + * responsible for explicitly setting this value back to null if the + * parent content listener is destroyed. + */ + attribute nsIURIContentListener parentContentListener; +}; + diff --git a/uriloader/base/nsIURILoader.idl b/uriloader/base/nsIURILoader.idl new file mode 100644 index 0000000000..74f21e3639 --- /dev/null +++ b/uriloader/base/nsIURILoader.idl @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURIContentListener; +interface nsIURI; +interface nsILoadGroup; +interface nsIProgressEventSink; +interface nsIChannel; +interface nsIRequest; +interface nsIStreamListener; +interface nsIInputStream; +interface nsIInterfaceRequestor; + +/** + * The uri dispatcher is responsible for taking uri's, determining + * the content and routing the opened url to the correct content + * handler. + * + * When you encounter a url you want to open, you typically call + * openURI, passing it the content listener for the window the uri is + * originating from. The uri dispatcher opens the url to discover the + * content type. It then gives the content listener first crack at + * handling the content. If it doesn't want it, the dispatcher tries + * to hand it off one of the registered content listeners. This allows + * running applications the chance to jump in and handle the content. + * + * If that also fails, then the uri dispatcher goes to the registry + * looking for the preferred content handler for the content type + * of the uri. The content handler may create an app instance + * or it may hand the contents off to a platform specific plugin + * or helper app. Or it may hand the url off to an OS registered + * application. + */ +[scriptable, uuid(8762c4e7-be35-4958-9b81-a05685bb516d)] +interface nsIURILoader : nsISupports +{ + /** + * @name Flags for opening URIs. + */ + /* @{ */ + /** + * Should the content be displayed in a container that prefers the + * content-type, or will any container do. + */ + const unsigned long IS_CONTENT_PREFERRED = 1 << 0; + /** + * If this flag is set, only the listener of the specified window context will + * be considered for content handling; if it refuses the load, an error will + * be indicated. + */ + const unsigned long DONT_RETARGET = 1 << 1; + /* @} */ + + /** + * As applications such as messenger and the browser are instantiated, + * they register content listener's with the uri dispatcher corresponding + * to content windows within that application. + * + * Note to self: we may want to optimize things a bit more by requiring + * the content types the registered content listener cares about. + * + * @param aContentListener + * The listener to register. This listener must implement + * nsISupportsWeakReference. + * + * @see the nsIURILoader class description + */ + void registerContentListener (in nsIURIContentListener aContentListener); + void unRegisterContentListener (in nsIURIContentListener aContentListener); + + /** + * OpenURI requires the following parameters..... + * @param aChannel + * The channel that should be opened. This must not be asyncOpen'd yet! + * If a loadgroup is set on the channel, it will get replaced with a + * different one. + * @param aFlags + * Combination (bitwise OR) of the flags specified above. 0 indicates + * default handling. + * @param aWindowContext + * If you are running the url from a doc shell or a web shell, this is + * your window context. If you have a content listener you want to + * give first crack to, the uri loader needs to be able to get it + * from the window context. We will also be using the window context + * to get at the progress event sink interface. + * <b>Must not be null!</b> + */ + void openURI(in nsIChannel aChannel, + in unsigned long aFlags, + in nsIInterfaceRequestor aWindowContext); + + /** + * Loads data from a channel. This differs from openURI in that the channel + * may already be opened, and that it returns a stream listener into which the + * caller should pump data. The caller is responsible for opening the channel + * and pumping the channel's data into the returned stream listener. + * + * Note: If the channel already has a loadgroup, it will be replaced with the + * window context's load group, or null if the context doesn't have one. + * + * If the window context's nsIURIContentListener refuses the load immediately + * (e.g. in nsIURIContentListener::onStartURIOpen), this method will return + * NS_ERROR_WONT_HANDLE_CONTENT. At that point, the caller should probably + * cancel the channel if it's already open (this method will not cancel the + * channel). + * + * If flags include DONT_RETARGET, and the content listener refuses the load + * during onStartRequest (e.g. in canHandleContent/isPreferred), then the + * returned stream listener's onStartRequest method will return + * NS_ERROR_WONT_HANDLE_CONTENT. + * + * @param aChannel + * The channel that should be loaded. The channel may already be + * opened. It must not be closed (i.e. this must be called before the + * channel calls onStopRequest on its stream listener). + * @param aFlags + * Combination (bitwise OR) of the flags specified above. 0 indicates + * default handling. + * @param aWindowContext + * If you are running the url from a doc shell or a web shell, this is + * your window context. If you have a content listener you want to + * give first crack to, the uri loader needs to be able to get it + * from the window context. We will also be using the window context + * to get at the progress event sink interface. + * <b>Must not be null!</b> + */ + nsIStreamListener openChannel(in nsIChannel aChannel, + in unsigned long aFlags, + in nsIInterfaceRequestor aWindowContext); + + /** + * Stops an in progress load + */ + void stop(in nsISupports aLoadCookie); +}; + diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl new file mode 100644 index 0000000000..9d17d0a4d4 --- /dev/null +++ b/uriloader/base/nsIWebProgress.idl @@ -0,0 +1,153 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; +interface nsIWebProgressListener; + +/** + * The nsIWebProgress interface is used to add or remove nsIWebProgressListener + * instances to observe the loading of asynchronous requests (usually in the + * context of a DOM window). + * + * nsIWebProgress instances may be arranged in a parent-child configuration, + * corresponding to the parent-child configuration of their respective DOM + * windows. However, in some cases a nsIWebProgress instance may not have an + * associated DOM window. The parent-child relationship of nsIWebProgress + * instances is not made explicit by this interface, but the relationship may + * exist in some implementations. + * + * A nsIWebProgressListener instance receives notifications for the + * nsIWebProgress instance to which it added itself, and it may also receive + * notifications from any nsIWebProgress instances that are children of that + * nsIWebProgress instance. + */ +[scriptable, uuid(c4d64640-b332-4db6-a2a5-e08566000dc9)] +interface nsIWebProgress : nsISupports +{ + /** + * The following flags may be combined to form the aNotifyMask parameter for + * the addProgressListener method. They limit the set of events that are + * delivered to an nsIWebProgressListener instance. + */ + + /** + * These flags indicate the state transistions to observe, corresponding to + * nsIWebProgressListener::onStateChange. + * + * NOTIFY_STATE_REQUEST + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_REQUEST. + * + * NOTIFY_STATE_DOCUMENT + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_DOCUMENT. + * + * NOTIFY_STATE_NETWORK + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_NETWORK. + * + * NOTIFY_STATE_WINDOW + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_WINDOW. + * + * NOTIFY_STATE_ALL + * Receive all onStateChange events. + */ + const unsigned long NOTIFY_STATE_REQUEST = 0x00000001; + const unsigned long NOTIFY_STATE_DOCUMENT = 0x00000002; + const unsigned long NOTIFY_STATE_NETWORK = 0x00000004; + const unsigned long NOTIFY_STATE_WINDOW = 0x00000008; + const unsigned long NOTIFY_STATE_ALL = 0x0000000f; + + /** + * These flags indicate the other events to observe, corresponding to the + * other four methods defined on nsIWebProgressListener. + * + * NOTIFY_PROGRESS + * Receive onProgressChange events. + * + * NOTIFY_STATUS + * Receive onStatusChange events. + * + * NOTIFY_SECURITY + * Receive onSecurityChange events. + * + * NOTIFY_LOCATION + * Receive onLocationChange events. + * + * NOTIFY_REFRESH + * Receive onRefreshAttempted events. + * This is defined on nsIWebProgressListener2. + */ + const unsigned long NOTIFY_PROGRESS = 0x00000010; + const unsigned long NOTIFY_STATUS = 0x00000020; + const unsigned long NOTIFY_SECURITY = 0x00000040; + const unsigned long NOTIFY_LOCATION = 0x00000080; + const unsigned long NOTIFY_REFRESH = 0x00000100; + + /** + * This flag enables all notifications. + */ + const unsigned long NOTIFY_ALL = 0x000001ff; + + /** + * Registers a listener to receive web progress events. + * + * @param aListener + * The listener interface to be called when a progress event occurs. + * This object must also implement nsISupportsWeakReference. + * @param aNotifyMask + * The types of notifications to receive. + * + * @throw NS_ERROR_INVALID_ARG + * Indicates that aListener was either null or that it does not + * support weak references. + * @throw NS_ERROR_FAILURE + * Indicates that aListener was already registered. + */ + void addProgressListener(in nsIWebProgressListener aListener, + in unsigned long aNotifyMask); + + /** + * Removes a previously registered listener of progress events. + * + * @param aListener + * The listener interface previously registered with a call to + * addProgressListener. + * + * @throw NS_ERROR_FAILURE + * Indicates that aListener was not registered. + */ + void removeProgressListener(in nsIWebProgressListener aListener); + + /** + * The DOM window associated with this nsIWebProgress instance. + * + * @throw NS_ERROR_FAILURE + * Indicates that there is no associated DOM window. + */ + readonly attribute mozIDOMWindowProxy DOMWindow; + readonly attribute uint64_t DOMWindowID; + + /** + * Indicates whether DOMWindow.top == DOMWindow. + */ + readonly attribute boolean isTopLevel; + + /** + * Indicates whether or not a document is currently being loaded + * in the context of this nsIWebProgress instance. + */ + readonly attribute boolean isLoadingDocument; + + /** + * Contains a load type as specified by the load* constants in + * nsIDocShellLoadInfo.idl. + */ + readonly attribute unsigned long loadType; +}; diff --git a/uriloader/base/nsIWebProgressListener.idl b/uriloader/base/nsIWebProgressListener.idl new file mode 100644 index 0000000000..714b931a3e --- /dev/null +++ b/uriloader/base/nsIWebProgressListener.idl @@ -0,0 +1,425 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIWebProgress; +interface nsIRequest; +interface nsIURI; + +/** + * The nsIWebProgressListener interface is implemented by clients wishing to + * listen in on the progress associated with the loading of asynchronous + * requests in the context of a nsIWebProgress instance as well as any child + * nsIWebProgress instances. nsIWebProgress.idl describes the parent-child + * relationship of nsIWebProgress instances. + */ +[scriptable, uuid(a9df523b-efe2-421e-9d8e-3d7f807dda4c)] +interface nsIWebProgressListener : nsISupports +{ + /** + * State Transition Flags + * + * These flags indicate the various states that requests may transition + * through as they are being loaded. These flags are mutually exclusive. + * + * For any given request, onStateChange is called once with the STATE_START + * flag, zero or more times with the STATE_TRANSFERRING flag or once with the + * STATE_REDIRECTING flag, and then finally once with the STATE_STOP flag. + * NOTE: For document requests, a second STATE_STOP is generated (see the + * description of STATE_IS_WINDOW for more details). + * + * STATE_START + * This flag indicates the start of a request. This flag is set when a + * request is initiated. The request is complete when onStateChange is + * called for the same request with the STATE_STOP flag set. + * + * STATE_REDIRECTING + * This flag indicates that a request is being redirected. The request + * passed to onStateChange is the request that is being redirected. When a + * redirect occurs, a new request is generated automatically to process the + * new request. Expect a corresponding STATE_START event for the new + * request, and a STATE_STOP for the redirected request. + * + * STATE_TRANSFERRING + * This flag indicates that data for a request is being transferred to an + * end consumer. This flag indicates that the request has been targeted, + * and that the user may start seeing content corresponding to the request. + * + * STATE_NEGOTIATING + * This flag is not used. + * + * STATE_STOP + * This flag indicates the completion of a request. The aStatus parameter + * to onStateChange indicates the final status of the request. + */ + const unsigned long STATE_START = 0x00000001; + const unsigned long STATE_REDIRECTING = 0x00000002; + const unsigned long STATE_TRANSFERRING = 0x00000004; + const unsigned long STATE_NEGOTIATING = 0x00000008; + const unsigned long STATE_STOP = 0x00000010; + + + /** + * State Type Flags + * + * These flags further describe the entity for which the state transition is + * occuring. These flags are NOT mutually exclusive (i.e., an onStateChange + * event may indicate some combination of these flags). + * + * STATE_IS_REQUEST + * This flag indicates that the state transition is for a request, which + * includes but is not limited to document requests. (See below for a + * description of document requests.) Other types of requests, such as + * requests for inline content (e.g., images and stylesheets) are + * considered normal requests. + * + * STATE_IS_DOCUMENT + * This flag indicates that the state transition is for a document request. + * This flag is set in addition to STATE_IS_REQUEST. A document request + * supports the nsIChannel interface and its loadFlags attribute includes + * the nsIChannel::LOAD_DOCUMENT_URI flag. + * + * A document request does not complete until all requests associated with + * the loading of its corresponding document have completed. This includes + * other document requests (e.g., corresponding to HTML <iframe> elements). + * The document corresponding to a document request is available via the + * DOMWindow attribute of onStateChange's aWebProgress parameter. + * + * STATE_IS_NETWORK + * This flag indicates that the state transition corresponds to the start + * or stop of activity in the indicated nsIWebProgress instance. This flag + * is accompanied by either STATE_START or STATE_STOP, and it may be + * combined with other State Type Flags. + * + * Unlike STATE_IS_WINDOW, this flag is only set when activity within the + * nsIWebProgress instance being observed starts or stops. If activity + * only occurs in a child nsIWebProgress instance, then this flag will be + * set to indicate the start and stop of that activity. + * + * For example, in the case of navigation within a single frame of a HTML + * frameset, a nsIWebProgressListener instance attached to the + * nsIWebProgress of the frameset window will receive onStateChange calls + * with the STATE_IS_NETWORK flag set to indicate the start and stop of + * said navigation. In other words, an observer of an outer window can + * determine when activity, that may be constrained to a child window or + * set of child windows, starts and stops. + * + * STATE_IS_WINDOW + * This flag indicates that the state transition corresponds to the start + * or stop of activity in the indicated nsIWebProgress instance. This flag + * is accompanied by either STATE_START or STATE_STOP, and it may be + * combined with other State Type Flags. + * + * This flag is similar to STATE_IS_DOCUMENT. However, when a document + * request completes, two onStateChange calls with STATE_STOP are + * generated. The document request is passed as aRequest to both calls. + * The first has STATE_IS_REQUEST and STATE_IS_DOCUMENT set, and the second + * has the STATE_IS_WINDOW flag set (and possibly the STATE_IS_NETWORK flag + * set as well -- see above for a description of when the STATE_IS_NETWORK + * flag may be set). This second STATE_STOP event may be useful as a way + * to partition the work that occurs when a document request completes. + */ + const unsigned long STATE_IS_REQUEST = 0x00010000; + const unsigned long STATE_IS_DOCUMENT = 0x00020000; + const unsigned long STATE_IS_NETWORK = 0x00040000; + const unsigned long STATE_IS_WINDOW = 0x00080000; + + + /** + * State Modifier Flags + * + * These flags further describe the transition which is occuring. These + * flags are NOT mutually exclusive (i.e., an onStateChange event may + * indicate some combination of these flags). + * + * STATE_RESTORING + * This flag indicates that the state transition corresponds to the start + * or stop of activity for restoring a previously-rendered presentation. + * As such, there is no actual network activity associated with this + * request, and any modifications made to the document or presentation + * when it was originally loaded will still be present. + */ + const unsigned long STATE_RESTORING = 0x01000000; + + /** + * State Security Flags + * + * These flags describe the security state reported by a call to the + * onSecurityChange method. These flags are mutually exclusive. + * + * STATE_IS_INSECURE + * This flag indicates that the data corresponding to the request + * was received over an insecure channel. + * + * STATE_IS_BROKEN + * This flag indicates an unknown security state. This may mean that the + * request is being loaded as part of a page in which some content was + * received over an insecure channel. + * + * STATE_IS_SECURE + * This flag indicates that the data corresponding to the request was + * received over a secure channel. The degree of security is expressed by + * STATE_SECURE_HIGH, STATE_SECURE_MED, or STATE_SECURE_LOW. + */ + const unsigned long STATE_IS_INSECURE = 0x00000004; + const unsigned long STATE_IS_BROKEN = 0x00000001; + const unsigned long STATE_IS_SECURE = 0x00000002; + + /** + * Mixed active content flags + * + * May be set in addition to the State Security Flags, to indicate that + * mixed active content has been encountered. + * + * STATE_BLOCKED_MIXED_ACTIVE_CONTENT + * Mixed active content has been blocked from loading. + * + * STATE_LOADED_MIXED_ACTIVE_CONTENT + * Mixed active content has been loaded. State should be STATE_IS_BROKEN. + */ + const unsigned long STATE_BLOCKED_MIXED_ACTIVE_CONTENT = 0x00000010; + const unsigned long STATE_LOADED_MIXED_ACTIVE_CONTENT = 0x00000020; + + /** + * Mixed display content flags + * + * May be set in addition to the State Security Flags, to indicate that + * mixed display content has been encountered. + * + * STATE_BLOCKED_MIXED_DISPLAY_CONTENT + * Mixed display content has been blocked from loading. + * + * STATE_LOADED_MIXED_DISPLAY_CONTENT + * Mixed display content has been loaded. State should be STATE_IS_BROKEN. + */ + const unsigned long STATE_BLOCKED_MIXED_DISPLAY_CONTENT = 0x00000100; + const unsigned long STATE_LOADED_MIXED_DISPLAY_CONTENT = 0x00000200; + + /** + * Tracking content flags + * + * May be set in addition to the State security Flags, to indicate that + * tracking content has been encountered. + * + * STATE_BLOCKED_TRACKING_CONTENT + * Tracking content has been blocked from loading. + * + * STATE_LOADED_TRACKING_CONTENT + * Tracking content has been loaded. + */ + const unsigned long STATE_BLOCKED_TRACKING_CONTENT = 0x00001000; + const unsigned long STATE_LOADED_TRACKING_CONTENT = 0x00002000; + + /** + * Security Strength Flags + * + * These flags describe the security strength and accompany STATE_IS_SECURE + * in a call to the onSecurityChange method. These flags are mutually + * exclusive. + * + * These flags are not meant to provide a precise description of data + * transfer security. These are instead intended as a rough indicator that + * may be used to, for example, color code a security indicator or otherwise + * provide basic data transfer security feedback to the user. + * + * STATE_SECURE_HIGH + * This flag indicates a high degree of security. + * + * STATE_SECURE_MED + * This flag indicates a medium degree of security. + * + * STATE_SECURE_LOW + * This flag indicates a low degree of security. + */ + const unsigned long STATE_SECURE_HIGH = 0x00040000; + const unsigned long STATE_SECURE_MED = 0x00010000; + const unsigned long STATE_SECURE_LOW = 0x00020000; + + /** + * State bits for EV == Extended Validation == High Assurance + * + * These flags describe the level of identity verification + * in a call to the onSecurityChange method. + * + * STATE_IDENTITY_EV_TOPLEVEL + * The topmost document uses an EV cert. + * NOTE: Available since Gecko 1.9 + */ + + const unsigned long STATE_IDENTITY_EV_TOPLEVEL = 0x00100000; + + /** + * Broken state flags + * + * These flags describe the reason of the broken state. + * + * STATE_USES_SSL_3 + * The topmost document uses SSL 3.0. + * + * STATE_USES_WEAK_CRYPTO + * The topmost document uses a weak cipher suite such as RC4. + * + * STATE_CERT_USER_OVERRIDDEN + * The user has added a security exception for the site. + */ + const unsigned long STATE_USES_SSL_3 = 0x01000000; + const unsigned long STATE_USES_WEAK_CRYPTO = 0x02000000; + const unsigned long STATE_CERT_USER_OVERRIDDEN = 0x04000000; + + /** + * Notification indicating the state has changed for one of the requests + * associated with aWebProgress. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification + * @param aRequest + * The nsIRequest that has changed state. + * @param aStateFlags + * Flags indicating the new state. This value is a combination of one + * of the State Transition Flags and one or more of the State Type + * Flags defined above. Any undefined bits are reserved for future + * use. + * @param aStatus + * Error status code associated with the state change. This parameter + * should be ignored unless aStateFlags includes the STATE_STOP bit. + * The status code indicates success or failure of the request + * associated with the state change. NOTE: aStatus may be a success + * code even for server generated errors, such as the HTTP 404 error. + * In such cases, the request itself should be queried for extended + * error information (e.g., for HTTP requests see nsIHttpChannel). + */ + void onStateChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in unsigned long aStateFlags, + in nsresult aStatus); + + /** + * Notification that the progress has changed for one of the requests + * associated with aWebProgress. Progress totals are reset to zero when all + * requests in aWebProgress complete (corresponding to onStateChange being + * called with aStateFlags including the STATE_STOP and STATE_IS_WINDOW + * flags). + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new progress. + * @param aCurSelfProgress + * The current progress for aRequest. + * @param aMaxSelfProgress + * The maximum progress for aRequest. + * @param aCurTotalProgress + * The current progress for all requests associated with aWebProgress. + * @param aMaxTotalProgress + * The total progress for all requests associated with aWebProgress. + * + * NOTE: If any progress value is unknown, or if its value would exceed the + * maximum value of type long, then its value is replaced with -1. + * + * NOTE: If the object also implements nsIWebProgressListener2 and the caller + * knows about that interface, this function will not be called. Instead, + * nsIWebProgressListener2::onProgressChange64 will be called. + */ + void onProgressChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in long aCurSelfProgress, + in long aMaxSelfProgress, + in long aCurTotalProgress, + in long aMaxTotalProgress); + + /** + * Flags for onLocationChange + * + * LOCATION_CHANGE_SAME_DOCUMENT + * This flag is on when |aWebProgress| did not load a new document. + * For example, the location change is due to an anchor scroll or a + * pushState/popState/replaceState. + * + * LOCATION_CHANGE_ERROR_PAGE + * This flag is on when |aWebProgress| redirected from the requested + * contents to an internal page to show error status, such as + * <about:neterror>, <about:certerror> and so on. + * + * Generally speaking, |aURI| and |aRequest| are the original data. DOM + * |window.location.href| is also the original location, while + * |document.documentURI| is the redirected location. Sometimes |aURI| is + * <about:blank> and |aRequest| is null when the original data does not + + remain. + * + * |aWebProgress| does NOT set this flag when it did not try to load a new + * document. In this case, it should set LOCATION_CHANGE_SAME_DOCUMENT. + */ + const unsigned long LOCATION_CHANGE_SAME_DOCUMENT = 0x00000001; + const unsigned long LOCATION_CHANGE_ERROR_PAGE = 0x00000002; + + /** + * Called when the location of the window being watched changes. This is not + * when a load is requested, but rather once it is verified that the load is + * going to occur in the given window. For instance, a load that starts in a + * window might send progress and status messages for the new site, but it + * will not send the onLocationChange until we are sure that we are loading + * this new page here. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The associated nsIRequest. This may be null in some cases. + * @param aLocation + * The URI of the location that is being loaded. + * @param aFlags + * This is a value which explains the situation or the reason why + * the location has changed. + */ + void onLocationChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in nsIURI aLocation, + [optional] in unsigned long aFlags); + + /** + * Notification that the status of a request has changed. The status message + * is intended to be displayed to the user (e.g., in the status bar of the + * browser). + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new status. + * @param aStatus + * This value is not an error code. Instead, it is a numeric value + * that indicates the current status of the request. This interface + * does not define the set of possible status codes. NOTE: Some + * status values are defined by nsITransport and nsISocketTransport. + * @param aMessage + * Localized text corresponding to aStatus. + */ + void onStatusChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in nsresult aStatus, + in wstring aMessage); + + /** + * Notification called for security progress. This method will be called on + * security transitions (eg HTTP -> HTTPS, HTTPS -> HTTP, FOO -> HTTPS) and + * after document load completion. It might also be called if an error + * occurs during network loading. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new security state. + * @param aState + * A value composed of the Security State Flags and the Security + * Strength Flags listed above. Any undefined bits are reserved for + * future use. + * + * NOTE: These notifications will only occur if a security package is + * installed. + */ + void onSecurityChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in unsigned long aState); +}; diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl new file mode 100644 index 0000000000..87701f8d2c --- /dev/null +++ b/uriloader/base/nsIWebProgressListener2.idl @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIWebProgressListener.idl" + +/** + * An extended version of nsIWebProgressListener. + */ +[scriptable, uuid(dde39de0-e4e0-11da-8ad9-0800200c9a66)] +interface nsIWebProgressListener2 : nsIWebProgressListener { + /** + * Notification that the progress has changed for one of the requests + * associated with aWebProgress. Progress totals are reset to zero when all + * requests in aWebProgress complete (corresponding to onStateChange being + * called with aStateFlags including the STATE_STOP and STATE_IS_WINDOW + * flags). + * + * This function is identical to nsIWebProgressListener::onProgressChange, + * except that this function supports 64-bit values. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new progress. + * @param aCurSelfProgress + * The current progress for aRequest. + * @param aMaxSelfProgress + * The maximum progress for aRequest. + * @param aCurTotalProgress + * The current progress for all requests associated with aWebProgress. + * @param aMaxTotalProgress + * The total progress for all requests associated with aWebProgress. + * + * NOTE: If any progress value is unknown, then its value is replaced with -1. + * + * @see nsIWebProgressListener2::onProgressChange64 + */ + void onProgressChange64(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in long long aCurSelfProgress, + in long long aMaxSelfProgress, + in long long aCurTotalProgress, + in long long aMaxTotalProgress); + + /** + * Notification that a refresh or redirect has been requested in aWebProgress + * For example, via a <meta http-equiv="refresh"> or an HTTP Refresh: header + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRefreshURI + * The new URI that aWebProgress has requested redirecting to. + * @param aMillis + * The delay (in milliseconds) before refresh. + * @param aSameURI + * True if aWebProgress is requesting a refresh of the + * current URI. + * False if aWebProgress is requesting a redirection to + * a different URI. + * + * @return True if the refresh may proceed. + * False if the refresh should be aborted. + */ + boolean onRefreshAttempted(in nsIWebProgress aWebProgress, + in nsIURI aRefreshURI, + in long aMillis, + in boolean aSameURI); +}; diff --git a/uriloader/base/nsURILoader.cpp b/uriloader/base/nsURILoader.cpp new file mode 100644 index 0000000000..69475d68fd --- /dev/null +++ b/uriloader/base/nsURILoader.cpp @@ -0,0 +1,966 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsURILoader.h" +#include "nsAutoPtr.h" +#include "nsIURIContentListener.h" +#include "nsIContentHandler.h" +#include "nsILoadGroup.h" +#include "nsIDocumentLoader.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsIIOService.h" +#include "nsIServiceManager.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsIChannel.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIProgressEventSink.h" +#include "nsIInputStream.h" +#include "nsIStreamConverterService.h" +#include "nsWeakReference.h" +#include "nsIHttpChannel.h" +#include "nsIMultiPartChannel.h" +#include "netCore.h" +#include "nsCRT.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIThreadRetargetableStreamListener.h" + +#include "nsXPIDLString.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsReadableUtils.h" +#include "nsError.h" + +#include "nsICategoryManager.h" +#include "nsCExternalHandlerService.h" // contains contractids for the helper app service + +#include "nsIMIMEHeaderParam.h" +#include "nsNetCID.h" + +#include "nsMimeTypes.h" + +#include "nsDocLoader.h" +#include "mozilla/Attributes.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" + +mozilla::LazyLogModule nsURILoader::mLog("URILoader"); + +#define LOG(args) MOZ_LOG(nsURILoader::mLog, mozilla::LogLevel::Debug, args) +#define LOG_ERROR(args) MOZ_LOG(nsURILoader::mLog, mozilla::LogLevel::Error, args) +#define LOG_ENABLED() MOZ_LOG_TEST(nsURILoader::mLog, mozilla::LogLevel::Debug) + +#define NS_PREF_DISABLE_BACKGROUND_HANDLING \ + "security.exthelperapp.disable_background_handling" + +/** + * The nsDocumentOpenInfo contains the state required when a single + * document is being opened in order to discover the content type... + * Each instance remains alive until its target URL has been loaded + * (or aborted). + */ +class nsDocumentOpenInfo final : public nsIStreamListener + , public nsIThreadRetargetableStreamListener +{ +public: + // Needed for nsCOMPtr to work right... Don't call this! + nsDocumentOpenInfo(); + + // Real constructor + // aFlags is a combination of the flags on nsIURILoader + nsDocumentOpenInfo(nsIInterfaceRequestor* aWindowContext, + uint32_t aFlags, + nsURILoader* aURILoader); + + NS_DECL_THREADSAFE_ISUPPORTS + + /** + * Prepares this object for receiving data. The stream + * listener methods of this class must not be called before calling this + * method. + */ + nsresult Prepare(); + + // Call this (from OnStartRequest) to attempt to find an nsIStreamListener to + // take the data off our hands. + nsresult DispatchContent(nsIRequest *request, nsISupports * aCtxt); + + // Call this if we need to insert a stream converter from aSrcContentType to + // aOutContentType into the StreamListener chain. DO NOT call it if the two + // types are the same, since no conversion is needed in that case. + nsresult ConvertData(nsIRequest *request, + nsIURIContentListener *aListener, + const nsACString & aSrcContentType, + const nsACString & aOutContentType); + + /** + * Function to attempt to use aListener to handle the load. If + * true is returned, nothing else needs to be done; if false + * is returned, then a different way of handling the load should be + * tried. + */ + bool TryContentListener(nsIURIContentListener* aListener, + nsIChannel* aChannel); + + // nsIRequestObserver methods: + NS_DECL_NSIREQUESTOBSERVER + + // nsIStreamListener methods: + NS_DECL_NSISTREAMLISTENER + + // nsIThreadRetargetableStreamListener + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER +protected: + ~nsDocumentOpenInfo(); + +protected: + /** + * The first content listener to try dispatching data to. Typically + * the listener associated with the entity that originated the load. + */ + nsCOMPtr<nsIURIContentListener> m_contentListener; + + /** + * The stream listener to forward nsIStreamListener notifications + * to. This is set once the load is dispatched. + */ + nsCOMPtr<nsIStreamListener> m_targetStreamListener; + + /** + * A pointer to the entity that originated the load. We depend on getting + * things like nsIURIContentListeners, nsIDOMWindows, etc off of it. + */ + nsCOMPtr<nsIInterfaceRequestor> m_originalContext; + + /** + * IS_CONTENT_PREFERRED is used for the boolean to pass to CanHandleContent + * (also determines whether we use CanHandleContent or IsPreferred). + * DONT_RETARGET means that we will only try m_originalContext, no other + * listeners. + */ + uint32_t mFlags; + + /** + * The type of the data we will be trying to dispatch. + */ + nsCString mContentType; + + /** + * Reference to the URILoader service so we can access its list of + * nsIURIContentListeners. + */ + RefPtr<nsURILoader> mURILoader; +}; + +NS_IMPL_ADDREF(nsDocumentOpenInfo) +NS_IMPL_RELEASE(nsDocumentOpenInfo) + +NS_INTERFACE_MAP_BEGIN(nsDocumentOpenInfo) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) +NS_INTERFACE_MAP_END_THREADSAFE + +nsDocumentOpenInfo::nsDocumentOpenInfo() +{ + NS_NOTREACHED("This should never be called\n"); +} + +nsDocumentOpenInfo::nsDocumentOpenInfo(nsIInterfaceRequestor* aWindowContext, + uint32_t aFlags, + nsURILoader* aURILoader) + : m_originalContext(aWindowContext), + mFlags(aFlags), + mURILoader(aURILoader) +{ +} + +nsDocumentOpenInfo::~nsDocumentOpenInfo() +{ +} + +nsresult nsDocumentOpenInfo::Prepare() +{ + LOG(("[0x%p] nsDocumentOpenInfo::Prepare", this)); + + nsresult rv; + + // ask our window context if it has a uri content listener... + m_contentListener = do_GetInterface(m_originalContext, &rv); + return rv; +} + +NS_IMETHODIMP nsDocumentOpenInfo::OnStartRequest(nsIRequest *request, nsISupports * aCtxt) +{ + LOG(("[0x%p] nsDocumentOpenInfo::OnStartRequest", this)); + MOZ_ASSERT(request); + if (!request) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = NS_OK; + + // + // Deal with "special" HTTP responses: + // + // - In the case of a 204 (No Content) or 205 (Reset Content) response, do + // not try to find a content handler. Return NS_BINDING_ABORTED to cancel + // the request. This has the effect of ensuring that the DocLoader does + // not try to interpret this as a real request. + // + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request, &rv)); + + if (NS_SUCCEEDED(rv)) { + uint32_t responseCode = 0; + + rv = httpChannel->GetResponseStatus(&responseCode); + + if (NS_FAILED(rv)) { + LOG_ERROR((" Failed to get HTTP response status")); + + // behave as in the canceled case + return NS_OK; + } + + LOG((" HTTP response status: %d", responseCode)); + + if (204 == responseCode || 205 == responseCode) { + return NS_BINDING_ABORTED; + } + + static bool sLargeAllocationHeaderEnabled = false; + static bool sCachedLargeAllocationPref = false; + if (!sCachedLargeAllocationPref) { + sCachedLargeAllocationPref = true; + mozilla::Preferences::AddBoolVarCache(&sLargeAllocationHeaderEnabled, + "dom.largeAllocationHeader.enabled"); + } + + if (sLargeAllocationHeaderEnabled) { + // If we have a Large-Allocation header, let's check if we should perform a process switch. + nsAutoCString largeAllocationHeader; + rv = httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("Large-Allocation"), largeAllocationHeader); + if (NS_SUCCEEDED(rv) && nsContentUtils::AttemptLargeAllocationLoad(httpChannel)) { + return NS_BINDING_ABORTED; + } + } + } + + // + // Make sure that the transaction has succeeded, so far... + // + nsresult status; + + rv = request->GetStatus(&status); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Unable to get request status!"); + if (NS_FAILED(rv)) return rv; + + if (NS_FAILED(status)) { + LOG_ERROR((" Request failed, status: 0x%08X", rv)); + + // + // The transaction has already reported an error - so it will be torn + // down. Therefore, it is not necessary to return an error code... + // + return NS_OK; + } + + rv = DispatchContent(request, aCtxt); + + LOG((" After dispatch, m_targetStreamListener: 0x%p, rv: 0x%08X", m_targetStreamListener.get(), rv)); + + NS_ASSERTION(NS_SUCCEEDED(rv) || !m_targetStreamListener, + "Must not have an m_targetStreamListener with a failure return!"); + + NS_ENSURE_SUCCESS(rv, rv); + + if (m_targetStreamListener) + rv = m_targetStreamListener->OnStartRequest(request, aCtxt); + + LOG((" OnStartRequest returning: 0x%08X", rv)); + + return rv; +} + +NS_IMETHODIMP +nsDocumentOpenInfo::CheckListenerChain() +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on the main thread!"); + nsresult rv = NS_OK; + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener = + do_QueryInterface(m_targetStreamListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + LOG(("[0x%p] nsDocumentOpenInfo::CheckListenerChain %s listener %p rv %x", + this, (NS_SUCCEEDED(rv) ? "success" : "failure"), + (nsIStreamListener*)m_targetStreamListener, rv)); + return rv; +} + +NS_IMETHODIMP +nsDocumentOpenInfo::OnDataAvailable(nsIRequest *request, nsISupports * aCtxt, + nsIInputStream * inStr, + uint64_t sourceOffset, uint32_t count) +{ + // if we have retarged to the end stream listener, then forward the call.... + // otherwise, don't do anything + + nsresult rv = NS_OK; + + if (m_targetStreamListener) + rv = m_targetStreamListener->OnDataAvailable(request, aCtxt, inStr, sourceOffset, count); + return rv; +} + +NS_IMETHODIMP nsDocumentOpenInfo::OnStopRequest(nsIRequest *request, nsISupports *aCtxt, + nsresult aStatus) +{ + LOG(("[0x%p] nsDocumentOpenInfo::OnStopRequest", this)); + + if ( m_targetStreamListener) + { + nsCOMPtr<nsIStreamListener> listener(m_targetStreamListener); + + // If this is a multipart stream, we could get another + // OnStartRequest after this... reset state. + m_targetStreamListener = nullptr; + mContentType.Truncate(); + listener->OnStopRequest(request, aCtxt, aStatus); + } + + // Remember... + // In the case of multiplexed streams (such as multipart/x-mixed-replace) + // these stream listener methods could be called again :-) + // + return NS_OK; +} + +nsresult nsDocumentOpenInfo::DispatchContent(nsIRequest *request, nsISupports * aCtxt) +{ + LOG(("[0x%p] nsDocumentOpenInfo::DispatchContent for type '%s'", this, mContentType.get())); + + NS_PRECONDITION(!m_targetStreamListener, + "Why do we already have a target stream listener?"); + + nsresult rv; + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + if (!aChannel) { + LOG_ERROR((" Request is not a channel. Bailing.")); + return NS_ERROR_FAILURE; + } + + NS_NAMED_LITERAL_CSTRING(anyType, "*/*"); + if (mContentType.IsEmpty() || mContentType == anyType) { + rv = aChannel->GetContentType(mContentType); + if (NS_FAILED(rv)) return rv; + LOG((" Got type from channel: '%s'", mContentType.get())); + } + + bool isGuessFromExt = + mContentType.LowerCaseEqualsASCII(APPLICATION_GUESS_FROM_EXT); + if (isGuessFromExt) { + // Reset to application/octet-stream for now; no one other than the + // external helper app service should see APPLICATION_GUESS_FROM_EXT. + mContentType = APPLICATION_OCTET_STREAM; + aChannel->SetContentType(NS_LITERAL_CSTRING(APPLICATION_OCTET_STREAM)); + } + + // Check whether the data should be forced to be handled externally. This + // could happen because the Content-Disposition header is set so, or, in the + // future, because the user has specified external handling for the MIME + // type. + bool forceExternalHandling = false; + uint32_t disposition; + rv = aChannel->GetContentDisposition(&disposition); + + bool allowContentDispositionToForceExternalHandling = true; + +#ifdef MOZ_B2G + + // On B2G, OMA content files should never be handled by an external handler + // (even if the server specifies Content-Disposition: attachment) because the + // data should never be stored on an unencrypted form. + allowContentDispositionToForceExternalHandling = + !mContentType.LowerCaseEqualsASCII("application/vnd.oma.drm.message"); + +#endif + + if (NS_SUCCEEDED(rv) && (disposition == nsIChannel::DISPOSITION_ATTACHMENT) && + allowContentDispositionToForceExternalHandling) { + forceExternalHandling = true; + } + + LOG((" forceExternalHandling: %s", forceExternalHandling ? "yes" : "no")); + + // The type or data the contentListener wants. + nsXPIDLCString desiredContentType; + + if (!forceExternalHandling) + { + // + // First step: See whether m_contentListener wants to handle this + // content type. + // + if (m_contentListener && TryContentListener(m_contentListener, aChannel)) { + LOG((" Success! Our default listener likes this type")); + // All done here + return NS_OK; + } + + // If we aren't allowed to try other listeners, just skip through to + // trying to convert the data. + if (!(mFlags & nsIURILoader::DONT_RETARGET)) { + + // + // Second step: See whether some other registered listener wants + // to handle this content type. + // + int32_t count = mURILoader->m_listeners.Count(); + nsCOMPtr<nsIURIContentListener> listener; + for (int32_t i = 0; i < count; i++) { + listener = do_QueryReferent(mURILoader->m_listeners[i]); + if (listener) { + if (TryContentListener(listener, aChannel)) { + LOG((" Found listener registered on the URILoader")); + return NS_OK; + } + } else { + // remove from the listener list, reset i and update count + mURILoader->m_listeners.RemoveObjectAt(i--); + --count; + } + } + + // + // Third step: Try to find a content listener that has not yet had + // the chance to register, as it is contained in a not-yet-loaded + // module, but which has registered a contract ID. + // + nsCOMPtr<nsICategoryManager> catman = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID); + if (catman) { + nsXPIDLCString contractidString; + rv = catman->GetCategoryEntry(NS_CONTENT_LISTENER_CATEGORYMANAGER_ENTRY, + mContentType.get(), + getter_Copies(contractidString)); + if (NS_SUCCEEDED(rv) && !contractidString.IsEmpty()) { + LOG((" Listener contractid for '%s' is '%s'", + mContentType.get(), contractidString.get())); + + listener = do_CreateInstance(contractidString); + LOG((" Listener from category manager: 0x%p", listener.get())); + + if (listener && TryContentListener(listener, aChannel)) { + LOG((" Listener from category manager likes this type")); + return NS_OK; + } + } + } + + // + // Fourth step: try to find an nsIContentHandler for our type. + // + nsAutoCString handlerContractID (NS_CONTENT_HANDLER_CONTRACTID_PREFIX); + handlerContractID += mContentType; + + nsCOMPtr<nsIContentHandler> contentHandler = + do_CreateInstance(handlerContractID.get()); + if (contentHandler) { + LOG((" Content handler found")); + rv = contentHandler->HandleContent(mContentType.get(), + m_originalContext, request); + // XXXbz returning an error code to represent handling the + // content is just bizarre! + if (rv != NS_ERROR_WONT_HANDLE_CONTENT) { + if (NS_FAILED(rv)) { + // The content handler has unexpectedly failed. Cancel the request + // just in case the handler didn't... + LOG((" Content handler failed. Aborting load")); + request->Cancel(rv); + } + else { + LOG((" Content handler taking over load")); + } + + return rv; + } + } + } else { + LOG((" DONT_RETARGET flag set, so skipped over random other content " + "listeners and content handlers")); + } + + // + // Fifth step: If no listener prefers this type, see if any stream + // converters exist to transform this content type into + // some other. + // + // Don't do this if the server sent us a MIME type of "*/*" because they saw + // it in our Accept header and got confused. + // XXXbz have to be careful here; may end up in some sort of bizarre infinite + // decoding loop. + if (mContentType != anyType) { + rv = ConvertData(request, m_contentListener, mContentType, anyType); + if (NS_FAILED(rv)) { + m_targetStreamListener = nullptr; + } else if (m_targetStreamListener) { + // We found a converter for this MIME type. We'll just pump data into it + // and let the downstream nsDocumentOpenInfo handle things. + LOG((" Converter taking over now")); + return NS_OK; + } + } + } + + NS_ASSERTION(!m_targetStreamListener, + "If we found a listener, why are we not using it?"); + + if (mFlags & nsIURILoader::DONT_RETARGET) { + LOG((" External handling forced or (listener not interested and no " + "stream converter exists), and retargeting disallowed -> aborting")); + return NS_ERROR_WONT_HANDLE_CONTENT; + } + + // Before dispatching to the external helper app service, check for an HTTP + // error page. If we got one, we don't want to handle it with a helper app, + // really. + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request)); + if (httpChannel) { + bool requestSucceeded; + httpChannel->GetRequestSucceeded(&requestSucceeded); + if (!requestSucceeded) { + // returning error from OnStartRequest will cancel the channel + return NS_ERROR_FILE_NOT_FOUND; + } + } + + // Sixth step: + // + // All attempts to dispatch this content have failed. Just pass it off to + // the helper app service. + // + + // + // Optionally, we may want to disable background handling by the external + // helper application service. + // + if (mozilla::Preferences::GetBool(NS_PREF_DISABLE_BACKGROUND_HANDLING, + false)) { + // First, we will ensure that the parent docshell is in an active + // state as we will disallow all external application handling unless it is + // in the foreground. + nsCOMPtr<nsIDocShell> docShell(do_GetInterface(m_originalContext)); + if (!docShell) { + // If we can't perform our security check we definitely don't want to go + // any further! + LOG(("Failed to get DocShell to ensure it is active before anding off to " + "helper app service. Aborting.")); + return NS_ERROR_FAILURE; + } + + // Ensure the DocShell is active before continuing. + bool isActive = false; + docShell->GetIsActive(&isActive); + if (!isActive) { + LOG((" Check for active DocShell returned false. Aborting hand off to " + "helper app service.")); + return NS_ERROR_DOM_SECURITY_ERR; + } + } + + nsCOMPtr<nsIExternalHelperAppService> helperAppService = + do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &rv); + if (helperAppService) { + LOG((" Passing load off to helper app service")); + + // Set these flags to indicate that the channel has been targeted and that + // we are not using the original consumer. + nsLoadFlags loadFlags = 0; + request->GetLoadFlags(&loadFlags); + request->SetLoadFlags(loadFlags | nsIChannel::LOAD_RETARGETED_DOCUMENT_URI + | nsIChannel::LOAD_TARGETED); + + if (isGuessFromExt) { + mContentType = APPLICATION_GUESS_FROM_EXT; + aChannel->SetContentType(NS_LITERAL_CSTRING(APPLICATION_GUESS_FROM_EXT)); + } + + rv = helperAppService->DoContent(mContentType, + request, + m_originalContext, + false, + nullptr, + getter_AddRefs(m_targetStreamListener)); + if (NS_FAILED(rv)) { + request->SetLoadFlags(loadFlags); + m_targetStreamListener = nullptr; + } + } + + NS_ASSERTION(m_targetStreamListener || NS_FAILED(rv), + "There is no way we should be successful at this point without a m_targetStreamListener"); + return rv; +} + +nsresult +nsDocumentOpenInfo::ConvertData(nsIRequest *request, + nsIURIContentListener* aListener, + const nsACString& aSrcContentType, + const nsACString& aOutContentType) +{ + LOG(("[0x%p] nsDocumentOpenInfo::ConvertData from '%s' to '%s'", this, + PromiseFlatCString(aSrcContentType).get(), + PromiseFlatCString(aOutContentType).get())); + + NS_PRECONDITION(aSrcContentType != aOutContentType, + "ConvertData called when the two types are the same!"); + nsresult rv = NS_OK; + + nsCOMPtr<nsIStreamConverterService> StreamConvService = + do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + LOG((" Got converter service")); + + // When applying stream decoders, it is necessary to "insert" an + // intermediate nsDocumentOpenInfo instance to handle the targeting of + // the "final" stream or streams. + // + // For certain content types (ie. multi-part/x-mixed-replace) the input + // stream is split up into multiple destination streams. This + // intermediate instance is used to target these "decoded" streams... + // + RefPtr<nsDocumentOpenInfo> nextLink = + new nsDocumentOpenInfo(m_originalContext, mFlags, mURILoader); + + LOG((" Downstream DocumentOpenInfo would be: 0x%p", nextLink.get())); + + // Make sure nextLink starts with the contentListener that said it wanted the + // results of this decode. + nextLink->m_contentListener = aListener; + // Also make sure it has to look for a stream listener to pump data into. + nextLink->m_targetStreamListener = nullptr; + + // Make sure that nextLink treats the data as aOutContentType when + // dispatching; that way even if the stream converters don't change the type + // on the channel we will still do the right thing. If aOutContentType is + // */*, that's OK -- that will just indicate to nextLink that it should get + // the type off the channel. + nextLink->mContentType = aOutContentType; + + // The following call sets m_targetStreamListener to the input end of the + // stream converter and sets the output end of the stream converter to + // nextLink. As we pump data into m_targetStreamListener the stream + // converter will convert it and pass the converted data to nextLink. + return StreamConvService->AsyncConvertData(PromiseFlatCString(aSrcContentType).get(), + PromiseFlatCString(aOutContentType).get(), + nextLink, + request, + getter_AddRefs(m_targetStreamListener)); +} + +bool +nsDocumentOpenInfo::TryContentListener(nsIURIContentListener* aListener, + nsIChannel* aChannel) +{ + LOG(("[0x%p] nsDocumentOpenInfo::TryContentListener; mFlags = 0x%x", + this, mFlags)); + + NS_PRECONDITION(aListener, "Must have a non-null listener"); + NS_PRECONDITION(aChannel, "Must have a channel"); + + bool listenerWantsContent = false; + nsXPIDLCString typeToUse; + + if (mFlags & nsIURILoader::IS_CONTENT_PREFERRED) { + aListener->IsPreferred(mContentType.get(), + getter_Copies(typeToUse), + &listenerWantsContent); + } else { + aListener->CanHandleContent(mContentType.get(), false, + getter_Copies(typeToUse), + &listenerWantsContent); + } + if (!listenerWantsContent) { + LOG((" Listener is not interested")); + return false; + } + + if (!typeToUse.IsEmpty() && typeToUse != mContentType) { + // Need to do a conversion here. + + nsresult rv = ConvertData(aChannel, aListener, mContentType, typeToUse); + + if (NS_FAILED(rv)) { + // No conversion path -- we don't want this listener, if we got one + m_targetStreamListener = nullptr; + } + + LOG((" Found conversion: %s", m_targetStreamListener ? "yes" : "no")); + + // m_targetStreamListener is now the input end of the converter, and we can + // just pump the data in there, if it exists. If it does not, we need to + // try other nsIURIContentListeners. + return m_targetStreamListener != nullptr; + } + + // At this point, aListener wants data of type mContentType. Let 'em have + // it. But first, if we are retargeting, set an appropriate flag on the + // channel + nsLoadFlags loadFlags = 0; + aChannel->GetLoadFlags(&loadFlags); + + // Set this flag to indicate that the channel has been targeted at a final + // consumer. This load flag is tested in nsDocLoader::OnProgress. + nsLoadFlags newLoadFlags = nsIChannel::LOAD_TARGETED; + + nsCOMPtr<nsIURIContentListener> originalListener = + do_GetInterface(m_originalContext); + if (originalListener != aListener) { + newLoadFlags |= nsIChannel::LOAD_RETARGETED_DOCUMENT_URI; + } + aChannel->SetLoadFlags(loadFlags | newLoadFlags); + + bool abort = false; + bool isPreferred = (mFlags & nsIURILoader::IS_CONTENT_PREFERRED) != 0; + nsresult rv = aListener->DoContent(mContentType, + isPreferred, + aChannel, + getter_AddRefs(m_targetStreamListener), + &abort); + + if (NS_FAILED(rv)) { + LOG_ERROR((" DoContent failed")); + + // Unset the RETARGETED_DOCUMENT_URI flag if we set it... + aChannel->SetLoadFlags(loadFlags); + m_targetStreamListener = nullptr; + return false; + } + + if (abort) { + // Nothing else to do here -- aListener is handling it all. Make + // sure m_targetStreamListener is null so we don't do anything + // after this point. + LOG((" Listener has aborted the load")); + m_targetStreamListener = nullptr; + } + + NS_ASSERTION(abort || m_targetStreamListener, "DoContent returned no listener?"); + + // aListener is handling the load from this point on. + return true; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////// +// Implementation of nsURILoader +/////////////////////////////////////////////////////////////////////////////////////////////// + +nsURILoader::nsURILoader() +{ +} + +nsURILoader::~nsURILoader() +{ +} + +NS_IMPL_ADDREF(nsURILoader) +NS_IMPL_RELEASE(nsURILoader) + +NS_INTERFACE_MAP_BEGIN(nsURILoader) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURILoader) + NS_INTERFACE_MAP_ENTRY(nsIURILoader) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP nsURILoader::RegisterContentListener(nsIURIContentListener * aContentListener) +{ + nsresult rv = NS_OK; + + nsWeakPtr weakListener = do_GetWeakReference(aContentListener); + NS_ASSERTION(weakListener, "your URIContentListener must support weak refs!\n"); + + if (weakListener) + m_listeners.AppendObject(weakListener); + + return rv; +} + +NS_IMETHODIMP nsURILoader::UnRegisterContentListener(nsIURIContentListener * aContentListener) +{ + nsWeakPtr weakListener = do_GetWeakReference(aContentListener); + if (weakListener) + m_listeners.RemoveObject(weakListener); + + return NS_OK; + +} + +NS_IMETHODIMP nsURILoader::OpenURI(nsIChannel *channel, + uint32_t aFlags, + nsIInterfaceRequestor *aWindowContext) +{ + NS_ENSURE_ARG_POINTER(channel); + + if (LOG_ENABLED()) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + nsAutoCString spec; + uri->GetAsciiSpec(spec); + LOG(("nsURILoader::OpenURI for %s", spec.get())); + } + + nsCOMPtr<nsIStreamListener> loader; + nsresult rv = OpenChannel(channel, + aFlags, + aWindowContext, + false, + getter_AddRefs(loader)); + + if (NS_SUCCEEDED(rv)) { + // this method is not complete!!! Eventually, we should first go + // to the content listener and ask them for a protocol handler... + // if they don't give us one, we need to go to the registry and get + // the preferred protocol handler. + + // But for now, I'm going to let necko do the work for us.... + rv = channel->AsyncOpen(loader, nullptr); + + // no content from this load - that's OK. + if (rv == NS_ERROR_NO_CONTENT) { + LOG((" rv is NS_ERROR_NO_CONTENT -- doing nothing")); + rv = NS_OK; + } + } else if (rv == NS_ERROR_WONT_HANDLE_CONTENT) { + // Not really an error, from this method's point of view + rv = NS_OK; + } + return rv; +} + +nsresult nsURILoader::OpenChannel(nsIChannel* channel, + uint32_t aFlags, + nsIInterfaceRequestor* aWindowContext, + bool aChannelIsOpen, + nsIStreamListener** aListener) +{ + NS_ASSERTION(channel, "Trying to open a null channel!"); + NS_ASSERTION(aWindowContext, "Window context must not be null"); + + if (LOG_ENABLED()) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + nsAutoCString spec; + uri->GetAsciiSpec(spec); + LOG(("nsURILoader::OpenChannel for %s", spec.get())); + } + + // Let the window context's uriListener know that the open is starting. This + // gives that window a chance to abort the load process. + nsCOMPtr<nsIURIContentListener> winContextListener(do_GetInterface(aWindowContext)); + if (winContextListener) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + if (uri) { + bool doAbort = false; + winContextListener->OnStartURIOpen(uri, &doAbort); + + if (doAbort) { + LOG((" OnStartURIOpen aborted load")); + return NS_ERROR_WONT_HANDLE_CONTENT; + } + } + } + + // we need to create a DocumentOpenInfo object which will go ahead and open + // the url and discover the content type.... + RefPtr<nsDocumentOpenInfo> loader = + new nsDocumentOpenInfo(aWindowContext, aFlags, this); + + // Set the correct loadgroup on the channel + nsCOMPtr<nsILoadGroup> loadGroup(do_GetInterface(aWindowContext)); + + if (!loadGroup) { + // XXXbz This context is violating what we'd like to be the new uriloader + // api.... Set up a nsDocLoader to handle the loadgroup for this context. + // This really needs to go away! + nsCOMPtr<nsIURIContentListener> listener(do_GetInterface(aWindowContext)); + if (listener) { + nsCOMPtr<nsISupports> cookie; + listener->GetLoadCookie(getter_AddRefs(cookie)); + if (!cookie) { + RefPtr<nsDocLoader> newDocLoader = new nsDocLoader(); + nsresult rv = newDocLoader->Init(); + if (NS_FAILED(rv)) + return rv; + rv = nsDocLoader::AddDocLoaderAsChildOfRoot(newDocLoader); + if (NS_FAILED(rv)) + return rv; + cookie = nsDocLoader::GetAsSupports(newDocLoader); + listener->SetLoadCookie(cookie); + } + loadGroup = do_GetInterface(cookie); + } + } + + // If the channel is pending, then we need to remove it from its current + // loadgroup + nsCOMPtr<nsILoadGroup> oldGroup; + channel->GetLoadGroup(getter_AddRefs(oldGroup)); + if (aChannelIsOpen && !SameCOMIdentity(oldGroup, loadGroup)) { + // It is important to add the channel to the new group before + // removing it from the old one, so that the load isn't considered + // done as soon as the request is removed. + loadGroup->AddRequest(channel, nullptr); + + if (oldGroup) { + oldGroup->RemoveRequest(channel, nullptr, NS_BINDING_RETARGETED); + } + } + + channel->SetLoadGroup(loadGroup); + + // prepare the loader for receiving data + nsresult rv = loader->Prepare(); + if (NS_SUCCEEDED(rv)) + NS_ADDREF(*aListener = loader); + return rv; +} + +NS_IMETHODIMP nsURILoader::OpenChannel(nsIChannel* channel, + uint32_t aFlags, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aListener) +{ + bool pending; + if (NS_FAILED(channel->IsPending(&pending))) { + pending = false; + } + + return OpenChannel(channel, aFlags, aWindowContext, pending, aListener); +} + +NS_IMETHODIMP nsURILoader::Stop(nsISupports* aLoadCookie) +{ + nsresult rv; + nsCOMPtr<nsIDocumentLoader> docLoader; + + NS_ENSURE_ARG_POINTER(aLoadCookie); + + docLoader = do_GetInterface(aLoadCookie, &rv); + if (docLoader) { + rv = docLoader->Stop(); + } + return rv; +} + diff --git a/uriloader/base/nsURILoader.h b/uriloader/base/nsURILoader.h new file mode 100644 index 0000000000..2c5648dbae --- /dev/null +++ b/uriloader/base/nsURILoader.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsURILoader_h__ +#define nsURILoader_h__ + +#include "nsCURILoader.h" +#include "nsISupportsUtils.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsString.h" +#include "nsIWeakReference.h" +#include "mozilla/Attributes.h" + +#include "mozilla/Logging.h" + +class nsDocumentOpenInfo; + +class nsURILoader final : public nsIURILoader +{ +public: + NS_DECL_NSIURILOADER + NS_DECL_ISUPPORTS + + nsURILoader(); + +protected: + ~nsURILoader(); + + /** + * Equivalent to nsIURILoader::openChannel, but allows specifying whether the + * channel is opened already. + */ + MOZ_MUST_USE nsresult OpenChannel(nsIChannel* channel, + uint32_t aFlags, + nsIInterfaceRequestor* aWindowContext, + bool aChannelOpen, + nsIStreamListener** aListener); + + /** + * we shouldn't need to have an owning ref count on registered + * content listeners because they are supposed to unregister themselves + * when they go away. This array stores weak references + */ + nsCOMArray<nsIWeakReference> m_listeners; + + /** + * Logging. The module is called "URILoader" + */ + static mozilla::LazyLogModule mLog; + + friend class nsDocumentOpenInfo; +}; + +#endif /* nsURILoader_h__ */ diff --git a/uriloader/exthandler/ContentHandlerService.cpp b/uriloader/exthandler/ContentHandlerService.cpp new file mode 100644 index 0000000000..75575a7304 --- /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 0000000000..e39e89bc90 --- /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 0000000000..584e596b9f --- /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 0000000000..eecca01e61 --- /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 0000000000..a8ddc54c84 --- /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 0000000000..752da996d6 --- /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 0000000000..6ef6833a84 --- /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 0000000000..3f9de04a8d --- /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 0000000000..37f5860187 --- /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 0000000000..c7d526c19d --- /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 0000000000..bcaab60ac3 --- /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 0000000000..4c7ffff484 --- /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 0000000000..bb38724638 --- /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 0000000000..f4f8a7013c --- /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 0000000000..a1e2e43637 --- /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 0000000000..f417b5b9f9 --- /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 0000000000..f2618c7e6f --- /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 0000000000..ee9bc8570d --- /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 0000000000..569d715bdf --- /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 0000000000..3a170dcf6d --- /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 0000000000..4f3623894f --- /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 0000000000..d1342ec181 --- /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 0000000000..99a280bfcc --- /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 0000000000..1a428a62db --- /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 0000000000..cea2d701ec --- /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 0000000000..402ec52953 --- /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 0000000000..fd633ab726 --- /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 0000000000..298357f757 --- /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 0000000000..64d3a82dea --- /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 0000000000..7371e1f42d --- /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 0000000000..00f12f5448 --- /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 0000000000..6a3ca08af6 --- /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 0000000000..6aa25a01db --- /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 0000000000..ce3f7b90f5 --- /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 0000000000..8ace9a2cd4 --- /dev/null +++ b/uriloader/exthandler/nsContentHandlerApp.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsContentHandlerAppImpl_h__ +#define __nsContentHandlerAppImpl_h__ + +#include <contentaction/contentaction.h> +#include "nsString.h" +#include "nsIMIMEInfo.h" + +class nsContentHandlerApp : public nsIHandlerApp +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + + nsContentHandlerApp(nsString aName, nsCString aType, ContentAction::Action& aAction); + virtual ~nsContentHandlerApp() { } + +protected: + nsString mName; + nsCString mType; + nsString mDetailedDescription; + + ContentAction::Action mAction; +}; +#endif diff --git a/uriloader/exthandler/nsDBusHandlerApp.cpp b/uriloader/exthandler/nsDBusHandlerApp.cpp new file mode 100644 index 0000000000..bed2ecd989 --- /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 0000000000..a2835d98f2 --- /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 0000000000..51a7ee0f6a --- /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 0000000000..ceec66661d --- /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 0000000000..5b3d07b4ce --- /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 0000000000..426c98da18 --- /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 0000000000..c932f9f5dc --- /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 0000000000..5ed86c7987 --- /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 0000000000..7c186deee1 --- /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 0000000000..bfdfff5cea --- /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 0000000000..44d7560306 --- /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 0000000000..f58f6981dd --- /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 0000000000..3573497591 --- /dev/null +++ b/uriloader/exthandler/nsIExternalURLHandlerService.idl @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIMIMEInfo.idl" + +/** + * The external URL handler service is used for finding + * platform-specific applications for handling particular URLs. + */ + +[scriptable, uuid(56c5c7d3-6fd3-43f8-9429-4397e111453a)] +interface nsIExternalURLHandlerService : nsISupports +{ + /** + * Given a URL, looks up the handler info from the OS. This should be + * overridden by each OS's implementation. + * + * @param aURL The URL we are looking for. + * @param aFound Was an OS default handler for this URL found? + * @return An nsIHanderInfo for the protocol. + */ + nsIHandlerInfo getURLHandlerInfoFromOS(in nsIURI aURL, + out boolean aFound); + +}; diff --git a/uriloader/exthandler/nsIHandlerService.idl b/uriloader/exthandler/nsIHandlerService.idl new file mode 100644 index 0000000000..efea43937d --- /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 0000000000..f8190e744b --- /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 0000000000..f1b65dca21 --- /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 0000000000..30b410ce32 --- /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 0000000000..59886e4651 --- /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 0000000000..34f2442423 --- /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 0000000000..65d600f551 --- /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 0000000000..02b1d02af3 --- /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 0000000000..13250c8ab9 --- /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 0000000000..2efbed9065 --- /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 0000000000..9647dcf8cd --- /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 0000000000..40f7ef71e2 --- /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 0000000000..996e5ad699 --- /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 0000000000..ac3e662588 --- /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 0000000000..83ba0d1a54 --- /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 0000000000..597f9442d9 --- /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 0000000000..dad0493f81 --- /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 0000000000..ae191dc7b3 --- /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 0000000000..5a929ba994 --- /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 0000000000..e5d73a2320 --- /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 0000000000..fafe0b4c56 --- /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 0000000000..48301be5b4 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) + return; + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader("Content-Type", "application/octet-stream; name=\"" + name + "\""); + response.setHeader("Content-Disposition", "inline; filename=\"" + name + "\""); +} diff --git a/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js b/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js new file mode 100644 index 0000000000..16c82d818e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidi_chromeScript.js @@ -0,0 +1,28 @@ +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1"; +const HELPERAPP_DIALOG_CID = + Components.ID(Cc[HELPERAPP_DIALOG_CONTRACT].number); + +const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator).generateUUID(); + +function HelperAppLauncherDialog() {} +HelperAppLauncherDialog.prototype = { + show: function(aLauncher, aWindowContext, aReason) { + sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]) +}; + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory(FAKE_CID, "", HELPERAPP_DIALOG_CONTRACT, + XPCOMUtils._getFactory(HelperAppLauncherDialog)); + +addMessageListener("unregister", function() { + registrar.registerFactory(HELPERAPP_DIALOG_CID, "", + HELPERAPP_DIALOG_CONTRACT, null); + sendAsyncMessage("unregistered"); +}); diff --git a/uriloader/exthandler/tests/moz.build b/uriloader/exthandler/tests/moz.build new file mode 100644 index 0000000000..6aadfdc527 --- /dev/null +++ b/uriloader/exthandler/tests/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini'] + +XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini'] + +BROWSER_CHROME_MANIFESTS += ['mochitest/browser.ini'] + +# The encoding test is already implemented in the Downloads API by a set of +# test cases with the string "content_encoding" in their names. +if not CONFIG['MOZ_JSDOWNLOADS']: + XPCSHELL_TESTS_MANIFESTS += ['unit_ipc/xpcshell.ini'] + +GeckoSimplePrograms([ + 'WriteArgument', +], linkage=None) + +USE_LIBS += [ + 'nspr', +] diff --git a/uriloader/exthandler/tests/unit/head_handlerService.js b/uriloader/exthandler/tests/unit/head_handlerService.js new file mode 100644 index 0000000000..8b6803d241 --- /dev/null +++ b/uriloader/exthandler/tests/unit/head_handlerService.js @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Inspired by the Places infrastructure in head_bookmarks.js + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cr = Components.results; +var Cu = Components.utils; + +var HandlerServiceTest = { + //**************************************************************************// + // Convenience Getters + + __dirSvc: null, + get _dirSvc() { + if (!this.__dirSvc) + this.__dirSvc = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + QueryInterface(Ci.nsIDirectoryService); + return this.__dirSvc; + }, + + __consoleSvc: null, + get _consoleSvc() { + if (!this.__consoleSvc) + this.__consoleSvc = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__consoleSvc; + }, + + + //**************************************************************************// + // nsISupports + + interfaces: [Ci.nsIDirectoryServiceProvider, Ci.nsISupports], + + QueryInterface: function HandlerServiceTest_QueryInterface(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + }, + + + //**************************************************************************// + // Initialization & Destruction + + init: function HandlerServiceTest_init() { + // Register ourselves as a directory provider for the datasource file + // if there isn't one registered already. + try { + this._dirSvc.get("UMimTyp", Ci.nsIFile); + } catch (ex) { + this._dirSvc.registerProvider(this); + this._providerRegistered = true; + } + + // Delete the existing datasource file, if any, so we start from scratch. + // We also do this after finishing the tests, so there shouldn't be an old + // file lying around, but just in case we delete it here as well. + this._deleteDatasourceFile(); + + // Turn on logging so we can troubleshoot problems with the tests. + var prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefBranch); + prefBranch.setBoolPref("browser.contentHandling.log", true); + }, + + destroy: function HandlerServiceTest_destroy() { + // Delete the existing datasource file, if any, so we don't leave test files + // lying around and we start from scratch the next time. + this._deleteDatasourceFile(); + // Unregister the directory service provider + if (this._providerRegistered) + this._dirSvc.unregisterProvider(this); + }, + + + //**************************************************************************// + // nsIDirectoryServiceProvider + + getFile: function HandlerServiceTest_getFile(property, persistent) { + this.log("getFile: requesting " + property); + + persistent.value = true; + + if (property == "UMimTyp") { + var datasourceFile = this._dirSvc.get("CurProcD", Ci.nsIFile); + datasourceFile.append("mimeTypes.rdf"); + return datasourceFile; + } + + // This causes extraneous errors to show up in the log when the directory + // service asks us first for CurProcD and MozBinD. I wish there was a way + // to suppress those errors. + this.log("the following NS_ERROR_FAILURE exception in " + + "nsIDirectoryServiceProvider::getFile is expected, " + + "as we don't provide the '" + property + "' file"); + throw Cr.NS_ERROR_FAILURE; + }, + + + //**************************************************************************// + // Utilities + + /** + * Delete the datasource file. + */ + _deleteDatasourceFile: function HandlerServiceTest__deleteDatasourceFile() { + var file = this._dirSvc.get("UMimTyp", Ci.nsIFile); + if (file.exists()) + file.remove(false); + }, + + /** + * Get the contents of the datasource as a serialized string. Useful for + * debugging problems with test failures, i.e.: + * + * HandlerServiceTest.log(HandlerServiceTest.getDatasourceContents()); + * + * @returns {string} the serialized datasource + */ + getDatasourceContents: function HandlerServiceTest_getDatasourceContents() { + var rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); + + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var fileHandler = ioService.getProtocolHandler("file"). + QueryInterface(Ci.nsIFileProtocolHandler); + var fileURL = fileHandler.getURLSpecFromFile(this.getDatasourceFile()); + var ds = rdf.GetDataSourceBlocking(fileURL); + + var outputStream = { + data: "", + close: function() {}, + flush: function() {}, + write: function (buffer,count) { + this.data += buffer; + return count; + }, + writeFrom: function (stream,count) {}, + isNonBlocking: false + }; + + ds.QueryInterface(Components.interfaces.nsIRDFXMLSource); + ds.Serialize(outputStream); + + return outputStream.data; + }, + + /** + * Log a message to the console and the test log. + */ + log: function HandlerServiceTest_log(message) { + message = "*** HandlerServiceTest: " + message; + this._consoleSvc.logStringMessage(message); + print(message); + } + +}; + +HandlerServiceTest.init(); diff --git a/uriloader/exthandler/tests/unit/mailcap b/uriloader/exthandler/tests/unit/mailcap new file mode 100644 index 0000000000..dc93ef8042 --- /dev/null +++ b/uriloader/exthandler/tests/unit/mailcap @@ -0,0 +1,2 @@ +text/plain; cat '%s'; needsterminal +text/plain; sed '%s' diff --git a/uriloader/exthandler/tests/unit/tail_handlerService.js b/uriloader/exthandler/tests/unit/tail_handlerService.js new file mode 100644 index 0000000000..1d1989127e --- /dev/null +++ b/uriloader/exthandler/tests/unit/tail_handlerService.js @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +HandlerServiceTest.destroy(); diff --git a/uriloader/exthandler/tests/unit/test_badMIMEType.js b/uriloader/exthandler/tests/unit/test_badMIMEType.js new file mode 100644 index 0000000000..df1202a0f2 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_badMIMEType.js @@ -0,0 +1,26 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + // "text/plain" has an 0xFF character appended to it. This means it's an + // invalid string, which is tricky to enter using a text editor (I used + // emacs' hexl-mode). It also means an ordinary text editor might drop it + // or convert it to something that *is* valid (in UTF8). So we measure + // its length to make sure this hasn't happened. + var badMimeType = "text/plainÿ"; + do_check_eq(badMimeType.length, 11); + + try { + var type = Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService). + getFromTypeAndExtension(badMimeType, "txt"); + } catch (e if (e instanceof Ci.nsIException && + e.result == Cr.NS_ERROR_NOT_AVAILABLE)) { + // This is an expected exception, thrown if the type can't be determined + } finally { + } + // Not crashing is good enough + do_check_eq(true, true); +} diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js new file mode 100644 index 0000000000..d83c486bb4 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js @@ -0,0 +1,53 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 508030 <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 0000000000..1ae2a6fcfa --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js @@ -0,0 +1,186 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 484579 <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 0000000000..3facc63aeb --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService.js @@ -0,0 +1,470 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + //**************************************************************************// + // Constants + + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"]. + getService(Ci.nsIHandlerService); + + const mimeSvc = Cc["@mozilla.org/mime;1"]. + getService(Ci.nsIMIMEService); + + const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService); + + const prefSvc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService); + + const ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + const env = Cc["@mozilla.org/process/environment;1"]. + getService(Components.interfaces.nsIEnvironment); + + const rootPrefBranch = prefSvc.getBranch(""); + + let noMailto = false; + if (mozinfo.os == "win") { + // Check mailto handler from registry. + // If registry entry is nothing, no mailto handler + let regSvc = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + try { + regSvc.open(regSvc.ROOT_KEY_CLASSES_ROOT, + "mailto", + regSvc.ACCESS_READ); + noMailto = false; + } catch (ex) { + noMailto = true; + } + regSvc.close(); + } + + if (mozinfo.os == "linux") { + // Check mailto handler from GIO + // If there isn't one, then we have no mailto handler + let gIOSvc = Cc["@mozilla.org/gio-service;1"]. + createInstance(Ci.nsIGIOService); + try { + gIOSvc.getAppForURIScheme("mailto"); + noMailto = false; + } catch (ex) { + noMailto = true; + } + } + + //**************************************************************************// + // Sample Data + + // It doesn't matter whether or not this nsIFile is actually executable, + // only that it has a path and exists. Since we don't know any executable + // that exists on all platforms (except possibly the application being + // tested, but there doesn't seem to be a way to get a reference to that + // from the directory service), we use the temporary directory itself. + var executable = HandlerServiceTest._dirSvc.get("TmpD", Ci.nsIFile); + // XXX We could, of course, create an actual executable in the directory: + //executable.append("localhandler"); + //if (!executable.exists()) + // executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + + var localHandler = { + name: "Local Handler", + executable: executable, + interfaces: [Ci.nsIHandlerApp, Ci.nsILocalHandlerApp, Ci.nsISupports], + QueryInterface: function(iid) { + if (!this.interfaces.some( function(v) { return iid.equals(v) } )) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + } + }; + + var webHandler = Cc["@mozilla.org/uriloader/web-handler-app;1"]. + createInstance(Ci.nsIWebHandlerApp); + webHandler.name = "Web Handler"; + webHandler.uriTemplate = "http://www.example.com/?%s"; + + // FIXME: these tests create and manipulate enough variables that it would + // make sense to move each test into its own scope so we don't run the risk + // of one test stomping on another's data. + + + //**************************************************************************// + // Test Default Properties + + // Get a handler info for a MIME type that neither the application nor + // the OS knows about and make sure its properties are set to the proper + // default values. + + var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + // Make sure it's also an nsIHandlerInfo. + do_check_true(handlerInfo instanceof Ci.nsIHandlerInfo); + + do_check_eq(handlerInfo.type, "nonexistent/type"); + + // Deprecated property, but we should still make sure it's set correctly. + do_check_eq(handlerInfo.MIMEType, "nonexistent/type"); + + // These properties are the ones the handler service knows how to store. + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk); + do_check_eq(handlerInfo.preferredApplicationHandler, null); + do_check_eq(handlerInfo.possibleApplicationHandlers.length, 0); + do_check_true(handlerInfo.alwaysAskBeforeHandling); + + // These properties are initialized to default values by the service, + // so we might as well make sure they're initialized to the right defaults. + do_check_eq(handlerInfo.description, ""); + do_check_eq(handlerInfo.hasDefaultHandler, false); + do_check_eq(handlerInfo.defaultDescription, ""); + + // test some default protocol info properties + var haveDefaultHandlersVersion = false; + try { + // If we have a defaultHandlersVersion pref, then assume that we're in the + // firefox tree and that we'll also have default handlers. + // Bug 395131 has been filed to make this test work more generically + // by providing our own prefs for this test rather than this icky + // special casing. + rootPrefBranch.getCharPref("gecko.handlerService.defaultHandlersVersion"); + haveDefaultHandlersVersion = true; + } catch (ex) {} + + const kExternalWarningDefault = + "network.protocol-handler.warn-external-default"; + prefSvc.setBoolPref(kExternalWarningDefault, true); + + // XXX add more thorough protocol info property checking + + // no OS default handler exists + var protoInfo = protoSvc.getProtocolHandlerInfo("x-moz-rheet"); + do_check_eq(protoInfo.preferredAction, protoInfo.alwaysAsk); + do_check_true(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: false + const kExternalWarningPrefPrefix = "network.protocol-handler.warn-external."; + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", false); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_false(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", true); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + // OS handler isn't included in possibleApplicationHandlers, so length is 0 + // Once they become instances of nsILocalHandlerApp, this number will need + // to change. + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_true(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: false + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + else + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + + // Win7+ or Linux's GIO might not have a default mailto: handler + if (noMailto) + do_check_true(protoInfo.alwaysAskBeforeHandling); + else + do_check_false(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", true); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) { + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + // Win7+ or Linux's GIO may have no default mailto: handler. Otherwise + // alwaysAskBeforeHandling is expected to be false here, because although + // the pref is true, the value in RDF is false. The injected mailto handler + // carried over the default pref value, and so when we set the pref above + // to true it's ignored. + if (noMailto) + do_check_true(protoInfo.alwaysAskBeforeHandling); + else + do_check_false(protoInfo.alwaysAskBeforeHandling); + + } else { + do_check_eq(0, protoInfo.possibleApplicationHandlers.length); + do_check_true(protoInfo.alwaysAskBeforeHandling); + } + + if (haveDefaultHandlersVersion) { + // Now set the value stored in RDF to true, and the pref to false, to make + // sure we still get the right value. (Basically, same thing as above but + // with the values reversed.) + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo.alwaysAskBeforeHandling = true; + handlerSvc.store(protoInfo); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + do_check_eq(2, protoInfo.possibleApplicationHandlers.length); + do_check_true(protoInfo.alwaysAskBeforeHandling); + } + + + //**************************************************************************// + // Test Round-Trip Data Integrity + + // Test round-trip data integrity by setting the properties of the handler + // info object to different values, telling the handler service to store the + // object, and then retrieving a new info object for the same type and making + // sure its properties are identical. + + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.preferredApplicationHandler = localHandler; + handlerInfo.alwaysAskBeforeHandling = false; + + handlerSvc.store(handlerInfo); + + handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + + do_check_neq(handlerInfo.preferredApplicationHandler, null); + var preferredHandler = handlerInfo.preferredApplicationHandler; + do_check_eq(typeof preferredHandler, "object"); + do_check_eq(preferredHandler.name, "Local Handler"); + do_check_true(preferredHandler instanceof Ci.nsILocalHandlerApp); + preferredHandler.QueryInterface(Ci.nsILocalHandlerApp); + do_check_eq(preferredHandler.executable.path, localHandler.executable.path); + + do_check_false(handlerInfo.alwaysAskBeforeHandling); + + // Make sure the handler service's enumerate method lists all known handlers. + var handlerInfo2 = mimeSvc.getFromTypeAndExtension("nonexistent/type2", null); + handlerSvc.store(handlerInfo2); + var handlerTypes = ["nonexistent/type", "nonexistent/type2"]; + if (haveDefaultHandlersVersion) { + handlerTypes.push("webcal"); + handlerTypes.push("mailto"); + handlerTypes.push("irc"); + handlerTypes.push("ircs"); + } + var handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) { + var handler = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo); + do_check_neq(handlerTypes.indexOf(handler.type), -1); + handlerTypes.splice(handlerTypes.indexOf(handler.type), 1); + } + do_check_eq(handlerTypes.length, 0); + + // Make sure the handler service's remove method removes a handler record. + handlerSvc.remove(handlerInfo2); + handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) + do_check_neq(handlers.getNext().QueryInterface(Ci.nsIHandlerInfo).type, + handlerInfo2.type); + + // Make sure we can store and retrieve a handler info object with no preferred + // handler. + var noPreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/no-preferred-handler", null); + handlerSvc.store(noPreferredHandlerInfo); + noPreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/no-preferred-handler", null); + do_check_eq(noPreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure that the handler service removes an existing handler record + // if we store a handler info object with no preferred handler. + var removePreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null); + removePreferredHandlerInfo.preferredApplicationHandler = localHandler; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null); + removePreferredHandlerInfo.preferredApplicationHandler = null; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null); + do_check_eq(removePreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure we can store and retrieve a handler info object with possible + // handlers. We test both adding and removing handlers. + + // Get a handler info and make sure it has no possible handlers. + var possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Store and re-retrieve the handler and make sure it still has no possible + // handlers. + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Add two handlers, store the object, re-retrieve it, and make sure it has + // two handlers. + possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler, + false); + possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler, + false); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 2); + + // Figure out which is the local and which is the web handler and the index + // in the array of the local handler, which is the one we're going to remove + // to test removal of a handler. + var handler1 = possibleHandlersInfo.possibleApplicationHandlers. + queryElementAt(0, Ci.nsIHandlerApp); + var handler2 = possibleHandlersInfo.possibleApplicationHandlers. + queryElementAt(1, Ci.nsIHandlerApp); + var localPossibleHandler, webPossibleHandler, localIndex; + if (handler1 instanceof Ci.nsILocalHandlerApp) + [localPossibleHandler, webPossibleHandler, localIndex] = [handler1, + handler2, + 0]; + else + [localPossibleHandler, webPossibleHandler, localIndex] = [handler2, + handler1, + 1]; + localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp); + webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp); + + // Make sure the two handlers are the ones we stored. + do_check_eq(localPossibleHandler.name, localHandler.name); + do_check_true(localPossibleHandler.equals(localHandler)); + do_check_eq(webPossibleHandler.name, webHandler.name); + do_check_true(webPossibleHandler.equals(webHandler)); + + // Remove a handler, store the object, re-retrieve it, and make sure + // it only has one handler. + possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = + mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null); + do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 1); + + // Make sure the handler is the one we didn't remove. + webPossibleHandler = possibleHandlersInfo.possibleApplicationHandlers. + queryElementAt(0, Ci.nsIWebHandlerApp); + do_check_eq(webPossibleHandler.name, webHandler.name); + do_check_true(webPossibleHandler.equals(webHandler)); + + ////////////////////////////////////////////////////// + // handler info command line parameters and equality + var localApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + var handlerApp = localApp.QueryInterface(Ci.nsIHandlerApp); + + do_check_true(handlerApp.equals(localApp)); + + localApp.executable = executable; + + do_check_eq(0, localApp.parameterCount); + localApp.appendParameter("-test1"); + do_check_eq(1, localApp.parameterCount); + localApp.appendParameter("-test2"); + do_check_eq(2, localApp.parameterCount); + do_check_true(localApp.parameterExists("-test1")); + do_check_true(localApp.parameterExists("-test2")); + do_check_false(localApp.parameterExists("-false")); + localApp.clearParameters(); + do_check_eq(0, localApp.parameterCount); + + var localApp2 = Cc["@mozilla.org/uriloader/local-handler-app;1"]. + createInstance(Ci.nsILocalHandlerApp); + + localApp2.executable = executable; + + localApp.clearParameters(); + do_check_true(localApp.equals(localApp2)); + + // equal: + // cut -d 1 -f 2 + // cut -d 1 -f 2 + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test3"); + do_check_true(localApp.equals(localApp2)); + + // not equal: + // cut -d 1 -f 2 + // cut -f 1 -d 2 + + localApp.clearParameters(); + localApp2.clearParameters(); + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test3"); + do_check_false(localApp2.equals(localApp)); + + var str; + str = localApp.getParameter(0) + do_check_eq(str, "-test1"); + str = localApp.getParameter(1) + do_check_eq(str, "-test2"); + str = localApp.getParameter(2) + do_check_eq(str, "-test3"); + + // FIXME: test round trip integrity for a protocol. + // FIXME: test round trip integrity for a handler info with a web handler. + + //**************************************************************************// + // getTypeFromExtension tests + + // test nonexistent extension + var lolType = handlerSvc.getTypeFromExtension("lolcat"); + do_check_eq(lolType, ""); + + + // add a handler for the extension + var lolHandler = mimeSvc.getFromTypeAndExtension("application/lolcat", null); + + do_check_false(lolHandler.extensionExists("lolcat")); + lolHandler.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + lolHandler.preferredApplicationHandler = localHandler; + lolHandler.alwaysAskBeforeHandling = false; + + // store the handler + do_check_false(handlerSvc.exists(lolHandler)); + handlerSvc.store(lolHandler); + do_check_true(handlerSvc.exists(lolHandler)); + + // Get a file:// string pointing to mimeTypes.rdf + var rdfFile = HandlerServiceTest._dirSvc.get("UMimTyp", Ci.nsIFile); + var fileHandler = ioService.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler); + var rdfFileURI = fileHandler.getURLSpecFromFile(rdfFile); + + // Assign a file extenstion to the handler. handlerSvc.store() doesn't + // actually store any file extensions added with setFileExtensions(), you + // have to wade into RDF muck to do so. + + // Based on toolkit/mozapps/downloads/content/helperApps.js :: addExtension() + var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); + var mimeSource = gRDF.GetUnicodeResource("urn:mimetype:application/lolcat"); + var valueProperty = gRDF.GetUnicodeResource("http://home.netscape.com/NC-rdf#fileExtensions"); + var mimeLiteral = gRDF.GetLiteral("lolcat"); + + var DS = gRDF.GetDataSourceBlocking(rdfFileURI); + DS.Assert(mimeSource, valueProperty, mimeLiteral, true); + + + // test now-existent extension + lolType = handlerSvc.getTypeFromExtension("lolcat"); + do_check_eq(lolType, "application/lolcat"); + + // test mailcap entries with needsterminal are ignored on non-Windows non-Mac. + if (mozinfo.os != "win" && mozinfo.os != "mac") { + env.set('PERSONAL_MAILCAP', do_get_file('mailcap').path); + handlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", null); + do_check_eq(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useSystemDefault); + do_check_eq(handlerInfo.defaultDescription, "sed"); + } +} diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js new file mode 100644 index 0000000000..38622c840a --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.js @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Encoded test URI to work on all platforms/independent of file encoding +const kTestURI = "http://\u65e5\u672c\u8a93.jp/"; +const kExpectedURI = "http://xn--wgv71a309e.jp/"; +const kOutputFile = "result.txt"; + +// Try several times in case the box we're running on is slow. +const kMaxCheckExistAttempts = 30; // seconds +var gCheckExistsAttempts = 0; + +const tempDir = do_get_tempdir(); + +function checkFile() { + // This is where we expect the output + var tempFile = tempDir.clone(); + tempFile.append(kOutputFile); + + if (!tempFile.exists()) { + if (gCheckExistsAttempts >= kMaxCheckExistAttempts) { + do_throw("Expected File " + tempFile.path + " does not exist after " + + kMaxCheckExistAttempts + " seconds"); + } + else { + ++gCheckExistsAttempts; + // Wait a bit longer then try again + do_timeout(1000, checkFile); + return; + } + } + + // Now read it + var fstream = + Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + var sstream = + Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.nsIScriptableInputStream); + fstream.init(tempFile, -1, 0, 0); + sstream.init(fstream); + + // Read the first line only as that's the one we expect WriteArguments + // to be writing the argument to. + var data = sstream.read(4096); + + sstream.close(); + fstream.close(); + + // Now remove the old file + tempFile.remove(false); + + // This currently fails on Mac with an argument like -psn_0_nnnnnn + // This seems to be to do with how the executable is called, but I couldn't + // find a way around it. + // Additionally the lack of OS detection in xpcshell tests sucks, so we'll + // have to check for the argument mac gives us. + if (data.substring(0, 7) != "-psn_0_") + do_check_eq(data, kExpectedURI); + + do_test_finished(); +} + +function run_test() { + if (mozinfo.os == "mac") { + dump("INFO | test_punycodeURIs.js | Skipping test on mac, bug 599475") + return; + } + + // set up the uri to test with + var ioService = + Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + // set up the local handler object + var localHandler = + Components.classes["@mozilla.org/uriloader/local-handler-app;1"] + .createInstance(Components.interfaces.nsILocalHandlerApp); + localHandler.name = "Test Local Handler App"; + + // WriteArgument will just dump its arguments to a file for us. + var processDir = do_get_cwd(); + var exe = processDir.clone(); + exe.append("WriteArgument"); + + if (!exe.exists()) { + // Maybe we are on windows + exe.leafName = "WriteArgument.exe"; + if (!exe.exists()) + do_throw("Could not locate the WriteArgument tests executable\n"); + } + + var outFile = tempDir.clone(); + outFile.append(kOutputFile); + + // Set an environment variable for WriteArgument to pick up + var envSvc = + Components.classes["@mozilla.org/process/environment;1"] + .getService(Components.interfaces.nsIEnvironment); + + // The Write Argument file needs to know where its libraries are, so + // just force the path variable + // For mac + var greDir = HandlerServiceTest._dirSvc.get("GreD", Components.interfaces.nsIFile); + + envSvc.set("DYLD_LIBRARY_PATH", greDir.path); + // For Linux + envSvc.set("LD_LIBRARY_PATH", greDir.path); + //XXX: handle windows + + // Now tell it where we want the file. + envSvc.set("WRITE_ARGUMENT_FILE", outFile.path); + + var uri = ioService.newURI(kTestURI, null, null); + + // Just check we've got these matching, if we haven't there's a problem + // with ascii spec or our test case. + do_check_eq(uri.asciiSpec, kExpectedURI); + + localHandler.executable = exe; + localHandler.launchWithURI(uri); + + do_test_pending(); + do_timeout(1000, checkFile); +} diff --git a/uriloader/exthandler/tests/unit/xpcshell.ini b/uriloader/exthandler/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..e268ff9c37 --- /dev/null +++ b/uriloader/exthandler/tests/unit/xpcshell.ini @@ -0,0 +1,15 @@ +[DEFAULT] +head = head_handlerService.js +tail = tail_handlerService.js +run-sequentially = Bug 912235 - Intermittent failures + +[test_getTypeFromExtension_ext_to_type_mapping.js] +[test_getTypeFromExtension_with_empty_Content_Type.js] +[test_badMIMEType.js] +[test_handlerService.js] +support-files = mailcap +# Bug 676997: test consistently fails on Android +fail-if = os == "android" +[test_punycodeURIs.js] +# Bug 676997: test consistently fails on Android +fail-if = os == "android" diff --git a/uriloader/exthandler/tests/unit_ipc/test_encoding.js b/uriloader/exthandler/tests/unit_ipc/test_encoding.js new file mode 100644 index 0000000000..a2a00937a1 --- /dev/null +++ b/uriloader/exthandler/tests/unit_ipc/test_encoding.js @@ -0,0 +1,231 @@ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://testing-common/MockRegistrar.js"); + +do_get_profile(); + +var DownloadListener = { + init: function () { + let obs = Services.obs; + obs.addObserver(this, "dl-done", true); + }, + + observe: function (subject, topic, data) { + this.onFinished(subject, topic, data); + }, + + QueryInterface: function (iid) { + if (iid.equals(Ci.nsIObserver) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + } +} +DownloadListener.init(); + +function HelperAppDlg() { } +HelperAppDlg.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), + show: function (launcher, ctx, reason, usePrivateUI) { + launcher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToFile; + launcher.launchWithApplication(null, false); + } +} + +// Override the download-manager-ui to prevent anyone from trying to open +// a window. +function DownloadMgrUI() { } +DownloadMgrUI.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]), + show: function (ir, aID, reason) { }, + + visible: false, + + getAttention: function () { } +} + +function AlertsSVC() { } +AlertsSVC.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService]), + showAlertNotification: function (url, title, text, clickable, cookie, listener, name) { }, +} + +MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", + HelperAppDlg); +MockRegistrar.register("@mozilla.org/download-manager-ui;1", + DownloadMgrUI); +MockRegistrar.register("@mozilla.org/alerts-service;1", + AlertsSVC); + +function initChildTestEnv() +{ + sendCommand(' \ + const Cc = Components.classes; \ + const Ci = Components.interfaces; \ + const Cr = Components.results; \ + const Cu = Components.utils; \ + Cu.import("resource://gre/modules/Services.jsm"); \ + function WindowContext() { } \ + \ + WindowContext.prototype = { \ + getInterface: function (iid) { \ + if (iid.equals(Ci.nsIInterfaceRequestor) || \ + iid.equals(Ci.nsIURIContentListener) || \ + iid.equals(Ci.nsILoadGroup) || \ + iid.equals(Ci.nsIDocumentLoader) || \ + iid.equals(Ci.nsIDOMWindow)) \ + return this; \ + \ + throw Cr.NS_ERROR_NO_INTERFACE; \ + }, \ + \ + /* nsIURIContentListener */ \ + onStartURIOpen: function (uri) { }, \ + isPreferred: function (type, desiredtype) { return false; }, \ + \ + /* nsILoadGroup */ \ + addRequest: function (request, context) { }, \ + removeRequest: function (request, context, status) { } \ + }; \ + \ + var ioservice = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);\ + var uriloader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);\ + '); +} + +function testFinisher(endFunc) { + let ef = endFunc; + return function (file) { + ef(file); + runNextTest(); + } +} + +function runChildTestSet(set) +{ + DownloadListener.onFinished = testFinisher(set[2]); + sendCommand('\ + let uri = ioservice.newURI("http://localhost:4444' + set[0] + '", null, null); \ + let channel = NetUtil.newChannel({uri: uri, loadUsingSystemPrincipal: true}); \ + uriloader.openURI(channel, Ci.nsIURILoader.IS_CONTENT_PREFERRED, new WindowContext()); \ + '); +} + +var httpserver = null; +var currentTest = 0; +function runNextTest() +{ + if (currentTest == tests.length) { + httpserver.stop(do_test_finished); + return; + } + + let set = tests[currentTest++]; + runChildTestSet(set); +} + +const responseBody = [0x1f, 0x8b, 0x08, 0x00, 0x16, 0x5a, 0x8a, 0x48, 0x02, + 0x03, 0x2b, 0x49, 0x2d, 0x2e, 0xe1, 0x02, 0x00, 0xc6, + 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00]; + +/* + * First test: a file with Content-Type application/x-gzip and Content-Encoding gzip + * should not be decoded in a round-trip + */ +function testResponse1(metadata, response) { + response.setHeader("Content-Type", "application/x-gzip", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest1(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test1.gz") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + do_check_matches(str, responseBody); +} + +/* + * Second test: a file with Content-Type text/html and Content-Encoding gzip + * should not be decoded in a round-trip, if its filename ends in ".gz". + * We specify a Content-disposition header to force it to be saved as a file. + */ +function testResponse2(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest2(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test2.gz") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + do_check_matches(str, responseBody); +} + +function testResponse3(metadata, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Disposition", "attachment", false); + + var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + bos.writeByteArray(responseBody, responseBody.length); +} + +function finishTest3(subject, topic, data) { + let file = subject.QueryInterface(Ci.nsIDownload).targetFile; + do_check_true(file.path.search("test3.txt") != 0); + let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + fis.init(file, -1, -1, 0); + let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fis); + let str = bis.readByteArray(bis.available()); + let decodedBody = [ 116, 101, 115, 116, 10 ]; // 't','e','s','t','\n' + do_check_matches(str, decodedBody); +} + +var tests = [ + [ "/test1.gz", testResponse1, finishTest1 ], + [ "/test2.gz", testResponse2, finishTest2 ], + [ "/test3.txt", testResponse3, finishTest3 ], +]; + +function run_test() { +// do_load_child_test_harness(); + httpserver = new HttpServer(); + httpserver.start(4444); + do_test_pending(); + + initChildTestEnv(); + + for (let set of tests) + httpserver.registerPathHandler(set[0], set[1]); + + runNextTest(); +} diff --git a/uriloader/exthandler/tests/unit_ipc/xpcshell.ini b/uriloader/exthandler/tests/unit_ipc/xpcshell.ini new file mode 100644 index 0000000000..962b93decd --- /dev/null +++ b/uriloader/exthandler/tests/unit_ipc/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = +tail = + +[test_encoding.js] +# Bug 676995: test hangs consistently on Android +# Bug 907732: thunderbird still uses legacy downloads manager. +skip-if = (os == "android" || buildapp == '../mail') diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h new file mode 100644 index 0000000000..9d5ef31da1 --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nslocalhandlerappuikit_h_ +#define nslocalhandlerappuikit_h_ + +#include "nsLocalHandlerApp.h" + +class nsLocalHandlerAppUIKit final : public nsLocalHandlerApp +{ +public: + nsLocalHandlerAppUIKit() + {} + ~nsLocalHandlerAppUIKit() + {} + + nsLocalHandlerAppUIKit(const char16_t* aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) + {} + + nsLocalHandlerAppUIKit(const nsAString& aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) + {} + + + NS_IMETHOD LaunchWithURI(nsIURI* aURI, nsIInterfaceRequestor* aWindowContext); +}; + +#endif /* nslocalhandlerappuikit_h_ */ diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm new file mode 100644 index 0000000000..afa600721e --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <CoreFoundation/CoreFoundation.h> + +#include "nsLocalHandlerAppUIKit.h" +#include "nsIURI.h" + +NS_IMETHODIMP +nsLocalHandlerAppUIKit::LaunchWithURI(nsIURI* aURI, + nsIInterfaceRequestor* aWindowContext) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h new file mode 100644 index 0000000000..3f6bc8b42f --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUIKit_h_ +#define nsMIMEInfoUIKit_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUIKit final : public nsMIMEInfoImpl +{ +public: + explicit nsMIMEInfoUIKit(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) + {} + nsMIMEInfoUIKit(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) + {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile); + +protected: + virtual nsresult LoadUriInternal(nsIURI* aURI); +#ifdef DEBUG + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) + { + NS_NOTREACHED("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif + static nsresult OpenApplicationWithURI(nsIFile* aApplication, + const nsCString& aURI); +}; + +#endif diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm new file mode 100644 index 0000000000..4b950dffc4 --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUIKit.h" +#include "nsIFileURL.h" + +NS_IMETHODIMP +nsMIMEInfoUIKit::LaunchWithFile(nsIFile* aFile) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult +nsMIMEInfoUIKit::LoadUriInternal(nsIURI* aURI) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.h b/uriloader/exthandler/uikit/nsOSHelperAppService.h new file mode 100644 index 0000000000..1f33be0fba --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and +// is implemented on each platform. It contains platform specific code for +// finding helper applications for a given mime type in addition to launching +// those applications. This is the UIKit version. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsOSHelperAppService final : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval); + + // method overrides --> used to hook the mime service into internet config.... + NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo); + already_AddRefed<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 0000000000..dfee24b2db --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" + +nsOSHelperAppService::nsOSHelperAppService() + : nsExternalHelperAppService() +{} + +nsOSHelperAppService::~nsOSHelperAppService() +{} + +nsresult +nsOSHelperAppService::OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) +{ + *aHandlerExists = false; + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult +nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath, + nsIFile** aFile) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo) +{ + return nsExternalHelperAppService::GetFromTypeAndExtension(aType, + aFileExt, + aMIMEInfo); +} + +already_AddRefed<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 0000000000..3b1794b5e8 --- /dev/null +++ b/uriloader/exthandler/unix/nsExternalSharingAppService.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_EXTERNAL_SHARING_APP_SERVICE_H +#define NS_EXTERNAL_SHARING_APP_SERVICE_H + +#include "nsIExternalSharingAppService.h" +#include "nsAutoPtr.h" + +class ShareUiInterface; + +#define NS_EXTERNALSHARINGAPPSERVICE_CID \ + {0xea782c90, 0xc6ec, 0x11df, \ + {0xbd, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66, 0x74}} + +class nsExternalSharingAppService : public nsIExternalSharingAppService +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALSHARINGAPPSERVICE + + nsExternalSharingAppService(); + +private: + ~nsExternalSharingAppService(); + +protected: + nsAutoPtr<ShareUiInterface> mShareUi; +}; +#endif diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.cpp b/uriloader/exthandler/unix/nsGNOMERegistry.cpp new file mode 100644 index 0000000000..bf71ae913c --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.cpp @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsGNOMERegistry.h" +#include "nsString.h" +#include "nsIComponentManager.h" +#include "nsMIMEInfoUnix.h" +#include "nsAutoPtr.h" +#include "nsIGIOService.h" + +/* static */ bool +nsGNOMERegistry::HandlerExists(const char *aProtocolScheme) +{ + nsCOMPtr<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 0000000000..fe42ee622f --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.h @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsGNOMERegistry_h +#define nsGNOMERegistry_h + +#include "nsIURI.h" +#include "nsCOMPtr.h" + +class nsMIMEInfoBase; + +class nsGNOMERegistry +{ + public: + static bool HandlerExists(const char *aProtocolScheme); + + static nsresult LoadURL(nsIURI *aURL); + + static void GetAppDescForScheme(const nsACString& aScheme, + nsAString& aDesc); + + static already_AddRefed<nsMIMEInfoBase> GetFromExtension(const nsACString& aFileExt); + + static already_AddRefed<nsMIMEInfoBase> GetFromType(const nsACString& aMIMEType); +}; + +#endif // nsGNOMERegistry_h diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp new file mode 100644 index 0000000000..e516608873 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUnix.h" +#include "nsGNOMERegistry.h" +#include "nsIGIOService.h" +#include "nsNetCID.h" +#include "nsIIOService.h" +#include "nsAutoPtr.h" +#ifdef MOZ_ENABLE_DBUS +#include "nsDBusHandlerApp.h" +#endif + +nsresult +nsMIMEInfoUnix::LoadUriInternal(nsIURI * aURI) +{ + return nsGNOMERegistry::LoadURL(aURI); +} + +NS_IMETHODIMP +nsMIMEInfoUnix::GetHasDefaultHandler(bool *_retval) +{ + // if mDefaultApplication is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (mDefaultApplication) + return nsMIMEInfoImpl::GetHasDefaultHandler(_retval); + + *_retval = false; + + if (mClass == eProtocolInfo) { + *_retval = nsGNOMERegistry::HandlerExists(mSchemeOrType.get()); + } else { + RefPtr<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 0000000000..65f6d996d4 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUnix_h_ +#define nsMIMEInfoUnix_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUnix : public nsMIMEInfoImpl +{ +public: + explicit nsMIMEInfoUnix(const char *aMIMEType = "") : nsMIMEInfoImpl(aMIMEType) {} + explicit nsMIMEInfoUnix(const nsACString& aMIMEType) : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUnix(const nsACString& aType, HandlerClass aClass) : + nsMIMEInfoImpl(aType, aClass) {} + static bool HandlerExists(const char *aProtocolScheme); + +protected: + NS_IMETHOD GetHasDefaultHandler(bool *_retval); + + virtual nsresult LoadUriInternal(nsIURI *aURI); + + virtual nsresult LaunchDefaultWithFile(nsIFile *aFile); +#if defined(MOZ_ENABLE_CONTENTACTION) + NS_IMETHOD GetPossibleApplicationHandlers(nsIMutableArray * *aPossibleAppHandlers); +#endif +}; + +#endif // nsMIMEInfoUnix_h_ diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.cpp b/uriloader/exthandler/unix/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..84e1cf5b04 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp @@ -0,0 +1,1537 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <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 0000000000..33eb934ed7 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.h @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is implemented on each +// platform. It contains platform specific code for finding helper applications for a given mime type +// in addition to launching those applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsMIMEInfoImpl.h" +#include "nsCOMPtr.h" + +class nsILineInputStream; + +class nsOSHelperAppService : public nsExternalHelperAppService +{ +public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // method overrides for mime.types and mime.info look up steps + already_AddRefed<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 0000000000..2c7171c877 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp @@ -0,0 +1,883 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsArrayEnumerator.h" +#include "nsCOMArray.h" +#include "nsLocalFile.h" +#include "nsMIMEInfoWin.h" +#include "nsNetUtil.h" +#include <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 0000000000..0a5b678e8a --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.h @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoWin_h_ +#define nsMIMEInfoWin_h_ + +#include "nsMIMEInfoImpl.h" +#include "nsIPropertyBag.h" +#include "nsIMutableArray.h" +#include "nsTArray.h" + +class nsMIMEInfoWin : public nsMIMEInfoBase, public nsIPropertyBag { + virtual ~nsMIMEInfoWin(); + + public: + nsMIMEInfoWin(const char* aType = "") : nsMIMEInfoBase(aType) {} + nsMIMEInfoWin(const nsACString& aMIMEType) : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoWin(const nsACString& aType, HandlerClass aClass) : + nsMIMEInfoBase(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile); + NS_IMETHOD GetHasDefaultHandler(bool * _retval); + NS_IMETHOD GetPossibleLocalHandlers(nsIArray **_retval); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROPERTYBAG + + void SetDefaultApplicationHandler(nsIFile* aDefaultApplication) + { + mDefaultApplication = aDefaultApplication; + } + + protected: + virtual nsresult LoadUriInternal(nsIURI *aURI); + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile); + + private: + nsCOMPtr<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 0000000000..f01f3b49b5 --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp @@ -0,0 +1,626 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsXPIDLString.h" +#include "nsIURL.h" +#include "nsIMIMEInfo.h" +#include "nsMIMEInfoWin.h" +#include "nsMimeTypes.h" +#include "nsIProcess.h" +#include "plstr.h" +#include "nsAutoPtr.h" +#include "nsNativeCharsetUtils.h" +#include "nsLocalFile.h" +#include "nsIWindowsRegKey.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WindowsVersion.h" + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include <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 0000000000..b00529433c --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is implemented on each +// platform. It contains platform specific code for finding helper applications for a given mime type +// in addition to launching those applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsMIMEInfoImpl.h" +#include "nsCOMPtr.h" +#include <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__ diff --git a/uriloader/moz.build b/uriloader/moz.build new file mode 100644 index 0000000000..840c9cc05e --- /dev/null +++ b/uriloader/moz.build @@ -0,0 +1,11 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'base', + 'exthandler', + 'prefetch', +] diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.cpp b/uriloader/prefetch/OfflineCacheUpdateChild.cpp new file mode 100644 index 0000000000..555508c373 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateChild.cpp @@ -0,0 +1,528 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BackgroundUtils.h" +#include "OfflineCacheUpdateChild.h" +#include "nsOfflineCacheUpdate.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/TabChild.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoCommon.h" + +#include "nsIApplicationCacheContainer.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheService.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "nsPIDOMWindow.h" +#include "nsIDOMOfflineResourceList.h" +#include "nsIDocument.h" +#include "nsIObserverService.h" +#include "nsIURL.h" +#include "nsITabChild.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" + +using namespace mozilla::ipc; +using namespace mozilla::net; +using mozilla::dom::TabChild; +using mozilla::dom::ContentChild; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +extern mozilla::LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +namespace mozilla { +namespace docshell { + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateChild::nsISupports +//----------------------------------------------------------------------------- + +NS_INTERFACE_MAP_BEGIN(OfflineCacheUpdateChild) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIOfflineCacheUpdate) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(OfflineCacheUpdateChild) +NS_IMPL_RELEASE(OfflineCacheUpdateChild) + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateChild <public> +//----------------------------------------------------------------------------- + +OfflineCacheUpdateChild::OfflineCacheUpdateChild(nsPIDOMWindowInner* aWindow) + : mState(STATE_UNINITIALIZED) + , mIsUpgrade(false) + , mSucceeded(false) + , mWindow(aWindow) + , mByteProgress(0) +{ +} + +OfflineCacheUpdateChild::~OfflineCacheUpdateChild() +{ + LOG(("OfflineCacheUpdateChild::~OfflineCacheUpdateChild [%p]", this)); +} + +void +OfflineCacheUpdateChild::GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver> &aObservers) +{ + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer) + aObservers.AppendObject(observer); + else + mWeakObservers.RemoveObjectAt(i--); + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + aObservers.AppendObject(mObservers[i]); + } +} + +void +OfflineCacheUpdateChild::SetDocument(nsIDOMDocument *aDocument) +{ + // The design is one document for one cache update on the content process. + NS_ASSERTION(!mDocument, "Setting more then a single document on a child offline cache update"); + + LOG(("Document %p added to update child %p", aDocument, this)); + + // Add document only if it was not loaded from an offline cache. + // If it were loaded from an offline cache then it has already + // been associated with it and must not be again cached as + // implicit (which are the reasons we collect documents here). + nsCOMPtr<nsIDocument> document = do_QueryInterface(aDocument); + if (!document) + return; + + nsIChannel* channel = document->GetChannel(); + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(channel); + if (!appCacheChannel) + return; + + bool loadedFromAppCache; + appCacheChannel->GetLoadedFromApplicationCache(&loadedFromAppCache); + if (loadedFromAppCache) + return; + + mDocument = aDocument; +} + +nsresult +OfflineCacheUpdateChild::AssociateDocument(nsIDOMDocument *aDocument, + nsIApplicationCache *aApplicationCache) +{ + // Check that the document that requested this update was + // previously associated with an application cache. If not, it + // should be associated with the new one. + nsCOMPtr<nsIApplicationCacheContainer> container = + do_QueryInterface(aDocument); + if (!container) + return NS_OK; + + nsCOMPtr<nsIApplicationCache> existingCache; + nsresult rv = container->GetApplicationCache(getter_AddRefs(existingCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!existingCache) { + if (LOG_ENABLED()) { + nsAutoCString clientID; + if (aApplicationCache) { + aApplicationCache->GetClientID(clientID); + } + LOG(("Update %p: associating app cache %s to document %p", + this, clientID.get(), aDocument)); + } + + rv = container->SetApplicationCache(aApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateChild::nsIOfflineCacheUpdate +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +OfflineCacheUpdateChild::Init(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal *aLoadingPrincipal, + nsIDOMDocument *aDocument, + nsIFile *aCustomProfileDir) +{ + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) + return NS_ERROR_FAILURE; + + if (aCustomProfileDir) { + NS_ERROR("Custom Offline Cache Update not supported on child process"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + LOG(("OfflineCacheUpdateChild::Init [%p]", this)); + + // Only http and https applications are supported. + bool match; + rv = aManifestURI->SchemeIs("http", &match); + NS_ENSURE_SUCCESS(rv, rv); + + if (!match) { + rv = aManifestURI->SchemeIs("https", &match); + NS_ENSURE_SUCCESS(rv, rv); + if (!match) + return NS_ERROR_ABORT; + } + + mManifestURI = aManifestURI; + + rv = mManifestURI->GetAsciiHost(mUpdateDomain); + NS_ENSURE_SUCCESS(rv, rv); + + mDocumentURI = aDocumentURI; + mLoadingPrincipal = aLoadingPrincipal; + + mState = STATE_INITIALIZED; + + if (aDocument) + SetDocument(aDocument); + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::InitPartial(nsIURI *aManifestURI, + const nsACString& clientID, + nsIURI *aDocumentURI, + nsIPrincipal *aLoadingPrincipal) +{ + NS_NOTREACHED("Not expected to do partial offline cache updates" + " on the child process"); + // For now leaving this method, we may discover we need it. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::InitForUpdateCheck(nsIURI *aManifestURI, + nsIPrincipal* aLoadingPrincipal, + nsIObserver *aObserver) +{ + NS_NOTREACHED("Not expected to do only update checks" + " from the child process"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetUpdateDomain(nsACString &aUpdateDomain) +{ + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + aUpdateDomain = mUpdateDomain; + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetStatus(uint16_t *aStatus) +{ + switch (mState) { + case STATE_CHECKING : + *aStatus = nsIDOMOfflineResourceList::CHECKING; + return NS_OK; + case STATE_DOWNLOADING : + *aStatus = nsIDOMOfflineResourceList::DOWNLOADING; + return NS_OK; + default : + *aStatus = nsIDOMOfflineResourceList::IDLE; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetPartial(bool *aPartial) +{ + *aPartial = false; + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetManifestURI(nsIURI **aManifestURI) +{ + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + NS_IF_ADDREF(*aManifestURI = mManifestURI); + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetSucceeded(bool *aSucceeded) +{ + NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE); + + *aSucceeded = mSucceeded; + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetIsUpgrade(bool *aIsUpgrade) +{ + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + *aIsUpgrade = mIsUpgrade; + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::AddDynamicURI(nsIURI *aURI) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::Cancel() +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::AddObserver(nsIOfflineCacheUpdateObserver *aObserver, + bool aHoldWeak) +{ + LOG(("OfflineCacheUpdateChild::AddObserver [%p]", this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + if (aHoldWeak) { + nsCOMPtr<nsIWeakReference> weakRef = do_GetWeakReference(aObserver); + mWeakObservers.AppendObject(weakRef); + } else { + mObservers.AppendObject(aObserver); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::RemoveObserver(nsIOfflineCacheUpdateObserver *aObserver) +{ + LOG(("OfflineCacheUpdateChild::RemoveObserver [%p]", this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer == aObserver) { + mWeakObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + if (mObservers[i] == aObserver) { + mObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetByteProgress(uint64_t * _result) +{ + NS_ENSURE_ARG(_result); + + *_result = mByteProgress; + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::Schedule() +{ + LOG(("OfflineCacheUpdateChild::Schedule [%p]", this)); + + NS_ASSERTION(mWindow, "Window must be provided to the offline cache update child"); + + nsCOMPtr<nsPIDOMWindowInner> window = mWindow.forget(); + nsCOMPtr<nsIDocShell >docshell = window->GetDocShell(); + if (!docshell) { + NS_WARNING("doc shell tree item is null"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsITabChild> tabchild = docshell->GetTabChild(); + // because owner implements nsITabChild, we can assume that it is + // the one and only TabChild. + TabChild* child = tabchild ? static_cast<TabChild*>(tabchild.get()) : nullptr; + + if (MissingRequiredTabChild(child, "offlinecacheupdate")) { + return NS_ERROR_FAILURE; + } + + URIParams manifestURI, documentURI; + SerializeURI(mManifestURI, manifestURI); + SerializeURI(mDocumentURI, documentURI); + + nsresult rv = NS_OK; + PrincipalInfo loadingPrincipalInfo; + rv = PrincipalToPrincipalInfo(mLoadingPrincipal, + &loadingPrincipalInfo); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-added")); + observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-added", + nullptr); + LOG(("Done offline-cache-update-added")); + } + + // mDocument is non-null if both: + // 1. this update was initiated by a document that referred a manifest + // 2. the document has not already been loaded from the application cache + // This tells the update to cache this document even in case the manifest + // has not been changed since the last fetch. + // See also nsOfflineCacheUpdate::ScheduleImplicit. + bool stickDocument = mDocument != nullptr; + + // Need to addref ourself here, because the IPC stack doesn't hold + // a reference to us. Will be released in RecvFinish() that identifies + // the work has been done. + ContentChild::GetSingleton()->SendPOfflineCacheUpdateConstructor( + this, manifestURI, documentURI, loadingPrincipalInfo, + stickDocument); + + // ContentChild::DeallocPOfflineCacheUpdate will release this. + NS_ADDREF_THIS(); + + return NS_OK; +} + +bool +OfflineCacheUpdateChild::RecvAssociateDocuments(const nsCString &cacheGroupId, + const nsCString &cacheClientId) +{ + LOG(("OfflineCacheUpdateChild::RecvAssociateDocuments [%p, cache=%s]", this, cacheClientId.get())); + + nsresult rv; + + nsCOMPtr<nsIApplicationCache> cache = + do_CreateInstance(NS_APPLICATIONCACHE_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return true; + + cache->InitAsHandle(cacheGroupId, cacheClientId); + + if (mDocument) { + AssociateDocument(mDocument, cache); + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) + observers[i]->ApplicationCacheAvailable(cache); + + return true; +} + +bool +OfflineCacheUpdateChild::RecvNotifyStateEvent(const uint32_t &event, + const uint64_t &byteProgress) +{ + LOG(("OfflineCacheUpdateChild::RecvNotifyStateEvent [%p]", this)); + + mByteProgress = byteProgress; + + // Convert the public observer state to our internal state + switch (event) { + case nsIOfflineCacheUpdateObserver::STATE_CHECKING: + mState = STATE_CHECKING; + break; + + case nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING: + mState = STATE_DOWNLOADING; + break; + + default: + break; + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) + observers[i]->UpdateStateChanged(this, event); + + return true; +} + +bool +OfflineCacheUpdateChild::RecvFinish(const bool &succeeded, + const bool &isUpgrade) +{ + LOG(("OfflineCacheUpdateChild::RecvFinish [%p]", this)); + + RefPtr<OfflineCacheUpdateChild> kungFuDeathGrip(this); + + mState = STATE_FINISHED; + mSucceeded = succeeded; + mIsUpgrade = isUpgrade; + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-completed")); + observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-completed", + nullptr); + LOG(("Done offline-cache-update-completed")); + } + + // This is by contract the last notification from the parent, release + // us now. This is corresponding to AddRef in Schedule(). + // TabChild::DeallocPOfflineCacheUpdate will call Release. + OfflineCacheUpdateChild::Send__delete__(this); + + return true; +} + +} // namespace docshell +} // namespace mozilla diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.h b/uriloader/prefetch/OfflineCacheUpdateChild.h new file mode 100644 index 0000000000..89d1e6f1f8 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateChild.h @@ -0,0 +1,94 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdateChild_h +#define nsOfflineCacheUpdateChild_h + +#include "mozilla/docshell/POfflineCacheUpdateChild.h" +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIDOMDocument.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "nsString.h" +#include "nsWeakReference.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace docshell { + +class OfflineCacheUpdateChild : public nsIOfflineCacheUpdate + , public POfflineCacheUpdateChild +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATE + + virtual bool + RecvNotifyStateEvent(const uint32_t& stateEvent, + const uint64_t& byteProgress) override; + + virtual bool + RecvAssociateDocuments( + const nsCString& cacheGroupId, + const nsCString& cacheClientId) override; + + virtual bool + RecvFinish(const bool& succeeded, + const bool& isUpgrade) override; + + explicit OfflineCacheUpdateChild(nsPIDOMWindowInner* aWindow); + + void SetDocument(nsIDOMDocument *aDocument); + +private: + ~OfflineCacheUpdateChild(); + + nsresult AssociateDocument(nsIDOMDocument *aDocument, + nsIApplicationCache *aApplicationCache); + void GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver> &aObservers); + nsresult Finish(); + + enum { + STATE_UNINITIALIZED, + STATE_INITIALIZED, + STATE_CHECKING, + STATE_DOWNLOADING, + STATE_CANCELLED, + STATE_FINISHED + } mState; + + bool mIsUpgrade; + bool mSucceeded; + + nsCString mUpdateDomain; + nsCOMPtr<nsIURI> mManifestURI; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + + nsCOMPtr<nsIObserverService> mObserverService; + + /* Clients watching this update for changes */ + nsCOMArray<nsIWeakReference> mWeakObservers; + nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers; + + /* Document that requested this update */ + nsCOMPtr<nsIDOMDocument> mDocument; + + /* Keep reference to the window that owns this update to call the + parent offline cache update construcor */ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + + uint64_t mByteProgress; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/OfflineCacheUpdateGlue.cpp b/uriloader/prefetch/OfflineCacheUpdateGlue.cpp new file mode 100644 index 0000000000..71ca986ffb --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateGlue.cpp @@ -0,0 +1,228 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OfflineCacheUpdateGlue.h" +#include "nsOfflineCacheUpdate.h" +#include "mozilla/Services.h" + +#include "nsIApplicationCache.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheContainer.h" +#include "nsIChannel.h" +#include "nsIDocument.h" +#include "mozilla/Logging.h" + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Info level information and places all output in +// the file offlineupdate.log +// +extern mozilla::LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +namespace mozilla { +namespace docshell { + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateGlue::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(OfflineCacheUpdateGlue, + nsIOfflineCacheUpdate, + nsIOfflineCacheUpdateObserver, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateGlue <public> +//----------------------------------------------------------------------------- + +OfflineCacheUpdateGlue::OfflineCacheUpdateGlue() +: mCoalesced(false) +{ + LOG(("OfflineCacheUpdateGlue::OfflineCacheUpdateGlue [%p]", this)); +} + +OfflineCacheUpdateGlue::~OfflineCacheUpdateGlue() +{ + LOG(("OfflineCacheUpdateGlue::~OfflineCacheUpdateGlue [%p]", this)); +} + +nsIOfflineCacheUpdate* +OfflineCacheUpdateGlue::EnsureUpdate() +{ + if (!mUpdate) { + mUpdate = new nsOfflineCacheUpdate(); + LOG(("OfflineCacheUpdateGlue [%p] is using update [%p]", this, mUpdate.get())); + } + + return mUpdate; +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::Schedule() +{ + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-added")); + observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-added", + nullptr); + LOG(("Done offline-cache-update-added")); + } + + if (!EnsureUpdate()) + return NS_ERROR_NULL_POINTER; + + // Do not use weak reference, we must survive! + mUpdate->AddObserver(this, false); + + if (mCoalesced) // already scheduled + return NS_OK; + + return mUpdate->Schedule(); +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::Init(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIDOMDocument *aDocument, + nsIFile *aCustomProfileDir) +{ + nsresult rv; + + nsAutoCString originSuffix; + rv = aLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (service) { + service->FindUpdate(aManifestURI, originSuffix, aCustomProfileDir, + getter_AddRefs(mUpdate)); + mCoalesced = !!mUpdate; + } + + if (!EnsureUpdate()) + return NS_ERROR_NULL_POINTER; + + mDocumentURI = aDocumentURI; + mLoadingPrincipal = aLoadingPrincipal; + + if (aDocument) + SetDocument(aDocument); + + if (mCoalesced) { // already initialized + LOG(("OfflineCacheUpdateGlue %p coalesced with update %p", this, mUpdate.get())); + return NS_OK; + } + + return mUpdate->Init(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr, + aCustomProfileDir); +} + +void +OfflineCacheUpdateGlue::SetDocument(nsIDOMDocument *aDocument) +{ + // The design is one document for one cache update on the content process. + NS_ASSERTION(!mDocument, + "Setting more then a single document on an instance of OfflineCacheUpdateGlue"); + + LOG(("Document %p added to update glue %p", aDocument, this)); + + // Add document only if it was not loaded from an offline cache. + // If it were loaded from an offline cache then it has already + // been associated with it and must not be again cached as + // implicit (which are the reasons we collect documents here). + nsCOMPtr<nsIDocument> document = do_QueryInterface(aDocument); + if (!document) + return; + + nsIChannel* channel = document->GetChannel(); + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(channel); + if (!appCacheChannel) + return; + + bool loadedFromAppCache; + appCacheChannel->GetLoadedFromApplicationCache(&loadedFromAppCache); + if (loadedFromAppCache) + return; + + if (EnsureUpdate()) { + mUpdate->StickDocument(mDocumentURI); + } + + mDocument = aDocument; +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::UpdateStateChanged(nsIOfflineCacheUpdate *aUpdate, uint32_t state) +{ + if (state == nsIOfflineCacheUpdateObserver::STATE_FINISHED) { + LOG(("OfflineCacheUpdateGlue got STATE_FINISHED [%p]", this)); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-completed")); + observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-completed", + nullptr); + LOG(("Done offline-cache-update-completed")); + } + + aUpdate->RemoveObserver(this); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::ApplicationCacheAvailable(nsIApplicationCache *aApplicationCache) +{ + NS_ENSURE_ARG(aApplicationCache); + + // Check that the document that requested this update was + // previously associated with an application cache. If not, it + // should be associated with the new one. + nsCOMPtr<nsIApplicationCacheContainer> container = + do_QueryInterface(mDocument); + if (!container) + return NS_OK; + + nsCOMPtr<nsIApplicationCache> existingCache; + nsresult rv = container->GetApplicationCache(getter_AddRefs(existingCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!existingCache) { + if (LOG_ENABLED()) { + nsAutoCString clientID; + if (aApplicationCache) { + aApplicationCache->GetClientID(clientID); + } + LOG(("Update %p: associating app cache %s to document %p", + this, clientID.get(), mDocument.get())); + } + + rv = container->SetApplicationCache(aApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +} // namespace docshell +} // namespace mozilla diff --git a/uriloader/prefetch/OfflineCacheUpdateGlue.h b/uriloader/prefetch/OfflineCacheUpdateGlue.h new file mode 100644 index 0000000000..92201ec82b --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateGlue.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdateGlue_h +#define nsOfflineCacheUpdateGlue_h + +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsOfflineCacheUpdate; + +namespace mozilla { +namespace docshell { + +// Like FORWARD_SAFE except methods: +// Schedule +// Init +#define NS_ADJUSTED_FORWARD_NSIOFFLINECACHEUPDATE(_to) \ + NS_IMETHOD GetStatus(uint16_t *aStatus) override { return !_to ? NS_ERROR_NULL_POINTER : _to->GetStatus(aStatus); } \ + NS_IMETHOD GetPartial(bool *aPartial) override { return !_to ? NS_ERROR_NULL_POINTER : _to->GetPartial(aPartial); } \ + NS_IMETHOD GetIsUpgrade(bool *aIsUpgrade) override { return !_to ? NS_ERROR_NULL_POINTER : _to->GetIsUpgrade(aIsUpgrade); } \ + NS_IMETHOD GetUpdateDomain(nsACString & aUpdateDomain) override { return !_to ? NS_ERROR_NULL_POINTER : _to->GetUpdateDomain(aUpdateDomain); } \ + NS_IMETHOD GetManifestURI(nsIURI **aManifestURI) override { return !_to ? NS_ERROR_NULL_POINTER : _to->GetManifestURI(aManifestURI); } \ + NS_IMETHOD GetSucceeded(bool *aSucceeded) override { return !_to ? NS_ERROR_NULL_POINTER : _to->GetSucceeded(aSucceeded); } \ + NS_IMETHOD InitPartial(nsIURI *aManifestURI, const nsACString & aClientID, nsIURI *aDocumentURI, nsIPrincipal *aLoadingPrincipal) override { return !_to ? NS_ERROR_NULL_POINTER : _to->InitPartial(aManifestURI, aClientID, aDocumentURI, aLoadingPrincipal); } \ + NS_IMETHOD InitForUpdateCheck(nsIURI *aManifestURI, nsIPrincipal* aLoadingPrincipal, nsIObserver *aObserver) override { return !_to ? NS_ERROR_NULL_POINTER : _to->InitForUpdateCheck(aManifestURI, aLoadingPrincipal, aObserver); } \ + NS_IMETHOD AddDynamicURI(nsIURI *aURI) override { return !_to ? NS_ERROR_NULL_POINTER : _to->AddDynamicURI(aURI); } \ + NS_IMETHOD AddObserver(nsIOfflineCacheUpdateObserver *aObserver, bool aHoldWeak) override { return !_to ? NS_ERROR_NULL_POINTER : _to->AddObserver(aObserver, aHoldWeak); } \ + NS_IMETHOD RemoveObserver(nsIOfflineCacheUpdateObserver *aObserver) override { return !_to ? NS_ERROR_NULL_POINTER : _to->RemoveObserver(aObserver); } \ + NS_IMETHOD GetByteProgress(uint64_t * _result) override { return !_to ? NS_ERROR_NULL_POINTER : _to->GetByteProgress(_result); } \ + NS_IMETHOD Cancel() override { return !_to ? NS_ERROR_NULL_POINTER : _to->Cancel(); } + +class OfflineCacheUpdateGlue final : public nsSupportsWeakReference + , public nsIOfflineCacheUpdate + , public nsIOfflineCacheUpdateObserver +{ +public: + NS_DECL_ISUPPORTS + +private: + nsIOfflineCacheUpdate* EnsureUpdate(); + +public: + NS_ADJUSTED_FORWARD_NSIOFFLINECACHEUPDATE(EnsureUpdate()) + NS_IMETHOD Schedule(void) override; + NS_IMETHOD Init(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIDOMDocument *aDocument, + nsIFile *aCustomProfileDir) override; + + NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER + + OfflineCacheUpdateGlue(); + + void SetDocument(nsIDOMDocument *aDocument); + +private: + ~OfflineCacheUpdateGlue(); + + RefPtr<nsOfflineCacheUpdate> mUpdate; + bool mCoalesced; + + /* Document that requested this update */ + nsCOMPtr<nsIDOMDocument> mDocument; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/OfflineCacheUpdateParent.cpp b/uriloader/prefetch/OfflineCacheUpdateParent.cpp new file mode 100644 index 0000000000..0381ec3f62 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateParent.cpp @@ -0,0 +1,294 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OfflineCacheUpdateParent.h" + +#include "BackgroundUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/TabParent.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" +#include "nsOfflineCacheUpdate.h" +#include "nsIApplicationCache.h" +#include "nsIScriptSecurityManager.h" +#include "nsNetUtil.h" + +using namespace mozilla::ipc; +using mozilla::BasePrincipal; +using mozilla::DocShellOriginAttributes; +using mozilla::PrincipalOriginAttributes; +using mozilla::dom::TabParent; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +extern mozilla::LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +namespace mozilla { +namespace docshell { + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateParent::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(OfflineCacheUpdateParent, + nsIOfflineCacheUpdateObserver, + nsILoadContext) + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateParent <public> +//----------------------------------------------------------------------------- + + +OfflineCacheUpdateParent::OfflineCacheUpdateParent() + : mIPCClosed(false) +{ + // Make sure the service has been initialized + nsOfflineCacheUpdateService::EnsureService(); + + LOG(("OfflineCacheUpdateParent::OfflineCacheUpdateParent [%p]", this)); +} + +OfflineCacheUpdateParent::~OfflineCacheUpdateParent() +{ + LOG(("OfflineCacheUpdateParent::~OfflineCacheUpdateParent [%p]", this)); +} + +void +OfflineCacheUpdateParent::ActorDestroy(ActorDestroyReason why) +{ + mIPCClosed = true; +} + +nsresult +OfflineCacheUpdateParent::Schedule(const URIParams& aManifestURI, + const URIParams& aDocumentURI, + const PrincipalInfo& aLoadingPrincipalInfo, + const bool& stickDocument) +{ + LOG(("OfflineCacheUpdateParent::RecvSchedule [%p]", this)); + + nsresult rv; + + RefPtr<nsOfflineCacheUpdate> update; + nsCOMPtr<nsIURI> manifestURI = DeserializeURI(aManifestURI); + if (!manifestURI) + return NS_ERROR_FAILURE; + + mLoadingPrincipal = PrincipalInfoToPrincipal(aLoadingPrincipalInfo, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) + return NS_ERROR_FAILURE; + + bool offlinePermissionAllowed = false; + + rv = service->OfflineAppAllowed( + mLoadingPrincipal, nullptr, &offlinePermissionAllowed); + NS_ENSURE_SUCCESS(rv, rv); + + if (!offlinePermissionAllowed) + return NS_ERROR_DOM_SECURITY_ERR; + + nsCOMPtr<nsIURI> documentURI = DeserializeURI(aDocumentURI); + if (!documentURI) + return NS_ERROR_FAILURE; + + if (!NS_SecurityCompareURIs(manifestURI, documentURI, false)) + return NS_ERROR_DOM_SECURITY_ERR; + + nsAutoCString originSuffix; + rv = mLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + service->FindUpdate(manifestURI, + originSuffix, + nullptr, + getter_AddRefs(update)); + if (!update) { + update = new nsOfflineCacheUpdate(); + + // Leave aDocument argument null. Only glues and children keep + // document instances. + rv = update->Init(manifestURI, documentURI, mLoadingPrincipal, nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + // Must add before Schedule() call otherwise we would miss + // oncheck event notification. + update->AddObserver(this, false); + + rv = update->Schedule(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + update->AddObserver(this, false); + } + + if (stickDocument) { + nsCOMPtr<nsIURI> stickURI; + documentURI->Clone(getter_AddRefs(stickURI)); + update->StickDocument(stickURI); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::UpdateStateChanged(nsIOfflineCacheUpdate *aUpdate, uint32_t state) +{ + if (mIPCClosed) + return NS_ERROR_UNEXPECTED; + + LOG(("OfflineCacheUpdateParent::StateEvent [%p]", this)); + + uint64_t byteProgress; + aUpdate->GetByteProgress(&byteProgress); + Unused << SendNotifyStateEvent(state, byteProgress); + + if (state == nsIOfflineCacheUpdateObserver::STATE_FINISHED) { + // Tell the child the particulars after the update has finished. + // Sending the Finish event will release the child side of the protocol + // and notify "offline-cache-update-completed" on the child process. + bool isUpgrade; + aUpdate->GetIsUpgrade(&isUpgrade); + bool succeeded; + aUpdate->GetSucceeded(&succeeded); + + Unused << SendFinish(succeeded, isUpgrade); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::ApplicationCacheAvailable(nsIApplicationCache *aApplicationCache) +{ + if (mIPCClosed) + return NS_ERROR_UNEXPECTED; + + NS_ENSURE_ARG(aApplicationCache); + + nsCString cacheClientId; + aApplicationCache->GetClientID(cacheClientId); + nsCString cacheGroupId; + aApplicationCache->GetGroupID(cacheGroupId); + + Unused << SendAssociateDocuments(cacheGroupId, cacheClientId); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateParent::nsILoadContext +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetAssociatedWindow(mozIDOMWindowProxy** aAssociatedWindow) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetTopWindow(mozIDOMWindowProxy** aTopWindow) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetTopFrameElement(nsIDOMElement** aElement) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetNestedFrameId(uint64_t* aId) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetIsContent(bool *aIsContent) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetUsePrivateBrowsing(bool *aUsePrivateBrowsing) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP +OfflineCacheUpdateParent::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::SetPrivateBrowsing(bool aUsePrivateBrowsing) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetUseRemoteTabs(bool *aUseRemoteTabs) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::SetRemoteTabs(bool aUseRemoteTabs) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetIsInIsolatedMozBrowserElement(bool *aIsInIsolatedMozBrowserElement) +{ + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + return mLoadingPrincipal->GetIsInIsolatedMozBrowserElement(aIsInIsolatedMozBrowserElement); +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetAppId(uint32_t *aAppId) +{ + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + return mLoadingPrincipal->GetAppId(aAppId); +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetOriginAttributes(JS::MutableHandleValue aAttrs) +{ + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + MOZ_ASSERT(cx); + + nsresult rv = mLoadingPrincipal->GetOriginAttributes(cx, aAttrs); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::IsTrackingProtectionOn(bool* aIsTrackingProtectionOn) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace docshell +} // namespace mozilla diff --git a/uriloader/prefetch/OfflineCacheUpdateParent.h b/uriloader/prefetch/OfflineCacheUpdateParent.h new file mode 100644 index 0000000000..f6dbc1cb2a --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateParent.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdateParent_h +#define nsOfflineCacheUpdateParent_h + +#include "mozilla/docshell/POfflineCacheUpdateParent.h" +#include "mozilla/BasePrincipal.h" +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsILoadContext.h" + +class nsIPrincipal; + +namespace mozilla { + +namespace ipc { +class URIParams; +} // namespace ipc + +namespace docshell { + +class OfflineCacheUpdateParent : public POfflineCacheUpdateParent + , public nsIOfflineCacheUpdateObserver + , public nsILoadContext +{ + typedef mozilla::ipc::URIParams URIParams; + typedef mozilla::ipc::PrincipalInfo PrincipalInfo; + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER + NS_DECL_NSILOADCONTEXT + + nsresult + Schedule(const URIParams& manifestURI, + const URIParams& documentURI, + const PrincipalInfo& loadingPrincipalInfo, + const bool& stickDocument); + + void + StopSendingMessagesToChild() + { + mIPCClosed = true; + } + + explicit OfflineCacheUpdateParent(); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; +private: + ~OfflineCacheUpdateParent(); + + bool mIPCClosed; + + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/POfflineCacheUpdate.ipdl b/uriloader/prefetch/POfflineCacheUpdate.ipdl new file mode 100644 index 0000000000..e624752522 --- /dev/null +++ b/uriloader/prefetch/POfflineCacheUpdate.ipdl @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PContent; + +namespace mozilla { +namespace docshell { + +//------------------------------------------------------------------- +protocol POfflineCacheUpdate +{ + manager PContent; + +parent: + async __delete__(); + +child: + async NotifyStateEvent(uint32_t stateEvent, uint64_t byteProgress); + async AssociateDocuments(nsCString cacheGroupId, nsCString cacheClientId); + async Finish(bool succeeded, bool isUpgrade); +}; + +} +} diff --git a/uriloader/prefetch/moz.build b/uriloader/prefetch/moz.build new file mode 100644 index 0000000000..348d57a92c --- /dev/null +++ b/uriloader/prefetch/moz.build @@ -0,0 +1,45 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPIDL_SOURCES += [ + 'nsIOfflineCacheUpdate.idl', + 'nsIPrefetchService.idl', +] + +XPIDL_MODULE = 'prefetch' + +EXPORTS += [ + 'nsCPrefetchService.h', +] + +EXPORTS.mozilla.docshell += [ + 'OfflineCacheUpdateChild.h', + 'OfflineCacheUpdateParent.h', +] + +UNIFIED_SOURCES += [ + 'nsOfflineCacheUpdate.cpp', + 'nsOfflineCacheUpdateService.cpp', + 'nsPrefetchService.cpp', + 'OfflineCacheUpdateChild.cpp', + 'OfflineCacheUpdateGlue.cpp', + 'OfflineCacheUpdateParent.cpp', +] + +IPDL_SOURCES += [ + 'POfflineCacheUpdate.ipdl', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +LOCAL_INCLUDES += [ + '/dom/base', +] + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/uriloader/prefetch/nsCPrefetchService.h b/uriloader/prefetch/nsCPrefetchService.h new file mode 100644 index 0000000000..d74d89fe7b --- /dev/null +++ b/uriloader/prefetch/nsCPrefetchService.h @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsCPrefetchService_h__ +#define nsCPrefetchService_h__ + +#include "nsIPrefetchService.h" + +/** + * nsPrefetchService : nsIPrefetchService + */ +#define NS_PREFETCHSERVICE_CONTRACTID \ + "@mozilla.org/prefetch-service;1" +#define NS_PREFETCHSERVICE_CID \ +{ /* 6b8bdffc-3394-417d-be83-a81b7c0f63bf */ \ + 0x6b8bdffc, \ + 0x3394, \ + 0x417d, \ + {0xbe, 0x83, 0xa8, 0x1b, 0x7c, 0x0f, 0x63, 0xbf} \ +} + +/** + * nsOfflineCacheUpdateService : nsIOfflineCacheUpdateService + */ + +#define NS_OFFLINECACHEUPDATESERVICE_CONTRACTID \ + "@mozilla.org/offlinecacheupdate-service;1" +#define NS_OFFLINECACHEUPDATESERVICE_CID \ +{ /* ec06f3fc-70db-4ecd-94e0-a6e91ca44d8a */ \ + 0xec06f3fc, \ + 0x70db, \ + 0x4ecd , \ + {0x94, 0xe0, 0xa6, 0xe9, 0x1c, 0xa4, 0x4d, 0x8a} \ +} + +/** + * nsOfflineCacheUpdate : nsIOfflineCacheUpdate + */ + +#define NS_OFFLINECACHEUPDATE_CONTRACTID \ + "@mozilla.org/offlinecacheupdate;1" +#define NS_OFFLINECACHEUPDATE_CID \ +{ /* e56f5e01-c7cc-4675-a9d7-b8f1e4127295 */ \ + 0xe56f5e01, \ + 0xc7cc, \ + 0x4675, \ + {0xa9, 0xd7, 0xb8, 0xf1, 0xe4, 0x12, 0x72, 0x95} \ +} + + +#endif // !nsCPrefetchService_h__ diff --git a/uriloader/prefetch/nsIOfflineCacheUpdate.idl b/uriloader/prefetch/nsIOfflineCacheUpdate.idl new file mode 100644 index 0000000000..1308a8de27 --- /dev/null +++ b/uriloader/prefetch/nsIOfflineCacheUpdate.idl @@ -0,0 +1,292 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindow; +interface nsIURI; +interface nsIDOMNode; +interface nsIDOMDocument; +interface nsIOfflineCacheUpdate; +interface nsIPrincipal; +interface nsIPrefBranch; +interface nsIApplicationCache; +interface nsIFile; +interface nsIObserver; + +[scriptable, uuid(47360d57-8ef4-4a5d-8865-1a27a739ad1a)] +interface nsIOfflineCacheUpdateObserver : nsISupports { + const unsigned long STATE_ERROR = 1; + const unsigned long STATE_CHECKING = 2; + const unsigned long STATE_NOUPDATE = 3; + const unsigned long STATE_OBSOLETE = 4; + const unsigned long STATE_DOWNLOADING = 5; + const unsigned long STATE_ITEMSTARTED = 6; + const unsigned long STATE_ITEMCOMPLETED = 7; + const unsigned long STATE_ITEMPROGRESS = 8; + const unsigned long STATE_FINISHED = 10; + + /** + * aUpdate has changed its state. + * + * @param aUpdate + * The nsIOfflineCacheUpdate being processed. + * @param event + * See enumeration above + */ + void updateStateChanged(in nsIOfflineCacheUpdate aUpdate, in uint32_t state); + + /** + * Informs the observer about an application being available to associate. + * + * @param applicationCache + * The application cache instance that has been created or found by the + * update to associate with + */ + void applicationCacheAvailable(in nsIApplicationCache applicationCache); +}; + +/** + * An nsIOfflineCacheUpdate is used to update an application's offline + * resources. + * + * It can be used to perform partial or complete updates. + * + * One update object will be updating at a time. The active object will + * load its items one by one, sending itemCompleted() to any registered + * observers. + */ +[scriptable, uuid(6e3e26ea-45b2-4db7-9e4a-93b965679298)] +interface nsIOfflineCacheUpdate : nsISupports { + /** + * Fetch the status of the running update. This will return a value + * defined in nsIDOMOfflineResourceList. + */ + readonly attribute unsigned short status; + + /** + * TRUE if the update is being used to add specific resources. + * FALSE if the complete cache update process is happening. + */ + readonly attribute boolean partial; + + /** + * TRUE if this is an upgrade attempt, FALSE if it is a new cache + * attempt. + */ + readonly attribute boolean isUpgrade; + + /** + * The domain being updated, and the domain that will own any URIs added + * with this update. + */ + readonly attribute ACString updateDomain; + + /** + * The manifest for the offline application being updated. + */ + readonly attribute nsIURI manifestURI; + + /** + * TRUE if the cache update completed successfully. + */ + readonly attribute boolean succeeded; + + /** + * Initialize the update. + * + * @param aManifestURI + * The manifest URI to be checked. + * @param aDocumentURI + * The page that is requesting the update. + * @param aLoadingPrincipal + * The principal of the page that is requesting the update. + */ + void init(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in nsIDOMDocument aDocument, + [optional] in nsIFile aCustomProfileDir); + + /** + * Initialize the update for partial processing. + * + * @param aManifestURI + * The manifest URI of the related cache. + * @param aClientID + * Client ID of the cache to store resource to. This ClientID + * must be ID of cache in the cache group identified by + * the manifest URI passed in the first parameter. + * @param aDocumentURI + * The page that is requesting the update. May be null + * when this information is unknown. + */ + void initPartial(in nsIURI aManifestURI, in ACString aClientID, + in nsIURI aDocumentURI, in nsIPrincipal aPrincipal); + + /** + * Initialize the update to only check whether there is an update + * to the manifest available (if it has actually changed on the server). + * + * @param aManifestURI + * The manifest URI of the related cache. + * @param aObserver + * nsIObserver implementation that receives the result. + * When aTopic == "offline-cache-update-available" there is an update to + * to download. Update of the app cache will lead to a new version + * download. + * When aTopic == "offline-cache-update-unavailable" then there is no + * update available (the manifest has not changed on the server). + */ + void initForUpdateCheck(in nsIURI aManifestURI, + in nsIPrincipal aLoadingPrincipal, + in nsIObserver aObserver); + + /** + * Add a dynamic URI to the offline cache as part of the update. + * + * @param aURI + * The URI to add. + */ + void addDynamicURI(in nsIURI aURI); + + /** + * Add the update to the offline update queue. An offline-cache-update-added + * event will be sent to the observer service. + */ + void schedule(); + + /** + * Observe loads that are added to the update. + * + * @param aObserver + * object that notifications will be sent to. + * @param aHoldWeak + * TRUE if you want the update to hold a weak reference to the + * observer, FALSE for a strong reference. + */ + void addObserver(in nsIOfflineCacheUpdateObserver aObserver, + in boolean aHoldWeak); + + /** + * Remove an observer from the update. + * + * @param aObserver + * the observer to remove. + */ + void removeObserver(in nsIOfflineCacheUpdateObserver aObserver); + + /** + * Cancel the update when still in progress. This stops all running resource + * downloads and discards the downloaded cache version. Throws when update + * has already finished and made the new cache version active. + */ + void cancel(); + + /** + * Return the number of bytes downloaded so far + */ + readonly attribute uint64_t byteProgress; +}; + +[scriptable, uuid(44971e74-37e4-4140-8677-a4cf213a3f4b)] +interface nsIOfflineCacheUpdateService : nsISupports { + /** + * Constants for the offline-app permission. + * + * XXX: This isn't a great place for this, but it's really the only + * private offline-app-related interface + */ + + /** + * Allow the domain to use offline APIs, and don't warn about excessive + * usage. + */ + const unsigned long ALLOW_NO_WARN = 3; + + /** + * Access to the list of cache updates that have been scheduled. + */ + readonly attribute unsigned long numUpdates; + nsIOfflineCacheUpdate getUpdate(in unsigned long index); + + /** + * Schedule a cache update for a given offline manifest. If an + * existing update is scheduled or running, that update will be returned. + * Otherwise a new update will be scheduled. + */ + nsIOfflineCacheUpdate scheduleUpdate(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in mozIDOMWindow aWindow); + + /** + * Schedule a cache update for a given offline manifest using app cache + * bound to the given appID+inIsolatedMozBrowser flag. If an existing update + * is scheduled or running, that update will be returned. Otherwise a new + * update will be scheduled. + */ + nsIOfflineCacheUpdate scheduleAppUpdate(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in nsIFile aProfileDir); + + /** + * Schedule a cache update for a manifest when the document finishes + * loading. + */ + void scheduleOnDocumentStop(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in nsIDOMDocument aDocument); + + /** + * Schedule a check to see if an update is available. + * + * This will not update or make any changes to the appcache. + * It only notifies the observer to indicate whether the manifest has + * changed on the server (or not): a changed manifest means that an + * update is available. + * + * For arguments see nsIOfflineCacheUpdate.initForUpdateCheck() method + * description. + */ + void checkForUpdate(in nsIURI aManifestURI, + in nsIPrincipal aLoadingPrincipal, + in nsIObserver aObserver); + + /** + * Checks whether a principal should have access to the offline + * cache. + * @param aPrincipal + * The principal to check. + * @param aPrefBranch + * The pref branch to use to check the + * offline-apps.allow_by_default pref. If not specified, + * the pref service will be used. + */ + boolean offlineAppAllowed(in nsIPrincipal aPrincipal, + in nsIPrefBranch aPrefBranch); + + /** + * Checks whether a document at the given URI should have access + * to the offline cache. + * @param aURI + * The URI to check + * @param aPrefBranch + * The pref branch to use to check the + * offline-apps.allow_by_default pref. If not specified, + * the pref service will be used. + */ + boolean offlineAppAllowedForURI(in nsIURI aURI, + in nsIPrefBranch aPrefBranch); + + /** + * Sets the "offline-app" permission for the principal. + * In the single process model calls directly on permission manager. + * In the multi process model dispatches to the parent process. + */ + void allowOfflineApp(in nsIPrincipal aPrincipal); +}; diff --git a/uriloader/prefetch/nsIPrefetchService.idl b/uriloader/prefetch/nsIPrefetchService.idl new file mode 100644 index 0000000000..198320dd2d --- /dev/null +++ b/uriloader/prefetch/nsIPrefetchService.idl @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURI; +interface nsIDOMNode; +interface nsISimpleEnumerator; + +[scriptable, uuid(422a1807-4e7f-463d-b8d7-ca2ceb9b5d53)] +interface nsIPrefetchService : nsISupports +{ + /** + * Enqueue a request to prefetch the specified URI. + * + * @param aURI the URI of the document to prefetch + * @param aReferrerURI the URI of the referring page + * @param aSource the DOM node (such as a <link> tag) that requested this + * fetch, or null if the prefetch was not requested by a DOM node. + * @param aExplicit the link element has an explicit prefetch link type + */ + void prefetchURI(in nsIURI aURI, + in nsIURI aReferrerURI, + in nsIDOMNode aSource, + in boolean aExplicit); + + /** + * Find out if there are any prefetches running or queued + */ + boolean hasMoreElements(); + + /** + * Cancel prefetch + */ + void cancelPrefetchURI(in nsIURI aURI, in nsIDOMNode aSource); +}; diff --git a/uriloader/prefetch/nsOfflineCacheUpdate.cpp b/uriloader/prefetch/nsOfflineCacheUpdate.cpp new file mode 100644 index 0000000000..4b6cd4d0cf --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdate.cpp @@ -0,0 +1,2471 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOfflineCacheUpdate.h" + +#include "nsCPrefetchService.h" +#include "nsCURILoader.h" +#include "nsIApplicationCacheContainer.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheService.h" +#include "nsICachingChannel.h" +#include "nsIContent.h" +#include "mozilla/dom/Element.h" +#include "nsIDocumentLoader.h" +#include "nsIDOMElement.h" +#include "nsIDOMWindow.h" +#include "nsIDOMOfflineResourceList.h" +#include "nsIDocument.h" +#include "nsIObserverService.h" +#include "nsIURL.h" +#include "nsIWebProgress.h" +#include "nsICryptoHash.h" +#include "nsICacheEntry.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "nsIConsoleService.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "nsContentUtils.h" +#include "nsIPrincipal.h" + +#include "nsXULAppAPI.h" + +using namespace mozilla; + +static const uint32_t kRescheduleLimit = 3; +// Max number of retries for every entry of pinned app. +static const uint32_t kPinnedEntryRetriesLimit = 3; +// Maximum number of parallel items loads +static const uint32_t kParallelLoadLimit = 15; + +// Quota for offline apps when preloading +static const int32_t kCustomProfileQuota = 512000; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +extern LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +class AutoFreeArray { +public: + AutoFreeArray(uint32_t count, char **values) + : mCount(count), mValues(values) {}; + ~AutoFreeArray() { NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(mCount, mValues); } +private: + uint32_t mCount; + char **mValues; +}; + +namespace { + +nsresult +DropReferenceFromURL(nsIURI * aURI) +{ + // XXXdholbert If this SetRef fails, callers of this method probably + // want to call aURI->CloneIgnoringRef() and use the result of that. + return aURI->SetRef(EmptyCString()); +} + +void +LogToConsole(const char * message, nsOfflineCacheUpdateItem * item = nullptr) +{ + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) + { + nsAutoString messageUTF16 = NS_ConvertUTF8toUTF16(message); + if (item && item->mURI) { + messageUTF16.AppendLiteral(", URL="); + messageUTF16.Append( + NS_ConvertUTF8toUTF16(item->mURI->GetSpecOrDefault())); + } + consoleService->LogStringMessage(messageUTF16.get()); + } +} + +} // namespace + +//----------------------------------------------------------------------------- +// nsManifestCheck +//----------------------------------------------------------------------------- + +class nsManifestCheck final : public nsIStreamListener + , public nsIChannelEventSink + , public nsIInterfaceRequestor +{ +public: + nsManifestCheck(nsOfflineCacheUpdate *aUpdate, + nsIURI *aURI, + nsIURI *aReferrerURI, + nsIPrincipal* aLoadingPrincipal) + : mUpdate(aUpdate) + , mURI(aURI) + , mReferrerURI(aReferrerURI) + , mLoadingPrincipal(aLoadingPrincipal) + {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + + nsresult Begin(); + +private: + + ~nsManifestCheck() {} + + static nsresult ReadManifest(nsIInputStream *aInputStream, + void *aClosure, + const char *aFromSegment, + uint32_t aOffset, + uint32_t aCount, + uint32_t *aBytesConsumed); + + RefPtr<nsOfflineCacheUpdate> mUpdate; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mReferrerURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsICryptoHash> mManifestHash; + nsCOMPtr<nsIChannel> mChannel; +}; + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsISupports +//----------------------------------------------------------------------------- +NS_IMPL_ISUPPORTS(nsManifestCheck, + nsIRequestObserver, + nsIStreamListener, + nsIChannelEventSink, + nsIInterfaceRequestor) + +//----------------------------------------------------------------------------- +// nsManifestCheck <public> +//----------------------------------------------------------------------------- + +nsresult +nsManifestCheck::Begin() +{ + nsresult rv; + mManifestHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mManifestHash->Init(nsICryptoHash::MD5); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewChannel(getter_AddRefs(mChannel), + mURI, + mLoadingPrincipal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_OTHER, + nullptr, // loadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_BYPASS_CACHE); + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr<nsIHttpChannel> httpChannel = + do_QueryInterface(mChannel); + if (httpChannel) { + httpChannel->SetReferrer(mReferrerURI); + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("offline-resource"), + false); + } + + return mChannel->AsyncOpen2(this); +} + +//----------------------------------------------------------------------------- +// nsManifestCheck <public> +//----------------------------------------------------------------------------- + +/* static */ nsresult +nsManifestCheck::ReadManifest(nsIInputStream *aInputStream, + void *aClosure, + const char *aFromSegment, + uint32_t aOffset, + uint32_t aCount, + uint32_t *aBytesConsumed) +{ + nsManifestCheck *manifestCheck = + static_cast<nsManifestCheck*>(aClosure); + + nsresult rv; + *aBytesConsumed = aCount; + + rv = manifestCheck->mManifestHash->Update( + reinterpret_cast<const uint8_t *>(aFromSegment), aCount); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsManifestCheck::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + return NS_OK; +} + +NS_IMETHODIMP +nsManifestCheck::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + nsIInputStream *aStream, + uint64_t aOffset, + uint32_t aCount) +{ + uint32_t bytesRead; + aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead); + return NS_OK; +} + +NS_IMETHODIMP +nsManifestCheck::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatus) +{ + nsAutoCString manifestHash; + if (NS_SUCCEEDED(aStatus)) { + mManifestHash->Finish(true, manifestHash); + } + + mUpdate->ManifestCheckCompleted(aStatus, manifestHash); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsManifestCheck::GetInterface(const nsIID &aIID, void **aResult) +{ + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIChannelEventSink *>(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsManifestCheck::AsyncOnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback *callback) +{ + // Redirects should cause the load (and therefore the update) to fail. + if (aFlags & nsIChannelEventSink::REDIRECT_INTERNAL) { + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + + LogToConsole("Manifest check failed because its response is a redirect"); + + aOldChannel->Cancel(NS_ERROR_ABORT); + return NS_ERROR_ABORT; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateItem, + nsIRequestObserver, + nsIStreamListener, + nsIRunnable, + nsIInterfaceRequestor, + nsIChannelEventSink) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem <public> +//----------------------------------------------------------------------------- + +nsOfflineCacheUpdateItem::nsOfflineCacheUpdateItem(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache *aApplicationCache, + nsIApplicationCache *aPreviousApplicationCache, + uint32_t type, + uint32_t loadFlags) + : mURI(aURI) + , mReferrerURI(aReferrerURI) + , mLoadingPrincipal(aLoadingPrincipal) + , mApplicationCache(aApplicationCache) + , mPreviousApplicationCache(aPreviousApplicationCache) + , mItemType(type) + , mLoadFlags(loadFlags) + , mChannel(nullptr) + , mState(LoadStatus::UNINITIALIZED) + , mBytesRead(0) +{ +} + +nsOfflineCacheUpdateItem::~nsOfflineCacheUpdateItem() +{ +} + +nsresult +nsOfflineCacheUpdateItem::OpenChannel(nsOfflineCacheUpdate *aUpdate) +{ + if (LOG_ENABLED()) { + LOG(("%p: Opening channel for %s", this, + mURI->GetSpecOrDefault().get())); + } + + if (mUpdate) { + // Holding a reference to the update means this item is already + // in progress (has a channel, or is just in between OnStopRequest() + // and its Run() call. We must never open channel on this item again. + LOG((" %p is already running! ignoring", this)); + return NS_ERROR_ALREADY_OPENED; + } + + nsresult rv = nsOfflineCacheUpdate::GetCacheKey(mURI, mCacheKey); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t flags = nsIRequest::LOAD_BACKGROUND | + nsICachingChannel::LOAD_ONLY_IF_MODIFIED; + + if (mApplicationCache == mPreviousApplicationCache) { + // Same app cache to read from and to write to is used during + // an only-update-check procedure. Here we protect the existing + // cache from being modified. + flags |= nsIRequest::INHIBIT_CACHING; + } + + flags |= mLoadFlags; + + rv = NS_NewChannel(getter_AddRefs(mChannel), + mURI, + mLoadingPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER, + nullptr, // aLoadGroup + this, // aCallbacks + flags); + + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(mChannel, &rv); + + // Support for nsIApplicationCacheChannel is required. + NS_ENSURE_SUCCESS(rv, rv); + + // Use the existing application cache as the cache to check. + rv = appCacheChannel->SetApplicationCache(mPreviousApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the new application cache as the target for write. + rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr<nsIHttpChannel> httpChannel = + do_QueryInterface(mChannel); + if (httpChannel) { + httpChannel->SetReferrer(mReferrerURI); + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("offline-resource"), + false); + } + + rv = mChannel->AsyncOpen2(this); + NS_ENSURE_SUCCESS(rv, rv); + + mUpdate = aUpdate; + + mState = LoadStatus::REQUESTED; + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdateItem::Cancel() +{ + if (mChannel) { + mChannel->Cancel(NS_ERROR_ABORT); + mChannel = nullptr; + } + + mState = LoadStatus::UNINITIALIZED; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + mState = LoadStatus::RECEIVING; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + nsIInputStream *aStream, + uint64_t aOffset, + uint32_t aCount) +{ + uint32_t bytesRead = 0; + aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead); + mBytesRead += bytesRead; + LOG(("loaded %u bytes into offline cache [offset=%llu]\n", + bytesRead, aOffset)); + + mUpdate->OnByteProgress(bytesRead); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatus) +{ + if (LOG_ENABLED()) { + LOG(("%p: Done fetching offline item %s [status=%x]\n", + this, mURI->GetSpecOrDefault().get(), aStatus)); + } + + if (mBytesRead == 0 && aStatus == NS_OK) { + // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was + // specified), but the object should report loadedSize as if it + // did. + mChannel->GetContentLength(&mBytesRead); + mUpdate->OnByteProgress(mBytesRead); + } + + if (NS_FAILED(aStatus)) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + bool isNoStore; + if (NS_SUCCEEDED(httpChannel->IsNoStoreResponse(&isNoStore)) + && isNoStore) { + LogToConsole("Offline cache manifest item has Cache-control: no-store header", + this); + } + } + } + + // We need to notify the update that the load is complete, but we + // want to give the channel a chance to close the cache entries. + NS_DispatchToCurrentThread(this); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIRunnable +//----------------------------------------------------------------------------- +NS_IMETHODIMP +nsOfflineCacheUpdateItem::Run() +{ + // Set mState to LOADED here rather than in OnStopRequest to prevent + // race condition when checking state of all mItems in ProcessNextURI(). + // If state would have been set in OnStopRequest we could mistakenly + // take this item as already finished and finish the update process too + // early when ProcessNextURI() would get called between OnStopRequest() + // and Run() of this item. Finish() would then have been called twice. + mState = LoadStatus::LOADED; + + RefPtr<nsOfflineCacheUpdate> update; + update.swap(mUpdate); + update->LoadCompleted(this); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::GetInterface(const nsIID &aIID, void **aResult) +{ + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIChannelEventSink *>(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::AsyncOnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback *cb) +{ + if (!(aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)) { + // Don't allow redirect in case of non-internal redirect and cancel + // the channel to clean the cache entry. + LogToConsole("Offline cache manifest failed because an item redirects", this); + + aOldChannel->Cancel(NS_ERROR_ABORT); + return NS_ERROR_ABORT; + } + + nsCOMPtr<nsIURI> newURI; + nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(aNewChannel); + if (appCacheChannel) { + rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoCString oldScheme; + mURI->GetScheme(oldScheme); + + bool match; + if (NS_FAILED(newURI->SchemeIs(oldScheme.get(), &match)) || !match) { + LOG(("rejected: redirected to a different scheme\n")); + return NS_ERROR_ABORT; + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("offline-resource"), + false); + + mChannel = aNewChannel; + + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +nsresult +nsOfflineCacheUpdateItem::GetRequestSucceeded(bool * succeeded) +{ + *succeeded = false; + + if (!mChannel) + return NS_OK; + + nsresult rv; + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool reqSucceeded; + rv = httpChannel->GetRequestSucceeded(&reqSucceeded); + if (NS_ERROR_NOT_AVAILABLE == rv) + return NS_OK; + NS_ENSURE_SUCCESS(rv, rv); + + if (!reqSucceeded) { + LOG(("Request failed")); + return NS_OK; + } + + nsresult channelStatus; + rv = httpChannel->GetStatus(&channelStatus); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_FAILED(channelStatus)) { + LOG(("Channel status=0x%08x", channelStatus)); + return NS_OK; + } + + *succeeded = true; + return NS_OK; +} + +bool +nsOfflineCacheUpdateItem::IsScheduled() +{ + return mState == LoadStatus::UNINITIALIZED; +} + +bool +nsOfflineCacheUpdateItem::IsInProgress() +{ + return mState == LoadStatus::REQUESTED || + mState == LoadStatus::RECEIVING; +} + +bool +nsOfflineCacheUpdateItem::IsCompleted() +{ + return mState == LoadStatus::LOADED; +} + +nsresult +nsOfflineCacheUpdateItem::GetStatus(uint16_t *aStatus) +{ + if (!mChannel) { + *aStatus = 0; + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t httpStatus; + rv = httpChannel->GetResponseStatus(&httpStatus); + if (rv == NS_ERROR_NOT_AVAILABLE) { + *aStatus = 0; + return NS_OK; + } + + NS_ENSURE_SUCCESS(rv, rv); + *aStatus = uint16_t(httpStatus); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineManifestItem +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// nsOfflineManifestItem <public> +//----------------------------------------------------------------------------- + +nsOfflineManifestItem::nsOfflineManifestItem(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache *aApplicationCache, + nsIApplicationCache *aPreviousApplicationCache) + : nsOfflineCacheUpdateItem(aURI, aReferrerURI, aLoadingPrincipal, + aApplicationCache, aPreviousApplicationCache, + nsIApplicationCache::ITEM_MANIFEST, 0) + , mParserState(PARSE_INIT) + , mNeedsUpdate(true) + , mStrictFileOriginPolicy(false) + , mManifestHashInitialized(false) +{ + ReadStrictFileOriginPolicyPref(); +} + +nsOfflineManifestItem::~nsOfflineManifestItem() +{ +} + +//----------------------------------------------------------------------------- +// nsOfflineManifestItem <private> +//----------------------------------------------------------------------------- + +/* static */ +nsresult +nsOfflineManifestItem::ReadManifest(nsIInputStream *aInputStream, + void *aClosure, + const char *aFromSegment, + uint32_t aOffset, + uint32_t aCount, + uint32_t *aBytesConsumed) +{ + nsOfflineManifestItem *manifest = + static_cast<nsOfflineManifestItem*>(aClosure); + + nsresult rv; + + *aBytesConsumed = aCount; + + if (manifest->mParserState == PARSE_ERROR) { + // parse already failed, ignore this + return NS_OK; + } + + if (!manifest->mManifestHashInitialized) { + // Avoid re-creation of crypto hash when it fails from some reason the first time + manifest->mManifestHashInitialized = true; + + manifest->mManifestHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv); + if (NS_SUCCEEDED(rv)) { + rv = manifest->mManifestHash->Init(nsICryptoHash::MD5); + if (NS_FAILED(rv)) { + manifest->mManifestHash = nullptr; + LOG(("Could not initialize manifest hash for byte-to-byte check, rv=%08x", rv)); + } + } + } + + if (manifest->mManifestHash) { + rv = manifest->mManifestHash->Update(reinterpret_cast<const uint8_t *>(aFromSegment), aCount); + if (NS_FAILED(rv)) { + manifest->mManifestHash = nullptr; + LOG(("Could not update manifest hash, rv=%08x", rv)); + } + } + + manifest->mReadBuf.Append(aFromSegment, aCount); + + nsCString::const_iterator begin, iter, end; + manifest->mReadBuf.BeginReading(begin); + manifest->mReadBuf.EndReading(end); + + for (iter = begin; iter != end; iter++) { + if (*iter == '\r' || *iter == '\n') { + rv = manifest->HandleManifestLine(begin, iter); + + if (NS_FAILED(rv)) { + LOG(("HandleManifestLine failed with 0x%08x", rv)); + *aBytesConsumed = 0; // Avoid assertion failure in stream tee + return NS_ERROR_ABORT; + } + + begin = iter; + begin++; + } + } + + // any leftovers are saved for next time + manifest->mReadBuf = Substring(begin, end); + + return NS_OK; +} + +nsresult +nsOfflineManifestItem::AddNamespace(uint32_t namespaceType, + const nsCString &namespaceSpec, + const nsCString &data) + +{ + nsresult rv; + if (!mNamespaces) { + mNamespaces = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIApplicationCacheNamespace> ns = + do_CreateInstance(NS_APPLICATIONCACHENAMESPACE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ns->Init(namespaceType, namespaceSpec, data); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mNamespaces->AppendElement(ns, false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsOfflineManifestItem::HandleManifestLine(const nsCString::const_iterator &aBegin, + const nsCString::const_iterator &aEnd) +{ + nsCString::const_iterator begin = aBegin; + nsCString::const_iterator end = aEnd; + + // all lines ignore trailing spaces and tabs + nsCString::const_iterator last = end; + --last; + while (end != begin && (*last == ' ' || *last == '\t')) { + --end; + --last; + } + + if (mParserState == PARSE_INIT) { + // Allow a UTF-8 BOM + if (begin != end && static_cast<unsigned char>(*begin) == 0xef) { + if (++begin == end || static_cast<unsigned char>(*begin) != 0xbb || + ++begin == end || static_cast<unsigned char>(*begin) != 0xbf) { + mParserState = PARSE_ERROR; + LogToConsole("Offline cache manifest BOM error", this); + return NS_OK; + } + ++begin; + } + + const nsCSubstring &magic = Substring(begin, end); + + if (!magic.EqualsLiteral("CACHE MANIFEST")) { + mParserState = PARSE_ERROR; + LogToConsole("Offline cache manifest magic incorrect", this); + return NS_OK; + } + + mParserState = PARSE_CACHE_ENTRIES; + return NS_OK; + } + + // lines other than the first ignore leading spaces and tabs + while (begin != end && (*begin == ' ' || *begin == '\t')) + begin++; + + // ignore blank lines and comments + if (begin == end || *begin == '#') + return NS_OK; + + const nsCSubstring &line = Substring(begin, end); + + if (line.EqualsLiteral("CACHE:")) { + mParserState = PARSE_CACHE_ENTRIES; + return NS_OK; + } + + if (line.EqualsLiteral("FALLBACK:")) { + mParserState = PARSE_FALLBACK_ENTRIES; + return NS_OK; + } + + if (line.EqualsLiteral("NETWORK:")) { + mParserState = PARSE_BYPASS_ENTRIES; + return NS_OK; + } + + // Every other section type we don't know must be silently ignored. + nsCString::const_iterator lastChar = end; + if (*(--lastChar) == ':') { + mParserState = PARSE_UNKNOWN_SECTION; + return NS_OK; + } + + nsresult rv; + + switch(mParserState) { + case PARSE_INIT: + case PARSE_ERROR: { + // this should have been dealt with earlier + return NS_ERROR_FAILURE; + } + + case PARSE_UNKNOWN_SECTION: { + // just jump over + return NS_OK; + } + + case PARSE_CACHE_ENTRIES: { + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), line, nullptr, mURI); + if (NS_FAILED(rv)) + break; + if (NS_FAILED(DropReferenceFromURL(uri))) + break; + + nsAutoCString scheme; + uri->GetScheme(scheme); + + // Manifest URIs must have the same scheme as the manifest. + bool match; + if (NS_FAILED(mURI->SchemeIs(scheme.get(), &match)) || !match) + break; + + mExplicitURIs.AppendObject(uri); + + if (!NS_SecurityCompareURIs(mURI, uri, + mStrictFileOriginPolicy)) { + mAnonymousURIs.AppendObject(uri); + } + + break; + } + + case PARSE_FALLBACK_ENTRIES: { + int32_t separator = line.FindChar(' '); + if (separator == kNotFound) { + separator = line.FindChar('\t'); + if (separator == kNotFound) + break; + } + + nsCString namespaceSpec(Substring(line, 0, separator)); + nsCString fallbackSpec(Substring(line, separator + 1)); + namespaceSpec.CompressWhitespace(); + fallbackSpec.CompressWhitespace(); + + nsCOMPtr<nsIURI> namespaceURI; + rv = NS_NewURI(getter_AddRefs(namespaceURI), namespaceSpec, nullptr, mURI); + if (NS_FAILED(rv)) + break; + if (NS_FAILED(DropReferenceFromURL(namespaceURI))) + break; + rv = namespaceURI->GetAsciiSpec(namespaceSpec); + if (NS_FAILED(rv)) + break; + + nsCOMPtr<nsIURI> fallbackURI; + rv = NS_NewURI(getter_AddRefs(fallbackURI), fallbackSpec, nullptr, mURI); + if (NS_FAILED(rv)) + break; + if (NS_FAILED(DropReferenceFromURL(fallbackURI))) + break; + rv = fallbackURI->GetAsciiSpec(fallbackSpec); + if (NS_FAILED(rv)) + break; + + // Manifest and namespace must be same origin + if (!NS_SecurityCompareURIs(mURI, namespaceURI, + mStrictFileOriginPolicy)) + break; + + // Fallback and namespace must be same origin + if (!NS_SecurityCompareURIs(namespaceURI, fallbackURI, + mStrictFileOriginPolicy)) + break; + + mFallbackURIs.AppendObject(fallbackURI); + + AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_FALLBACK, + namespaceSpec, fallbackSpec); + break; + } + + case PARSE_BYPASS_ENTRIES: { + if (line[0] == '*' && (line.Length() == 1 || line[1] == ' ' || line[1] == '\t')) + { + // '*' indicates to make the online whitelist wildcard flag open, + // i.e. do allow load of resources not present in the offline cache + // or not conforming any namespace. + // We achive that simply by adding an 'empty' - i.e. universal + // namespace of BYPASS type into the cache. + AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, + EmptyCString(), EmptyCString()); + break; + } + + nsCOMPtr<nsIURI> bypassURI; + rv = NS_NewURI(getter_AddRefs(bypassURI), line, nullptr, mURI); + if (NS_FAILED(rv)) + break; + + nsAutoCString scheme; + bypassURI->GetScheme(scheme); + bool equals; + if (NS_FAILED(mURI->SchemeIs(scheme.get(), &equals)) || !equals) + break; + if (NS_FAILED(DropReferenceFromURL(bypassURI))) + break; + nsCString spec; + if (NS_FAILED(bypassURI->GetAsciiSpec(spec))) + break; + + AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, + spec, EmptyCString()); + break; + } + } + + return NS_OK; +} + +nsresult +nsOfflineManifestItem::GetOldManifestContentHash(nsIRequest *aRequest) +{ + nsresult rv; + + nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // load the main cache token that is actually the old offline cache token and + // read previous manifest content hash value + nsCOMPtr<nsISupports> cacheToken; + cachingChannel->GetCacheToken(getter_AddRefs(cacheToken)); + if (cacheToken) { + nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheDescriptor->GetMetaDataElement("offline-manifest-hash", getter_Copies(mOldManifestHashValue)); + if (NS_FAILED(rv)) + mOldManifestHashValue.Truncate(); + } + + return NS_OK; +} + +nsresult +nsOfflineManifestItem::CheckNewManifestContentHash(nsIRequest *aRequest) +{ + nsresult rv; + + if (!mManifestHash) { + // Nothing to compare against... + return NS_OK; + } + + nsCString newManifestHashValue; + rv = mManifestHash->Finish(true, mManifestHashValue); + mManifestHash = nullptr; + + if (NS_FAILED(rv)) { + LOG(("Could not finish manifest hash, rv=%08x", rv)); + // This is not critical error + return NS_OK; + } + + if (!ParseSucceeded()) { + // Parsing failed, the hash is not valid + return NS_OK; + } + + if (mOldManifestHashValue == mManifestHashValue) { + LOG(("Update not needed, downloaded manifest content is byte-for-byte identical")); + mNeedsUpdate = false; + } + + // Store the manifest content hash value to the new + // offline cache token + nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> cacheToken; + cachingChannel->GetOfflineCacheToken(getter_AddRefs(cacheToken)); + if (cacheToken) { + nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash", mManifestHashValue.get()); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +void +nsOfflineManifestItem::ReadStrictFileOriginPolicyPref() +{ + mStrictFileOriginPolicy = + Preferences::GetBool("security.fileuri.strict_origin_policy", true); +} + +NS_IMETHODIMP +nsOfflineManifestItem::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + nsresult rv; + + nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool succeeded; + rv = channel->GetRequestSucceeded(&succeeded); + NS_ENSURE_SUCCESS(rv, rv); + + if (!succeeded) { + LOG(("HTTP request failed")); + LogToConsole("Offline cache manifest HTTP request failed", this); + mParserState = PARSE_ERROR; + return NS_ERROR_ABORT; + } + + rv = GetOldManifestContentHash(aRequest); + NS_ENSURE_SUCCESS(rv, rv); + + return nsOfflineCacheUpdateItem::OnStartRequest(aRequest, aContext); +} + +NS_IMETHODIMP +nsOfflineManifestItem::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + nsIInputStream *aStream, + uint64_t aOffset, + uint32_t aCount) +{ + uint32_t bytesRead = 0; + aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead); + mBytesRead += bytesRead; + + if (mParserState == PARSE_ERROR) { + LOG(("OnDataAvailable is canceling the request due a parse error\n")); + return NS_ERROR_ABORT; + } + + LOG(("loaded %u bytes into offline cache [offset=%u]\n", + bytesRead, aOffset)); + + // All the parent method does is read and discard, don't bother + // chaining up. + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineManifestItem::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatus) +{ + if (mBytesRead == 0) { + // We didn't need to read (because LOAD_ONLY_IF_MODIFIED was + // specified). + mNeedsUpdate = false; + } else { + // Handle any leftover manifest data. + nsCString::const_iterator begin, end; + mReadBuf.BeginReading(begin); + mReadBuf.EndReading(end); + nsresult rv = HandleManifestLine(begin, end); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CheckNewManifestContentHash(aRequest); + NS_ENSURE_SUCCESS(rv, rv); + } + + return nsOfflineCacheUpdateItem::OnStopRequest(aRequest, aContext, aStatus); +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdate, + nsIOfflineCacheUpdateObserver, + nsIOfflineCacheUpdate, + nsIRunnable) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate <public> +//----------------------------------------------------------------------------- + +nsOfflineCacheUpdate::nsOfflineCacheUpdate() + : mState(STATE_UNINITIALIZED) + , mAddedItems(false) + , mPartialUpdate(false) + , mOnlyCheckUpdate(false) + , mSucceeded(true) + , mObsolete(false) + , mItemsInProgress(0) + , mRescheduleCount(0) + , mPinnedEntryRetriesCount(0) + , mPinned(false) + , mByteProgress(0) +{ +} + +nsOfflineCacheUpdate::~nsOfflineCacheUpdate() +{ + LOG(("nsOfflineCacheUpdate::~nsOfflineCacheUpdate [%p]", this)); +} + +/* static */ +nsresult +nsOfflineCacheUpdate::GetCacheKey(nsIURI *aURI, nsACString &aKey) +{ + aKey.Truncate(); + + nsCOMPtr<nsIURI> newURI; + nsresult rv = aURI->CloneIgnoringRef(getter_AddRefs(newURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = newURI->GetAsciiSpec(aKey); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::InitInternal(nsIURI *aManifestURI, + nsIPrincipal* aLoadingPrincipal) +{ + nsresult rv; + + // Only http and https applications are supported. + bool match; + rv = aManifestURI->SchemeIs("http", &match); + NS_ENSURE_SUCCESS(rv, rv); + + if (!match) { + rv = aManifestURI->SchemeIs("https", &match); + NS_ENSURE_SUCCESS(rv, rv); + if (!match) + return NS_ERROR_ABORT; + } + + mManifestURI = aManifestURI; + mLoadingPrincipal = aLoadingPrincipal; + + rv = mManifestURI->GetAsciiHost(mUpdateDomain); + NS_ENSURE_SUCCESS(rv, rv); + + mPartialUpdate = false; + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::Init(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIDOMDocument *aDocument, + nsIFile *aCustomProfileDir) +{ + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) + return NS_ERROR_FAILURE; + + LOG(("nsOfflineCacheUpdate::Init [%p]", this)); + + rv = InitInternal(aManifestURI, aLoadingPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString originSuffix; + rv = aLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + mDocumentURI = aDocumentURI; + + if (aCustomProfileDir) { + rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, mGroupID); + NS_ENSURE_SUCCESS(rv, rv); + + // Create only a new offline application cache in the custom profile + // This is a preload of a new cache. + + // XXX Custom updates don't support "updating" of an existing cache + // in the custom profile at the moment. This support can be, though, + // simply added as well when needed. + mPreviousApplicationCache = nullptr; + + rv = cacheService->CreateCustomApplicationCache(mGroupID, + aCustomProfileDir, + kCustomProfileQuota, + getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + mCustomProfileDir = aCustomProfileDir; + } + else { + rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, mGroupID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->GetActiveCache(mGroupID, + getter_AddRefs(mPreviousApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->CreateApplicationCache(mGroupID, + getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI, + nullptr, + &mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + mState = STATE_INITIALIZED; + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::InitForUpdateCheck(nsIURI *aManifestURI, + nsIPrincipal* aLoadingPrincipal, + nsIObserver *aObserver) +{ + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) + return NS_ERROR_FAILURE; + + LOG(("nsOfflineCacheUpdate::InitForUpdateCheck [%p]", this)); + + rv = InitInternal(aManifestURI, aLoadingPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString originSuffix; + rv = aLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, mGroupID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->GetActiveCache(mGroupID, + getter_AddRefs(mPreviousApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + // To load the manifest properly using current app cache to satisfy and + // also to compare the cached content hash value we have to set 'some' + // app cache to write to on the channel. Otherwise the cached version will + // be used and no actual network request will be made. We use the same + // app cache here. OpenChannel prevents caching in this case using + // INHIBIT_CACHING load flag. + mApplicationCache = mPreviousApplicationCache; + + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aManifestURI, + nullptr, + &mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + mUpdateAvailableObserver = aObserver; + mOnlyCheckUpdate = true; + + mState = STATE_INITIALIZED; + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::InitPartial(nsIURI *aManifestURI, + const nsACString& clientID, + nsIURI *aDocumentURI, + nsIPrincipal *aLoadingPrincipal) +{ + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) + return NS_ERROR_FAILURE; + + LOG(("nsOfflineCacheUpdate::InitPartial [%p]", this)); + + mPartialUpdate = true; + mDocumentURI = aDocumentURI; + mLoadingPrincipal = aLoadingPrincipal; + + mManifestURI = aManifestURI; + rv = mManifestURI->GetAsciiHost(mUpdateDomain); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->GetApplicationCache(clientID, + getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mApplicationCache) { + nsAutoCString manifestSpec; + rv = GetCacheKey(mManifestURI, manifestSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->CreateApplicationCache + (manifestSpec, getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mApplicationCache->GetManifestURI(getter_AddRefs(mManifestURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString groupID; + rv = mApplicationCache->GetGroupID(groupID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI, + nullptr, + &mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + mState = STATE_INITIALIZED; + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::HandleManifest(bool *aDoUpdate) +{ + // Be pessimistic + *aDoUpdate = false; + + bool succeeded; + nsresult rv = mManifestItem->GetRequestSucceeded(&succeeded); + NS_ENSURE_SUCCESS(rv, rv); + + if (!succeeded || !mManifestItem->ParseSucceeded()) { + return NS_ERROR_FAILURE; + } + + if (!mManifestItem->NeedsUpdate()) { + return NS_OK; + } + + // Add items requested by the manifest. + const nsCOMArray<nsIURI> &manifestURIs = mManifestItem->GetExplicitURIs(); + for (int32_t i = 0; i < manifestURIs.Count(); i++) { + rv = AddURI(manifestURIs[i], nsIApplicationCache::ITEM_EXPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + } + + const nsCOMArray<nsIURI> &anonURIs = mManifestItem->GetAnonymousURIs(); + for (int32_t i = 0; i < anonURIs.Count(); i++) { + rv = AddURI(anonURIs[i], nsIApplicationCache::ITEM_EXPLICIT, + nsIRequest::LOAD_ANONYMOUS); + NS_ENSURE_SUCCESS(rv, rv); + } + + const nsCOMArray<nsIURI> &fallbackURIs = mManifestItem->GetFallbackURIs(); + for (int32_t i = 0; i < fallbackURIs.Count(); i++) { + rv = AddURI(fallbackURIs[i], nsIApplicationCache::ITEM_FALLBACK); + NS_ENSURE_SUCCESS(rv, rv); + } + + // The document that requested the manifest is implicitly included + // as part of that manifest update. + rv = AddURI(mDocumentURI, nsIApplicationCache::ITEM_IMPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + + // Add items previously cached implicitly + rv = AddExistingItems(nsIApplicationCache::ITEM_IMPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + + // Add items requested by the script API + rv = AddExistingItems(nsIApplicationCache::ITEM_DYNAMIC); + NS_ENSURE_SUCCESS(rv, rv); + + // Add opportunistically cached items conforming current opportunistic + // namespace list + rv = AddExistingItems(nsIApplicationCache::ITEM_OPPORTUNISTIC, + &mManifestItem->GetOpportunisticNamespaces()); + NS_ENSURE_SUCCESS(rv, rv); + + *aDoUpdate = true; + + return NS_OK; +} + +bool +nsOfflineCacheUpdate::CheckUpdateAvailability() +{ + nsresult rv; + + bool succeeded; + rv = mManifestItem->GetRequestSucceeded(&succeeded); + NS_ENSURE_SUCCESS(rv, false); + + if (!succeeded || !mManifestItem->ParseSucceeded()) { + return false; + } + + if (!mPinned) { + uint16_t status; + rv = mManifestItem->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, false); + + // Treat these as there would be an update available, + // since this is indication of demand to remove this + // offline cache. + if (status == 404 || status == 410) { + return true; + } + } + + return mManifestItem->NeedsUpdate(); +} + +void +nsOfflineCacheUpdate::LoadCompleted(nsOfflineCacheUpdateItem *aItem) +{ + nsresult rv; + + LOG(("nsOfflineCacheUpdate::LoadCompleted [%p]", this)); + + if (mState == STATE_FINISHED) { + LOG((" after completion, ignoring")); + return; + } + + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + if (mState == STATE_CANCELLED) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + if (mState == STATE_CHECKING) { + // Manifest load finished. + + if (mOnlyCheckUpdate) { + Finish(); + NotifyUpdateAvailability(CheckUpdateAvailability()); + return; + } + + NS_ASSERTION(mManifestItem, + "Must have a manifest item in STATE_CHECKING."); + NS_ASSERTION(mManifestItem == aItem, + "Unexpected aItem in nsOfflineCacheUpdate::LoadCompleted"); + + // A 404 or 410 is interpreted as an intentional removal of + // the manifest file, rather than a transient server error. + // Obsolete this cache group if one of these is returned. + uint16_t status; + rv = mManifestItem->GetStatus(&status); + if (status == 404 || status == 410) { + LogToConsole("Offline cache manifest removed, cache cleared", mManifestItem); + mSucceeded = false; + if (mPreviousApplicationCache) { + if (mPinned) { + // Do not obsolete a pinned application. + NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE); + } else { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_OBSOLETE); + mObsolete = true; + } + } else { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + mObsolete = true; + } + Finish(); + return; + } + + bool doUpdate; + if (NS_FAILED(HandleManifest(&doUpdate))) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + if (!doUpdate) { + LogToConsole("Offline cache doesn't need to update", mManifestItem); + + mSucceeded = false; + + AssociateDocuments(mPreviousApplicationCache); + + ScheduleImplicit(); + + // If we didn't need an implicit update, we can + // send noupdate and end the update now. + if (!mImplicitUpdate) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE); + Finish(); + } + return; + } + + rv = mApplicationCache->MarkEntry(mManifestItem->mCacheKey, + mManifestItem->mItemType); + if (NS_FAILED(rv)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + mState = STATE_DOWNLOADING; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING); + + // Start fetching resources. + ProcessNextURI(); + + return; + } + + // Normal load finished. + if (mItemsInProgress) // Just to be safe here! + --mItemsInProgress; + + bool succeeded; + rv = aItem->GetRequestSucceeded(&succeeded); + + if (mPinned && NS_SUCCEEDED(rv) && succeeded) { + uint32_t dummy_cache_type; + rv = mApplicationCache->GetTypes(aItem->mCacheKey, &dummy_cache_type); + bool item_doomed = NS_FAILED(rv); // can not find it? -> doomed + + if (item_doomed && + mPinnedEntryRetriesCount < kPinnedEntryRetriesLimit && + (aItem->mItemType & (nsIApplicationCache::ITEM_EXPLICIT | + nsIApplicationCache::ITEM_FALLBACK))) { + rv = EvictOneNonPinned(); + if (NS_FAILED(rv)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + // This reverts the item state to UNINITIALIZED that makes it to + // be scheduled for download again. + rv = aItem->Cancel(); + if (NS_FAILED(rv)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + mPinnedEntryRetriesCount++; + + LogToConsole("An unpinned offline cache deleted"); + + // Retry this item. + ProcessNextURI(); + return; + } + } + + // According to parallelism this may imply more pinned retries count, + // but that is not critical, since at one moment the algoritm will + // stop anyway. Also, this code may soon be completely removed + // after we have a separate storage for pinned apps. + mPinnedEntryRetriesCount = 0; + + // Check for failures. 3XX, 4XX and 5XX errors on items explicitly + // listed in the manifest will cause the update to fail. + if (NS_FAILED(rv) || !succeeded) { + if (aItem->mItemType & + (nsIApplicationCache::ITEM_EXPLICIT | + nsIApplicationCache::ITEM_FALLBACK)) { + LogToConsole("Offline cache manifest item failed to load", aItem); + mSucceeded = false; + } + } else { + rv = mApplicationCache->MarkEntry(aItem->mCacheKey, aItem->mItemType); + if (NS_FAILED(rv)) { + mSucceeded = false; + } + } + + if (!mSucceeded) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMCOMPLETED); + + ProcessNextURI(); +} + +void +nsOfflineCacheUpdate::ManifestCheckCompleted(nsresult aStatus, + const nsCString &aManifestHash) +{ + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + if (NS_SUCCEEDED(aStatus)) { + nsAutoCString firstManifestHash; + mManifestItem->GetManifestHash(firstManifestHash); + if (aManifestHash != firstManifestHash) { + LOG(("Manifest has changed during cache items download [%p]", this)); + LogToConsole("Offline cache manifest changed during update", mManifestItem); + aStatus = NS_ERROR_FAILURE; + } + } + + if (NS_FAILED(aStatus)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + } + + if (NS_FAILED(aStatus) && mRescheduleCount < kRescheduleLimit) { + // Do the final stuff but prevent notification of STATE_FINISHED. + // That would disconnect listeners that are responsible for document + // association after a successful update. Forwarding notifications + // from a new update through this dead update to them is absolutely + // correct. + FinishNoNotify(); + + RefPtr<nsOfflineCacheUpdate> newUpdate = + new nsOfflineCacheUpdate(); + // Leave aDocument argument null. Only glues and children keep + // document instances. + newUpdate->Init(mManifestURI, mDocumentURI, mLoadingPrincipal, nullptr, + mCustomProfileDir); + + // In a rare case the manifest will not be modified on the next refetch + // transfer all master document URIs to the new update to ensure that + // all documents refering it will be properly cached. + for (int32_t i = 0; i < mDocumentURIs.Count(); i++) { + newUpdate->StickDocument(mDocumentURIs[i]); + } + + newUpdate->mRescheduleCount = mRescheduleCount + 1; + newUpdate->AddObserver(this, false); + newUpdate->Schedule(); + } + else { + LogToConsole("Offline cache update done", mManifestItem); + Finish(); + } +} + +nsresult +nsOfflineCacheUpdate::Begin() +{ + LOG(("nsOfflineCacheUpdate::Begin [%p]", this)); + + // Keep the object alive through a ProcessNextURI()/Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + mItemsInProgress = 0; + + if (mState == STATE_CANCELLED) { + nsresult rv = NS_DispatchToMainThread(NewRunnableMethod(this, + &nsOfflineCacheUpdate::AsyncFinishWithError)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + if (mPartialUpdate) { + mState = STATE_DOWNLOADING; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING); + ProcessNextURI(); + return NS_OK; + } + + // Start checking the manifest. + mManifestItem = new nsOfflineManifestItem(mManifestURI, + mDocumentURI, + mLoadingPrincipal, + mApplicationCache, + mPreviousApplicationCache); + if (!mManifestItem) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mState = STATE_CHECKING; + mByteProgress = 0; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_CHECKING); + + nsresult rv = mManifestItem->OpenChannel(this); + if (NS_FAILED(rv)) { + LoadCompleted(mManifestItem); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate <private> +//----------------------------------------------------------------------------- + +nsresult +nsOfflineCacheUpdate::AddExistingItems(uint32_t aType, + nsTArray<nsCString>* namespaceFilter) +{ + if (!mPreviousApplicationCache) { + return NS_OK; + } + + if (namespaceFilter && namespaceFilter->Length() == 0) { + // Don't bother to walk entries when there are no namespaces + // defined. + return NS_OK; + } + + uint32_t count = 0; + char **keys = nullptr; + nsresult rv = mPreviousApplicationCache->GatherEntries(aType, + &count, &keys); + NS_ENSURE_SUCCESS(rv, rv); + + AutoFreeArray autoFree(count, keys); + + for (uint32_t i = 0; i < count; i++) { + if (namespaceFilter) { + bool found = false; + for (uint32_t j = 0; j < namespaceFilter->Length() && !found; j++) { + found = StringBeginsWith(nsDependentCString(keys[i]), + namespaceFilter->ElementAt(j)); + } + + if (!found) + continue; + } + + nsCOMPtr<nsIURI> uri; + if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), keys[i]))) { + rv = AddURI(uri, aType); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::ProcessNextURI() +{ + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p, inprogress=%d, numItems=%d]", + this, mItemsInProgress, mItems.Length())); + + if (mState != STATE_DOWNLOADING) { + LOG((" should only be called from the DOWNLOADING state, ignoring")); + return NS_ERROR_UNEXPECTED; + } + + nsOfflineCacheUpdateItem * runItem = nullptr; + uint32_t completedItems = 0; + for (uint32_t i = 0; i < mItems.Length(); ++i) { + nsOfflineCacheUpdateItem * item = mItems[i]; + + if (item->IsScheduled()) { + runItem = item; + break; + } + + if (item->IsCompleted()) + ++completedItems; + } + + if (completedItems == mItems.Length()) { + LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p]: all items loaded", this)); + + if (mPartialUpdate) { + return Finish(); + } else { + // Verify that the manifest wasn't changed during the + // update, to prevent capturing a cache while the server + // is being updated. The check will call + // ManifestCheckCompleted() when it's done. + RefPtr<nsManifestCheck> manifestCheck = + new nsManifestCheck(this, mManifestURI, mDocumentURI, mLoadingPrincipal); + if (NS_FAILED(manifestCheck->Begin())) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + return Finish(); + } + + return NS_OK; + } + } + + if (!runItem) { + LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p]:" + " No more items to include in parallel load", this)); + return NS_OK; + } + + if (LOG_ENABLED()) { + LOG(("%p: Opening channel for %s", this, + runItem->mURI->GetSpecOrDefault().get())); + } + + ++mItemsInProgress; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMSTARTED); + + nsresult rv = runItem->OpenChannel(this); + if (NS_FAILED(rv)) { + LoadCompleted(runItem); + return rv; + } + + if (mItemsInProgress >= kParallelLoadLimit) { + LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p]:" + " At parallel load limit", this)); + return NS_OK; + } + + // This calls this method again via a post triggering + // a parallel item load + return NS_DispatchToCurrentThread(this); +} + +void +nsOfflineCacheUpdate::GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver> &aObservers) +{ + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer) + aObservers.AppendObject(observer); + else + mWeakObservers.RemoveObjectAt(i--); + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + aObservers.AppendObject(mObservers[i]); + } +} + +void +nsOfflineCacheUpdate::NotifyState(uint32_t state) +{ + LOG(("nsOfflineCacheUpdate::NotifyState [%p, %d]", this, state)); + + if (state == STATE_ERROR) { + LogToConsole("Offline cache update error", mManifestItem); + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) { + observers[i]->UpdateStateChanged(this, state); + } +} + +void +nsOfflineCacheUpdate::NotifyUpdateAvailability(bool updateAvailable) +{ + if (!mUpdateAvailableObserver) + return; + + LOG(("nsOfflineCacheUpdate::NotifyUpdateAvailability [this=%p, avail=%d]", + this, updateAvailable)); + + const char* topic = updateAvailable + ? "offline-cache-update-available" + : "offline-cache-update-unavailable"; + + nsCOMPtr<nsIObserver> observer; + observer.swap(mUpdateAvailableObserver); + observer->Observe(mManifestURI, topic, nullptr); +} + +void +nsOfflineCacheUpdate::AssociateDocuments(nsIApplicationCache* cache) +{ + if (!cache) { + LOG(("nsOfflineCacheUpdate::AssociateDocuments bypassed" + ", no cache provided [this=%p]", this)); + return; + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) { + observers[i]->ApplicationCacheAvailable(cache); + } +} + +void +nsOfflineCacheUpdate::StickDocument(nsIURI *aDocumentURI) +{ + if (!aDocumentURI) + return; + + mDocumentURIs.AppendObject(aDocumentURI); +} + +void +nsOfflineCacheUpdate::SetOwner(nsOfflineCacheUpdateOwner *aOwner) +{ + NS_ASSERTION(!mOwner, "Tried to set cache update owner twice."); + mOwner = aOwner; +} + +bool +nsOfflineCacheUpdate::IsForGroupID(const nsCSubstring &groupID) +{ + return mGroupID == groupID; +} + +bool +nsOfflineCacheUpdate::IsForProfile(nsIFile* aCustomProfileDir) +{ + if (!mCustomProfileDir && !aCustomProfileDir) + return true; + if (!mCustomProfileDir || !aCustomProfileDir) + return false; + + bool equals; + nsresult rv = mCustomProfileDir->Equals(aCustomProfileDir, &equals); + + return NS_SUCCEEDED(rv) && equals; +} + +nsresult +nsOfflineCacheUpdate::UpdateFinished(nsOfflineCacheUpdate *aUpdate) +{ + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + mImplicitUpdate = nullptr; + + NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE); + Finish(); + + return NS_OK; +} + +void +nsOfflineCacheUpdate::OnByteProgress(uint64_t byteIncrement) +{ + mByteProgress += byteIncrement; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMPROGRESS); +} + +nsresult +nsOfflineCacheUpdate::ScheduleImplicit() +{ + if (mDocumentURIs.Count() == 0) + return NS_OK; + + nsresult rv; + + RefPtr<nsOfflineCacheUpdate> update = new nsOfflineCacheUpdate(); + NS_ENSURE_TRUE(update, NS_ERROR_OUT_OF_MEMORY); + + nsAutoCString clientID; + if (mPreviousApplicationCache) { + rv = mPreviousApplicationCache->GetClientID(clientID); + NS_ENSURE_SUCCESS(rv, rv); + } + else if (mApplicationCache) { + rv = mApplicationCache->GetClientID(clientID); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + NS_ERROR("Offline cache update not having set mApplicationCache?"); + } + + rv = update->InitPartial(mManifestURI, clientID, mDocumentURI, mLoadingPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t i = 0; i < mDocumentURIs.Count(); i++) { + rv = update->AddURI(mDocumentURIs[i], + nsIApplicationCache::ITEM_IMPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + } + + update->SetOwner(this); + rv = update->Begin(); + NS_ENSURE_SUCCESS(rv, rv); + + mImplicitUpdate = update; + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::FinishNoNotify() +{ + LOG(("nsOfflineCacheUpdate::Finish [%p]", this)); + + mState = STATE_FINISHED; + + if (!mPartialUpdate && !mOnlyCheckUpdate) { + if (mSucceeded) { + nsIArray *namespaces = mManifestItem->GetNamespaces(); + nsresult rv = mApplicationCache->AddNamespaces(namespaces); + if (NS_FAILED(rv)) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + mSucceeded = false; + } + + rv = mApplicationCache->Activate(); + if (NS_FAILED(rv)) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + mSucceeded = false; + } + + AssociateDocuments(mApplicationCache); + } + + if (mObsolete) { + nsCOMPtr<nsIApplicationCacheService> appCacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID); + if (appCacheService) { + nsAutoCString groupID; + mApplicationCache->GetGroupID(groupID); + appCacheService->DeactivateGroup(groupID); + } + } + + if (!mSucceeded) { + // Update was not merged, mark all the loads as failures + for (uint32_t i = 0; i < mItems.Length(); i++) { + mItems[i]->Cancel(); + } + + mApplicationCache->Discard(); + } + } + + nsresult rv = NS_OK; + + if (mOwner) { + rv = mOwner->UpdateFinished(this); + // mozilla::WeakPtr is missing some key features, like setting it to + // null explicitly. + mOwner = mozilla::WeakPtr<nsOfflineCacheUpdateOwner>(); + } + + return rv; +} + +nsresult +nsOfflineCacheUpdate::Finish() +{ + nsresult rv = FinishNoNotify(); + + NotifyState(nsIOfflineCacheUpdateObserver::STATE_FINISHED); + + return rv; +} + +void +nsOfflineCacheUpdate::AsyncFinishWithError() +{ + NotifyState(nsOfflineCacheUpdate::STATE_ERROR); + Finish(); +} + +static nsresult +EvictOneOfCacheGroups(nsIApplicationCacheService *cacheService, + uint32_t count, const char * const *groups) +{ + nsresult rv; + unsigned int i; + + for (i = 0; i < count; i++) { + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), groups[i]); + NS_ENSURE_SUCCESS(rv, rv); + + nsDependentCString group_name(groups[i]); + nsCOMPtr<nsIApplicationCache> cache; + rv = cacheService->GetActiveCache(group_name, getter_AddRefs(cache)); + // Maybe someone in another thread or process have deleted it. + if (NS_FAILED(rv) || !cache) + continue; + + bool pinned; + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(uri, + nullptr, + &pinned); + NS_ENSURE_SUCCESS(rv, rv); + + if (!pinned) { + rv = cache->Discard(); + return NS_OK; + } + } + + return NS_ERROR_FILE_NOT_FOUND; +} + +nsresult +nsOfflineCacheUpdate::EvictOneNonPinned() +{ + nsresult rv; + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t count; + char **groups; + rv = cacheService->GetGroupsTimeOrdered(&count, &groups); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EvictOneOfCacheGroups(cacheService, count, groups); + + NS_FREE_XPCOM_ALLOCATED_POINTER_ARRAY(count, groups); + return rv; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate::nsIOfflineCacheUpdate +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetUpdateDomain(nsACString &aUpdateDomain) +{ + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + aUpdateDomain = mUpdateDomain; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetStatus(uint16_t *aStatus) +{ + switch (mState) { + case STATE_CHECKING : + *aStatus = nsIDOMOfflineResourceList::CHECKING; + return NS_OK; + case STATE_DOWNLOADING : + *aStatus = nsIDOMOfflineResourceList::DOWNLOADING; + return NS_OK; + default : + *aStatus = nsIDOMOfflineResourceList::IDLE; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetPartial(bool *aPartial) +{ + *aPartial = mPartialUpdate || mOnlyCheckUpdate; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetManifestURI(nsIURI **aManifestURI) +{ + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + NS_IF_ADDREF(*aManifestURI = mManifestURI); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetSucceeded(bool *aSucceeded) +{ + NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE); + + *aSucceeded = mSucceeded; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetIsUpgrade(bool *aIsUpgrade) +{ + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + *aIsUpgrade = (mPreviousApplicationCache != nullptr); + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdate::AddURI(nsIURI *aURI, uint32_t aType, uint32_t aLoadFlags) +{ + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + if (mState >= STATE_DOWNLOADING) + return NS_ERROR_NOT_AVAILABLE; + + // Resource URIs must have the same scheme as the manifest. + nsAutoCString scheme; + aURI->GetScheme(scheme); + + bool match; + if (NS_FAILED(mManifestURI->SchemeIs(scheme.get(), &match)) || !match) + return NS_ERROR_FAILURE; + + // Don't fetch the same URI twice. + for (uint32_t i = 0; i < mItems.Length(); i++) { + bool equals; + if (NS_SUCCEEDED(mItems[i]->mURI->Equals(aURI, &equals)) && equals && + mItems[i]->mLoadFlags == aLoadFlags) { + // retain both types. + mItems[i]->mItemType |= aType; + return NS_OK; + } + } + + RefPtr<nsOfflineCacheUpdateItem> item = + new nsOfflineCacheUpdateItem(aURI, + mDocumentURI, + mLoadingPrincipal, + mApplicationCache, + mPreviousApplicationCache, + aType, + aLoadFlags); + if (!item) return NS_ERROR_OUT_OF_MEMORY; + + mItems.AppendElement(item); + mAddedItems = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::AddDynamicURI(nsIURI *aURI) +{ + if (GeckoProcessType_Default != XRE_GetProcessType()) + return NS_ERROR_NOT_IMPLEMENTED; + + // If this is a partial update and the resource is already in the + // cache, we should only mark the entry, not fetch it again. + if (mPartialUpdate) { + nsAutoCString key; + GetCacheKey(aURI, key); + + uint32_t types; + nsresult rv = mApplicationCache->GetTypes(key, &types); + if (NS_SUCCEEDED(rv)) { + if (!(types & nsIApplicationCache::ITEM_DYNAMIC)) { + mApplicationCache->MarkEntry + (key, nsIApplicationCache::ITEM_DYNAMIC); + } + return NS_OK; + } + } + + return AddURI(aURI, nsIApplicationCache::ITEM_DYNAMIC); +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::Cancel() +{ + LOG(("nsOfflineCacheUpdate::Cancel [%p]", this)); + + if ((mState == STATE_FINISHED) || (mState == STATE_CANCELLED)) { + return NS_ERROR_NOT_AVAILABLE; + } + + mState = STATE_CANCELLED; + mSucceeded = false; + + // Cancel all running downloads + for (uint32_t i = 0; i < mItems.Length(); ++i) { + nsOfflineCacheUpdateItem * item = mItems[i]; + + if (item->IsInProgress()) + item->Cancel(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::AddObserver(nsIOfflineCacheUpdateObserver *aObserver, + bool aHoldWeak) +{ + LOG(("nsOfflineCacheUpdate::AddObserver [%p] to update [%p]", aObserver, this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + if (aHoldWeak) { + nsCOMPtr<nsIWeakReference> weakRef = do_GetWeakReference(aObserver); + mWeakObservers.AppendObject(weakRef); + } else { + mObservers.AppendObject(aObserver); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::RemoveObserver(nsIOfflineCacheUpdateObserver *aObserver) +{ + LOG(("nsOfflineCacheUpdate::RemoveObserver [%p] from update [%p]", aObserver, this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer == aObserver) { + mWeakObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + if (mObservers[i] == aObserver) { + mObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetByteProgress(uint64_t * _result) +{ + NS_ENSURE_ARG(_result); + + *_result = mByteProgress; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::Schedule() +{ + LOG(("nsOfflineCacheUpdate::Schedule [%p]", this)); + + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + + if (!service) { + return NS_ERROR_FAILURE; + } + + return service->ScheduleUpdate(this); +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::UpdateStateChanged(nsIOfflineCacheUpdate *aUpdate, + uint32_t aState) +{ + if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED) { + // Take the mSucceeded flag from the underlying update, we will be + // queried for it soon. mSucceeded of this update is false (manifest + // check failed) but the subsequent re-fetch update might succeed + bool succeeded; + aUpdate->GetSucceeded(&succeeded); + mSucceeded = succeeded; + } + + NotifyState(aState); + if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED) + aUpdate->RemoveObserver(this); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::ApplicationCacheAvailable(nsIApplicationCache *applicationCache) +{ + AssociateDocuments(applicationCache); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate::nsIRunable +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdate::Run() +{ + ProcessNextURI(); + return NS_OK; +} diff --git a/uriloader/prefetch/nsOfflineCacheUpdate.h b/uriloader/prefetch/nsOfflineCacheUpdate.h new file mode 100644 index 0000000000..4ccba41359 --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdate.h @@ -0,0 +1,381 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdate_h__ +#define nsOfflineCacheUpdate_h__ + +#include "nsIOfflineCacheUpdate.h" + +#include "nsAutoPtr.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIChannelEventSink.h" +#include "nsIDOMDocument.h" +#include "nsIDOMNode.h" +#include "nsIInterfaceRequestor.h" +#include "nsIMutableArray.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIApplicationCache.h" +#include "nsIRequestObserver.h" +#include "nsIRunnable.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsIWebProgressListener.h" +#include "nsClassHashtable.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWeakReference.h" +#include "nsICryptoHash.h" +#include "mozilla/Attributes.h" +#include "mozilla/WeakPtr.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" + +class nsOfflineCacheUpdate; + +class nsOfflineCacheUpdateItem : public nsIStreamListener + , public nsIRunnable + , public nsIInterfaceRequestor + , public nsIChannelEventSink +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIRUNNABLE + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + nsOfflineCacheUpdateItem(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache *aApplicationCache, + nsIApplicationCache *aPreviousApplicationCache, + uint32_t aType, + uint32_t aLoadFlags); + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mReferrerURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsIApplicationCache> mApplicationCache; + nsCOMPtr<nsIApplicationCache> mPreviousApplicationCache; + nsCString mCacheKey; + uint32_t mItemType; + uint32_t mLoadFlags; + + nsresult OpenChannel(nsOfflineCacheUpdate *aUpdate); + nsresult Cancel(); + nsresult GetRequestSucceeded(bool * succeeded); + + bool IsInProgress(); + bool IsScheduled(); + bool IsCompleted(); + + nsresult GetStatus(uint16_t *aStatus); + +private: + enum LoadStatus : uint16_t { + UNINITIALIZED = 0U, + REQUESTED = 1U, + RECEIVING = 2U, + LOADED = 3U + }; + + RefPtr<nsOfflineCacheUpdate> mUpdate; + nsCOMPtr<nsIChannel> mChannel; + uint16_t mState; + +protected: + virtual ~nsOfflineCacheUpdateItem(); + + int64_t mBytesRead; +}; + + +class nsOfflineManifestItem : public nsOfflineCacheUpdateItem +{ +public: + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + nsOfflineManifestItem(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache *aApplicationCache, + nsIApplicationCache *aPreviousApplicationCache); + virtual ~nsOfflineManifestItem(); + + nsCOMArray<nsIURI> &GetExplicitURIs() { return mExplicitURIs; } + nsCOMArray<nsIURI> &GetAnonymousURIs() { return mAnonymousURIs; } + nsCOMArray<nsIURI> &GetFallbackURIs() { return mFallbackURIs; } + + nsTArray<nsCString> &GetOpportunisticNamespaces() + { return mOpportunisticNamespaces; } + nsIArray *GetNamespaces() + { return mNamespaces.get(); } + + bool ParseSucceeded() + { return (mParserState != PARSE_INIT && mParserState != PARSE_ERROR); } + bool NeedsUpdate() { return mParserState != PARSE_INIT && mNeedsUpdate; } + + void GetManifestHash(nsCString &aManifestHash) + { aManifestHash = mManifestHashValue; } + +private: + static nsresult ReadManifest(nsIInputStream *aInputStream, + void *aClosure, + const char *aFromSegment, + uint32_t aOffset, + uint32_t aCount, + uint32_t *aBytesConsumed); + + nsresult AddNamespace(uint32_t namespaceType, + const nsCString &namespaceSpec, + const nsCString &data); + + nsresult HandleManifestLine(const nsCString::const_iterator &aBegin, + const nsCString::const_iterator &aEnd); + + /** + * Saves "offline-manifest-hash" meta data from the old offline cache + * token to mOldManifestHashValue member to be compared on + * successfull load. + */ + nsresult GetOldManifestContentHash(nsIRequest *aRequest); + /** + * This method setups the mNeedsUpdate to false when hash value + * of the just downloaded manifest file is the same as stored in cache's + * "offline-manifest-hash" meta data. Otherwise stores the new value + * to this meta data. + */ + nsresult CheckNewManifestContentHash(nsIRequest *aRequest); + + void ReadStrictFileOriginPolicyPref(); + + enum { + PARSE_INIT, + PARSE_CACHE_ENTRIES, + PARSE_FALLBACK_ENTRIES, + PARSE_BYPASS_ENTRIES, + PARSE_UNKNOWN_SECTION, + PARSE_ERROR + } mParserState; + + nsCString mReadBuf; + + nsCOMArray<nsIURI> mExplicitURIs; + nsCOMArray<nsIURI> mAnonymousURIs; + nsCOMArray<nsIURI> mFallbackURIs; + + // All opportunistic caching namespaces. Used to decide whether + // to include previously-opportunistically-cached entries. + nsTArray<nsCString> mOpportunisticNamespaces; + + // Array of nsIApplicationCacheNamespace objects specified by the + // manifest. + nsCOMPtr<nsIMutableArray> mNamespaces; + + bool mNeedsUpdate; + bool mStrictFileOriginPolicy; + + // manifest hash data + nsCOMPtr<nsICryptoHash> mManifestHash; + bool mManifestHashInitialized; + nsCString mManifestHashValue; + nsCString mOldManifestHashValue; +}; + +class nsOfflineCacheUpdateOwner + : public mozilla::SupportsWeakPtr<nsOfflineCacheUpdateOwner> +{ +public: + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(nsOfflineCacheUpdateOwner) + virtual ~nsOfflineCacheUpdateOwner() {} + virtual nsresult UpdateFinished(nsOfflineCacheUpdate *aUpdate) = 0; +}; + +class nsOfflineCacheUpdate final : public nsIOfflineCacheUpdate + , public nsIOfflineCacheUpdateObserver + , public nsIRunnable + , public nsOfflineCacheUpdateOwner +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATE + NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER + NS_DECL_NSIRUNNABLE + + nsOfflineCacheUpdate(); + + static nsresult GetCacheKey(nsIURI *aURI, nsACString &aKey); + + nsresult Init(); + + nsresult Begin(); + + void LoadCompleted(nsOfflineCacheUpdateItem *aItem); + void ManifestCheckCompleted(nsresult aStatus, + const nsCString &aManifestHash); + void StickDocument(nsIURI *aDocumentURI); + + void SetOwner(nsOfflineCacheUpdateOwner *aOwner); + + bool IsForGroupID(const nsCSubstring &groupID); + bool IsForProfile(nsIFile* aCustomProfileDir); + + virtual nsresult UpdateFinished(nsOfflineCacheUpdate *aUpdate) override; + +protected: + ~nsOfflineCacheUpdate(); + + friend class nsOfflineCacheUpdateItem; + void OnByteProgress(uint64_t byteIncrement); + +private: + nsresult InitInternal(nsIURI *aManifestURI, nsIPrincipal* aPrincipal); + nsresult HandleManifest(bool *aDoUpdate); + nsresult AddURI(nsIURI *aURI, uint32_t aItemType, uint32_t aLoadFlags = 0); + + nsresult ProcessNextURI(); + + // Adds items from the previous cache witha type matching aType. + // If namespaceFilter is non-null, only items matching the + // specified namespaces will be added. + nsresult AddExistingItems(uint32_t aType, + nsTArray<nsCString>* namespaceFilter = nullptr); + nsresult ScheduleImplicit(); + void AssociateDocuments(nsIApplicationCache* cache); + bool CheckUpdateAvailability(); + void NotifyUpdateAvailability(bool updateAvailable); + + void GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver> &aObservers); + void NotifyState(uint32_t state); + nsresult Finish(); + nsresult FinishNoNotify(); + + void AsyncFinishWithError(); + + // Find one non-pinned cache group and evict it. + nsresult EvictOneNonPinned(); + + enum { + STATE_UNINITIALIZED, + STATE_INITIALIZED, + STATE_CHECKING, + STATE_DOWNLOADING, + STATE_CANCELLED, + STATE_FINISHED + } mState; + + mozilla::WeakPtr<nsOfflineCacheUpdateOwner> mOwner; + + bool mAddedItems; + bool mPartialUpdate; + bool mOnlyCheckUpdate; + bool mSucceeded; + bool mObsolete; + + nsCString mUpdateDomain; + nsCString mGroupID; + nsCOMPtr<nsIURI> mManifestURI; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsIFile> mCustomProfileDir; + + nsCOMPtr<nsIObserver> mUpdateAvailableObserver; + + nsCOMPtr<nsIApplicationCache> mApplicationCache; + nsCOMPtr<nsIApplicationCache> mPreviousApplicationCache; + + nsCOMPtr<nsIObserverService> mObserverService; + + RefPtr<nsOfflineManifestItem> mManifestItem; + + /* Items being updated */ + uint32_t mItemsInProgress; + nsTArray<RefPtr<nsOfflineCacheUpdateItem> > mItems; + + /* Clients watching this update for changes */ + nsCOMArray<nsIWeakReference> mWeakObservers; + nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers; + + /* Documents that requested this update */ + nsCOMArray<nsIURI> mDocumentURIs; + + /* Reschedule count. When an update is rescheduled due to + * mismatched manifests, the reschedule count will be increased. */ + uint32_t mRescheduleCount; + + /* Whena an entry for a pinned app is retried, retries count is + * increaded. */ + uint32_t mPinnedEntryRetriesCount; + + RefPtr<nsOfflineCacheUpdate> mImplicitUpdate; + + bool mPinned; + + uint64_t mByteProgress; +}; + +class nsOfflineCacheUpdateService final : public nsIOfflineCacheUpdateService + , public nsIObserver + , public nsOfflineCacheUpdateOwner + , public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATESERVICE + NS_DECL_NSIOBSERVER + + nsOfflineCacheUpdateService(); + + nsresult Init(); + + nsresult ScheduleUpdate(nsOfflineCacheUpdate *aUpdate); + nsresult FindUpdate(nsIURI *aManifestURI, + nsACString const &aOriginSuffix, + nsIFile *aCustomProfileDir, + nsOfflineCacheUpdate **aUpdate); + + nsresult Schedule(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIDOMDocument *aDocument, + nsPIDOMWindowInner* aWindow, + nsIFile* aCustomProfileDir, + nsIOfflineCacheUpdate **aUpdate); + + virtual nsresult UpdateFinished(nsOfflineCacheUpdate *aUpdate) override; + + /** + * Returns the singleton nsOfflineCacheUpdateService without an addref, or + * nullptr if the service couldn't be created. + */ + static nsOfflineCacheUpdateService *EnsureService(); + + /** Addrefs and returns the singleton nsOfflineCacheUpdateService. */ + static nsOfflineCacheUpdateService *GetInstance(); + + static nsresult OfflineAppPinnedForURI(nsIURI *aDocumentURI, + nsIPrefBranch *aPrefBranch, + bool *aPinned); + + static nsTHashtable<nsCStringHashKey>* AllowedDomains(); + +private: + ~nsOfflineCacheUpdateService(); + + nsresult ProcessNextUpdate(); + + nsTArray<RefPtr<nsOfflineCacheUpdate> > mUpdates; + static nsTHashtable<nsCStringHashKey>* mAllowedDomains; + + bool mDisabled; + bool mUpdateRunning; + bool mLowFreeSpace; +}; + +#endif diff --git a/uriloader/prefetch/nsOfflineCacheUpdateService.cpp b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp new file mode 100644 index 0000000000..adb3fd5160 --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp @@ -0,0 +1,736 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OfflineCacheUpdateChild.h" +#include "OfflineCacheUpdateParent.h" +#include "nsXULAppAPI.h" +#include "OfflineCacheUpdateGlue.h" +#include "nsOfflineCacheUpdate.h" + +#include "nsCPrefetchService.h" +#include "nsCURILoader.h" +#include "nsIApplicationCacheContainer.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheService.h" +#include "nsICachingChannel.h" +#include "nsIContent.h" +#include "nsIDocShell.h" +#include "nsIDocumentLoader.h" +#include "nsIDOMElement.h" +#include "nsIDOMWindow.h" +#include "nsIDOMOfflineResourceList.h" +#include "nsIDocument.h" +#include "nsIObserverService.h" +#include "nsIURL.h" +#include "nsIWebProgress.h" +#include "nsIWebNavigation.h" +#include "nsICryptoHash.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "mozilla/Unused.h" +#include "nsIDiskSpaceWatcher.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "nsContentUtils.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static nsOfflineCacheUpdateService *gOfflineCacheUpdateService = nullptr; +static bool sAllowOfflineCache = true; + +nsTHashtable<nsCStringHashKey>* nsOfflineCacheUpdateService::mAllowedDomains = nullptr; + +nsTHashtable<nsCStringHashKey>* nsOfflineCacheUpdateService::AllowedDomains() +{ + if (!mAllowedDomains) + mAllowedDomains = new nsTHashtable<nsCStringHashKey>(); + + return mAllowedDomains; +} + + +typedef mozilla::docshell::OfflineCacheUpdateParent OfflineCacheUpdateParent; +typedef mozilla::docshell::OfflineCacheUpdateChild OfflineCacheUpdateChild; +typedef mozilla::docshell::OfflineCacheUpdateGlue OfflineCacheUpdateGlue; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +LazyLogModule gOfflineCacheUpdateLog("nsOfflineCacheUpdate"); + +#undef LOG +#define LOG(args) MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +//----------------------------------------------------------------------------- +// nsOfflineCachePendingUpdate +//----------------------------------------------------------------------------- + +class nsOfflineCachePendingUpdate final : public nsIWebProgressListener + , public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBPROGRESSLISTENER + + nsOfflineCachePendingUpdate(nsOfflineCacheUpdateService *aService, + nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIDOMDocument *aDocument) + : mService(aService) + , mManifestURI(aManifestURI) + , mDocumentURI(aDocumentURI) + , mLoadingPrincipal(aLoadingPrincipal) + , mDidReleaseThis(false) + { + mDocument = do_GetWeakReference(aDocument); + } + +private: + ~nsOfflineCachePendingUpdate() {} + + RefPtr<nsOfflineCacheUpdateService> mService; + nsCOMPtr<nsIURI> mManifestURI; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsIWeakReference> mDocument; + bool mDidReleaseThis; +}; + +NS_IMPL_ISUPPORTS(nsOfflineCachePendingUpdate, + nsIWebProgressListener, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIWebProgressListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnProgressChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + int32_t curSelfProgress, + int32_t maxSelfProgress, + int32_t curTotalProgress, + int32_t maxTotalProgress) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest *aRequest, + uint32_t progressStateFlags, + nsresult aStatus) +{ + if (mDidReleaseThis) { + return NS_OK; + } + nsCOMPtr<nsIDOMDocument> updateDoc = do_QueryReferent(mDocument); + if (!updateDoc) { + // The document that scheduled this update has gone away, + // we don't need to listen anymore. + aWebProgress->RemoveProgressListener(this); + MOZ_ASSERT(!mDidReleaseThis); + mDidReleaseThis = true; + NS_RELEASE_THIS(); + return NS_OK; + } + + if (!(progressStateFlags & STATE_STOP)) { + return NS_OK; + } + + nsCOMPtr<mozIDOMWindowProxy> windowProxy; + aWebProgress->GetDOMWindow(getter_AddRefs(windowProxy)); + if (!windowProxy) return NS_OK; + + auto* outerWindow = nsPIDOMWindowOuter::From(windowProxy); + nsPIDOMWindowInner* innerWindow = outerWindow->GetCurrentInnerWindow(); + + nsCOMPtr<nsIDocument> progressDoc = outerWindow->GetDoc(); + if (!progressDoc) return NS_OK; + + if (!SameCOMIdentity(progressDoc, updateDoc)) { + return NS_OK; + } + + LOG(("nsOfflineCachePendingUpdate::OnStateChange [%p, doc=%p]", + this, progressDoc.get())); + + // Only schedule the update if the document loaded successfully + if (NS_SUCCEEDED(aStatus)) { + nsCOMPtr<nsIOfflineCacheUpdate> update; + mService->Schedule(mManifestURI, mDocumentURI, mLoadingPrincipal, updateDoc, innerWindow, + nullptr, getter_AddRefs(update)); + if (mDidReleaseThis) { + return NS_OK; + } + } + + aWebProgress->RemoveProgressListener(this); + MOZ_ASSERT(!mDidReleaseThis); + mDidReleaseThis = true; + NS_RELEASE_THIS(); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI *location, + uint32_t aFlags) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + uint32_t state) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateService, + nsIOfflineCacheUpdateService, + nsIObserver, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService <public> +//----------------------------------------------------------------------------- + +nsOfflineCacheUpdateService::nsOfflineCacheUpdateService() + : mDisabled(false) + , mUpdateRunning(false) + , mLowFreeSpace(false) +{ + MOZ_ASSERT(NS_IsMainThread()); + Preferences::AddBoolVarCache(&sAllowOfflineCache, + "browser.cache.offline.enable", + true); +} + +nsOfflineCacheUpdateService::~nsOfflineCacheUpdateService() +{ + gOfflineCacheUpdateService = nullptr; +} + +nsresult +nsOfflineCacheUpdateService::Init() +{ + // Observe xpcom-shutdown event + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) + return NS_ERROR_FAILURE; + + nsresult rv = observerService->AddObserver(this, + NS_XPCOM_SHUTDOWN_OBSERVER_ID, + true); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the current status of the disk in terms of free space and observe + // low device storage notifications. + nsCOMPtr<nsIDiskSpaceWatcher> diskSpaceWatcherService = + do_GetService("@mozilla.org/toolkit/disk-space-watcher;1"); + if (diskSpaceWatcherService) { + diskSpaceWatcherService->GetIsDiskFull(&mLowFreeSpace); + } else { + NS_WARNING("Could not get disk status from nsIDiskSpaceWatcher"); + } + + rv = observerService->AddObserver(this, "disk-space-watcher", false); + NS_ENSURE_SUCCESS(rv, rv); + + gOfflineCacheUpdateService = this; + + return NS_OK; +} + +/* static */ +nsOfflineCacheUpdateService * +nsOfflineCacheUpdateService::GetInstance() +{ + if (!gOfflineCacheUpdateService) { + gOfflineCacheUpdateService = new nsOfflineCacheUpdateService(); + if (!gOfflineCacheUpdateService) + return nullptr; + NS_ADDREF(gOfflineCacheUpdateService); + nsresult rv = gOfflineCacheUpdateService->Init(); + if (NS_FAILED(rv)) { + NS_RELEASE(gOfflineCacheUpdateService); + return nullptr; + } + return gOfflineCacheUpdateService; + } + + NS_ADDREF(gOfflineCacheUpdateService); + + return gOfflineCacheUpdateService; +} + +/* static */ +nsOfflineCacheUpdateService * +nsOfflineCacheUpdateService::EnsureService() +{ + if (!gOfflineCacheUpdateService) { + // Make the service manager hold a long-lived reference to the service + nsCOMPtr<nsIOfflineCacheUpdateService> service = + do_GetService(NS_OFFLINECACHEUPDATESERVICE_CONTRACTID); + } + + return gOfflineCacheUpdateService; +} + +nsresult +nsOfflineCacheUpdateService::ScheduleUpdate(nsOfflineCacheUpdate *aUpdate) +{ + LOG(("nsOfflineCacheUpdateService::Schedule [%p, update=%p]", + this, aUpdate)); + + aUpdate->SetOwner(this); + + mUpdates.AppendElement(aUpdate); + ProcessNextUpdate(); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::ScheduleOnDocumentStop(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIDOMDocument *aDocument) +{ + LOG(("nsOfflineCacheUpdateService::ScheduleOnDocumentStop [%p, manifestURI=%p, documentURI=%p doc=%p]", + this, aManifestURI, aDocumentURI, aDocument)); + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDocument); + nsCOMPtr<nsIWebProgress> progress = do_QueryInterface(doc->GetContainer()); + NS_ENSURE_TRUE(progress, NS_ERROR_INVALID_ARG); + + // Proceed with cache update + RefPtr<nsOfflineCachePendingUpdate> update = + new nsOfflineCachePendingUpdate(this, aManifestURI, aDocumentURI, + aLoadingPrincipal, aDocument); + NS_ENSURE_TRUE(update, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = progress->AddProgressListener + (update, nsIWebProgress::NOTIFY_STATE_DOCUMENT); + NS_ENSURE_SUCCESS(rv, rv); + + // The update will release when it has scheduled itself. + Unused << update.forget(); + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdateService::UpdateFinished(nsOfflineCacheUpdate *aUpdate) +{ + LOG(("nsOfflineCacheUpdateService::UpdateFinished [%p, update=%p]", + this, aUpdate)); + + NS_ASSERTION(mUpdates.Length() > 0 && + mUpdates[0] == aUpdate, "Unknown update completed"); + + // keep this item alive until we're done notifying observers + RefPtr<nsOfflineCacheUpdate> update = mUpdates[0]; + Unused << update; + mUpdates.RemoveElementAt(0); + mUpdateRunning = false; + + ProcessNextUpdate(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService <private> +//----------------------------------------------------------------------------- + +nsresult +nsOfflineCacheUpdateService::ProcessNextUpdate() +{ + LOG(("nsOfflineCacheUpdateService::ProcessNextUpdate [%p, num=%d]", + this, mUpdates.Length())); + + if (mDisabled) + return NS_ERROR_ABORT; + + if (mUpdateRunning) + return NS_OK; + + if (mUpdates.Length() > 0) { + mUpdateRunning = true; + // Canceling the update before Begin() call will make the update + // asynchronously finish with an error. + if (mLowFreeSpace) { + mUpdates[0]->Cancel(); + } + return mUpdates[0]->Begin(); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIOfflineCacheUpdateService +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateService::GetNumUpdates(uint32_t *aNumUpdates) +{ + LOG(("nsOfflineCacheUpdateService::GetNumUpdates [%p]", this)); + + *aNumUpdates = mUpdates.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::GetUpdate(uint32_t aIndex, + nsIOfflineCacheUpdate **aUpdate) +{ + LOG(("nsOfflineCacheUpdateService::GetUpdate [%p, %d]", this, aIndex)); + + if (aIndex < mUpdates.Length()) { + NS_ADDREF(*aUpdate = mUpdates[aIndex]); + } else { + *aUpdate = nullptr; + } + + return NS_OK; +} + +nsresult +nsOfflineCacheUpdateService::FindUpdate(nsIURI *aManifestURI, + nsACString const &aOriginSuffix, + nsIFile *aCustomProfileDir, + nsOfflineCacheUpdate **aUpdate) +{ + nsresult rv; + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString groupID; + rv = cacheService->BuildGroupIDForSuffix(aManifestURI, aOriginSuffix, groupID); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsOfflineCacheUpdate> update; + for (uint32_t i = 0; i < mUpdates.Length(); i++) { + update = mUpdates[i]; + + bool partial; + rv = update->GetPartial(&partial); + NS_ENSURE_SUCCESS(rv, rv); + + if (partial) { + // Partial updates aren't considered + continue; + } + + if (update->IsForGroupID(groupID) && update->IsForProfile(aCustomProfileDir)) { + update.swap(*aUpdate); + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult +nsOfflineCacheUpdateService::Schedule(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIDOMDocument *aDocument, + nsPIDOMWindowInner* aWindow, + nsIFile* aCustomProfileDir, + nsIOfflineCacheUpdate **aUpdate) +{ + nsCOMPtr<nsIOfflineCacheUpdate> update; + if (GeckoProcessType_Default != XRE_GetProcessType()) { + update = new OfflineCacheUpdateChild(aWindow); + } + else { + update = new OfflineCacheUpdateGlue(); + } + + nsresult rv; + + if (aWindow) { + // Ensure there is window.applicationCache object that is + // responsible for association of the new applicationCache + // with the corresponding document. Just ignore the result. + nsCOMPtr<nsIDOMOfflineResourceList> appCacheWindowObject = + aWindow->GetApplicationCache(); + } + + rv = update->Init(aManifestURI, aDocumentURI, aLoadingPrincipal, aDocument, + aCustomProfileDir); + NS_ENSURE_SUCCESS(rv, rv); + + rv = update->Schedule(); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*aUpdate = update); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::ScheduleUpdate(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + mozIDOMWindow* aWindow, + nsIOfflineCacheUpdate **aUpdate) +{ + return Schedule(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr, + nsPIDOMWindowInner::From(aWindow), nullptr, aUpdate); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::ScheduleAppUpdate(nsIURI *aManifestURI, + nsIURI *aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsIFile *aProfileDir, + nsIOfflineCacheUpdate **aUpdate) +{ + return Schedule(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr, nullptr, + aProfileDir, aUpdate); +} + +NS_IMETHODIMP nsOfflineCacheUpdateService::CheckForUpdate(nsIURI *aManifestURI, + nsIPrincipal* aLoadingPrincipal, + nsIObserver *aObserver) +{ + if (GeckoProcessType_Default != XRE_GetProcessType()) { + // Not intended to support this on child processes + return NS_ERROR_NOT_IMPLEMENTED; + } + + nsCOMPtr<nsIOfflineCacheUpdate> update = new OfflineCacheUpdateGlue(); + + nsresult rv; + + rv = update->InitForUpdateCheck(aManifestURI, aLoadingPrincipal, aObserver); + NS_ENSURE_SUCCESS(rv, rv); + + rv = update->Schedule(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateService::Observe(nsISupports *aSubject, + const char *aTopic, + const char16_t *aData) +{ + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + if (mUpdates.Length() > 0) + mUpdates[0]->Cancel(); + mDisabled = true; + } + + if (!strcmp(aTopic, "disk-space-watcher")) { + if (NS_LITERAL_STRING("full").Equals(aData)) { + mLowFreeSpace = true; + for (uint32_t i = 0; i < mUpdates.Length(); i++) { + mUpdates[i]->Cancel(); + } + } else if (NS_LITERAL_STRING("free").Equals(aData)) { + mLowFreeSpace = false; + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIOfflineCacheUpdateService +//----------------------------------------------------------------------------- + +static nsresult +OfflineAppPermForPrincipal(nsIPrincipal *aPrincipal, + nsIPrefBranch *aPrefBranch, + bool pinned, + bool *aAllowed) +{ + *aAllowed = false; + + if (!sAllowOfflineCache) { + return NS_OK; + } + + if (!aPrincipal) + return NS_ERROR_INVALID_ARG; + + nsCOMPtr<nsIURI> uri; + aPrincipal->GetURI(getter_AddRefs(uri)); + + if (!uri) + return NS_OK; + + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri); + if (!innerURI) + return NS_OK; + + // only http and https applications can use offline APIs. + bool match; + nsresult rv = innerURI->SchemeIs("http", &match); + NS_ENSURE_SUCCESS(rv, rv); + + if (!match) { + rv = innerURI->SchemeIs("https", &match); + NS_ENSURE_SUCCESS(rv, rv); + if (!match) { + return NS_OK; + } + } + + nsAutoCString domain; + rv = innerURI->GetAsciiHost(domain); + NS_ENSURE_SUCCESS(rv, rv); + + if (nsOfflineCacheUpdateService::AllowedDomains()->Contains(domain)) { + *aAllowed = true; + return NS_OK; + } + + nsCOMPtr<nsIPermissionManager> permissionManager = + services::GetPermissionManager(); + if (!permissionManager) { + return NS_OK; + } + + uint32_t perm; + const char *permName = pinned ? "pin-app" : "offline-app"; + permissionManager->TestExactPermissionFromPrincipal(aPrincipal, permName, &perm); + + if (perm == nsIPermissionManager::ALLOW_ACTION || + perm == nsIOfflineCacheUpdateService::ALLOW_NO_WARN) { + *aAllowed = true; + } + + // offline-apps.allow_by_default is now effective at the cache selection + // algorithm code (nsContentSink). + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::OfflineAppAllowed(nsIPrincipal *aPrincipal, + nsIPrefBranch *aPrefBranch, + bool *aAllowed) +{ + return OfflineAppPermForPrincipal(aPrincipal, aPrefBranch, false, aAllowed); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::OfflineAppAllowedForURI(nsIURI *aURI, + nsIPrefBranch *aPrefBranch, + bool *aAllowed) +{ + PrincipalOriginAttributes attrs; + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(aURI, attrs); + return OfflineAppPermForPrincipal(principal, aPrefBranch, false, aAllowed); +} + +nsresult +nsOfflineCacheUpdateService::OfflineAppPinnedForURI(nsIURI *aDocumentURI, + nsIPrefBranch *aPrefBranch, + bool *aPinned) +{ + PrincipalOriginAttributes attrs; + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(aDocumentURI, attrs); + return OfflineAppPermForPrincipal(principal, aPrefBranch, true, aPinned); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::AllowOfflineApp(nsIPrincipal *aPrincipal) +{ + nsresult rv; + + if (!sAllowOfflineCache) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (GeckoProcessType_Default != XRE_GetProcessType()) { + ContentChild* child = ContentChild::GetSingleton(); + + if (!child->SendSetOfflinePermission(IPC::Principal(aPrincipal))) { + return NS_ERROR_FAILURE; + } + + nsAutoCString domain; + rv = aPrincipal->GetBaseDomain(domain); + NS_ENSURE_SUCCESS(rv, rv); + + nsOfflineCacheUpdateService::AllowedDomains()->PutEntry(domain); + } + else { + nsCOMPtr<nsIPermissionManager> permissionManager = + services::GetPermissionManager(); + if (!permissionManager) + return NS_ERROR_NOT_AVAILABLE; + + rv = permissionManager->AddFromPrincipal( + aPrincipal, "offline-app", nsIPermissionManager::ALLOW_ACTION, + nsIPermissionManager::EXPIRE_NEVER, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} diff --git a/uriloader/prefetch/nsPrefetchService.cpp b/uriloader/prefetch/nsPrefetchService.cpp new file mode 100644 index 0000000000..bd2b10d304 --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.cpp @@ -0,0 +1,931 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsPrefetchService.h" +#include "nsICacheEntry.h" +#include "nsIServiceManager.h" +#include "nsICategoryManager.h" +#include "nsIObserverService.h" +#include "nsIWebProgress.h" +#include "nsCURILoader.h" +#include "nsICacheInfoChannel.h" +#include "nsIHttpChannel.h" +#include "nsIURL.h" +#include "nsISimpleEnumerator.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsXPIDLString.h" +#include "nsReadableUtils.h" +#include "nsStreamUtils.h" +#include "nsAutoPtr.h" +#include "prtime.h" +#include "mozilla/Logging.h" +#include "plstr.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "nsIDOMNode.h" +#include "nsINode.h" +#include "nsIDocument.h" +#include "nsContentUtils.h" + +using namespace mozilla; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsPrefetch:5 +// set MOZ_LOG_FILE=prefetch.log +// +// this enables LogLevel::Debug level information and places all output in +// the file prefetch.log +// +static LazyLogModule gPrefetchLog("nsPrefetch"); + +#undef LOG +#define LOG(args) MOZ_LOG(gPrefetchLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gPrefetchLog, mozilla::LogLevel::Debug) + +#define PREFETCH_PREF "network.prefetch-next" +#define PARALLELISM_PREF "network.prefetch-next.parallelism" +#define AGGRESSIVE_PREF "network.prefetch-next.aggressive" + +//----------------------------------------------------------------------------- +// helpers +//----------------------------------------------------------------------------- + +static inline uint32_t +PRTimeToSeconds(PRTime t_usec) +{ + PRTime usec_per_sec = PR_USEC_PER_SEC; + return uint32_t(t_usec /= usec_per_sec); +} + +#define NowInSeconds() PRTimeToSeconds(PR_Now()) + +//----------------------------------------------------------------------------- +// nsPrefetchNode <public> +//----------------------------------------------------------------------------- + +nsPrefetchNode::nsPrefetchNode(nsPrefetchService *aService, + nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource) + : mURI(aURI) + , mReferrerURI(aReferrerURI) + , mService(aService) + , mChannel(nullptr) + , mBytesRead(0) + , mShouldFireLoadEvent(false) +{ + nsCOMPtr<nsIWeakReference> source = do_GetWeakReference(aSource); + mSources.AppendElement(source); +} + +nsresult +nsPrefetchNode::OpenChannel() +{ + if (mSources.IsEmpty()) { + // Don't attempt to prefetch if we don't have a source node + // (which should never happen). + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsINode> source; + while (!mSources.IsEmpty() && !(source = do_QueryReferent(mSources.ElementAt(0)))) { + // If source is null remove it. + // (which should never happen). + mSources.RemoveElementAt(0); + } + + if (!source) { + // Don't attempt to prefetch if we don't have a source node + // (which should never happen). + + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsILoadGroup> loadGroup = source->OwnerDoc()->GetDocumentLoadGroup(); + CORSMode corsMode = CORS_NONE; + net::ReferrerPolicy referrerPolicy = net::RP_Unset; + if (source->IsHTMLElement(nsGkAtoms::link)) { + dom::HTMLLinkElement* link = static_cast<dom::HTMLLinkElement*>(source.get()); + corsMode = link->GetCORSMode(); + referrerPolicy = link->GetLinkReferrerPolicy(); + } + + if (referrerPolicy == net::RP_Unset) { + referrerPolicy = source->OwnerDoc()->GetReferrerPolicy(); + } + + uint32_t securityFlags; + if (corsMode == CORS_NONE) { + securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; + } else { + securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS; + if (corsMode == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + } + nsresult rv = NS_NewChannelInternal(getter_AddRefs(mChannel), + mURI, + source, + source->NodePrincipal(), + nullptr, //aTriggeringPrincipal + securityFlags, + nsIContentPolicy::TYPE_OTHER, + loadGroup, // aLoadGroup + this, // aCallbacks + nsIRequest::LOAD_BACKGROUND | + nsICachingChannel::LOAD_ONLY_IF_MODIFIED); + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr<nsIHttpChannel> httpChannel = + do_QueryInterface(mChannel); + if (httpChannel) { + httpChannel->SetReferrerWithPolicy(mReferrerURI, referrerPolicy); + httpChannel->SetRequestHeader( + NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("prefetch"), + false); + } + + rv = mChannel->AsyncOpen2(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Drop the ref to the channel, because we don't want to end up with + // cycles through it. + mChannel = nullptr; + } + return rv; +} + +nsresult +nsPrefetchNode::CancelChannel(nsresult error) +{ + mChannel->Cancel(error); + mChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsPrefetchNode, + nsIRequestObserver, + nsIStreamListener, + nsIInterfaceRequestor, + nsIChannelEventSink, + nsIRedirectResultListener) + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ + nsresult rv; + + nsCOMPtr<nsIHttpChannel> httpChannel = + do_QueryInterface(aRequest, &rv); + if (NS_FAILED(rv)) return rv; + + // if the load is cross origin without CORS, or the CORS access is rejected, + // always fire load event to avoid leaking site information. + nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->GetLoadInfo(); + mShouldFireLoadEvent = loadInfo->GetTainting() == LoadTainting::Opaque || + (loadInfo->GetTainting() == LoadTainting::CORS && + (NS_FAILED(httpChannel->GetStatus(&rv)) || + NS_FAILED(rv))); + + // no need to prefetch http error page + bool requestSucceeded; + if (NS_FAILED(httpChannel->GetRequestSucceeded(&requestSucceeded)) || + !requestSucceeded) { + return NS_BINDING_ABORTED; + } + + nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel = + do_QueryInterface(aRequest, &rv); + if (NS_FAILED(rv)) return rv; + + // no need to prefetch a document that is already in the cache + bool fromCache; + if (NS_SUCCEEDED(cacheInfoChannel->IsFromCache(&fromCache)) && + fromCache) { + LOG(("document is already in the cache; canceling prefetch\n")); + // although it's canceled we still want to fire load event + mShouldFireLoadEvent = true; + return NS_BINDING_ABORTED; + } + + // + // no need to prefetch a document that must be requested fresh each + // and every time. + // + uint32_t expTime; + if (NS_SUCCEEDED(cacheInfoChannel->GetCacheTokenExpirationTime(&expTime))) { + if (NowInSeconds() >= expTime) { + LOG(("document cannot be reused from cache; " + "canceling prefetch\n")); + return NS_BINDING_ABORTED; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchNode::OnDataAvailable(nsIRequest *aRequest, + nsISupports *aContext, + nsIInputStream *aStream, + uint64_t aOffset, + uint32_t aCount) +{ + uint32_t bytesRead = 0; + aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead); + mBytesRead += bytesRead; + LOG(("prefetched %u bytes [offset=%llu]\n", bytesRead, aOffset)); + return NS_OK; +} + + +NS_IMETHODIMP +nsPrefetchNode::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatus) +{ + LOG(("done prefetching [status=%x]\n", aStatus)); + + if (mBytesRead == 0 && aStatus == NS_OK && mChannel) { + // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was + // specified), but the object should report loadedSize as if it + // did. + mChannel->GetContentLength(&mBytesRead); + } + + mService->NotifyLoadCompleted(this); + mService->DispatchEvent(this, mShouldFireLoadEvent || NS_SUCCEEDED(aStatus)); + mService->ProcessNextURI(this); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::GetInterface(const nsIID &aIID, void **aResult) +{ + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIChannelEventSink *>(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIRedirectResultListener *>(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::AsyncOnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback *callback) +{ + nsCOMPtr<nsIURI> newURI; + nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) + return rv; + + bool match; + rv = newURI->SchemeIs("http", &match); + if (NS_FAILED(rv) || !match) { + rv = newURI->SchemeIs("https", &match); + if (NS_FAILED(rv) || !match) { + LOG(("rejected: URL is not of type http/https\n")); + return NS_ERROR_ABORT; + } + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"), + NS_LITERAL_CSTRING("prefetch"), + false); + + // Assign to mChannel after we get notification about success of the + // redirect in OnRedirectResult. + mRedirectChannel = aNewChannel; + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIRedirectResultListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::OnRedirectResult(bool proceeding) +{ + if (proceeding && mRedirectChannel) + mChannel = mRedirectChannel; + + mRedirectChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService <public> +//----------------------------------------------------------------------------- + +nsPrefetchService::nsPrefetchService() + : mMaxParallelism(6) + , mStopCount(0) + , mHaveProcessed(false) + , mDisabled(true) + , mAggressive(false) +{ +} + +nsPrefetchService::~nsPrefetchService() +{ + Preferences::RemoveObserver(this, PREFETCH_PREF); + Preferences::RemoveObserver(this, PARALLELISM_PREF); + Preferences::RemoveObserver(this, AGGRESSIVE_PREF); + // cannot reach destructor if prefetch in progress (listener owns reference + // to this service) + EmptyQueue(); +} + +nsresult +nsPrefetchService::Init() +{ + nsresult rv; + + // read prefs and hook up pref observer + mDisabled = !Preferences::GetBool(PREFETCH_PREF, !mDisabled); + Preferences::AddWeakObserver(this, PREFETCH_PREF); + + mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); + if (mMaxParallelism < 1) { + mMaxParallelism = 1; + } + Preferences::AddWeakObserver(this, PARALLELISM_PREF); + + mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false); + Preferences::AddWeakObserver(this, AGGRESSIVE_PREF); + + // Observe xpcom-shutdown event + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) + return NS_ERROR_FAILURE; + + rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mDisabled) + AddProgressListener(); + + return NS_OK; +} + +void +nsPrefetchService::ProcessNextURI(nsPrefetchNode *aFinished) +{ + nsresult rv; + + if (aFinished) { + mCurrentNodes.RemoveElement(aFinished); + } + + if (mCurrentNodes.Length() >= static_cast<uint32_t>(mMaxParallelism)) { + // We already have enough prefetches going on, so hold off + // for now. + return; + } + + do { + if (mQueue.empty()) { + break; + } + RefPtr<nsPrefetchNode> node = mQueue.front().forget(); + mQueue.pop_front(); + + if (LOG_ENABLED()) { + LOG(("ProcessNextURI [%s]\n", + node->mURI->GetSpecOrDefault().get())); } + + // + // if opening the channel fails (e.g. security check returns an error), + // send an error event and then just skip to the next uri + // + rv = node->OpenChannel(); + if (NS_SUCCEEDED(rv)) { + mCurrentNodes.AppendElement(node); + } else { + DispatchEvent(node, false); + } + } + while (NS_FAILED(rv)); +} + +void +nsPrefetchService::NotifyLoadRequested(nsPrefetchNode *node) +{ + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) + return; + + observerService->NotifyObservers(static_cast<nsIStreamListener*>(node), + "prefetch-load-requested", nullptr); +} + +void +nsPrefetchService::NotifyLoadCompleted(nsPrefetchNode *node) +{ + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) + return; + + observerService->NotifyObservers(static_cast<nsIStreamListener*>(node), + "prefetch-load-completed", nullptr); +} + +void +nsPrefetchService::DispatchEvent(nsPrefetchNode *node, bool aSuccess) +{ + for (uint32_t i = 0; i < node->mSources.Length(); i++) { + nsCOMPtr<nsINode> domNode = do_QueryReferent(node->mSources.ElementAt(i)); + if (domNode && domNode->IsInComposedDoc()) { + nsContentUtils::DispatchTrustedEvent(domNode->OwnerDoc(), + domNode, + aSuccess ? + NS_LITERAL_STRING("load") : + NS_LITERAL_STRING("error"), + /* aCanBubble = */ false, + /* aCancelable = */ false); + } + } +} + +//----------------------------------------------------------------------------- +// nsPrefetchService <private> +//----------------------------------------------------------------------------- + +void +nsPrefetchService::AddProgressListener() +{ + // Register as an observer for the document loader + nsCOMPtr<nsIWebProgress> progress = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); + if (progress) + progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); +} + +void +nsPrefetchService::RemoveProgressListener() +{ + // Register as an observer for the document loader + nsCOMPtr<nsIWebProgress> progress = + do_GetService(NS_DOCUMENTLOADER_SERVICE_CONTRACTID); + if (progress) + progress->RemoveProgressListener(this); +} + +nsresult +nsPrefetchService::EnqueueURI(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + nsPrefetchNode **aNode) +{ + RefPtr<nsPrefetchNode> node = new nsPrefetchNode(this, aURI, aReferrerURI, + aSource); + mQueue.push_back(node); + node.forget(aNode); + return NS_OK; +} + +void +nsPrefetchService::EmptyQueue() +{ + while (!mQueue.empty()) { + mQueue.pop_back(); + } +} + +void +nsPrefetchService::StartPrefetching() +{ + // + // at initialization time we might miss the first DOCUMENT START + // notification, so we have to be careful to avoid letting our + // stop count go negative. + // + if (mStopCount > 0) + mStopCount--; + + LOG(("StartPrefetching [stopcount=%d]\n", mStopCount)); + + // only start prefetching after we've received enough DOCUMENT + // STOP notifications. we do this inorder to defer prefetching + // until after all sub-frames have finished loading. + if (!mStopCount) { + mHaveProcessed = true; + while (!mQueue.empty() && mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) { + ProcessNextURI(nullptr); + } + } +} + +void +nsPrefetchService::StopPrefetching() +{ + mStopCount++; + + LOG(("StopPrefetching [stopcount=%d]\n", mStopCount)); + + // only kill the prefetch queue if we are actively prefetching right now + if (mCurrentNodes.IsEmpty()) { + return; + } + + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + } + mCurrentNodes.Clear(); + EmptyQueue(); +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsPrefetchService, + nsIPrefetchService, + nsIWebProgressListener, + nsIObserver, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIPrefetchService +//----------------------------------------------------------------------------- + +nsresult +nsPrefetchService::Prefetch(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + bool aExplicit) +{ + nsresult rv; + + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aReferrerURI); + + if (LOG_ENABLED()) { + LOG(("PrefetchURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + if (mDisabled) { + LOG(("rejected: prefetch service is disabled\n")); + return NS_ERROR_ABORT; + } + + // + // XXX we should really be asking the protocol handler if it supports + // caching, so we can determine if there is any value to prefetching. + // for now, we'll only prefetch http links since we know that's the + // most common case. ignore https links since https content only goes + // into the memory cache. + // + // XXX we might want to either leverage nsIProtocolHandler::protocolFlags + // or possibly nsIRequest::loadFlags to determine if this URI should be + // prefetched. + // + bool match; + rv = aURI->SchemeIs("http", &match); + if (NS_FAILED(rv) || !match) { + rv = aURI->SchemeIs("https", &match); + if (NS_FAILED(rv) || !match) { + LOG(("rejected: URL is not of type http/https\n")); + return NS_ERROR_ABORT; + } + } + + // + // the referrer URI must be http: + // + rv = aReferrerURI->SchemeIs("http", &match); + if (NS_FAILED(rv) || !match) { + rv = aReferrerURI->SchemeIs("https", &match); + if (NS_FAILED(rv) || !match) { + LOG(("rejected: referrer URL is neither http nor https\n")); + return NS_ERROR_ABORT; + } + } + + // skip URLs that contain query strings, except URLs for which prefetching + // has been explicitly requested. + if (!aExplicit) { + nsCOMPtr<nsIURL> url(do_QueryInterface(aURI, &rv)); + if (NS_FAILED(rv)) return rv; + nsAutoCString query; + rv = url->GetQuery(query); + if (NS_FAILED(rv) || !query.IsEmpty()) { + LOG(("rejected: URL has a query string\n")); + return NS_ERROR_ABORT; + } + } + + // + // Check whether it is being prefetched. + // + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + bool equals; + if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && + equals) { + nsCOMPtr<nsIWeakReference> source = do_GetWeakReference(aSource); + if (mCurrentNodes[i]->mSources.IndexOf(source) == + mCurrentNodes[i]->mSources.NoIndex) { + LOG(("URL is already being prefetched, add a new reference " + "document\n")); + mCurrentNodes[i]->mSources.AppendElement(source); + return NS_OK; + } else { + LOG(("URL is already being prefetched by this document")); + return NS_ERROR_ABORT; + } + } + } + + // + // Check whether it is on the prefetch queue. + // + for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt = mQueue.begin(); + nodeIt != mQueue.end(); nodeIt++) { + bool equals; + RefPtr<nsPrefetchNode> node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsCOMPtr<nsIWeakReference> source = do_GetWeakReference(aSource); + if (node->mSources.IndexOf(source) == + node->mSources.NoIndex) { + LOG(("URL is already being prefetched, add a new reference " + "document\n")); + node->mSources.AppendElement(do_GetWeakReference(aSource)); + return NS_OK; + } else { + LOG(("URL is already being prefetched by this document")); + return NS_ERROR_ABORT; + } + + } + } + + RefPtr<nsPrefetchNode> enqueuedNode; + rv = EnqueueURI(aURI, aReferrerURI, aSource, + getter_AddRefs(enqueuedNode)); + NS_ENSURE_SUCCESS(rv, rv); + + NotifyLoadRequested(enqueuedNode); + + // if there are no pages loading, kick off the request immediately + if (mStopCount == 0 && mHaveProcessed) { + ProcessNextURI(nullptr); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::CancelPrefetchURI(nsIURI* aURI, + nsIDOMNode* aSource) +{ + NS_ENSURE_ARG_POINTER(aURI); + + if (LOG_ENABLED()) { + LOG(("CancelPrefetchURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + // + // look in current prefetches + // + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + bool equals; + if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && + equals) { + nsCOMPtr<nsIWeakReference> source = do_GetWeakReference(aSource); + if (mCurrentNodes[i]->mSources.IndexOf(source) != + mCurrentNodes[i]->mSources.NoIndex) { + mCurrentNodes[i]->mSources.RemoveElement(source); + if (mCurrentNodes[i]->mSources.IsEmpty()) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + mCurrentNodes.RemoveElementAt(i); + } + return NS_OK; + } + return NS_ERROR_FAILURE; + } + } + + // + // look into the prefetch queue + // + for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt = mQueue.begin(); + nodeIt != mQueue.end(); nodeIt++) { + bool equals; + RefPtr<nsPrefetchNode> node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsCOMPtr<nsIWeakReference> source = do_GetWeakReference(aSource); + if (node->mSources.IndexOf(source) != + node->mSources.NoIndex) { + +#ifdef DEBUG + int32_t inx = node->mSources.IndexOf(source); + nsCOMPtr<nsIDOMNode> domNode = + do_QueryReferent(node->mSources.ElementAt(inx)); + MOZ_ASSERT(domNode); +#endif + + node->mSources.RemoveElement(source); + if (node->mSources.IsEmpty()) { + mQueue.erase(nodeIt); + } + return NS_OK; + } + return NS_ERROR_FAILURE; + } + } + + // not found! + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsPrefetchService::PrefetchURI(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + bool aExplicit) +{ + return Prefetch(aURI, aReferrerURI, aSource, aExplicit); +} + +NS_IMETHODIMP +nsPrefetchService::HasMoreElements(bool *aHasMore) +{ + *aHasMore = (mCurrentNodes.Length() || !mQueue.empty()); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIWebProgressListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchService::OnProgressChange(nsIWebProgress *aProgress, + nsIRequest *aRequest, + int32_t curSelfProgress, + int32_t maxSelfProgress, + int32_t curTotalProgress, + int32_t maxTotalProgress) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest *aRequest, + uint32_t progressStateFlags, + nsresult aStatus) +{ + if (mAggressive) { + LOG(("Document load state is ignored in aggressive mode")); + return NS_OK; + } + + if (progressStateFlags & STATE_IS_DOCUMENT) { + if (progressStateFlags & STATE_STOP) + StartPrefetching(); + else if (progressStateFlags & STATE_START) + StopPrefetching(); + } + + return NS_OK; +} + + +NS_IMETHODIMP +nsPrefetchService::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI *location, + uint32_t aFlags) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnSecurityChange(nsIWebProgress *aWebProgress, + nsIRequest *aRequest, + uint32_t state) +{ + NS_NOTREACHED("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchService::Observe(nsISupports *aSubject, + const char *aTopic, + const char16_t *aData) +{ + LOG(("nsPrefetchService::Observe [topic=%s]\n", aTopic)); + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + StopPrefetching(); + EmptyQueue(); + mDisabled = true; + } + else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + const nsCString converted = NS_ConvertUTF16toUTF8(aData); + const char* pref = converted.get(); + if (!strcmp(pref, PREFETCH_PREF)) { + if (Preferences::GetBool(PREFETCH_PREF, false)) { + if (mDisabled) { + LOG(("enabling prefetching\n")); + mDisabled = false; + AddProgressListener(); + } + } else { + if (!mDisabled) { + LOG(("disabling prefetching\n")); + StopPrefetching(); + EmptyQueue(); + mDisabled = true; + RemoveProgressListener(); + } + } + } else if (!strcmp(pref, PARALLELISM_PREF)) { + mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); + if (mMaxParallelism < 1) { + mMaxParallelism = 1; + } + // If our parallelism has increased, go ahead and kick off enough + // prefetches to fill up our allowance. If we're now over our + // allowance, we'll just silently let some of them finish to get + // back below our limit. + while (!mQueue.empty() && mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) { + ProcessNextURI(nullptr); + } + } else if (!strcmp(pref, AGGRESSIVE_PREF)) { + mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false); + // in aggressive mode, clear stop count and start prefetching immediately + if (mAggressive) { + mStopCount = 0; + StartPrefetching(); + } + } + } + + return NS_OK; +} + +// vim: ts=4 sw=4 expandtab diff --git a/uriloader/prefetch/nsPrefetchService.h b/uriloader/prefetch/nsPrefetchService.h new file mode 100644 index 0000000000..883449e689 --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.h @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsPrefetchService_h__ +#define nsPrefetchService_h__ + +#include "nsCPrefetchService.h" +#include "nsIObserver.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" +#include "nsIRedirectResultListener.h" +#include "nsIWebProgressListener.h" +#include "nsIStreamListener.h" +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsWeakReference.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" +#include "mozilla/Attributes.h" +#include <deque> + +class nsPrefetchService; +class nsPrefetchNode; + +//----------------------------------------------------------------------------- +// nsPrefetchService +//----------------------------------------------------------------------------- + +class nsPrefetchService final : public nsIPrefetchService + , public nsIWebProgressListener + , public nsIObserver + , public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPREFETCHSERVICE + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIOBSERVER + + nsPrefetchService(); + + nsresult Init(); + void ProcessNextURI(nsPrefetchNode *aFinished); + + void NotifyLoadRequested(nsPrefetchNode *node); + void NotifyLoadCompleted(nsPrefetchNode *node); + void DispatchEvent(nsPrefetchNode *node, bool aSuccess); + +private: + ~nsPrefetchService(); + + nsresult Prefetch(nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource, + bool aExplicit); + + void AddProgressListener(); + void RemoveProgressListener(); + nsresult EnqueueURI(nsIURI *aURI, nsIURI *aReferrerURI, + nsIDOMNode *aSource, nsPrefetchNode **node); + void EmptyQueue(); + + void StartPrefetching(); + void StopPrefetching(); + + std::deque<RefPtr<nsPrefetchNode>> mQueue; + nsTArray<RefPtr<nsPrefetchNode>> mCurrentNodes; + int32_t mMaxParallelism; + int32_t mStopCount; + // true if pending document loads have ever reached zero. + int32_t mHaveProcessed; + bool mDisabled; + + // In usual case prefetch does not start until all normal loads are done. + // Aggressive mode ignores normal loads and just start prefetch ASAP. + // It's mainly for testing purpose and discoraged for normal use; + // see https://bugzilla.mozilla.org/show_bug.cgi?id=1281415 for details. + bool mAggressive; +}; + +//----------------------------------------------------------------------------- +// nsPrefetchNode +//----------------------------------------------------------------------------- + +class nsPrefetchNode final : public nsIStreamListener + , public nsIInterfaceRequestor + , public nsIChannelEventSink + , public nsIRedirectResultListener +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + + nsPrefetchNode(nsPrefetchService *aPrefetchService, + nsIURI *aURI, + nsIURI *aReferrerURI, + nsIDOMNode *aSource); + + nsresult OpenChannel(); + nsresult CancelChannel(nsresult error); + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mReferrerURI; + nsTArray<nsCOMPtr<nsIWeakReference>> mSources; + +private: + ~nsPrefetchNode() {} + + RefPtr<nsPrefetchService> mService; + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsIChannel> mRedirectChannel; + int64_t mBytesRead; + bool mShouldFireLoadEvent; +}; + +#endif // !nsPrefetchService_h__ |