diff options
Diffstat (limited to 'uriloader')
183 files changed, 34125 insertions, 0 deletions
diff --git a/uriloader/base/moz.build b/uriloader/base/moz.build new file mode 100644 index 0000000000..f0ced2555b --- /dev/null +++ b/uriloader/base/moz.build @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +include("/ipc/chromium/chromium-config.mozbuild") + +XPIDL_SOURCES += [ + "nsCURILoader.idl", + "nsIContentHandler.idl", + "nsIDocumentLoader.idl", + "nsITransfer.idl", + "nsIURIContentListener.idl", + "nsIURILoader.idl", + "nsIWebProgress.idl", + "nsIWebProgressListener.idl", + "nsIWebProgressListener2.idl", +] + +XPIDL_MODULE = "uriloader" + +EXPORTS += [ + "nsDocLoader.h", + "nsURILoader.h", +] + +UNIFIED_SOURCES += [ + "nsDocLoader.cpp", + "nsURILoader.cpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", +] + +FINAL_LIBRARY = "xul" diff --git a/uriloader/base/nsCURILoader.idl b/uriloader/base/nsCURILoader.idl new file mode 100644 index 0000000000..b77422abe9 --- /dev/null +++ b/uriloader/base/nsCURILoader.idl @@ -0,0 +1,36 @@ +/* -*- Mode: IDL; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIURILoader.idl" + +/* +nsCURILoader implements: +------------------------- +nsIURILoader +*/ + +%{ C++ +#define NS_CONTENT_HANDLER_CONTRACTID "@mozilla.org/uriloader/content-handler;1" +#define NS_CONTENT_HANDLER_CONTRACTID_PREFIX NS_CONTENT_HANDLER_CONTRACTID "?type=" + +/** + * A category where content listeners can register. The name of the entry must + * be the content that this listener wants to handle, the value must be a + * contract ID for the listener. It will be created using createInstance (not + * getService). + * + * Listeners added this way are tried after the initial target of the load and + * after explicitly registered listeners (nsIURILoader::registerContentListener). + * + * These listeners must implement at least nsIURIContentListener (and + * nsISupports). + * + * @see nsICategoryManager + * @see nsIURIContentListener + */ +#define NS_CONTENT_LISTENER_CATEGORYMANAGER_ENTRY "external-uricontentlisteners" + +%} diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp new file mode 100644 index 0000000000..bf7902f5dd --- /dev/null +++ b/uriloader/base/nsDocLoader.cpp @@ -0,0 +1,1574 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nspr.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/Components.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Logging.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/PresShell.h" + +#include "nsDocLoader.h" +#include "nsDocShell.h" +#include "nsLoadGroup.h" +#include "nsNetUtil.h" +#include "nsIHttpChannel.h" +#include "nsIWebNavigation.h" +#include "nsIWebProgressListener2.h" + +#include "nsString.h" + +#include "nsCOMPtr.h" +#include "nscore.h" +#include "nsIWeakReferenceUtils.h" +#include "nsQueryObject.h" + +#include "nsPIDOMWindow.h" +#include "nsGlobalWindow.h" + +#include "nsIStringBundle.h" + +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocGroup.h" +#include "nsPresContext.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIBrowserDOMWindow.h" +#include "nsGlobalWindow.h" +#include "mozilla/ThrottledEventQueue.h" +using namespace mozilla; +using mozilla::DebugOnly; +using mozilla::eLoad; +using mozilla::EventDispatcher; +using mozilla::LogLevel; +using mozilla::WidgetEvent; +using mozilla::dom::BrowserChild; +using mozilla::dom::BrowsingContext; +using mozilla::dom::Document; + +// +// Log module for nsIDocumentLoader logging... +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=DocLoader:5 +// set MOZ_LOG_FILE=debug.log +// +// this enables LogLevel::Debug level information and places all output in +// the file 'debug.log'. +// +mozilla::LazyLogModule gDocLoaderLog("DocLoader"); + +#if defined(DEBUG) +void GetURIStringFromRequest(nsIRequest* request, nsACString& name) { + if (request) + request->GetName(name); + else + name.AssignLiteral("???"); +} +#endif /* DEBUG */ + +void nsDocLoader::RequestInfoHashInitEntry(PLDHashEntryHdr* entry, + const void* key) { + // Initialize the entry with placement new + new (entry) nsRequestInfo(key); +} + +void nsDocLoader::RequestInfoHashClearEntry(PLDHashTable* table, + PLDHashEntryHdr* entry) { + nsRequestInfo* info = static_cast<nsRequestInfo*>(entry); + info->~nsRequestInfo(); +} + +// this is used for mListenerInfoList.Contains() +template <> +class nsDefaultComparator<nsDocLoader::nsListenerInfo, + nsIWebProgressListener*> { + public: + bool Equals(const nsDocLoader::nsListenerInfo& aInfo, + nsIWebProgressListener* const& aListener) const { + nsCOMPtr<nsIWebProgressListener> listener = + do_QueryReferent(aInfo.mWeakListener); + return aListener == listener; + } +}; + +/* static */ const PLDHashTableOps nsDocLoader::sRequestInfoHashOps = { + PLDHashTable::HashVoidPtrKeyStub, PLDHashTable::MatchEntryStub, + PLDHashTable::MoveEntryStub, nsDocLoader::RequestInfoHashClearEntry, + nsDocLoader::RequestInfoHashInitEntry}; + +nsDocLoader::nsDocLoader() + : mParent(nullptr), + mProgressStateFlags(0), + mCurrentSelfProgress(0), + mMaxSelfProgress(0), + mCurrentTotalProgress(0), + mMaxTotalProgress(0), + mRequestInfoHash(&sRequestInfoHashOps, sizeof(nsRequestInfo)), + mCompletedTotalProgress(0), + mIsLoadingDocument(false), + mIsRestoringDocument(false), + mDontFlushLayout(false), + mIsFlushingLayout(false), + mTreatAsBackgroundLoad(false), + mHasFakeOnLoadDispatched(false), + mIsReadyToHandlePostMessage(false), + mDocumentOpenedButNotLoaded(false) { + ClearInternalProgress(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader:%p: created.\n", this)); +} + +nsresult nsDocLoader::SetDocLoaderParent(nsDocLoader* aParent) { + mParent = aParent; + return NS_OK; +} + +nsresult nsDocLoader::Init() { + nsresult rv = NS_NewLoadGroup(getter_AddRefs(mLoadGroup), this); + if (NS_FAILED(rv)) return rv; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: load group %p.\n", this, mLoadGroup.get())); + + return NS_OK; +} + +nsresult nsDocLoader::InitWithBrowsingContext( + BrowsingContext* aBrowsingContext) { + RefPtr<net::nsLoadGroup> loadGroup = new net::nsLoadGroup(); + if (!aBrowsingContext->GetRequestContextId()) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv = loadGroup->InitWithRequestContextId( + aBrowsingContext->GetRequestContextId()); + if (NS_FAILED(rv)) return rv; + + rv = loadGroup->SetGroupObserver(this); + if (NS_FAILED(rv)) return rv; + + mLoadGroup = loadGroup; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: load group %p.\n", this, mLoadGroup.get())); + + return NS_OK; +} + +nsDocLoader::~nsDocLoader() { + /* + |ClearWeakReferences()| here is intended to prevent people holding + weak references from re-entering this destructor since |QueryReferent()| + will |AddRef()| me, and the subsequent |Release()| will try to destroy me. + At this point there should be only weak references remaining (otherwise, we + wouldn't be getting destroyed). + + An alternative would be incrementing our refcount (consider it a + compressed flag saying "Don't re-destroy."). I haven't yet decided which + is better. [scc] + */ + // XXXbz now that NS_IMPL_RELEASE stabilizes by setting refcount to 1, is + // this needed? + ClearWeakReferences(); + + Destroy(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, ("DocLoader:%p: deleted.\n", this)); +} + +/* + * Implementation of ISupports methods... + */ +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDocLoader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDocLoader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDocLoader) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDocumentLoader) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIDocumentLoader) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIWebProgress) + NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsDocLoader) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WEAK(nsDocLoader, mChildrenInOnload) + +/* + * Implementation of nsIInterfaceRequestor methods... + */ +NS_IMETHODIMP nsDocLoader::GetInterface(const nsIID& aIID, void** aSink) { + nsresult rv = NS_ERROR_NO_INTERFACE; + + NS_ENSURE_ARG_POINTER(aSink); + + if (aIID.Equals(NS_GET_IID(nsILoadGroup))) { + *aSink = mLoadGroup; + NS_IF_ADDREF((nsISupports*)*aSink); + rv = NS_OK; + } else { + rv = QueryInterface(aIID, aSink); + } + + return rv; +} + +/* static */ +already_AddRefed<nsDocLoader> nsDocLoader::GetAsDocLoader( + nsISupports* aSupports) { + RefPtr<nsDocLoader> ret = do_QueryObject(aSupports); + return ret.forget(); +} + +/* static */ +nsresult nsDocLoader::AddDocLoaderAsChildOfRoot(nsDocLoader* aDocLoader) { + nsCOMPtr<nsIDocumentLoader> docLoaderService = + components::DocLoader::Service(); + NS_ENSURE_TRUE(docLoaderService, NS_ERROR_UNEXPECTED); + + RefPtr<nsDocLoader> rootDocLoader = GetAsDocLoader(docLoaderService); + NS_ENSURE_TRUE(rootDocLoader, NS_ERROR_UNEXPECTED); + + return rootDocLoader->AddChildLoader(aDocLoader); +} + +NS_IMETHODIMP +nsDocLoader::Stop(void) { + nsresult rv = NS_OK; + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Stop() called\n", this)); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, Stop, ()); + + if (mLoadGroup) rv = mLoadGroup->Cancel(NS_BINDING_ABORTED); + + // Don't report that we're flushing layout so IsBusy returns false after a + // Stop call. + mIsFlushingLayout = false; + + // Clear out mChildrenInOnload. We're not going to fire our onload + // anyway at this point, and there's no issue with mChildrenInOnload + // after this, since mDocumentRequest will be null after the + // DocLoaderIsEmpty() call. + mChildrenInOnload.Clear(); + mOOPChildrenLoading.Clear(); + + // Make sure to call DocLoaderIsEmpty now so that we reset mDocumentRequest, + // etc, as needed. We could be getting into here from a subframe onload, in + // which case the call to DocLoaderIsEmpty() is coming but hasn't quite + // happened yet, Canceling the loadgroup did nothing (because it was already + // empty), and we're about to start a new load (which is what triggered this + // Stop() call). + + // XXXbz If the child frame loadgroups were requests in mLoadgroup, I suspect + // we wouldn't need the call here.... + + NS_ASSERTION(!IsBusy(), "Shouldn't be busy here"); + + // If Cancelling the load group only had pending subresource requests, then + // the group status will still be success, and we would fire the load event. + // We want to avoid that when we're aborting the load, so override the status + // with an explicit NS_BINDING_ABORTED value. + DocLoaderIsEmpty(false, Some(NS_BINDING_ABORTED)); + + return rv; +} + +bool nsDocLoader::TreatAsBackgroundLoad() { return mTreatAsBackgroundLoad; } + +void nsDocLoader::SetBackgroundLoadIframe() { mTreatAsBackgroundLoad = true; } + +bool nsDocLoader::IsBusy() { + nsresult rv; + + // + // A document loader is busy if either: + // + // 1. One of its children is in the middle of an onload handler. Note that + // the handler may have already removed this child from mChildList! + // 2. It is currently loading a document and either has parts of it still + // loading, or has a busy child docloader. + // 3. It's currently flushing layout in DocLoaderIsEmpty(). + // + + if (!mChildrenInOnload.IsEmpty() || !mOOPChildrenLoading.IsEmpty() || + mIsFlushingLayout) { + return true; + } + + /* Is this document loader busy? */ + if (!IsBlockingLoadEvent()) { + return false; + } + + // Check if any in-process sub-document is awaiting its 'load' event: + bool busy; + rv = mLoadGroup->IsPending(&busy); + if (NS_FAILED(rv)) { + return false; + } + if (busy) { + return true; + } + + /* check its child document loaders... */ + uint32_t count = mChildList.Length(); + for (uint32_t i = 0; i < count; i++) { + nsIDocumentLoader* loader = ChildAt(i); + + // If 'dom.cross_origin_iframes_loaded_in_background' is set, the parent + // document treats cross domain iframes as background loading frame + if (loader && static_cast<nsDocLoader*>(loader)->TreatAsBackgroundLoad()) { + continue; + } + // This is a safe cast, because we only put nsDocLoader objects into the + // array + if (loader && static_cast<nsDocLoader*>(loader)->IsBusy()) return true; + } + + return false; +} + +NS_IMETHODIMP +nsDocLoader::GetContainer(nsISupports** aResult) { + NS_ADDREF(*aResult = static_cast<nsIDocumentLoader*>(this)); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetLoadGroup(nsILoadGroup** aResult) { + nsresult rv = NS_OK; + + if (nullptr == aResult) { + rv = NS_ERROR_NULL_POINTER; + } else { + *aResult = mLoadGroup; + NS_IF_ADDREF(*aResult); + } + return rv; +} + +void nsDocLoader::Destroy() { + Stop(); + + // Remove the document loader from the parent list of loaders... + if (mParent) { + DebugOnly<nsresult> rv = mParent->RemoveChildLoader(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RemoveChildLoader failed"); + } + + // Release all the information about network requests... + ClearRequestInfoHash(); + + mListenerInfoList.Clear(); + mListenerInfoList.Compact(); + + mDocumentRequest = nullptr; + + if (mLoadGroup) mLoadGroup->SetGroupObserver(nullptr); + + DestroyChildren(); +} + +void nsDocLoader::DestroyChildren() { + uint32_t count = mChildList.Length(); + // if the doc loader still has children...we need to enumerate the + // children and make them null out their back ptr to the parent doc + // loader + for (uint32_t i = 0; i < count; i++) { + nsIDocumentLoader* loader = ChildAt(i); + + if (loader) { + // This is a safe cast, as we only put nsDocLoader objects into the + // array + DebugOnly<nsresult> rv = + static_cast<nsDocLoader*>(loader)->SetDocLoaderParent(nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SetDocLoaderParent failed"); + } + } + mChildList.Clear(); +} + +NS_IMETHODIMP +nsDocLoader::OnStartRequest(nsIRequest* request) { + // called each time a request is added to the group. + + if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) { + nsAutoCString name; + request->GetName(name); + + uint32_t count = 0; + if (mLoadGroup) mLoadGroup->GetActiveCount(&count); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: OnStartRequest[%p](%s) mIsLoadingDocument=%s, %u " + "active URLs", + this, request, name.get(), (mIsLoadingDocument ? "true" : "false"), + count)); + } + + bool bJustStartedLoading = false; + + nsLoadFlags loadFlags = 0; + request->GetLoadFlags(&loadFlags); + + if (!mIsLoadingDocument && (loadFlags & nsIChannel::LOAD_DOCUMENT_URI)) { + bJustStartedLoading = true; + mIsLoadingDocument = true; + mDocumentOpenedButNotLoaded = false; + ClearInternalProgress(); // only clear our progress if we are starting a + // new load.... + } + + // + // Create a new nsRequestInfo for the request that is starting to + // load... + // + AddRequestInfo(request); + + // + // Only fire a doStartDocumentLoad(...) if the document loader + // has initiated a load... Otherwise, this notification has + // resulted from a request being added to the load group. + // + if (mIsLoadingDocument) { + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) { + // + // Make sure that the document channel is null at this point... + // (unless its been redirected) + // + NS_ASSERTION( + (loadFlags & nsIChannel::LOAD_REPLACE) || !(mDocumentRequest.get()), + "Overwriting an existing document channel!"); + + // This request is associated with the entire document... + mDocumentRequest = request; + mLoadGroup->SetDefaultLoadRequest(request); + + // Only fire the start document load notification for the first + // document URI... Do not fire it again for redirections + // + if (bJustStartedLoading) { + // Update the progress status state + mProgressStateFlags = nsIWebProgressListener::STATE_START; + + // Fire the start document load notification + doStartDocumentLoad(); + return NS_OK; + } + } + } + + NS_ASSERTION(!mIsLoadingDocument || mDocumentRequest, + "mDocumentRequest MUST be set for the duration of a page load!"); + + // This is the only way to catch document request start event after a redirect + // has occured without changing inherited Firefox behaviour significantly. + // Problem description: + // The combination of |STATE_START + STATE_IS_DOCUMENT| is only sent for + // initial request (see |doStartDocumentLoad| call above). + // And |STATE_REDIRECTING + STATE_IS_DOCUMENT| is sent with old channel, which + // makes it impossible to filter by destination URL (see + // |AsyncOnChannelRedirect| implementation). + // Fixing any of those bugs may cause unpredictable consequences in any part + // of the browser, so we just add a custom flag for this exact situation. + int32_t extraFlags = 0; + if (mIsLoadingDocument && !bJustStartedLoading && + (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) && + (loadFlags & nsIChannel::LOAD_REPLACE)) { + extraFlags = nsIWebProgressListener::STATE_IS_REDIRECTED_DOCUMENT; + } + doStartURLLoad(request, extraFlags); + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + nsresult rv = NS_OK; + + if (MOZ_LOG_TEST(gDocLoaderLog, LogLevel::Debug)) { + nsAutoCString name; + aRequest->GetName(name); + + uint32_t count = 0; + if (mLoadGroup) mLoadGroup->GetActiveCount(&count); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: OnStopRequest[%p](%s) status=%" PRIx32 + " mIsLoadingDocument=%s, mDocumentOpenedButNotLoaded=%s," + " %u active URLs", + this, aRequest, name.get(), static_cast<uint32_t>(aStatus), + (mIsLoadingDocument ? "true" : "false"), + (mDocumentOpenedButNotLoaded ? "true" : "false"), count)); + } + + bool bFireTransferring = false; + + // + // Set the Maximum progress to the same value as the current progress. + // Since the URI has finished loading, all the data is there. Also, + // this will allow a more accurate estimation of the max progress (in case + // the old value was unknown ie. -1) + // + nsRequestInfo* info = GetRequestInfo(aRequest); + if (info) { + // Null out mLastStatus now so we don't find it when looking for + // status from now on. This destroys the nsStatusInfo and hence + // removes it from our list. + info->mLastStatus = nullptr; + + int64_t oldMax = info->mMaxProgress; + + info->mMaxProgress = info->mCurrentProgress; + + // + // If a request whose content-length was previously unknown has just + // finished loading, then use this new data to try to calculate a + // mMaxSelfProgress... + // + if ((oldMax < int64_t(0)) && (mMaxSelfProgress < int64_t(0))) { + mMaxSelfProgress = CalculateMaxProgress(); + } + + // As we know the total progress of this request now, save it to be part + // of CalculateMaxProgress() result. We need to remove the info from the + // hash, see bug 480713. + mCompletedTotalProgress += info->mMaxProgress; + + // + // Determine whether a STATE_TRANSFERRING notification should be + // 'synthesized'. + // + // If nsRequestInfo::mMaxProgress (as stored in oldMax) and + // nsRequestInfo::mCurrentProgress are both 0, then the + // STATE_TRANSFERRING notification has not been fired yet... + // + if ((oldMax == 0) && (info->mCurrentProgress == 0)) { + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + + // Only fire a TRANSFERRING notification if the request is also a + // channel -- data transfer requires a nsIChannel! + // + if (channel) { + if (NS_SUCCEEDED(aStatus)) { + bFireTransferring = true; + } + // + // If the request failed (for any reason other than being + // redirected or retargeted), the TRANSFERRING notification can + // still be fired if a HTTP connection was established to a server. + // + else if (aStatus != NS_BINDING_REDIRECTED && + aStatus != NS_BINDING_RETARGETED) { + // + // Only if the load has been targeted (see bug 268483)... + // + uint32_t lf; + channel->GetLoadFlags(&lf); + if (lf & nsIChannel::LOAD_TARGETED) { + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aRequest)); + if (httpChannel) { + uint32_t responseCode; + rv = httpChannel->GetResponseStatus(&responseCode); + if (NS_SUCCEEDED(rv)) { + // + // A valid server status indicates that a connection was + // established to the server... So, fire the notification + // even though a failure occurred later... + // + bFireTransferring = true; + } + } + } + } + } + } + } + + if (bFireTransferring) { + // Send a STATE_TRANSFERRING notification for the request. + int32_t flags; + + flags = nsIWebProgressListener::STATE_TRANSFERRING | + nsIWebProgressListener::STATE_IS_REQUEST; + // + // Move the WebProgress into the STATE_TRANSFERRING state if necessary... + // + if (mProgressStateFlags & nsIWebProgressListener::STATE_START) { + mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING; + + // Send STATE_TRANSFERRING for the document too... + flags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + } + + FireOnStateChange(this, aRequest, flags, NS_OK); + } + + // + // Fire the OnStateChange(...) notification for stop request + // + doStopURLLoad(aRequest, aStatus); + + // Clear this request out of the hash to avoid bypass of FireOnStateChange + // when address of the request is reused. + RemoveRequestInfo(aRequest); + + // + // Only fire the DocLoaderIsEmpty(...) if we may need to fire onload. + // + if (IsBlockingLoadEvent()) { + nsCOMPtr<nsIDocShell> ds = + do_QueryInterface(static_cast<nsIRequestObserver*>(this)); + bool doNotFlushLayout = false; + if (ds) { + // Don't do unexpected layout flushes while we're in process of restoring + // a document from the bfcache. + ds->GetRestoringDocument(&doNotFlushLayout); + } + DocLoaderIsEmpty(!doNotFlushLayout); + } + + return NS_OK; +} + +nsresult nsDocLoader::RemoveChildLoader(nsDocLoader* aChild) { + nsresult rv = mChildList.RemoveElement(aChild) ? NS_OK : NS_ERROR_FAILURE; + if (NS_SUCCEEDED(rv)) { + rv = aChild->SetDocLoaderParent(nullptr); + } + return rv; +} + +nsresult nsDocLoader::AddChildLoader(nsDocLoader* aChild) { + mChildList.AppendElement(aChild); + return aChild->SetDocLoaderParent(this); +} + +NS_IMETHODIMP nsDocLoader::GetDocumentChannel(nsIChannel** aChannel) { + if (!mDocumentRequest) { + *aChannel = nullptr; + return NS_OK; + } + + return CallQueryInterface(mDocumentRequest, aChannel); +} + +void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout, + const Maybe<nsresult>& aOverrideStatus) { + if (IsBlockingLoadEvent()) { + /* In the unimagineably rude circumstance that onload event handlers + triggered by this function actually kill the window ... ok, it's + not unimagineable; it's happened ... this deathgrip keeps this object + alive long enough to survive this function call. */ + nsCOMPtr<nsIDocumentLoader> kungFuDeathGrip(this); + + // Don't flush layout if we're still busy. + if (IsBusy()) { + return; + } + + NS_ASSERTION(!mIsFlushingLayout, "Someone screwed up"); + // We may not have a document request if we are in a + // document.open() situation. + NS_ASSERTION(mDocumentRequest || mDocumentOpenedButNotLoaded, + "No Document Request!"); + + // The load group for this DocumentLoader is idle. Flush if we need to. + if (aFlushLayout && !mDontFlushLayout) { + nsCOMPtr<Document> doc = do_GetInterface(GetAsSupports(this)); + if (doc) { + // We start loads from style resolution, so we need to flush out style + // no matter what. If we have user fonts, we also need to flush layout, + // since the reflow is what starts font loads. + mozilla::FlushType flushType = mozilla::FlushType::Style; + // Be safe in case this presshell is in teardown now + doc->FlushUserFontSet(); + if (doc->GetUserFontSet()) { + flushType = mozilla::FlushType::Layout; + } + mDontFlushLayout = mIsFlushingLayout = true; + doc->FlushPendingNotifications(flushType); + mDontFlushLayout = mIsFlushingLayout = false; + } + } + + // And now check whether we're really busy; that might have changed with + // the layout flush. + // + // Note, mDocumentRequest can be null while mDocumentOpenedButNotLoaded is + // false if the flushing above re-entered this method. + if (IsBusy() || (!mDocumentRequest && !mDocumentOpenedButNotLoaded)) { + return; + } + + if (mDocumentRequest) { + // Clear out our request info hash, now that our load really is done and + // we don't need it anymore to CalculateMaxProgress(). + ClearInternalProgress(); + + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Is now idle...\n", this)); + + nsCOMPtr<nsIRequest> docRequest = mDocumentRequest; + + mDocumentRequest = nullptr; + mIsLoadingDocument = false; + + // Update the progress status state - the document is done + mProgressStateFlags = nsIWebProgressListener::STATE_STOP; + + nsresult loadGroupStatus = NS_OK; + if (aOverrideStatus) { + loadGroupStatus = *aOverrideStatus; + } else { + mLoadGroup->GetStatus(&loadGroupStatus); + } + + // + // New code to break the circular reference between + // the load group and the docloader... + // + mLoadGroup->SetDefaultLoadRequest(nullptr); + + // Take a ref to our parent now so that we can call ChildDoneWithOnload() + // on it even if our onload handler removes us from the docloader tree. + RefPtr<nsDocLoader> parent = mParent; + + // Note that if calling ChildEnteringOnload() on the parent returns false + // then calling our onload handler is not safe. That can only happen on + // OOM, so that's ok. + if (!parent || parent->ChildEnteringOnload(this)) { + // Do nothing with our state after firing the + // OnEndDocumentLoad(...). The document loader may be loading a *new* + // document - if LoadDocument() was called from a handler! + // + doStopDocumentLoad(docRequest, loadGroupStatus); + + NotifyDoneWithOnload(parent); + } + } else { + MOZ_ASSERT(mDocumentOpenedButNotLoaded); + mDocumentOpenedButNotLoaded = false; + + // Make sure we do the ChildEnteringOnload/ChildDoneWithOnload even if we + // plan to skip firing our own load event, because otherwise we might + // never end up firing our parent's load event. + RefPtr<nsDocLoader> parent = mParent; + if (!parent || parent->ChildEnteringOnload(this)) { + nsresult loadGroupStatus = NS_OK; + mLoadGroup->GetStatus(&loadGroupStatus); + + // Can "doc" or "window" ever come back null here? Our state machine + // is complicated enough I wouldn't bet against it... + nsCOMPtr<Document> doc = do_GetInterface(GetAsSupports(this)); + if (doc) { + // Make sure we're not canceling the loadgroup. If we are, then just + // like the normal navigation case we should not fire a load event. + if (NS_SUCCEEDED(loadGroupStatus) || + loadGroupStatus == NS_ERROR_PARSED_DATA_CACHED) { + // The readyState change is required to pass + // dom/html/test/test_bug347174_write.html + doc->SetReadyStateInternal(Document::READYSTATE_COMPLETE, + /* updateTimingInformation = */ false); + doc->StopDocumentLoad(); + + nsCOMPtr<nsPIDOMWindowOuter> window = doc->GetWindow(); + if (window && !doc->SkipLoadEventAfterClose()) { + if (!mozilla::dom::DocGroup::TryToLoadIframesInBackground() || + (mozilla::dom::DocGroup::TryToLoadIframesInBackground() && + !HasFakeOnLoadDispatched())) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Firing load event for document.open\n", + this)); + + // This is a very cut-down version of + // nsDocumentViewer::LoadComplete that doesn't do various things + // that are not relevant here because this wasn't an actual + // navigation. + WidgetEvent event(true, eLoad); + event.mFlags.mBubbles = false; + event.mFlags.mCancelable = false; + // Dispatching to |window|, but using |document| as the target, + // per spec. + event.mTarget = doc; + nsEventStatus unused = nsEventStatus_eIgnore; + doc->SetLoadEventFiring(true); + EventDispatcher::Dispatch(window, nullptr, &event, nullptr, + &unused); + doc->SetLoadEventFiring(false); + + // Now unsuppress painting on the presshell, if we + // haven't done that yet. + RefPtr<PresShell> presShell = doc->GetPresShell(); + if (presShell && !presShell->IsDestroying()) { + presShell->UnsuppressPainting(); + + if (!presShell->IsDestroying()) { + presShell->LoadComplete(); + } + } + } + } + } else if (loadGroupStatus == NS_BINDING_ABORTED) { + doc->NotifyAbortedLoad(); + } + + if (doc->IsCurrentActiveDocument() && !doc->IsShowing() && + loadGroupStatus != NS_BINDING_ABORTED) { + nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(this); + bool isInUnload; + if (docShell && + NS_SUCCEEDED(docShell->GetIsInUnload(&isInUnload)) && + !isInUnload) { + doc->OnPageShow(false, nullptr); + } + } + } + NotifyDoneWithOnload(parent); + } + } + } +} + +void nsDocLoader::NotifyDoneWithOnload(nsDocLoader* aParent) { + if (aParent) { + // In-process parent: + aParent->ChildDoneWithOnload(this); + } + nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(this); + if (!docShell) { + return; + } + BrowsingContext* bc = nsDocShell::Cast(docShell)->GetBrowsingContext(); + if (bc->IsContentSubframe() && !bc->GetParent()->IsInProcess()) { + if (BrowserChild* browserChild = BrowserChild::GetFrom(docShell)) { + mozilla::Unused << browserChild->SendMaybeFireEmbedderLoadEvents( + dom::EmbedderElementEventType::NoEvent); + } + } +} + +void nsDocLoader::doStartDocumentLoad(void) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(mDocumentRequest, buffer); + MOZ_LOG( + gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for start document load (...)." + "\tURI: %s \n", + this, buffer.get())); +#endif /* DEBUG */ + + // Fire an OnStatus(...) notification STATE_START. This indicates + // that the document represented by mDocumentRequest has started to + // load... + FireOnStateChange(this, mDocumentRequest, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_DOCUMENT | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); +} + +void nsDocLoader::doStartURLLoad(nsIRequest* request, int32_t aExtraFlags) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange start url load (...)." + "\tURI: %s\n", + this, buffer.get())); +#endif /* DEBUG */ + + FireOnStateChange(this, request, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_REQUEST | aExtraFlags, + NS_OK); +} + +void nsDocLoader::doStopURLLoad(nsIRequest* request, nsresult aStatus) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for end url load (...)." + "\tURI: %s status=%" PRIx32 "\n", + this, buffer.get(), static_cast<uint32_t>(aStatus))); +#endif /* DEBUG */ + + FireOnStateChange(this, request, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_REQUEST, + aStatus); + + // Fire a status change message for the most recent unfinished + // request to make sure that the displayed status is not outdated. + if (!mStatusInfoList.isEmpty()) { + nsStatusInfo* statusInfo = mStatusInfoList.getFirst(); + FireOnStatusChange(this, statusInfo->mRequest, statusInfo->mStatusCode, + statusInfo->mStatusMessage.get()); + } +} + +void nsDocLoader::doStopDocumentLoad(nsIRequest* request, nsresult aStatus) { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: ++ Firing OnStateChange for end document load (...)." + "\tURI: %s Status=%" PRIx32 "\n", + this, buffer.get(), static_cast<uint32_t>(aStatus))); +#endif /* DEBUG */ + + // Firing STATE_STOP|STATE_IS_DOCUMENT will fire onload handlers. + // Grab our parent chain before doing that so we can still dispatch + // STATE_STOP|STATE_IS_WINDW_STATE_IS_NETWORK to them all, even if + // the onload handlers rearrange the docshell tree. + WebProgressList list; + GatherAncestorWebProgresses(list); + + // + // Fire an OnStateChange(...) notification indicating the the + // current document has finished loading... + // + int32_t flags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_DOCUMENT; + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(this, request, flags, aStatus); + } + + // + // Fire a final OnStateChange(...) notification indicating the the + // current document has finished loading... + // + flags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_WINDOW | + nsIWebProgressListener::STATE_IS_NETWORK; + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(this, request, flags, aStatus); + } +} + +//////////////////////////////////////////////////////////////////////////////////// +// The following section contains support for nsIWebProgress and related stuff +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsDocLoader::AddProgressListener(nsIWebProgressListener* aListener, + uint32_t aNotifyMask) { + if (mListenerInfoList.Contains(aListener)) { + // The listener is already registered! + return NS_ERROR_FAILURE; + } + + nsWeakPtr listener = do_GetWeakReference(aListener); + if (!listener) { + return NS_ERROR_INVALID_ARG; + } + + mListenerInfoList.AppendElement(nsListenerInfo(listener, aNotifyMask)); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::RemoveProgressListener(nsIWebProgressListener* aListener) { + return mListenerInfoList.RemoveElement(aListener) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsDocLoader::GetDOMWindow(mozIDOMWindowProxy** aResult) { + return CallGetInterface(this, aResult); +} + +NS_IMETHODIMP +nsDocLoader::GetIsTopLevel(bool* aResult) { + nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(this); + *aResult = docShell && docShell->GetBrowsingContext()->IsTop(); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetIsLoadingDocument(bool* aIsLoadingDocument) { + *aIsLoadingDocument = mIsLoadingDocument; + + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::GetLoadType(uint32_t* aLoadType) { + *aLoadType = 0; + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsDocLoader::GetTarget(nsIEventTarget** aTarget) { + nsCOMPtr<mozIDOMWindowProxy> window; + nsresult rv = GetDOMWindow(getter_AddRefs(window)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(window); + NS_ENSURE_STATE(global); + + nsCOMPtr<nsIEventTarget> target = + global->EventTargetFor(mozilla::TaskCategory::Other); + target.forget(aTarget); + return NS_OK; +} + +NS_IMETHODIMP +nsDocLoader::SetTarget(nsIEventTarget* aTarget) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +int64_t nsDocLoader::GetMaxTotalProgress() { + int64_t newMaxTotal = 0; + + uint32_t count = mChildList.Length(); + for (uint32_t i = 0; i < count; i++) { + int64_t individualProgress = 0; + nsIDocumentLoader* docloader = ChildAt(i); + if (docloader) { + // Cast is safe since all children are nsDocLoader too + individualProgress = ((nsDocLoader*)docloader)->GetMaxTotalProgress(); + } + if (individualProgress < int64_t(0)) // if one of the elements doesn't know + // it's size then none of them do + { + newMaxTotal = int64_t(-1); + break; + } else + newMaxTotal += individualProgress; + } + + int64_t progress = -1; + if (mMaxSelfProgress >= int64_t(0) && newMaxTotal >= int64_t(0)) + progress = newMaxTotal + mMaxSelfProgress; + + return progress; +} + +//////////////////////////////////////////////////////////////////////////////////// +// The following section contains support for nsIProgressEventSink which is used +// to pass progress and status between the actual request and the doc loader. +// The doc loader then turns around and makes the right web progress calls based +// on this information. +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsDocLoader::OnProgress(nsIRequest* aRequest, int64_t aProgress, + int64_t aProgressMax) { + int64_t progressDelta = 0; + + // + // Update the RequestInfo entry with the new progress data + // + if (nsRequestInfo* info = GetRequestInfo(aRequest)) { + // Update info->mCurrentProgress before we call FireOnStateChange, + // since that can make the "info" pointer invalid. + int64_t oldCurrentProgress = info->mCurrentProgress; + progressDelta = aProgress - oldCurrentProgress; + info->mCurrentProgress = aProgress; + + // suppress sending STATE_TRANSFERRING if this is upload progress (see bug + // 240053) + if (!info->mUploading && (int64_t(0) == oldCurrentProgress) && + (int64_t(0) == info->mMaxProgress)) { + // + // If we receive an OnProgress event from a toplevel channel that the URI + // Loader has not yet targeted, then we must suppress the event. This is + // necessary to ensure that webprogresslisteners do not get confused when + // the channel is finally targeted. See bug 257308. + // + nsLoadFlags lf = 0; + aRequest->GetLoadFlags(&lf); + if ((lf & nsIChannel::LOAD_DOCUMENT_URI) && + !(lf & nsIChannel::LOAD_TARGETED)) { + MOZ_LOG( + gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p Ignoring OnProgress while load is not targeted\n", + this)); + return NS_OK; + } + + // + // This is the first progress notification for the entry. If + // (aMaxProgress != -1) then the content-length of the data is known, + // so update mMaxSelfProgress... Otherwise, set it to -1 to indicate + // that the content-length is no longer known. + // + if (aProgressMax != -1) { + mMaxSelfProgress += aProgressMax; + info->mMaxProgress = aProgressMax; + } else { + mMaxSelfProgress = int64_t(-1); + info->mMaxProgress = int64_t(-1); + } + + // Send a STATE_TRANSFERRING notification for the request. + int32_t flags; + + flags = nsIWebProgressListener::STATE_TRANSFERRING | + nsIWebProgressListener::STATE_IS_REQUEST; + // + // Move the WebProgress into the STATE_TRANSFERRING state if necessary... + // + if (mProgressStateFlags & nsIWebProgressListener::STATE_START) { + mProgressStateFlags = nsIWebProgressListener::STATE_TRANSFERRING; + + // Send STATE_TRANSFERRING for the document too... + flags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + } + + FireOnStateChange(this, aRequest, flags, NS_OK); + } + + // Update our overall current progress count. + mCurrentSelfProgress += progressDelta; + } + // + // The request is not part of the load group, so ignore its progress + // information... + // + else { +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(aRequest, buffer); + MOZ_LOG( + gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p OOPS - No Request Info for: %s\n", this, buffer.get())); +#endif /* DEBUG */ + + return NS_OK; + } + + // + // Fire progress notifications out to any registered nsIWebProgressListeners + // + FireOnProgressChange(this, aRequest, aProgress, aProgressMax, progressDelta, + mCurrentTotalProgress, mMaxTotalProgress); + + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::OnStatus(nsIRequest* aRequest, nsresult aStatus, + const char16_t* aStatusArg) { + // + // Fire progress notifications out to any registered nsIWebProgressListeners + // + if (aStatus != NS_OK) { + // Remember the current status for this request + nsRequestInfo* info; + info = GetRequestInfo(aRequest); + if (info) { + bool uploading = (aStatus == NS_NET_STATUS_WRITING || + aStatus == NS_NET_STATUS_SENDING_TO); + // If switching from uploading to downloading (or vice versa), then we + // need to reset our progress counts. This is designed with HTTP form + // submission in mind, where an upload is performed followed by download + // of possibly several documents. + if (info->mUploading != uploading) { + mCurrentSelfProgress = mMaxSelfProgress = 0; + mCurrentTotalProgress = mMaxTotalProgress = 0; + mCompletedTotalProgress = 0; + info->mUploading = uploading; + info->mCurrentProgress = 0; + info->mMaxProgress = 0; + } + } + + nsCOMPtr<nsIStringBundleService> sbs = + mozilla::services::GetStringBundleService(); + if (!sbs) return NS_ERROR_FAILURE; + nsAutoString msg; + nsresult rv = sbs->FormatStatusMessage(aStatus, aStatusArg, msg); + if (NS_FAILED(rv)) return rv; + + // Keep around the message. In case a request finishes, we need to make sure + // to send the status message of another request to our user to that we + // don't display, for example, "Transferring" messages for requests that are + // already done. + if (info) { + if (!info->mLastStatus) { + info->mLastStatus = MakeUnique<nsStatusInfo>(aRequest); + } else { + // We're going to move it to the front of the list, so remove + // it from wherever it is now. + info->mLastStatus->remove(); + } + info->mLastStatus->mStatusMessage = msg; + info->mLastStatus->mStatusCode = aStatus; + // Put the info at the front of the list + mStatusInfoList.insertFront(info->mLastStatus.get()); + } + FireOnStatusChange(this, aRequest, aStatus, msg.get()); + } + return NS_OK; +} + +void nsDocLoader::ClearInternalProgress() { + ClearRequestInfoHash(); + + mCurrentSelfProgress = mMaxSelfProgress = 0; + mCurrentTotalProgress = mMaxTotalProgress = 0; + mCompletedTotalProgress = 0; + + mProgressStateFlags = nsIWebProgressListener::STATE_STOP; +} + +/** + * |_code| is executed for every listener matching |_flag| + * |listener| should be used inside |_code| as the nsIWebProgressListener var. + */ +#define NOTIFY_LISTENERS(_flag, _code) \ + PR_BEGIN_MACRO \ + nsCOMPtr<nsIWebProgressListener> listener; \ + ListenerArray::BackwardIterator iter(mListenerInfoList); \ + while (iter.HasMore()) { \ + nsListenerInfo& info = iter.GetNext(); \ + if (!(info.mNotifyMask & (_flag))) { \ + continue; \ + } \ + listener = do_QueryReferent(info.mWeakListener); \ + if (!listener) { \ + iter.Remove(); \ + continue; \ + } \ + _code \ + } \ + mListenerInfoList.Compact(); \ + PR_END_MACRO + +void nsDocLoader::FireOnProgressChange(nsDocLoader* aLoadInitiator, + nsIRequest* request, int64_t aProgress, + int64_t aProgressMax, + int64_t aProgressDelta, + int64_t aTotalProgress, + int64_t aMaxTotalProgress) { + if (mIsLoadingDocument) { + mCurrentTotalProgress += aProgressDelta; + mMaxTotalProgress = GetMaxTotalProgress(); + + aTotalProgress = mCurrentTotalProgress; + aMaxTotalProgress = mMaxTotalProgress; + } + +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(request, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Progress (%s): curSelf: %" PRId64 " maxSelf: %" PRId64 + " curTotal: %" PRId64 " maxTotal %" PRId64 "\n", + this, buffer.get(), aProgress, aProgressMax, aTotalProgress, + aMaxTotalProgress)); +#endif /* DEBUG */ + + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_PROGRESS, + // XXX truncates 64-bit to 32-bit + listener->OnProgressChange(aLoadInitiator, request, int32_t(aProgress), + int32_t(aProgressMax), int32_t(aTotalProgress), + int32_t(aMaxTotalProgress));); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnProgressChange(aLoadInitiator, request, aProgress, + aProgressMax, aProgressDelta, aTotalProgress, + aMaxTotalProgress); + } +} + +void nsDocLoader::GatherAncestorWebProgresses(WebProgressList& aList) { + for (nsDocLoader* loader = this; loader; loader = loader->mParent) { + aList.AppendElement(loader); + } +} + +void nsDocLoader::FireOnStateChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, int32_t aStateFlags, + nsresult aStatus) { + WebProgressList list; + GatherAncestorWebProgresses(list); + for (uint32_t i = 0; i < list.Length(); ++i) { + list[i]->DoFireOnStateChange(aProgress, aRequest, aStateFlags, aStatus); + } +} + +void nsDocLoader::DoFireOnStateChange(nsIWebProgress* const aProgress, + nsIRequest* const aRequest, + int32_t& aStateFlags, + const nsresult aStatus) { + // + // Remove the STATE_IS_NETWORK bit if necessary. + // + // The rule is to remove this bit, if the notification has been passed + // up from a child WebProgress, and the current WebProgress is already + // active... + // + if (mIsLoadingDocument && + (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) && + (this != aProgress)) { + aStateFlags &= ~nsIWebProgressListener::STATE_IS_NETWORK; + } + + // Add the STATE_RESTORING bit if necessary. + if (mIsRestoringDocument) + aStateFlags |= nsIWebProgressListener::STATE_RESTORING; + +#if defined(DEBUG) + nsAutoCString buffer; + + GetURIStringFromRequest(aRequest, buffer); + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: Status (%s): code: %x\n", this, buffer.get(), + aStateFlags)); +#endif /* DEBUG */ + + NS_ASSERTION(aRequest, + "Firing OnStateChange(...) notification with a NULL request!"); + + NOTIFY_LISTENERS( + ((aStateFlags >> 16) & nsIWebProgress::NOTIFY_STATE_ALL), + listener->OnStateChange(aProgress, aRequest, aStateFlags, aStatus);); +} + +void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aUri, + uint32_t aFlags) { + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_LOCATION, + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader [%p] calling %p->OnLocationChange to %s %x", this, + listener.get(), aUri->GetSpecOrDefault().get(), aFlags)); + listener->OnLocationChange(aWebProgress, aRequest, aUri, aFlags);); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnLocationChange(aWebProgress, aRequest, aUri, aFlags); + } +} + +void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_STATUS, + listener->OnStatusChange(aWebProgress, aRequest, aStatus, aMessage);); + + // Pass the notification up to the parent... + if (mParent) { + mParent->FireOnStatusChange(aWebProgress, aRequest, aStatus, aMessage); + } +} + +bool nsDocLoader::RefreshAttempted(nsIWebProgress* aWebProgress, nsIURI* aURI, + int32_t aDelay, bool aSameURI) { + /* + * Returns true if the refresh may proceed, + * false if the refresh should be blocked. + */ + bool allowRefresh = true; + + NOTIFY_LISTENERS( + nsIWebProgress::NOTIFY_REFRESH, + nsCOMPtr<nsIWebProgressListener2> listener2 = + do_QueryReferent(info.mWeakListener); + if (!listener2) continue; + + bool listenerAllowedRefresh; + nsresult listenerRV = listener2->OnRefreshAttempted( + aWebProgress, aURI, aDelay, aSameURI, &listenerAllowedRefresh); + if (NS_FAILED(listenerRV)) continue; + + allowRefresh = allowRefresh && listenerAllowedRefresh;); + + // Pass the notification up to the parent... + if (mParent) { + allowRefresh = allowRefresh && mParent->RefreshAttempted(aWebProgress, aURI, + aDelay, aSameURI); + } + + return allowRefresh; +} + +nsresult nsDocLoader::AddRequestInfo(nsIRequest* aRequest) { + if (!mRequestInfoHash.Add(aRequest, mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void nsDocLoader::RemoveRequestInfo(nsIRequest* aRequest) { + mRequestInfoHash.Remove(aRequest); +} + +nsDocLoader::nsRequestInfo* nsDocLoader::GetRequestInfo( + nsIRequest* aRequest) const { + return static_cast<nsRequestInfo*>(mRequestInfoHash.Search(aRequest)); +} + +void nsDocLoader::ClearRequestInfoHash(void) { mRequestInfoHash.Clear(); } + +int64_t nsDocLoader::CalculateMaxProgress() { + int64_t max = mCompletedTotalProgress; + for (auto iter = mRequestInfoHash.Iter(); !iter.Done(); iter.Next()) { + auto info = static_cast<const nsRequestInfo*>(iter.Get()); + + if (info->mMaxProgress < info->mCurrentProgress) { + return int64_t(-1); + } + max += info->mMaxProgress; + } + return max; +} + +NS_IMETHODIMP nsDocLoader::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* cb) { + if (aOldChannel) { + nsLoadFlags loadFlags = 0; + int32_t stateFlags = nsIWebProgressListener::STATE_REDIRECTING | + nsIWebProgressListener::STATE_IS_REQUEST; + + aOldChannel->GetLoadFlags(&loadFlags); + // If the document channel is being redirected, then indicate that the + // document is being redirected in the notification... + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) { + stateFlags |= nsIWebProgressListener::STATE_IS_DOCUMENT; + +#if defined(DEBUG) + // We only set mDocumentRequest in OnStartRequest(), but its possible + // to get a redirect before that for service worker interception. + if (mDocumentRequest) { + nsCOMPtr<nsIRequest> request(aOldChannel); + NS_ASSERTION(request == mDocumentRequest, "Wrong Document Channel"); + } +#endif /* DEBUG */ + } + + OnRedirectStateChange(aOldChannel, aNewChannel, aFlags, stateFlags); + FireOnStateChange(this, aOldChannel, stateFlags, NS_OK); + } + + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +void nsDocLoader::OnSecurityChange(nsISupports* aContext, uint32_t aState) { + // + // Fire progress notifications out to any registered nsIWebProgressListeners. + // + + nsCOMPtr<nsIRequest> request = do_QueryInterface(aContext); + nsIWebProgress* webProgress = static_cast<nsIWebProgress*>(this); + + NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_SECURITY, + listener->OnSecurityChange(webProgress, request, aState);); + + // Pass the notification up to the parent... + if (mParent) { + mParent->OnSecurityChange(aContext, aState); + } +} + +/* + * Implementation of nsISupportsPriority methods... + * + * The priority of the DocLoader _is_ the priority of its LoadGroup. + * + * XXX(darin): Once we start storing loadgroups in loadgroups, this code will + * go away. + */ + +NS_IMETHODIMP nsDocLoader::GetPriority(int32_t* aPriority) { + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup); + if (p) return p->GetPriority(aPriority); + + *aPriority = 0; + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::SetPriority(int32_t aPriority) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: SetPriority(%d) called\n", this, aPriority)); + + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup); + if (p) p->SetPriority(aPriority); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, SetPriority, + (aPriority)); + + return NS_OK; +} + +NS_IMETHODIMP nsDocLoader::AdjustPriority(int32_t aDelta) { + MOZ_LOG(gDocLoaderLog, LogLevel::Debug, + ("DocLoader:%p: AdjustPriority(%d) called\n", this, aDelta)); + + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(mLoadGroup); + if (p) p->AdjustPriority(aDelta); + + NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mChildList, AdjustPriority, + (aDelta)); + + return NS_OK; +} + +#if 0 +void nsDocLoader::DumpChannelInfo() +{ + nsChannelInfo *info; + int32_t i, count; + int32_t current=0, max=0; + + + printf("==== DocLoader=%x\n", this); + + count = mChannelInfoList.Count(); + for(i=0; i<count; i++) { + info = (nsChannelInfo *)mChannelInfoList.ElementAt(i); + +# if defined(DEBUG) + nsAutoCString buffer; + nsresult rv = NS_OK; + if (info->mURI) { + rv = info->mURI->GetSpec(buffer); + } + + printf(" [%d] current=%d max=%d [%s]\n", i, + info->mCurrentProgress, + info->mMaxProgress, buffer.get()); +# endif /* DEBUG */ + + current += info->mCurrentProgress; + if (max >= 0) { + if (info->mMaxProgress < info->mCurrentProgress) { + max = -1; + } else { + max += info->mMaxProgress; + } + } + } + + printf("\nCurrent=%d Total=%d\n====\n", current, max); +} +#endif /* 0 */ diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h new file mode 100644 index 0000000000..9f39fca7f2 --- /dev/null +++ b/uriloader/base/nsDocLoader.h @@ -0,0 +1,407 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDocLoader_h__ +#define nsDocLoader_h__ + +#include "nsIDocumentLoader.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsIRequestObserver.h" +#include "nsWeakReference.h" +#include "nsILoadGroup.h" +#include "nsCOMArray.h" +#include "nsTObserverArray.h" +#include "nsString.h" +#include "nsIChannel.h" +#include "nsIProgressEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIChannelEventSink.h" +#include "nsISupportsPriority.h" +#include "nsCOMPtr.h" +#include "PLDHashTable.h" +#include "nsCycleCollectionParticipant.h" + +#include "mozilla/LinkedList.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace dom { +class BrowserBridgeChild; +class BrowsingContext; +} // namespace dom +} // namespace mozilla + +/**************************************************************************** + * nsDocLoader implementation... + ****************************************************************************/ + +#define NS_THIS_DOCLOADER_IMPL_CID \ + { /* b4ec8387-98aa-4c08-93b6-6d23069c06f2 */ \ + 0xb4ec8387, 0x98aa, 0x4c08, { \ + 0x93, 0xb6, 0x6d, 0x23, 0x06, 0x9c, 0x06, 0xf2 \ + } \ + } + +class nsDocLoader : public nsIDocumentLoader, + public nsIRequestObserver, + public nsSupportsWeakReference, + public nsIProgressEventSink, + public nsIWebProgress, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsISupportsPriority { + public: + using BrowserBridgeChild = mozilla::dom::BrowserBridgeChild; + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_THIS_DOCLOADER_IMPL_CID) + + nsDocLoader(); + + [[nodiscard]] virtual nsresult Init(); + [[nodiscard]] nsresult InitWithBrowsingContext( + mozilla::dom::BrowsingContext* aBrowsingContext); + + static already_AddRefed<nsDocLoader> GetAsDocLoader(nsISupports* aSupports); + // Needed to deal with ambiguous inheritance from nsISupports... + static nsISupports* GetAsSupports(nsDocLoader* aDocLoader) { + return static_cast<nsIDocumentLoader*>(aDocLoader); + } + + // Add aDocLoader as a child to the docloader service. + [[nodiscard]] static nsresult AddDocLoaderAsChildOfRoot( + nsDocLoader* aDocLoader); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsDocLoader, nsIDocumentLoader) + + NS_DECL_NSIDOCUMENTLOADER + + // nsIProgressEventSink + NS_DECL_NSIPROGRESSEVENTSINK + + // nsIRequestObserver methods: (for observing the load group) + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIWEBPROGRESS + + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSISUPPORTSPRIORITY; // semicolon for clang-format bug 1629756 + + // Implementation specific methods... + + // Remove aChild from our childlist. This nulls out the child's mParent + // pointer. + [[nodiscard]] nsresult RemoveChildLoader(nsDocLoader* aChild); + + // Add aChild to our child list. This will set aChild's mParent pointer to + // |this|. + [[nodiscard]] nsresult AddChildLoader(nsDocLoader* aChild); + nsDocLoader* GetParent() const { return mParent; } + + struct nsListenerInfo { + nsListenerInfo(nsIWeakReference* aListener, unsigned long aNotifyMask) + : mWeakListener(aListener), mNotifyMask(aNotifyMask) {} + + // Weak pointer for the nsIWebProgressListener... + nsWeakPtr mWeakListener; + + // Mask indicating which notifications the listener wants to receive. + unsigned long mNotifyMask; + }; + + /** + * Fired when a security change occurs due to page transitions, + * or end document load. This interface should be called by + * a security package (eg Netscape Personal Security Manager) + * to notify nsIWebProgressListeners that security state has + * changed. State flags are in nsIWebProgressListener.idl + */ + void OnSecurityChange(nsISupports* aContext, uint32_t aState); + + void SetDocumentOpenedButNotLoaded() { mDocumentOpenedButNotLoaded = true; } + + bool TreatAsBackgroundLoad(); + + void SetFakeOnLoadDispatched() { mHasFakeOnLoadDispatched = true; }; + + bool HasFakeOnLoadDispatched() { return mHasFakeOnLoadDispatched; }; + + void ResetToFirstLoad() { + mHasFakeOnLoadDispatched = false; + mIsReadyToHandlePostMessage = false; + mTreatAsBackgroundLoad = false; + }; + + // Inform a parent docloader that a BrowserBridgeChild has been created for + // an OOP sub-document. + // (This is the OOP counterpart to ChildEnteringOnload below.) + void OOPChildLoadStarted(BrowserBridgeChild* aChild) { + MOZ_DIAGNOSTIC_ASSERT(!mOOPChildrenLoading.Contains(aChild)); + mOOPChildrenLoading.AppendElement(aChild); + } + + // Inform a parent docloader that the BrowserBridgeChild for one of its + // OOP sub-documents is done calling its onload handler. + // (This is the OOP counterpart to ChildDoneWithOnload below.) + void OOPChildLoadDone(BrowserBridgeChild* aChild) { + // aChild will not be in the list if nsDocLoader::Stop() was called, since + // that clears mOOPChildrenLoading. It also dispatches the 'load' event, + // so we don't need to call DocLoaderIsEmpty in that case. + if (mOOPChildrenLoading.RemoveElement(aChild)) { + DocLoaderIsEmpty(true); + } + } + + uint32_t ChildCount() const { return mChildList.Length(); } + + protected: + virtual ~nsDocLoader(); + + [[nodiscard]] virtual nsresult SetDocLoaderParent(nsDocLoader* aLoader); + + bool IsBusy(); + + void SetBackgroundLoadIframe(); + + void Destroy(); + virtual void DestroyChildren(); + + nsIDocumentLoader* ChildAt(int32_t i) { + return mChildList.SafeElementAt(i, nullptr); + } + + void FireOnProgressChange(nsDocLoader* aLoadInitiator, nsIRequest* request, + int64_t aProgress, int64_t aProgressMax, + int64_t aProgressDelta, int64_t aTotalProgress, + int64_t aMaxTotalProgress); + + // This should be at least 2 long since we'll generally always + // have the current page and the global docloader on the ancestor + // list. But to deal with frames it's better to make it a bit + // longer, and it's always a stack temporary so there's no real + // reason not to. + typedef AutoTArray<RefPtr<nsDocLoader>, 8> WebProgressList; + void GatherAncestorWebProgresses(WebProgressList& aList); + + void FireOnStateChange(nsIWebProgress* aProgress, nsIRequest* request, + int32_t aStateFlags, nsresult aStatus); + + // The guts of FireOnStateChange, but does not call itself on our ancestors. + // The arguments that are const are const so that we can detect cases when + // DoFireOnStateChange wants to propagate changes to the next web progress + // at compile time. The ones that are not, are references so that such + // changes can be propagated. + void DoFireOnStateChange(nsIWebProgress* const aProgress, + nsIRequest* const request, int32_t& aStateFlags, + const nsresult aStatus); + + void FireOnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsresult aStatus, const char16_t* aMessage); + + void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsIURI* aUri, uint32_t aFlags); + + [[nodiscard]] bool RefreshAttempted(nsIWebProgress* aWebProgress, + nsIURI* aURI, int32_t aDelay, + bool aSameURI); + + // this function is overridden by the docshell, it is provided so that we + // can pass more information about redirect state (the normal OnStateChange + // doesn't get the new channel). + // @param aRedirectFlags The flags being sent to OnStateChange that + // indicate the type of redirect. + // @param aStateFlags The channel flags normally sent to OnStateChange. + virtual void OnRedirectStateChange(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, + uint32_t aRedirectFlags, + uint32_t aStateFlags) {} + + void doStartDocumentLoad(); + void doStartURLLoad(nsIRequest* request, int32_t aExtraFlags); + void doStopURLLoad(nsIRequest* request, nsresult aStatus); + void doStopDocumentLoad(nsIRequest* request, nsresult aStatus); + + void NotifyDoneWithOnload(nsDocLoader* aParent); + + // Inform a parent docloader that aChild is about to call its onload + // handler. + [[nodiscard]] bool ChildEnteringOnload(nsIDocumentLoader* aChild) { + // It's ok if we're already in the list -- we'll just be in there twice + // and then the RemoveObject calls from ChildDoneWithOnload will remove + // us. + return mChildrenInOnload.AppendObject(aChild); + } + + // Inform a parent docloader that aChild is done calling its onload + // handler. + void ChildDoneWithOnload(nsIDocumentLoader* aChild) { + mChildrenInOnload.RemoveObject(aChild); + DocLoaderIsEmpty(true); + } + + // DocLoaderIsEmpty should be called whenever the docloader may be empty. + // This method is idempotent and does nothing if the docloader is not in + // fact empty. This method _does_ make sure that layout is flushed if our + // loadgroup has no active requests before checking for "real" emptiness if + // aFlushLayout is true. + // @param aOverrideStatus An optional status to use when notifying listeners + // of the completed load, instead of using the load group's status. + void DocLoaderIsEmpty( + bool aFlushLayout, + const mozilla::Maybe<nsresult>& aOverrideStatus = mozilla::Nothing()); + + protected: + struct nsStatusInfo : public mozilla::LinkedListElement<nsStatusInfo> { + nsString mStatusMessage; + nsresult mStatusCode; + // Weak mRequest is ok; we'll be told if it decides to go away. + nsIRequest* const mRequest; + + explicit nsStatusInfo(nsIRequest* aRequest) + : mStatusCode(NS_ERROR_NOT_INITIALIZED), mRequest(aRequest) { + MOZ_COUNT_CTOR(nsStatusInfo); + } + MOZ_COUNTED_DTOR(nsStatusInfo) + }; + + struct nsRequestInfo : public PLDHashEntryHdr { + explicit nsRequestInfo(const void* key) + : mKey(key), + mCurrentProgress(0), + mMaxProgress(0), + mUploading(false), + mLastStatus(nullptr) { + MOZ_COUNT_CTOR(nsRequestInfo); + } + + MOZ_COUNTED_DTOR(nsRequestInfo) + + nsIRequest* Request() { + return static_cast<nsIRequest*>(const_cast<void*>(mKey)); + } + + const void* mKey; // Must be first for the PLDHashTable stubs to work + int64_t mCurrentProgress; + int64_t mMaxProgress; + bool mUploading; + + mozilla::UniquePtr<nsStatusInfo> mLastStatus; + }; + + static void RequestInfoHashInitEntry(PLDHashEntryHdr* entry, const void* key); + static void RequestInfoHashClearEntry(PLDHashTable* table, + PLDHashEntryHdr* entry); + + // IMPORTANT: The ownership implicit in the following member + // variables has been explicitly checked and set using nsCOMPtr + // for owning pointers and raw COM interface pointers for weak + // (ie, non owning) references. If you add any members to this + // class, please make the ownership explicit (pinkerton, scc). + + nsCOMPtr<nsIRequest> mDocumentRequest; // [OWNER] ???compare with document + + nsDocLoader* mParent; // [WEAK] + + typedef nsAutoTObserverArray<nsListenerInfo, 8> ListenerArray; + ListenerArray mListenerInfoList; + + nsCOMPtr<nsILoadGroup> mLoadGroup; + // We hold weak refs to all our kids + nsTObserverArray<nsDocLoader*> mChildList; + + // The following member variables are related to the new nsIWebProgress + // feedback interfaces that travis cooked up. + int32_t mProgressStateFlags; + + int64_t mCurrentSelfProgress; + int64_t mMaxSelfProgress; + + int64_t mCurrentTotalProgress; + int64_t mMaxTotalProgress; + + PLDHashTable mRequestInfoHash; + int64_t mCompletedTotalProgress; + + mozilla::LinkedList<nsStatusInfo> mStatusInfoList; + + /* + * This flag indicates that the loader is loading a document. It is set + * from the call to LoadDocument(...) until the OnConnectionsComplete(...) + * notification is fired... + */ + bool mIsLoadingDocument; + + /* Flag to indicate that we're in the process of restoring a document. */ + bool mIsRestoringDocument; + + /* Flag to indicate that we're in the process of flushing layout + under DocLoaderIsEmpty() and should not do another flush. */ + bool mDontFlushLayout; + + /* Flag to indicate whether we should consider ourselves as currently + flushing layout for the purposes of IsBusy. For example, if Stop has + been called then IsBusy should return false even if we are still + flushing. */ + bool mIsFlushingLayout; + + bool mTreatAsBackgroundLoad; + + private: + bool mHasFakeOnLoadDispatched; + + bool mIsReadyToHandlePostMessage; + /** + * This flag indicates that the loader is waiting for completion of + * a document.open-triggered "document load". This is set when + * document.open() happens and sets up a new parser and cleared out + * when we go to fire our load event or end up with a new document + * channel. + */ + bool mDocumentOpenedButNotLoaded; + + static const PLDHashTableOps sRequestInfoHashOps; + + // A list of kids that are in the middle of their onload calls and will let + // us know once they're done. We don't want to fire onload for "normal" + // DocLoaderIsEmpty calls (those coming from requests finishing in our + // loadgroup) unless this is empty. + nsCOMArray<nsIDocumentLoader> mChildrenInOnload; + + // The OOP counterpart to mChildrenInOnload. + // Not holding strong refs here since we don't actually use the BBCs. + nsTArray<const BrowserBridgeChild*> mOOPChildrenLoading; + + int64_t GetMaxTotalProgress(); + + nsresult AddRequestInfo(nsIRequest* aRequest); + void RemoveRequestInfo(nsIRequest* aRequest); + nsRequestInfo* GetRequestInfo(nsIRequest* aRequest) const; + void ClearRequestInfoHash(); + int64_t CalculateMaxProgress(); + /// void DumpChannelInfo(void); + + // used to clear our internal progress state between loads... + void ClearInternalProgress(); + + /** + * Used to test whether we might need to fire a load event. This + * can happen when we have a document load going on, or when we've + * had document.open() called and haven't fired the corresponding + * load event yet. + */ + bool IsBlockingLoadEvent() const { + return mIsLoadingDocument || mDocumentOpenedButNotLoaded; + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsDocLoader, NS_THIS_DOCLOADER_IMPL_CID) + +static inline nsISupports* ToSupports(nsDocLoader* aDocLoader) { + return static_cast<nsIDocumentLoader*>(aDocLoader); +} + +#endif /* nsDocLoader_h__ */ diff --git a/uriloader/base/nsIContentHandler.idl b/uriloader/base/nsIContentHandler.idl new file mode 100644 index 0000000000..31ef87a8ba --- /dev/null +++ b/uriloader/base/nsIContentHandler.idl @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +interface nsIRequest; +interface nsIInterfaceRequestor; + +[scriptable, uuid(49439df2-b3d2-441c-bf62-866bdaf56fd2)] +interface nsIContentHandler : nsISupports +{ + /** + * Tells the content handler to take over handling the content. If this + * function succeeds, the URI Loader will leave this request alone, ignoring + * progress notifications. Failure of this method will cause the request to be + * cancelled, unless the error code is NS_ERROR_WONT_HANDLE_CONTENT (see + * below). + * + * @param aWindowContext + * Window context, used to get things like the current nsIDOMWindow + * for this request. May be null. + * @param aContentType + * The content type of aRequest + * @param aRequest + * A request whose content type is already known. + * + * @throw NS_ERROR_WONT_HANDLE_CONTENT Indicates that this handler does not + * want to handle this content. A different way for handling this + * content should be tried. + */ + void handleContent(in string aContentType, + in nsIInterfaceRequestor aWindowContext, + in nsIRequest aRequest); +}; diff --git a/uriloader/base/nsIDocumentLoader.idl b/uriloader/base/nsIDocumentLoader.idl new file mode 100644 index 0000000000..3bd960ac84 --- /dev/null +++ b/uriloader/base/nsIDocumentLoader.idl @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +interface nsILoadGroup; +interface nsIChannel; +interface nsIURI; +interface nsIWebProgress; +interface nsIRequest; + +/** + * An nsIDocumentLoader is an interface responsible for tracking groups of + * loads that belong together (images, external scripts, etc) and subdocuments + * (<iframe>, <frame>, etc). It is also responsible for sending + * nsIWebProgressListener notifications. + * XXXbz this interface should go away, we think... + */ +[scriptable, uuid(bbe961ee-59e9-42bb-be50-0331979bb79f)] +interface nsIDocumentLoader : nsISupports +{ + // Stop all loads in the loadgroup of this docloader + void stop(); + + // XXXbz is this needed? For embedding? What this does is does is not + // defined by this interface! + readonly attribute nsISupports container; + + // The loadgroup associated with this docloader + readonly attribute nsILoadGroup loadGroup; + + // The defaultLoadRequest of the loadgroup associated with this docloader + readonly attribute nsIChannel documentChannel; +}; + diff --git a/uriloader/base/nsITransfer.idl b/uriloader/base/nsITransfer.idl new file mode 100644 index 0000000000..34f855ff3d --- /dev/null +++ b/uriloader/base/nsITransfer.idl @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIWebProgressListener2.idl" + +interface nsIArray; +interface nsIURI; +interface nsICancelable; +interface nsIMIMEInfo; +interface nsIFile; +webidl BrowsingContext; + +[scriptable, uuid(37ec75d3-97ad-4da8-afaa-eabe5b4afd73)] +interface nsITransfer : nsIWebProgressListener2 { + + const unsigned long DOWNLOAD_ACCEPTABLE = 0; + const unsigned long DOWNLOAD_FORBIDDEN = 1; + const unsigned long DOWNLOAD_POTENTIALLY_UNSAFE = 2; + + /** + * Initializes the transfer with certain properties. This function must + * be called prior to accessing any properties on this interface. + * + * @param aSource The source URI of the transfer. Must not be null. + * + * @param aTarget The target URI of the transfer. Must not be null. + * + * @param aDisplayName The user-readable description of the transfer. + * Can be empty. + * + * @param aMIMEInfo The MIME info associated with the target, + * including MIME type and helper app when appropriate. + * This parameter is optional. + * + * @param startTime Time when the download started (ie, when the first + * response from the server was received) + * XXX presumably wbp and exthandler do this differently + * + * @param aTempFile The location of a temporary file; i.e. a file in which + * the received data will be stored, but which is not + * equal to the target file. (will be moved to the real + * target by the caller, when the download is finished) + * May be null. + * + * @param aCancelable An object that can be used to abort the download. + * Must not be null. + * Implementations are expected to hold a strong + * reference to this object until the download is + * finished, at which point they should release the + * reference. + * + * @param aIsPrivate Used to determine the privacy status of the new transfer. + * If true, indicates that the transfer was initiated from + * a source that desires privacy. + * + * @param aDownloadClassification Indicates wheter the dowload is unwanted, + * should be considered dangerous or insecure. + */ + void init(in nsIURI aSource, + in nsIURI aTarget, + in AString aDisplayName, + in nsIMIMEInfo aMIMEInfo, + in PRTime startTime, + in nsIFile aTempFile, + in nsICancelable aCancelable, + in boolean aIsPrivate, + in long aDownloadClassification); + + /** + * Same as init, but allows for passing the browsingContext + * which will allow for opening the download with the same + * userContextId + * + * @param aBrowsingContext BrowsingContext of the initiating document. + * + * @param aHandleInternally Set to true if the download should be opened within + * the browser. + */ + void initWithBrowsingContext(in nsIURI aSource, + in nsIURI aTarget, + in AString aDisplayName, + in nsIMIMEInfo aMIMEInfo, + in PRTime startTime, + in nsIFile aTempFile, + in nsICancelable aCancelable, + in boolean aIsPrivate, + in long aDownloadClassification, + in BrowsingContext aBrowsingContext, + in boolean aHandleInternally); + + /* + * Used to notify the transfer object of the hash of the downloaded file. + * Must be called on the main thread, only after the download has finished + * successfully. + * @param aHash The SHA-256 hash in raw bytes of the downloaded file. + */ + void setSha256Hash(in ACString aHash); + + /* + * Used to notify the transfer object of the signature of the downloaded + * file. Must be called on the main thread, only after the download has + * finished successfully. + * @param aSignatureInfo The Array of Array of Array of bytes + * certificates of the downloaded file. + */ + void setSignatureInfo(in Array<Array<Array<uint8_t> > > aSignatureInfo); + + /* + * Used to notify the transfer object of the redirects associated with the + * channel that terminated in the downloaded file. Must be called on the + * main thread, only after the download has finished successfully. + * @param aRedirects The nsIArray of nsIPrincipal of redirected URIs + * associated with the downloaded file. + */ + void setRedirects(in nsIArray aRedirects); +}; + +%{C++ +/** + * A component with this contract ID will be created each time a download is + * started, and nsITransfer::Init will be called on it and an observer will be set. + * + * Notifications of the download progress will happen via + * nsIWebProgressListener/nsIWebProgressListener2. + * + * INTERFACES THAT MUST BE IMPLEMENTED: + * nsITransfer + * nsIWebProgressListener + * nsIWebProgressListener2 + * + * XXX move this to nsEmbedCID.h once the interfaces (and the contract ID) are + * frozen. + */ +#define NS_TRANSFER_CONTRACTID "@mozilla.org/transfer;1" +%} diff --git a/uriloader/base/nsIURIContentListener.idl b/uriloader/base/nsIURIContentListener.idl new file mode 100644 index 0000000000..35ff3a9c91 --- /dev/null +++ b/uriloader/base/nsIURIContentListener.idl @@ -0,0 +1,124 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIRequest; +interface nsIStreamListener; +interface nsIURI; + +/** + * nsIURIContentListener is an interface used by components which + * want to know (and have a chance to handle) a particular content type. + * Typical usage scenarios will include running applications which register + * a nsIURIContentListener for each of its content windows with the uri + * dispatcher service. + */ +[scriptable, uuid(10a28f38-32e8-4c63-8aa1-12eaaebc369a)] +interface nsIURIContentListener : nsISupports +{ + /** + * Notifies the content listener to hook up an nsIStreamListener capable of + * consuming the data stream. + * + * @param aContentType Content type of the data. + * @param aIsContentPreferred Indicates whether the content should be + * preferred by this listener. + * @param aRequest Request that is providing the data. + * @param aContentHandler nsIStreamListener that will consume the data. + * This should be set to <code>nullptr</code> if + * this content listener can't handle the content + * type; in this case, doContent should also fail + * (i.e., return failure nsresult). + * + * @return <code>true</code> if the load should + * be aborted and consumer wants to + * handle the load completely by itself. This + * causes the URI Loader do nothing else... + * <code>false</code> if the URI Loader should + * continue handling the load and call the + * returned streamlistener's methods. + */ + boolean doContent(in ACString aContentType, + in boolean aIsContentPreferred, + in nsIRequest aRequest, + out nsIStreamListener aContentHandler); + + /** + * When given a uri to dispatch, if the URI is specified as 'preferred + * content' then the uri loader tries to find a preferred content handler + * for the content type. The thought is that many content listeners may + * be able to handle the same content type if they have to. i.e. the mail + * content window can handle text/html just like a browser window content + * listener. However, if the user clicks on a link with text/html content, + * then the browser window should handle that content and not the mail + * window where the user may have clicked the link. This is the difference + * between isPreferred and canHandleContent. + * + * @param aContentType Content type of the data. + * @param aDesiredContentType Indicates that aContentType must be converted + * to aDesiredContentType before processing the + * data. This causes a stream converted to be + * inserted into the nsIStreamListener chain. + * This argument can be <code>nullptr</code> if + * the content should be consumed directly as + * aContentType. + * + * @return <code>true</code> if this is a preferred + * content handler for aContentType; + * <code>false<code> otherwise. + */ + boolean isPreferred(in string aContentType, out string aDesiredContentType); + + /** + * When given a uri to dispatch, if the URI is not specified as 'preferred + * content' then the uri loader calls canHandleContent to see if the content + * listener is capable of handling the content. + * + * @param aContentType Content type of the data. + * @param aIsContentPreferred Indicates whether the content should be + * preferred by this listener. + * @param aDesiredContentType Indicates that aContentType must be converted + * to aDesiredContentType before processing the + * data. This causes a stream converted to be + * inserted into the nsIStreamListener chain. + * This argument can be <code>nullptr</code> if + * the content should be consumed directly as + * aContentType. + * + * @return <code>true</code> if the data can be consumed. + * <code>false</code> otherwise. + * + * Note: I really envision canHandleContent as a method implemented + * by the docshell as the implementation is generic to all doc + * shells. The isPreferred decision is a decision made by a top level + * application content listener that sits at the top of the docshell + * hierarchy. + */ + boolean canHandleContent(in string aContentType, + in boolean aIsContentPreferred, + out string aDesiredContentType); + + /** + * The load context associated with a particular content listener. + * The URI Loader stores and accesses this value as needed. + */ + attribute nsISupports loadCookie; + + /** + * The parent content listener if this particular listener is part of a chain + * of content listeners (i.e. a docshell!) + * + * @note If this attribute is set to an object that implements + * nsISupportsWeakReference, the implementation should get the + * nsIWeakReference and hold that. Otherwise, the implementation + * should not refcount this interface; it should assume that a non + * null value is always valid. In that case, the caller is + * responsible for explicitly setting this value back to null if the + * parent content listener is destroyed. + */ + attribute nsIURIContentListener parentContentListener; +}; + diff --git a/uriloader/base/nsIURILoader.idl b/uriloader/base/nsIURILoader.idl new file mode 100644 index 0000000000..273c0f4cdc --- /dev/null +++ b/uriloader/base/nsIURILoader.idl @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURIContentListener; +interface nsIURI; +interface nsILoadGroup; +interface nsIProgressEventSink; +interface nsIChannel; +interface nsIRequest; +interface nsIStreamListener; +interface nsIInputStream; +interface nsIInterfaceRequestor; + +/** + * The uri dispatcher is responsible for taking uri's, determining + * the content and routing the opened url to the correct content + * handler. + * + * When you encounter a url you want to open, you typically call + * openURI, passing it the content listener for the window the uri is + * originating from. The uri dispatcher opens the url to discover the + * content type. It then gives the content listener first crack at + * handling the content. If it doesn't want it, the dispatcher tries + * to hand it off one of the registered content listeners. This allows + * running applications the chance to jump in and handle the content. + * + * If that also fails, then the uri dispatcher goes to the registry + * looking for the preferred content handler for the content type + * of the uri. The content handler may create an app instance + * or it may hand the contents off to a platform specific plugin + * or helper app. Or it may hand the url off to an OS registered + * application. + */ +[scriptable, uuid(8762c4e7-be35-4958-9b81-a05685bb516d)] +interface nsIURILoader : nsISupports +{ + /** + * @name Flags for opening URIs. + */ + /* @{ */ + /** + * Should the content be displayed in a container that prefers the + * content-type, or will any container do. + */ + const unsigned long IS_CONTENT_PREFERRED = 1 << 0; + /** + * If this flag is set, only the listener of the specified window context will + * be considered for content handling; if it refuses the load, an error will + * be indicated. + */ + const unsigned long DONT_RETARGET = 1 << 1; + /* @} */ + + /** + * As applications such as messenger and the browser are instantiated, + * they register content listener's with the uri dispatcher corresponding + * to content windows within that application. + * + * Note to self: we may want to optimize things a bit more by requiring + * the content types the registered content listener cares about. + * + * @param aContentListener + * The listener to register. This listener must implement + * nsISupportsWeakReference. + * + * @see the nsIURILoader class description + */ + void registerContentListener (in nsIURIContentListener aContentListener); + void unRegisterContentListener (in nsIURIContentListener aContentListener); + + /** + * OpenURI requires the following parameters..... + * @param aChannel + * The channel that should be opened. This must not be asyncOpen'd yet! + * If a loadgroup is set on the channel, it will get replaced with a + * different one. + * @param aFlags + * Combination (bitwise OR) of the flags specified above. 0 indicates + * default handling. + * @param aWindowContext + * If you are running the url from a doc shell or a web shell, this is + * your window context. If you have a content listener you want to + * give first crack to, the uri loader needs to be able to get it + * from the window context. We will also be using the window context + * to get at the progress event sink interface. + * <b>Must not be null!</b> + */ + void openURI(in nsIChannel aChannel, + in unsigned long aFlags, + in nsIInterfaceRequestor aWindowContext); + + /** + * Loads data from a channel. This differs from openURI in that the channel + * may already be opened, and that it returns a stream listener into which the + * caller should pump data. The caller is responsible for opening the channel + * and pumping the channel's data into the returned stream listener. + * + * Note: If the channel already has a loadgroup, it will be replaced with the + * window context's load group, or null if the context doesn't have one. + * + * If the window context's nsIURIContentListener refuses the load immediately + * (e.g. in nsIURIContentListener::onStartURIOpen), this method will return + * NS_ERROR_WONT_HANDLE_CONTENT. At that point, the caller should probably + * cancel the channel if it's already open (this method will not cancel the + * channel). + * + * If flags include DONT_RETARGET, and the content listener refuses the load + * during onStartRequest (e.g. in canHandleContent/isPreferred), then the + * returned stream listener's onStartRequest method will return + * NS_ERROR_WONT_HANDLE_CONTENT. + * + * @param aChannel + * The channel that should be loaded. The channel may already be + * opened. It must not be closed (i.e. this must be called before the + * channel calls onStopRequest on its stream listener). + * @param aFlags + * Combination (bitwise OR) of the flags specified above. 0 indicates + * default handling. + * @param aWindowContext + * If you are running the url from a doc shell or a web shell, this is + * your window context. If you have a content listener you want to + * give first crack to, the uri loader needs to be able to get it + * from the window context. We will also be using the window context + * to get at the progress event sink interface. + * <b>Must not be null!</b> + */ + nsIStreamListener openChannel(in nsIChannel aChannel, + in unsigned long aFlags, + in nsIInterfaceRequestor aWindowContext); + + /** + * Stops an in progress load + */ + void stop(in nsISupports aLoadCookie); +}; + diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl new file mode 100644 index 0000000000..70079adfff --- /dev/null +++ b/uriloader/base/nsIWebProgress.idl @@ -0,0 +1,164 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; +interface nsIEventTarget; +interface nsIWebProgressListener; + +/** + * The nsIWebProgress interface is used to add or remove nsIWebProgressListener + * instances to observe the loading of asynchronous requests (usually in the + * context of a DOM window). + * + * nsIWebProgress instances may be arranged in a parent-child configuration, + * corresponding to the parent-child configuration of their respective DOM + * windows. However, in some cases a nsIWebProgress instance may not have an + * associated DOM window. The parent-child relationship of nsIWebProgress + * instances is not made explicit by this interface, but the relationship may + * exist in some implementations. + * + * A nsIWebProgressListener instance receives notifications for the + * nsIWebProgress instance to which it added itself, and it may also receive + * notifications from any nsIWebProgress instances that are children of that + * nsIWebProgress instance. + */ +[scriptable, uuid(c4d64640-b332-4db6-a2a5-e08566000dc9)] +interface nsIWebProgress : nsISupports +{ + /** + * The following flags may be combined to form the aNotifyMask parameter for + * the addProgressListener method. They limit the set of events that are + * delivered to an nsIWebProgressListener instance. + */ + + /** + * These flags indicate the state transistions to observe, corresponding to + * nsIWebProgressListener::onStateChange. + * + * NOTIFY_STATE_REQUEST + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_REQUEST. + * + * NOTIFY_STATE_DOCUMENT + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_DOCUMENT. + * + * NOTIFY_STATE_NETWORK + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_NETWORK. + * + * NOTIFY_STATE_WINDOW + * Only receive the onStateChange event if the aStateFlags parameter + * includes nsIWebProgressListener::STATE_IS_WINDOW. + * + * NOTIFY_STATE_ALL + * Receive all onStateChange events. + */ + const unsigned long NOTIFY_STATE_REQUEST = 0x00000001; + const unsigned long NOTIFY_STATE_DOCUMENT = 0x00000002; + const unsigned long NOTIFY_STATE_NETWORK = 0x00000004; + const unsigned long NOTIFY_STATE_WINDOW = 0x00000008; + const unsigned long NOTIFY_STATE_ALL = 0x0000000f; + + /** + * These flags indicate the other events to observe, corresponding to the + * other four methods defined on nsIWebProgressListener. + * + * NOTIFY_PROGRESS + * Receive onProgressChange events. + * + * NOTIFY_STATUS + * Receive onStatusChange events. + * + * NOTIFY_SECURITY + * Receive onSecurityChange events. + * + * NOTIFY_LOCATION + * Receive onLocationChange events. + * + * NOTIFY_CONTENT_BLOCKING + * Receive onContentBlockingEvent events. + * + * NOTIFY_REFRESH + * Receive onRefreshAttempted events. + * This is defined on nsIWebProgressListener2. + */ + const unsigned long NOTIFY_PROGRESS = 0x00000010; + const unsigned long NOTIFY_STATUS = 0x00000020; + const unsigned long NOTIFY_SECURITY = 0x00000040; + const unsigned long NOTIFY_LOCATION = 0x00000080; + const unsigned long NOTIFY_REFRESH = 0x00000100; + const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200; + + /** + * This flag enables all notifications. + */ + const unsigned long NOTIFY_ALL = 0x000003ff; + + /** + * Registers a listener to receive web progress events. + * + * @param aListener + * The listener interface to be called when a progress event occurs. + * This object must also implement nsISupportsWeakReference. + * @param aNotifyMask + * The types of notifications to receive. + * + * @throw NS_ERROR_INVALID_ARG + * Indicates that aListener was either null or that it does not + * support weak references. + * @throw NS_ERROR_FAILURE + * Indicates that aListener was already registered. + */ + void addProgressListener(in nsIWebProgressListener aListener, + in unsigned long aNotifyMask); + + /** + * Removes a previously registered listener of progress events. + * + * @param aListener + * The listener interface previously registered with a call to + * addProgressListener. + * + * @throw NS_ERROR_FAILURE + * Indicates that aListener was not registered. + */ + void removeProgressListener(in nsIWebProgressListener aListener); + + /** + * The DOM window associated with this nsIWebProgress instance. + * + * @throw NS_ERROR_FAILURE + * Indicates that there is no associated DOM window. + */ + readonly attribute mozIDOMWindowProxy DOMWindow; + + /** + * Indicates whether DOMWindow.top == DOMWindow. + */ + readonly attribute boolean isTopLevel; + + /** + * Indicates whether or not a document is currently being loaded + * in the context of this nsIWebProgress instance. + */ + readonly attribute boolean isLoadingDocument; + + /** + * Contains a load type as specified by the load* constants in + * nsIDocShellLoadInfo.idl. + */ + readonly attribute unsigned long loadType; + + /** + * Main thread event target to which progress updates should be + * dispatched. This typically will be a SchedulerEventTarget + * corresponding to the tab requesting updates. + */ + attribute nsIEventTarget target; +}; diff --git a/uriloader/base/nsIWebProgressListener.idl b/uriloader/base/nsIWebProgressListener.idl new file mode 100644 index 0000000000..4b46e1f5e7 --- /dev/null +++ b/uriloader/base/nsIWebProgressListener.idl @@ -0,0 +1,547 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIWebProgress; +interface nsIRequest; +interface nsIURI; + +/** + * The nsIWebProgressListener interface is implemented by clients wishing to + * listen in on the progress associated with the loading of asynchronous + * requests in the context of a nsIWebProgress instance as well as any child + * nsIWebProgress instances. nsIWebProgress.idl describes the parent-child + * relationship of nsIWebProgress instances. + */ +[scriptable, uuid(a9df523b-efe2-421e-9d8e-3d7f807dda4c)] +interface nsIWebProgressListener : nsISupports +{ + /** + * State Transition Flags + * + * These flags indicate the various states that requests may transition + * through as they are being loaded. These flags are mutually exclusive. + * + * For any given request, onStateChange is called once with the STATE_START + * flag, zero or more times with the STATE_TRANSFERRING flag or once with the + * STATE_REDIRECTING flag, and then finally once with the STATE_STOP flag. + * NOTE: For document requests, a second STATE_STOP is generated (see the + * description of STATE_IS_WINDOW for more details). + * + * STATE_START + * This flag indicates the start of a request. This flag is set when a + * request is initiated. The request is complete when onStateChange is + * called for the same request with the STATE_STOP flag set. + * + * STATE_REDIRECTING + * This flag indicates that a request is being redirected. The request + * passed to onStateChange is the request that is being redirected. When a + * redirect occurs, a new request is generated automatically to process the + * new request. Expect a corresponding STATE_START event for the new + * request, and a STATE_STOP for the redirected request. + * + * STATE_TRANSFERRING + * This flag indicates that data for a request is being transferred to an + * end consumer. This flag indicates that the request has been targeted, + * and that the user may start seeing content corresponding to the request. + * + * STATE_NEGOTIATING + * This flag is not used. + * + * STATE_STOP + * This flag indicates the completion of a request. The aStatus parameter + * to onStateChange indicates the final status of the request. + */ + const unsigned long STATE_START = 0x00000001; + const unsigned long STATE_REDIRECTING = 0x00000002; + const unsigned long STATE_TRANSFERRING = 0x00000004; + const unsigned long STATE_NEGOTIATING = 0x00000008; + const unsigned long STATE_STOP = 0x00000010; + + + /** + * State Type Flags + * + * These flags further describe the entity for which the state transition is + * occuring. These flags are NOT mutually exclusive (i.e., an onStateChange + * event may indicate some combination of these flags). + * + * STATE_IS_REQUEST + * This flag indicates that the state transition is for a request, which + * includes but is not limited to document requests. (See below for a + * description of document requests.) Other types of requests, such as + * requests for inline content (e.g., images and stylesheets) are + * considered normal requests. + * + * STATE_IS_DOCUMENT + * This flag indicates that the state transition is for a document request. + * This flag is set in addition to STATE_IS_REQUEST. A document request + * supports the nsIChannel interface and its loadFlags attribute includes + * the nsIChannel::LOAD_DOCUMENT_URI flag. + * + * A document request does not complete until all requests associated with + * the loading of its corresponding document have completed. This includes + * other document requests (e.g., corresponding to HTML <iframe> elements). + * The document corresponding to a document request is available via the + * DOMWindow attribute of onStateChange's aWebProgress parameter. + * + * STATE_IS_NETWORK + * This flag indicates that the state transition corresponds to the start + * or stop of activity in the indicated nsIWebProgress instance. This flag + * is accompanied by either STATE_START or STATE_STOP, and it may be + * combined with other State Type Flags. + * + * Unlike STATE_IS_WINDOW, this flag is only set when activity within the + * nsIWebProgress instance being observed starts or stops. If activity + * only occurs in a child nsIWebProgress instance, then this flag will be + * set to indicate the start and stop of that activity. + * + * For example, in the case of navigation within a single frame of a HTML + * frameset, a nsIWebProgressListener instance attached to the + * nsIWebProgress of the frameset window will receive onStateChange calls + * with the STATE_IS_NETWORK flag set to indicate the start and stop of + * said navigation. In other words, an observer of an outer window can + * determine when activity, that may be constrained to a child window or + * set of child windows, starts and stops. + * + * STATE_IS_WINDOW + * This flag indicates that the state transition corresponds to the start + * or stop of activity in the indicated nsIWebProgress instance. This flag + * is accompanied by either STATE_START or STATE_STOP, and it may be + * combined with other State Type Flags. + * + * This flag is similar to STATE_IS_DOCUMENT. However, when a document + * request completes, two onStateChange calls with STATE_STOP are + * generated. The document request is passed as aRequest to both calls. + * The first has STATE_IS_REQUEST and STATE_IS_DOCUMENT set, and the second + * has the STATE_IS_WINDOW flag set (and possibly the STATE_IS_NETWORK flag + * set as well -- see above for a description of when the STATE_IS_NETWORK + * flag may be set). This second STATE_STOP event may be useful as a way + * to partition the work that occurs when a document request completes. + * + * STATE_IS_REDIRECTED_DOCUMENT + * Same as STATE_IS_DOCUMENT, but sent only after a redirect has occured. + * Introduced in order not to confuse existing code with extra state change + * events. See |nsDocLoader::OnStartRequest| for more info. + */ + const unsigned long STATE_IS_REQUEST = 0x00010000; + const unsigned long STATE_IS_DOCUMENT = 0x00020000; + const unsigned long STATE_IS_NETWORK = 0x00040000; + const unsigned long STATE_IS_WINDOW = 0x00080000; + const unsigned long STATE_IS_REDIRECTED_DOCUMENT = 0x00100000; + + /** + * State Modifier Flags + * + * These flags further describe the transition which is occuring. These + * flags are NOT mutually exclusive (i.e., an onStateChange event may + * indicate some combination of these flags). + * + * STATE_RESTORING + * This flag indicates that the state transition corresponds to the start + * or stop of activity for restoring a previously-rendered presentation. + * As such, there is no actual network activity associated with this + * request, and any modifications made to the document or presentation + * when it was originally loaded will still be present. + */ + const unsigned long STATE_RESTORING = 0x01000000; + + /** + * State Security Flags + * + * These flags describe the security state reported by a call to the + * onSecurityChange method. These flags are mutually exclusive. + * + * STATE_IS_INSECURE + * This flag indicates that the data corresponding to the request + * was received over an insecure channel. + * + * STATE_IS_BROKEN + * This flag indicates an unknown security state. This may mean that the + * request is being loaded as part of a page in which some content was + * received over an insecure channel. + * + * STATE_IS_SECURE + * This flag indicates that the data corresponding to the request was + * received over a secure channel. The degree of security is expressed by + * STATE_SECURE_HIGH, STATE_SECURE_MED, or STATE_SECURE_LOW. + */ + const unsigned long STATE_IS_INSECURE = 0x00000004; + const unsigned long STATE_IS_BROKEN = 0x00000001; + const unsigned long STATE_IS_SECURE = 0x00000002; + + /** + * Mixed active content flags + * + * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT + * nsSecureBrowserUIImpl::CheckForBlockedContent(). + * + * May be set in addition to the State Security Flags, to indicate that + * mixed active content has been encountered. + * + * STATE_BLOCKED_MIXED_ACTIVE_CONTENT + * Mixed active content has been blocked from loading. + * + * STATE_LOADED_MIXED_ACTIVE_CONTENT + * Mixed active content has been loaded. State should be STATE_IS_BROKEN. + */ + const unsigned long STATE_BLOCKED_MIXED_ACTIVE_CONTENT = 0x00000010; + const unsigned long STATE_LOADED_MIXED_ACTIVE_CONTENT = 0x00000020; + + /** + * Mixed display content flags + * + * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT + * nsSecureBrowserUIImpl::CheckForBlockedContent(). + * + * May be set in addition to the State Security Flags, to indicate that + * mixed display content has been encountered. + * + * STATE_BLOCKED_MIXED_DISPLAY_CONTENT + * Mixed display content has been blocked from loading. + * + * STATE_LOADED_MIXED_DISPLAY_CONTENT + * Mixed display content has been loaded. State should be STATE_IS_BROKEN. + */ + const unsigned long STATE_BLOCKED_MIXED_DISPLAY_CONTENT = 0x00000100; + const unsigned long STATE_LOADED_MIXED_DISPLAY_CONTENT = 0x00000200; + + /** + * Diagnostic flags + * + * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT + * nsSecureBrowserUIImpl::CheckForBlockedContent(). + * + * May be set in addition to other security state flags to indicate that + * some state is countered that deserves a warning or error, but does not + * change the top level security state of the connection. + * + * STATE_CERT_DISTRUST_IMMINENT + * The certificate in use will be distrusted in the near future. + */ + const unsigned long STATE_CERT_DISTRUST_IMMINENT = 0x00010000; + + /** + * State bits for EV == Extended Validation == High Assurance + * + * These flags describe the level of identity verification + * in a call to the onSecurityChange method. + * + * STATE_IDENTITY_EV_TOPLEVEL + * The topmost document uses an EV cert. + * NOTE: Available since Gecko 1.9 + */ + + const unsigned long STATE_IDENTITY_EV_TOPLEVEL = 0x00100000; + + /** + * Broken state flags + * + * These flags describe the reason of the broken state. + * + * STATE_USES_SSL_3 + * The topmost document uses SSL 3.0. + * + * STATE_USES_WEAK_CRYPTO + * The topmost document uses a weak cipher suite such as RC4. + * + * STATE_CERT_USER_OVERRIDDEN + * The user has added a security exception for the site. + */ + const unsigned long STATE_USES_SSL_3 = 0x01000000; + const unsigned long STATE_USES_WEAK_CRYPTO = 0x02000000; + const unsigned long STATE_CERT_USER_OVERRIDDEN = 0x04000000; + + /** + * Content Blocking Event flags + * + * NOTE: IF YOU ARE ADDING MORE OF THESE FLAGS, MAKE SURE TO EDIT + * nsSecureBrowserUIImpl::CheckForBlockedContent() AND UPDATE THE + * CORRESPONDING LIST IN ContentBlockingController.java + * + * These flags describe the reason of cookie jar rejection. + * + * STATE_BLOCKED_TRACKING_CONTENT + * Tracking content has been blocked from loading. + * + * STATE_LOADED_LEVEL_1_TRACKING_CONTENT + * Tracking content from the Disconnect Level 1 list has been loaded. + * + * STATE_LOADED_LEVEL_2_TRACKING_CONTENT + * Tracking content from the Disconnect Level 2 list has been loaded. + * + * STATE_BLOCKED_FINGERPRINTING_CONTENT + * Fingerprinting content has been blocked from loading. + * + * STATE_LOADED_FINGERPRINTING_CONTENT + * Fingerprinting content has been loaded. + * + * STATE_BLOCKED_CRYPTOMINING_CONTENT + * Cryptomining content has been blocked from loading. + * + * STATE_LOADED_CRYPTOMINING_CONTENT + * Cryptomining content has been loaded. + * + * STATE_BLOCKED_UNSAFE_CONTENT + * Content which againts SafeBrowsing list has been blocked from loading. + * + * STATE_COOKIES_LOADED + * Performed a storage access check, which usually means something like a + * cookie or a storage item was loaded/stored on the current tab. + * Alternatively this could indicate that something in the current tab + * attempted to communicate with its same-origin counterparts in other + * tabs. + * + * STATE_COOKIES_LOADED_TRACKER + * Similar to STATE_COOKIES_LOADED, but only sent if the subject of the + * action was a third-party tracker when the active cookie policy imposes + * restrictions on such content. + * + * STATE_COOKIES_LOADED_SOCIALTRACKER + * Similar to STATE_COOKIES_LOADED, but only sent if the subject of the + * action was a third-party social tracker when the active cookie policy + * imposes restrictions on such content. + * + * STATE_COOKIES_BLOCKED_BY_PERMISSION + * Rejected for custom site permission. + * + * STATE_COOKIES_BLOCKED_TRACKER + * Rejected because the resource is a tracker and cookie policy doesn't + * allow its loading. + * + * STATE_COOKIES_BLOCKED_SOCIALTRACKER + * Rejected because the resource is a tracker from a social origin and + * cookie policy doesn't allow its loading. + * + * STATE_COOKIES_PARTITIONED_FOREIGN + * Rejected because the resource is a third-party and cookie policy forces + * third-party resources to be partitioned. + * + * STATE_COOKIES_BLOCKED_ALL + * Rejected because cookie policy blocks all cookies. + * + * STATE_COOKIES_BLOCKED_FOREIGN + * Rejected because cookie policy blocks 3rd party cookies. + * + * STATE_BLOCKED_SOCIALTRACKING_CONTENT + * SocialTracking content has been blocked from loading. + * + * STATE_LOADED_SOCIALTRACKING_CONTENT + * SocialTracking content has been loaded. + * + * STATE_UNBLOCKED_TRACKING_CONTENT + * Tracking content should be blocked from loading was unblocked. + * + */ + const unsigned long STATE_BLOCKED_TRACKING_CONTENT = 0x00001000; + const unsigned long STATE_LOADED_LEVEL_1_TRACKING_CONTENT = 0x00002000; + const unsigned long STATE_LOADED_LEVEL_2_TRACKING_CONTENT = 0x00100000; + const unsigned long STATE_BLOCKED_FINGERPRINTING_CONTENT = 0x00000040; + const unsigned long STATE_LOADED_FINGERPRINTING_CONTENT = 0x00000400; + const unsigned long STATE_BLOCKED_CRYPTOMINING_CONTENT = 0x00000800; + const unsigned long STATE_LOADED_CRYPTOMINING_CONTENT = 0x00200000; + const unsigned long STATE_BLOCKED_UNSAFE_CONTENT = 0x00004000; + const unsigned long STATE_COOKIES_LOADED = 0x00008000; + const unsigned long STATE_COOKIES_LOADED_TRACKER = 0x00040000; + const unsigned long STATE_COOKIES_LOADED_SOCIALTRACKER = 0x00080000; + const unsigned long STATE_COOKIES_BLOCKED_BY_PERMISSION = 0x10000000; + const unsigned long STATE_COOKIES_BLOCKED_TRACKER = 0x20000000; + const unsigned long STATE_COOKIES_BLOCKED_SOCIALTRACKER = 0x01000000; + const unsigned long STATE_COOKIES_BLOCKED_ALL = 0x40000000; + const unsigned long STATE_COOKIES_PARTITIONED_FOREIGN = 0x80000000; + const unsigned long STATE_COOKIES_BLOCKED_FOREIGN = 0x00000080; + const unsigned long STATE_BLOCKED_SOCIALTRACKING_CONTENT = 0x00010000; + const unsigned long STATE_LOADED_SOCIALTRACKING_CONTENT = 0x00020000; + const unsigned long STATE_UNBLOCKED_TRACKING_CONTENT = 0x00000010; + + /** + * Flag for HTTPS-Only Mode upgrades + * + * STATE_HTTPS_ONLY_MODE_UPGRADED + * When a request has been upgraded by HTTPS-Only Mode + * + * STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED + * When an upgraded request failed. + */ + const unsigned long STATE_HTTPS_ONLY_MODE_UPGRADED = 0x00400000; + const unsigned long STATE_HTTPS_ONLY_MODE_UPGRADE_FAILED = 0x00800000; + + /** + * Notification indicating the state has changed for one of the requests + * associated with aWebProgress. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification + * @param aRequest + * The nsIRequest that has changed state. + * @param aStateFlags + * Flags indicating the new state. This value is a combination of one + * of the State Transition Flags and one or more of the State Type + * Flags defined above. Any undefined bits are reserved for future + * use. + * @param aStatus + * Error status code associated with the state change. This parameter + * should be ignored unless aStateFlags includes the STATE_STOP bit. + * The status code indicates success or failure of the request + * associated with the state change. NOTE: aStatus may be a success + * code even for server generated errors, such as the HTTP 404 error. + * In such cases, the request itself should be queried for extended + * error information (e.g., for HTTP requests see nsIHttpChannel). + */ + void onStateChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in unsigned long aStateFlags, + in nsresult aStatus); + + /** + * Notification that the progress has changed for one of the requests + * associated with aWebProgress. Progress totals are reset to zero when all + * requests in aWebProgress complete (corresponding to onStateChange being + * called with aStateFlags including the STATE_STOP and STATE_IS_WINDOW + * flags). + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new progress. + * @param aCurSelfProgress + * The current progress for aRequest. + * @param aMaxSelfProgress + * The maximum progress for aRequest. + * @param aCurTotalProgress + * The current progress for all requests associated with aWebProgress. + * @param aMaxTotalProgress + * The total progress for all requests associated with aWebProgress. + * + * NOTE: If any progress value is unknown, or if its value would exceed the + * maximum value of type long, then its value is replaced with -1. + * + * NOTE: If the object also implements nsIWebProgressListener2 and the caller + * knows about that interface, this function will not be called. Instead, + * nsIWebProgressListener2::onProgressChange64 will be called. + */ + void onProgressChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in long aCurSelfProgress, + in long aMaxSelfProgress, + in long aCurTotalProgress, + in long aMaxTotalProgress); + + /** + * Flags for onLocationChange + * + * LOCATION_CHANGE_SAME_DOCUMENT + * This flag is on when |aWebProgress| did not load a new document. + * For example, the location change is due to an anchor scroll or a + * pushState/popState/replaceState. + * + * LOCATION_CHANGE_ERROR_PAGE + * This flag is on when |aWebProgress| redirected from the requested + * contents to an internal page to show error status, such as + * <about:neterror>, <about:certerror> and so on. + * + * Generally speaking, |aURI| and |aRequest| are the original data. DOM + * |window.location.href| is also the original location, while + * |document.documentURI| is the redirected location. Sometimes |aURI| is + * <about:blank> and |aRequest| is null when the original data does not + + remain. + * + * |aWebProgress| does NOT set this flag when it did not try to load a new + * document. In this case, it should set LOCATION_CHANGE_SAME_DOCUMENT. + * + * LOCATION_CHANGE_RELOAD + * This flag is on when reloading the current page, either from + * location.reload() or the browser UI. + */ + const unsigned long LOCATION_CHANGE_SAME_DOCUMENT = 0x00000001; + const unsigned long LOCATION_CHANGE_ERROR_PAGE = 0x00000002; + const unsigned long LOCATION_CHANGE_RELOAD = 0x00000004; + + /** + * Called when the location of the window being watched changes. This is not + * when a load is requested, but rather once it is verified that the load is + * going to occur in the given window. For instance, a load that starts in a + * window might send progress and status messages for the new site, but it + * will not send the onLocationChange until we are sure that we are loading + * this new page here. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The associated nsIRequest. This may be null in some cases. + * @param aLocation + * The URI of the location that is being loaded. + * @param aFlags + * This is a value which explains the situation or the reason why + * the location has changed. + */ + void onLocationChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in nsIURI aLocation, + [optional] in unsigned long aFlags); + + /** + * Notification that the status of a request has changed. The status message + * is intended to be displayed to the user (e.g., in the status bar of the + * browser). + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new status. + * @param aStatus + * This value is not an error code. Instead, it is a numeric value + * that indicates the current status of the request. This interface + * does not define the set of possible status codes. NOTE: Some + * status values are defined by nsITransport and nsISocketTransport. + * @param aMessage + * Localized text corresponding to aStatus. + */ + void onStatusChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in nsresult aStatus, + in wstring aMessage); + + /** + * Notification called for security progress. This method will be called on + * security transitions (eg HTTP -> HTTPS, HTTPS -> HTTP, FOO -> HTTPS) and + * after document load completion. It might also be called if an error + * occurs during network loading. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new security state. + * @param aState + * A value composed of the Security State Flags and the Security + * Strength Flags listed above. Any undefined bits are reserved for + * future use. + * + * NOTE: These notifications will only occur if a security package is + * installed. + */ + void onSecurityChange(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in unsigned long aState); + + /** + * Notification called for content blocking events. This method will be + * called when content gets allowed/blocked for various reasons per the + * Content Blocking rules. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new security state. + * @param aEvent + * A value composed of the Content Blocking Event Flags listed above. + * Any undefined bits are reserved for future use. + */ + void onContentBlockingEvent(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in unsigned long aEvent); +}; diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl new file mode 100644 index 0000000000..87701f8d2c --- /dev/null +++ b/uriloader/base/nsIWebProgressListener2.idl @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIWebProgressListener.idl" + +/** + * An extended version of nsIWebProgressListener. + */ +[scriptable, uuid(dde39de0-e4e0-11da-8ad9-0800200c9a66)] +interface nsIWebProgressListener2 : nsIWebProgressListener { + /** + * Notification that the progress has changed for one of the requests + * associated with aWebProgress. Progress totals are reset to zero when all + * requests in aWebProgress complete (corresponding to onStateChange being + * called with aStateFlags including the STATE_STOP and STATE_IS_WINDOW + * flags). + * + * This function is identical to nsIWebProgressListener::onProgressChange, + * except that this function supports 64-bit values. + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRequest + * The nsIRequest that has new progress. + * @param aCurSelfProgress + * The current progress for aRequest. + * @param aMaxSelfProgress + * The maximum progress for aRequest. + * @param aCurTotalProgress + * The current progress for all requests associated with aWebProgress. + * @param aMaxTotalProgress + * The total progress for all requests associated with aWebProgress. + * + * NOTE: If any progress value is unknown, then its value is replaced with -1. + * + * @see nsIWebProgressListener2::onProgressChange64 + */ + void onProgressChange64(in nsIWebProgress aWebProgress, + in nsIRequest aRequest, + in long long aCurSelfProgress, + in long long aMaxSelfProgress, + in long long aCurTotalProgress, + in long long aMaxTotalProgress); + + /** + * Notification that a refresh or redirect has been requested in aWebProgress + * For example, via a <meta http-equiv="refresh"> or an HTTP Refresh: header + * + * @param aWebProgress + * The nsIWebProgress instance that fired the notification. + * @param aRefreshURI + * The new URI that aWebProgress has requested redirecting to. + * @param aMillis + * The delay (in milliseconds) before refresh. + * @param aSameURI + * True if aWebProgress is requesting a refresh of the + * current URI. + * False if aWebProgress is requesting a redirection to + * a different URI. + * + * @return True if the refresh may proceed. + * False if the refresh should be aborted. + */ + boolean onRefreshAttempted(in nsIWebProgress aWebProgress, + in nsIURI aRefreshURI, + in long aMillis, + in boolean aSameURI); +}; diff --git a/uriloader/base/nsURILoader.cpp b/uriloader/base/nsURILoader.cpp new file mode 100644 index 0000000000..21aa14aa8f --- /dev/null +++ b/uriloader/base/nsURILoader.cpp @@ -0,0 +1,795 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsURILoader.h" +#include "nsIURIContentListener.h" +#include "nsIContentHandler.h" +#include "nsILoadGroup.h" +#include "nsIDocumentLoader.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsIChannel.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIInputStream.h" +#include "nsIStreamConverterService.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIHttpChannel.h" +#include "netCore.h" +#include "nsCRT.h" +#include "nsIDocShell.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsIChildChannel.h" +#include "nsExternalHelperAppService.h" + +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsReadableUtils.h" +#include "nsError.h" + +#include "nsICategoryManager.h" +#include "nsCExternalHandlerService.h" + +#include "nsNetCID.h" + +#include "nsMimeTypes.h" + +#include "nsDocLoader.h" +#include "mozilla/Attributes.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Preferences.h" +#include "mozilla/Unused.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_general.h" +#include "nsContentUtils.h" + +mozilla::LazyLogModule nsURILoader::mLog("URILoader"); + +#define LOG(args) MOZ_LOG(nsURILoader::mLog, mozilla::LogLevel::Debug, args) +#define LOG_ERROR(args) \ + MOZ_LOG(nsURILoader::mLog, mozilla::LogLevel::Error, args) +#define LOG_ENABLED() MOZ_LOG_TEST(nsURILoader::mLog, mozilla::LogLevel::Debug) + +NS_IMPL_ADDREF(nsDocumentOpenInfo) +NS_IMPL_RELEASE(nsDocumentOpenInfo) + +NS_INTERFACE_MAP_BEGIN(nsDocumentOpenInfo) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) +NS_INTERFACE_MAP_END + +nsDocumentOpenInfo::nsDocumentOpenInfo(nsIInterfaceRequestor* aWindowContext, + uint32_t aFlags, nsURILoader* aURILoader) + : m_originalContext(aWindowContext), + mFlags(aFlags), + mURILoader(aURILoader), + mDataConversionDepthLimit( + StaticPrefs::general_document_open_conversion_depth_limit()) {} + +nsDocumentOpenInfo::nsDocumentOpenInfo(uint32_t aFlags, + bool aAllowListenerConversions) + : m_originalContext(nullptr), + mFlags(aFlags), + mURILoader(nullptr), + mDataConversionDepthLimit( + StaticPrefs::general_document_open_conversion_depth_limit()), + mAllowListenerConversions(aAllowListenerConversions) {} + +nsDocumentOpenInfo::~nsDocumentOpenInfo() {} + +nsresult nsDocumentOpenInfo::Prepare() { + LOG(("[0x%p] nsDocumentOpenInfo::Prepare", this)); + + nsresult rv; + + // ask our window context if it has a uri content listener... + m_contentListener = do_GetInterface(m_originalContext, &rv); + return rv; +} + +NS_IMETHODIMP nsDocumentOpenInfo::OnStartRequest(nsIRequest* request) { + LOG(("[0x%p] nsDocumentOpenInfo::OnStartRequest", this)); + MOZ_ASSERT(request); + if (!request) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = NS_OK; + + // + // Deal with "special" HTTP responses: + // + // - In the case of a 204 (No Content) or 205 (Reset Content) response, do + // not try to find a content handler. Return NS_BINDING_ABORTED to cancel + // the request. This has the effect of ensuring that the DocLoader does + // not try to interpret this as a real request. + // + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request, &rv)); + + if (NS_SUCCEEDED(rv)) { + uint32_t responseCode = 0; + + rv = httpChannel->GetResponseStatus(&responseCode); + + if (NS_FAILED(rv)) { + LOG_ERROR((" Failed to get HTTP response status")); + + // behave as in the canceled case + return NS_OK; + } + + LOG((" HTTP response status: %d", responseCode)); + + if (204 == responseCode || 205 == responseCode) { + return NS_BINDING_ABORTED; + } + } + + // + // Make sure that the transaction has succeeded, so far... + // + nsresult status; + + rv = request->GetStatus(&status); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Unable to get request status!"); + if (NS_FAILED(rv)) return rv; + + if (NS_FAILED(status)) { + LOG_ERROR((" Request failed, status: 0x%08" PRIX32, + static_cast<uint32_t>(status))); + + // + // The transaction has already reported an error - so it will be torn + // down. Therefore, it is not necessary to return an error code... + // + return NS_OK; + } + + rv = DispatchContent(request, nullptr); + + LOG((" After dispatch, m_targetStreamListener: 0x%p, rv: 0x%08" PRIX32, + m_targetStreamListener.get(), static_cast<uint32_t>(rv))); + + NS_ASSERTION( + NS_SUCCEEDED(rv) || !m_targetStreamListener, + "Must not have an m_targetStreamListener with a failure return!"); + + NS_ENSURE_SUCCESS(rv, rv); + + if (m_targetStreamListener) + rv = m_targetStreamListener->OnStartRequest(request); + + LOG((" OnStartRequest returning: 0x%08" PRIX32, static_cast<uint32_t>(rv))); + + return rv; +} + +NS_IMETHODIMP +nsDocumentOpenInfo::CheckListenerChain() { + NS_ASSERTION(NS_IsMainThread(), "Should be on the main thread!"); + nsresult rv = NS_OK; + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener = + do_QueryInterface(m_targetStreamListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + LOG( + ("[0x%p] nsDocumentOpenInfo::CheckListenerChain %s listener %p rv " + "%" PRIx32, + this, (NS_SUCCEEDED(rv) ? "success" : "failure"), + (nsIStreamListener*)m_targetStreamListener, static_cast<uint32_t>(rv))); + return rv; +} + +NS_IMETHODIMP +nsDocumentOpenInfo::OnDataAvailable(nsIRequest* request, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + // if we have retarged to the end stream listener, then forward the call.... + // otherwise, don't do anything + + nsresult rv = NS_OK; + + if (m_targetStreamListener) + rv = m_targetStreamListener->OnDataAvailable(request, inStr, sourceOffset, + count); + return rv; +} + +NS_IMETHODIMP nsDocumentOpenInfo::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + LOG(("[0x%p] nsDocumentOpenInfo::OnStopRequest", this)); + + if (m_targetStreamListener) { + nsCOMPtr<nsIStreamListener> listener(m_targetStreamListener); + + // If this is a multipart stream, we could get another + // OnStartRequest after this... reset state. + m_targetStreamListener = nullptr; + mContentType.Truncate(); + listener->OnStopRequest(request, aStatus); + } + mUsedContentHandler = false; + + // Remember... + // In the case of multiplexed streams (such as multipart/x-mixed-replace) + // these stream listener methods could be called again :-) + // + return NS_OK; +} + +nsresult nsDocumentOpenInfo::DispatchContent(nsIRequest* request, + nsISupports* aCtxt) { + LOG(("[0x%p] nsDocumentOpenInfo::DispatchContent for type '%s'", this, + mContentType.get())); + + MOZ_ASSERT(!m_targetStreamListener, + "Why do we already have a target stream listener?"); + + nsresult rv; + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + if (!aChannel) { + LOG_ERROR((" Request is not a channel. Bailing.")); + return NS_ERROR_FAILURE; + } + + constexpr auto anyType = "*/*"_ns; + if (mContentType.IsEmpty() || mContentType == anyType) { + rv = aChannel->GetContentType(mContentType); + if (NS_FAILED(rv)) return rv; + LOG((" Got type from channel: '%s'", mContentType.get())); + } + + bool isGuessFromExt = + mContentType.LowerCaseEqualsASCII(APPLICATION_GUESS_FROM_EXT); + if (isGuessFromExt) { + // Reset to application/octet-stream for now; no one other than the + // external helper app service should see APPLICATION_GUESS_FROM_EXT. + mContentType = APPLICATION_OCTET_STREAM; + aChannel->SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM)); + } + + // Check whether the data should be forced to be handled externally. This + // could happen because the Content-Disposition header is set so, or, in the + // future, because the user has specified external handling for the MIME + // type. + bool forceExternalHandling = false; + uint32_t disposition; + rv = aChannel->GetContentDisposition(&disposition); + + if (NS_SUCCEEDED(rv) && disposition == nsIChannel::DISPOSITION_ATTACHMENT) { + forceExternalHandling = true; + } + + LOG((" forceExternalHandling: %s", forceExternalHandling ? "yes" : "no")); + + if (!forceExternalHandling) { + // + // First step: See whether m_contentListener wants to handle this + // content type. + // + if (TryDefaultContentListener(aChannel)) { + LOG((" Success! Our default listener likes this type")); + // All done here + return NS_OK; + } + + // If we aren't allowed to try other listeners, just skip through to + // trying to convert the data. + if (!(mFlags & nsIURILoader::DONT_RETARGET)) { + // + // Second step: See whether some other registered listener wants + // to handle this content type. + // + int32_t count = mURILoader ? mURILoader->m_listeners.Count() : 0; + nsCOMPtr<nsIURIContentListener> listener; + for (int32_t i = 0; i < count; i++) { + listener = do_QueryReferent(mURILoader->m_listeners[i]); + if (listener) { + if (TryContentListener(listener, aChannel)) { + LOG((" Found listener registered on the URILoader")); + return NS_OK; + } + } else { + // remove from the listener list, reset i and update count + mURILoader->m_listeners.RemoveObjectAt(i--); + --count; + } + } + + // + // Third step: Try to find a content listener that has not yet had + // the chance to register, as it is contained in a not-yet-loaded + // module, but which has registered a contract ID. + // + nsCOMPtr<nsICategoryManager> catman = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID); + if (catman) { + nsCString contractidString; + rv = catman->GetCategoryEntry(NS_CONTENT_LISTENER_CATEGORYMANAGER_ENTRY, + mContentType, contractidString); + if (NS_SUCCEEDED(rv) && !contractidString.IsEmpty()) { + LOG((" Listener contractid for '%s' is '%s'", mContentType.get(), + contractidString.get())); + + listener = do_CreateInstance(contractidString.get()); + LOG((" Listener from category manager: 0x%p", listener.get())); + + if (listener && TryContentListener(listener, aChannel)) { + LOG((" Listener from category manager likes this type")); + return NS_OK; + } + } + } + + // + // Fourth step: try to find an nsIContentHandler for our type. + // + nsAutoCString handlerContractID(NS_CONTENT_HANDLER_CONTRACTID_PREFIX); + handlerContractID += mContentType; + + nsCOMPtr<nsIContentHandler> contentHandler = + do_CreateInstance(handlerContractID.get()); + if (contentHandler) { + LOG((" Content handler found")); + // Note that m_originalContext can be nullptr when running this in + // the parent process on behalf on a docshell in the content process, + // and in that case we only support content handlers that don't need + // the context. + rv = contentHandler->HandleContent(mContentType.get(), + m_originalContext, request); + // XXXbz returning an error code to represent handling the + // content is just bizarre! + if (rv != NS_ERROR_WONT_HANDLE_CONTENT) { + if (NS_FAILED(rv)) { + // The content handler has unexpectedly failed. Cancel the request + // just in case the handler didn't... + LOG((" Content handler failed. Aborting load")); + request->Cancel(rv); + } else { + LOG((" Content handler taking over load")); + mUsedContentHandler = true; + } + + return rv; + } + } + } else { + LOG( + (" DONT_RETARGET flag set, so skipped over random other content " + "listeners and content handlers")); + } + + // + // Fifth step: If no listener prefers this type, see if any stream + // converters exist to transform this content type into + // some other. + // + // Don't do this if the server sent us a MIME type of "*/*" because they saw + // it in our Accept header and got confused. + // XXXbz have to be careful here; may end up in some sort of bizarre + // infinite decoding loop. + if (mContentType != anyType) { + rv = TryStreamConversion(aChannel); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + } + } + + NS_ASSERTION(!m_targetStreamListener, + "If we found a listener, why are we not using it?"); + + if (mFlags & nsIURILoader::DONT_RETARGET) { + LOG( + (" External handling forced or (listener not interested and no " + "stream converter exists), and retargeting disallowed -> aborting")); + return NS_ERROR_WONT_HANDLE_CONTENT; + } + + // Before dispatching to the external helper app service, check for an HTTP + // error page. If we got one, we don't want to handle it with a helper app, + // really. + // The WPT a-download-click-404.html requires us to silently handle this + // without displaying an error page, so we just return early here. + // See bug 1604308 for discussion around what the ideal behaviour is. + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(request)); + if (httpChannel) { + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + if (NS_FAILED(rv) || !requestSucceeded) { + return NS_OK; + } + } + + // Sixth step: + // + // All attempts to dispatch this content have failed. Just pass it off to + // the helper app service. + // + + nsCOMPtr<nsIExternalHelperAppService> helperAppService = + do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &rv); + if (helperAppService) { + LOG((" Passing load off to helper app service")); + + // Set these flags to indicate that the channel has been targeted and that + // we are not using the original consumer. + nsLoadFlags loadFlags = 0; + request->GetLoadFlags(&loadFlags); + request->SetLoadFlags(loadFlags | nsIChannel::LOAD_RETARGETED_DOCUMENT_URI | + nsIChannel::LOAD_TARGETED); + + if (isGuessFromExt) { + mContentType = APPLICATION_GUESS_FROM_EXT; + aChannel->SetContentType(nsLiteralCString(APPLICATION_GUESS_FROM_EXT)); + } + + rv = TryExternalHelperApp(helperAppService, aChannel); + if (NS_FAILED(rv)) { + request->SetLoadFlags(loadFlags); + m_targetStreamListener = nullptr; + } + } + + NS_ASSERTION(m_targetStreamListener || NS_FAILED(rv), + "There is no way we should be successful at this point without " + "a m_targetStreamListener"); + return rv; +} + +nsresult nsDocumentOpenInfo::TryExternalHelperApp( + nsIExternalHelperAppService* aHelperAppService, nsIChannel* aChannel) { + return aHelperAppService->DoContent(mContentType, aChannel, m_originalContext, + false, nullptr, + getter_AddRefs(m_targetStreamListener)); +} + +nsresult nsDocumentOpenInfo::ConvertData(nsIRequest* request, + nsIURIContentListener* aListener, + const nsACString& aSrcContentType, + const nsACString& aOutContentType) { + LOG(("[0x%p] nsDocumentOpenInfo::ConvertData from '%s' to '%s'", this, + PromiseFlatCString(aSrcContentType).get(), + PromiseFlatCString(aOutContentType).get())); + + if (mDataConversionDepthLimit == 0) { + LOG( + ("[0x%p] nsDocumentOpenInfo::ConvertData - reached the recursion " + "limit!", + this)); + // This will fall back to external helper app handling. + return NS_ERROR_ABORT; + } + + MOZ_ASSERT(aSrcContentType != aOutContentType, + "ConvertData called when the two types are the same!"); + + nsresult rv = NS_OK; + + nsCOMPtr<nsIStreamConverterService> StreamConvService = + do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + LOG((" Got converter service")); + + // When applying stream decoders, it is necessary to "insert" an + // intermediate nsDocumentOpenInfo instance to handle the targeting of + // the "final" stream or streams. + // + // For certain content types (ie. multi-part/x-mixed-replace) the input + // stream is split up into multiple destination streams. This + // intermediate instance is used to target these "decoded" streams... + // + RefPtr<nsDocumentOpenInfo> nextLink = Clone(); + + LOG((" Downstream DocumentOpenInfo would be: 0x%p", nextLink.get())); + + // Decrease the conversion recursion limit by one to prevent infinite loops. + nextLink->mDataConversionDepthLimit = mDataConversionDepthLimit - 1; + + // Make sure nextLink starts with the contentListener that said it wanted + // the results of this decode. + nextLink->m_contentListener = aListener; + // Also make sure it has to look for a stream listener to pump data into. + nextLink->m_targetStreamListener = nullptr; + + // Make sure that nextLink treats the data as aOutContentType when + // dispatching; that way even if the stream converters don't change the type + // on the channel we will still do the right thing. If aOutContentType is + // */*, that's OK -- that will just indicate to nextLink that it should get + // the type off the channel. + nextLink->mContentType = aOutContentType; + + // The following call sets m_targetStreamListener to the input end of the + // stream converter and sets the output end of the stream converter to + // nextLink. As we pump data into m_targetStreamListener the stream + // converter will convert it and pass the converted data to nextLink. + return StreamConvService->AsyncConvertData( + PromiseFlatCString(aSrcContentType).get(), + PromiseFlatCString(aOutContentType).get(), nextLink, request, + getter_AddRefs(m_targetStreamListener)); +} + +nsresult nsDocumentOpenInfo::TryStreamConversion(nsIChannel* aChannel) { + constexpr auto anyType = "*/*"_ns; + nsresult rv = ConvertData(aChannel, m_contentListener, mContentType, anyType); + if (NS_FAILED(rv)) { + m_targetStreamListener = nullptr; + } else if (m_targetStreamListener) { + // We found a converter for this MIME type. We'll just pump data into + // it and let the downstream nsDocumentOpenInfo handle things. + LOG((" Converter taking over now")); + } + return rv; +} + +bool nsDocumentOpenInfo::TryContentListener(nsIURIContentListener* aListener, + nsIChannel* aChannel) { + LOG(("[0x%p] nsDocumentOpenInfo::TryContentListener; mFlags = 0x%x", this, + mFlags)); + + MOZ_ASSERT(aListener, "Must have a non-null listener"); + MOZ_ASSERT(aChannel, "Must have a channel"); + + bool listenerWantsContent = false; + nsCString typeToUse; + + if (mFlags & nsIURILoader::IS_CONTENT_PREFERRED) { + aListener->IsPreferred(mContentType.get(), getter_Copies(typeToUse), + &listenerWantsContent); + } else { + aListener->CanHandleContent(mContentType.get(), false, + getter_Copies(typeToUse), + &listenerWantsContent); + } + if (!listenerWantsContent) { + LOG((" Listener is not interested")); + return false; + } + + if (!typeToUse.IsEmpty() && typeToUse != mContentType) { + // Need to do a conversion here. + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + if (mAllowListenerConversions) { + rv = ConvertData(aChannel, aListener, mContentType, typeToUse); + } + + if (NS_FAILED(rv)) { + // No conversion path -- we don't want this listener, if we got one + m_targetStreamListener = nullptr; + } + + LOG((" Found conversion: %s", m_targetStreamListener ? "yes" : "no")); + + // m_targetStreamListener is now the input end of the converter, and we can + // just pump the data in there, if it exists. If it does not, we need to + // try other nsIURIContentListeners. + return m_targetStreamListener != nullptr; + } + + // At this point, aListener wants data of type mContentType. Let 'em have + // it. But first, if we are retargeting, set an appropriate flag on the + // channel + nsLoadFlags loadFlags = 0; + aChannel->GetLoadFlags(&loadFlags); + + // Set this flag to indicate that the channel has been targeted at a final + // consumer. This load flag is tested in nsDocLoader::OnProgress. + nsLoadFlags newLoadFlags = nsIChannel::LOAD_TARGETED; + + nsCOMPtr<nsIURIContentListener> originalListener = + do_GetInterface(m_originalContext); + if (originalListener != aListener) { + newLoadFlags |= nsIChannel::LOAD_RETARGETED_DOCUMENT_URI; + } + aChannel->SetLoadFlags(loadFlags | newLoadFlags); + + bool abort = false; + bool isPreferred = (mFlags & nsIURILoader::IS_CONTENT_PREFERRED) != 0; + nsresult rv = + aListener->DoContent(mContentType, isPreferred, aChannel, + getter_AddRefs(m_targetStreamListener), &abort); + + if (NS_FAILED(rv)) { + LOG_ERROR((" DoContent failed")); + + // Unset the RETARGETED_DOCUMENT_URI flag if we set it... + aChannel->SetLoadFlags(loadFlags); + m_targetStreamListener = nullptr; + return false; + } + + if (abort) { + // Nothing else to do here -- aListener is handling it all. Make + // sure m_targetStreamListener is null so we don't do anything + // after this point. + LOG((" Listener has aborted the load")); + m_targetStreamListener = nullptr; + } + + NS_ASSERTION(abort || m_targetStreamListener, + "DoContent returned no listener?"); + + // aListener is handling the load from this point on. + return true; +} + +bool nsDocumentOpenInfo::TryDefaultContentListener(nsIChannel* aChannel) { + if (m_contentListener) { + return TryContentListener(m_contentListener, aChannel); + } + return false; +} + +/////////////////////////////////////////////////////////////////////////////////////////////// +// Implementation of nsURILoader +/////////////////////////////////////////////////////////////////////////////////////////////// + +nsURILoader::nsURILoader() {} + +nsURILoader::~nsURILoader() {} + +NS_IMPL_ADDREF(nsURILoader) +NS_IMPL_RELEASE(nsURILoader) + +NS_INTERFACE_MAP_BEGIN(nsURILoader) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURILoader) + NS_INTERFACE_MAP_ENTRY(nsIURILoader) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP nsURILoader::RegisterContentListener( + nsIURIContentListener* aContentListener) { + nsresult rv = NS_OK; + + nsWeakPtr weakListener = do_GetWeakReference(aContentListener); + NS_ASSERTION(weakListener, + "your URIContentListener must support weak refs!\n"); + + if (weakListener) m_listeners.AppendObject(weakListener); + + return rv; +} + +NS_IMETHODIMP nsURILoader::UnRegisterContentListener( + nsIURIContentListener* aContentListener) { + nsWeakPtr weakListener = do_GetWeakReference(aContentListener); + if (weakListener) m_listeners.RemoveObject(weakListener); + + return NS_OK; +} + +NS_IMETHODIMP nsURILoader::OpenURI(nsIChannel* channel, uint32_t aFlags, + nsIInterfaceRequestor* aWindowContext) { + NS_ENSURE_ARG_POINTER(channel); + + if (LOG_ENABLED()) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + nsAutoCString spec; + uri->GetAsciiSpec(spec); + LOG(("nsURILoader::OpenURI for %s", spec.get())); + } + + nsCOMPtr<nsIStreamListener> loader; + nsresult rv = OpenChannel(channel, aFlags, aWindowContext, false, + getter_AddRefs(loader)); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_WONT_HANDLE_CONTENT) { + // Not really an error, from this method's point of view + return NS_OK; + } + } + + // This method is not complete. Eventually, we should first go + // to the content listener and ask them for a protocol handler... + // if they don't give us one, we need to go to the registry and get + // the preferred protocol handler. + + // But for now, I'm going to let necko do the work for us.... + rv = channel->AsyncOpen(loader); + + // no content from this load - that's OK. + if (rv == NS_ERROR_NO_CONTENT) { + LOG((" rv is NS_ERROR_NO_CONTENT -- doing nothing")); + return NS_OK; + } + return rv; +} + +nsresult nsURILoader::OpenChannel(nsIChannel* channel, uint32_t aFlags, + nsIInterfaceRequestor* aWindowContext, + bool aChannelIsOpen, + nsIStreamListener** aListener) { + NS_ASSERTION(channel, "Trying to open a null channel!"); + NS_ASSERTION(aWindowContext, "Window context must not be null"); + + if (LOG_ENABLED()) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + nsAutoCString spec; + uri->GetAsciiSpec(spec); + LOG(("nsURILoader::OpenChannel for %s", spec.get())); + } + + // we need to create a DocumentOpenInfo object which will go ahead and open + // the url and discover the content type.... + RefPtr<nsDocumentOpenInfo> loader = + new nsDocumentOpenInfo(aWindowContext, aFlags, this); + + // Set the correct loadgroup on the channel + nsCOMPtr<nsILoadGroup> loadGroup(do_GetInterface(aWindowContext)); + + if (!loadGroup) { + // XXXbz This context is violating what we'd like to be the new uriloader + // api.... Set up a nsDocLoader to handle the loadgroup for this context. + // This really needs to go away! + nsCOMPtr<nsIURIContentListener> listener(do_GetInterface(aWindowContext)); + if (listener) { + nsCOMPtr<nsISupports> cookie; + listener->GetLoadCookie(getter_AddRefs(cookie)); + if (!cookie) { + RefPtr<nsDocLoader> newDocLoader = new nsDocLoader(); + nsresult rv = newDocLoader->Init(); + if (NS_FAILED(rv)) return rv; + rv = nsDocLoader::AddDocLoaderAsChildOfRoot(newDocLoader); + if (NS_FAILED(rv)) return rv; + cookie = nsDocLoader::GetAsSupports(newDocLoader); + listener->SetLoadCookie(cookie); + } + loadGroup = do_GetInterface(cookie); + } + } + + // If the channel is pending, then we need to remove it from its current + // loadgroup + nsCOMPtr<nsILoadGroup> oldGroup; + channel->GetLoadGroup(getter_AddRefs(oldGroup)); + if (aChannelIsOpen && !SameCOMIdentity(oldGroup, loadGroup)) { + // It is important to add the channel to the new group before + // removing it from the old one, so that the load isn't considered + // done as soon as the request is removed. + loadGroup->AddRequest(channel, nullptr); + + if (oldGroup) { + oldGroup->RemoveRequest(channel, nullptr, NS_BINDING_RETARGETED); + } + } + + channel->SetLoadGroup(loadGroup); + + // prepare the loader for receiving data + nsresult rv = loader->Prepare(); + if (NS_SUCCEEDED(rv)) NS_ADDREF(*aListener = loader); + return rv; +} + +NS_IMETHODIMP nsURILoader::OpenChannel(nsIChannel* channel, uint32_t aFlags, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aListener) { + bool pending; + if (NS_FAILED(channel->IsPending(&pending))) { + pending = false; + } + + return OpenChannel(channel, aFlags, aWindowContext, pending, aListener); +} + +NS_IMETHODIMP nsURILoader::Stop(nsISupports* aLoadCookie) { + nsresult rv; + nsCOMPtr<nsIDocumentLoader> docLoader; + + NS_ENSURE_ARG_POINTER(aLoadCookie); + + docLoader = do_GetInterface(aLoadCookie, &rv); + if (docLoader) { + rv = docLoader->Stop(); + } + return rv; +} diff --git a/uriloader/base/nsURILoader.h b/uriloader/base/nsURILoader.h new file mode 100644 index 0000000000..5d9eaf97c8 --- /dev/null +++ b/uriloader/base/nsURILoader.h @@ -0,0 +1,219 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsURILoader_h__ +#define nsURILoader_h__ + +#include "nsCURILoader.h" +#include "nsISupportsUtils.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsString.h" +#include "nsIWeakReference.h" +#include "mozilla/Attributes.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsIExternalHelperAppService.h" + +#include "mozilla/Logging.h" + +class nsDocumentOpenInfo; + +class nsURILoader final : public nsIURILoader { + public: + NS_DECL_NSIURILOADER + NS_DECL_ISUPPORTS + + nsURILoader(); + + protected: + ~nsURILoader(); + + /** + * Equivalent to nsIURILoader::openChannel, but allows specifying whether the + * channel is opened already. + */ + [[nodiscard]] nsresult OpenChannel(nsIChannel* channel, uint32_t aFlags, + nsIInterfaceRequestor* aWindowContext, + bool aChannelOpen, + nsIStreamListener** aListener); + + /** + * we shouldn't need to have an owning ref count on registered + * content listeners because they are supposed to unregister themselves + * when they go away. This array stores weak references + */ + nsCOMArray<nsIWeakReference> m_listeners; + + /** + * Logging. The module is called "URILoader" + */ + static mozilla::LazyLogModule mLog; + + friend class nsDocumentOpenInfo; +}; + +/** + * The nsDocumentOpenInfo contains the state required when a single + * document is being opened in order to discover the content type... + * Each instance remains alive until its target URL has been loaded + * (or aborted). + */ +class nsDocumentOpenInfo : public nsIStreamListener, + public nsIThreadRetargetableStreamListener { + public: + // Real constructor + // aFlags is a combination of the flags on nsIURILoader + nsDocumentOpenInfo(nsIInterfaceRequestor* aWindowContext, uint32_t aFlags, + nsURILoader* aURILoader); + nsDocumentOpenInfo(uint32_t aFlags, bool aAllowListenerConversions); + + NS_DECL_THREADSAFE_ISUPPORTS + + /** + * Prepares this object for receiving data. The stream + * listener methods of this class must not be called before calling this + * method. + */ + nsresult Prepare(); + + // Call this (from OnStartRequest) to attempt to find an nsIStreamListener to + // take the data off our hands. + nsresult DispatchContent(nsIRequest* request, nsISupports* aCtxt); + + // Call this if we need to insert a stream converter from aSrcContentType to + // aOutContentType into the StreamListener chain. DO NOT call it if the two + // types are the same, since no conversion is needed in that case. + nsresult ConvertData(nsIRequest* request, nsIURIContentListener* aListener, + const nsACString& aSrcContentType, + const nsACString& aOutContentType); + + /** + * Function to attempt to use aListener to handle the load. If + * true is returned, nothing else needs to be done; if false + * is returned, then a different way of handling the load should be + * tried. + */ + bool TryContentListener(nsIURIContentListener* aListener, + nsIChannel* aChannel); + + /** + * Virtual helper functions for content that we expect to be + * overriden when running in the parent process on behalf of + * a content process docshell. + * We also expect nsIStreamListener functions to be overriden + * to add functionality. + */ + + /** + * Attempt to create a steam converter converting from the + * current mContentType into something else. + * Sets m_targetStreamListener if it succeeds. + */ + virtual nsresult TryStreamConversion(nsIChannel* aChannel); + + /** + * Attempt to use the default content listener as our stream + * listener. + * Sets m_targetStreamListener if it succeeds. + */ + virtual bool TryDefaultContentListener(nsIChannel* aChannel); + + /** + * Attempt to pass aChannel onto the external helper app service. + * Sets m_targetStreamListener if it succeeds. + */ + virtual nsresult TryExternalHelperApp( + nsIExternalHelperAppService* aHelperAppService, nsIChannel* aChannel); + + /** + * Create another nsDocumentOpenInfo like this one, so that we can chain + * them together when we use a stream converter and don't know what the + * converted content type is until the converter outputs OnStartRequest. + */ + virtual nsDocumentOpenInfo* Clone() { + return new nsDocumentOpenInfo(m_originalContext, mFlags, mURILoader); + } + + // nsIRequestObserver methods: + NS_DECL_NSIREQUESTOBSERVER + + // nsIStreamListener methods: + NS_DECL_NSISTREAMLISTENER + + // nsIThreadRetargetableStreamListener + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + protected: + virtual ~nsDocumentOpenInfo(); + + protected: + /** + * The first content listener to try dispatching data to. Typically + * the listener associated with the entity that originated the load. + * This can be nullptr when running in the parent process for a content + * process docshell. + */ + nsCOMPtr<nsIURIContentListener> m_contentListener; + + /** + * The stream listener to forward nsIStreamListener notifications + * to. This is set once the load is dispatched. + */ + nsCOMPtr<nsIStreamListener> m_targetStreamListener; + + /** + * A pointer to the entity that originated the load. We depend on getting + * things like nsIURIContentListeners, nsIDOMWindows, etc off of it. + * This can be nullptr when running in the parent process for a content + * process docshell. + */ + nsCOMPtr<nsIInterfaceRequestor> m_originalContext; + + /** + * IS_CONTENT_PREFERRED is used for the boolean to pass to CanHandleContent + * (also determines whether we use CanHandleContent or IsPreferred). + * DONT_RETARGET means that we will only try m_originalContext, no other + * listeners. + */ + uint32_t mFlags; + + /** + * The type of the data we will be trying to dispatch. + */ + nsCString mContentType; + + /** + * Reference to the URILoader service so we can access its list of + * nsIURIContentListeners. + * This can be nullptr when running in the parent process for a content + * process docshell. + */ + RefPtr<nsURILoader> mURILoader; + + /** + * Limit of data conversion depth to prevent infinite conversion loops + */ + uint32_t mDataConversionDepthLimit; + + /** + * Set to true if OnStartRequest handles the content using an + * nsIContentHandler, and the content is consumed despite + * m_targetStreamListener being nullptr. + */ + bool mUsedContentHandler = false; + + /** + * True if we allow nsIURIContentListeners to return a requested + * input typeToUse, and attempt to create a matching stream converter. + * This is false when running in the parent process for a content process + * docshell + */ + bool mAllowListenerConversions = true; +}; + +#endif /* nsURILoader_h__ */ diff --git a/uriloader/docs/index.rst b/uriloader/docs/index.rst new file mode 100644 index 0000000000..4943040b84 --- /dev/null +++ b/uriloader/docs/index.rst @@ -0,0 +1,10 @@ +File Handling +============= + +This covers how files requested for display are loaded. + +.. toctree:: + :maxdepth: 2 + + uriloader + exthandler/index diff --git a/uriloader/docs/uriloader.rst b/uriloader/docs/uriloader.rst new file mode 100644 index 0000000000..e0763233e5 --- /dev/null +++ b/uriloader/docs/uriloader.rst @@ -0,0 +1,46 @@ +.. _uri_loader_service: + +URI Loader Service +================== + +As its name might suggest the URI loader service is responsible for loading URIs +but it is also responsible for deciding how to handle that content, whether to +display it as part of a DOM window or hand it off to something else. + +It is generally used when loading content for display to the user, normally from +``nsDocShell`` for display as a webpage or ``nsObjectLoadingContent`` for display inside +a webpage's ``<object>`` tag. The normal entrypoint is through ``nsIURILoader::OpenURI``. + +The URI loader starts the load and registers an ``nsDocumentOpenInfo`` as a stream +listener for the content. Once headers have been received `DispatchContent <https://searchfox.org/mozilla-central/search?q=nsDocumentOpenInfo%3A%3ADispatchContent&path=>`_ +then decides what to do with the content as it may need to be handled by something +other than the caller. It uses a few criteria to decide this including: + +* Content-Type header. +* Content-Disposition header. +* Load flags. + +Part of this handling may include running the content through a registered stream +converter to convert the content type from one to another. This is done through +the `stream converter service <https://searchfox.org/mozilla-central/source/netwerk/streamconv>`_. +When this happens a new ``nsDocumentOpenInfo`` is created to handle the new content +in the same way as the current content. + +The rough flow goes as follows (though note that this can vary depending on the +flags passed to the loader service): + +1. The caller may provide an ``nsIURIContentListener`` which can offer to handle + the content type or a content type that we can convert the original type to). + If so the load is passed off to the listener. +2. Global instances of ``nsIURIContentListener`` can be registered with the URI + loader service so these are consulted in the same way. +3. Global instances of ``nsIURIContentListener`` can be registered in the category + manager so these are consulted in the same way. +4. Global instances of ``nsIContentHandler`` can be registered. If one agrees to + handle the content then the load is handed over to it. +5. We attempt to convert the content to a different type. +6. The load is handed over to the :ref:`External Helper App Service <external_helper_app_service>`. + +For the most part the process ends at step 1 because nsDocShell passes a ``nsDSURIContentListener`` +for the ``nsIURIContentListener`` consulted first and it accepts most of the +`web content types <https://searchfox.org/mozilla-central/search?q=CONTENTDLF_CATEGORIES&redirect=false>`_. diff --git a/uriloader/exthandler/ContentHandlerService.cpp b/uriloader/exthandler/ContentHandlerService.cpp new file mode 100644 index 0000000000..96eb6a0cbb --- /dev/null +++ b/uriloader/exthandler/ContentHandlerService.cpp @@ -0,0 +1,248 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ContentHandlerService.h" +#include "HandlerServiceChild.h" +#include "ContentChild.h" +#include "nsIMutableArray.h" +#include "nsIMIMEInfo.h" +#include "nsIStringEnumerator.h" +#include "nsReadableUtils.h" + +using mozilla::dom::ContentChild; +using mozilla::dom::HandlerInfo; +using mozilla::dom::PHandlerServiceChild; + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(ContentHandlerService, nsIHandlerService) + +ContentHandlerService::ContentHandlerService() {} + +nsresult ContentHandlerService::Init() { + if (!XRE_IsContentProcess()) { + return NS_ERROR_FAILURE; + } + ContentChild* cpc = ContentChild::GetSingleton(); + + mHandlerServiceChild = new HandlerServiceChild(); + if (!cpc->SendPHandlerServiceConstructor(mHandlerServiceChild)) { + mHandlerServiceChild = nullptr; + } + return NS_OK; +} + +void ContentHandlerService::nsIHandlerInfoToHandlerInfo( + nsIHandlerInfo* aInfo, HandlerInfo* aHandlerInfo) { + nsCString type; + aInfo->GetType(type); + nsCOMPtr<nsIMIMEInfo> mimeInfo = do_QueryInterface(aInfo); + bool isMIMEInfo = !!mimeInfo; + nsString description; + aInfo->GetDescription(description); + bool alwaysAskBeforeHandling; + aInfo->GetAlwaysAskBeforeHandling(&alwaysAskBeforeHandling); + nsCOMPtr<nsIHandlerApp> app; + aInfo->GetPreferredApplicationHandler(getter_AddRefs(app)); + nsString name; + nsString detailedDescription; + if (app) { + app->GetName(name); + app->GetDetailedDescription(detailedDescription); + } + HandlerApp happ(name, detailedDescription); + nsTArray<HandlerApp> happs; + nsCOMPtr<nsIMutableArray> apps; + aInfo->GetPossibleApplicationHandlers(getter_AddRefs(apps)); + if (apps) { + unsigned int length; + apps->GetLength(&length); + for (unsigned int i = 0; i < length; i++) { + apps->QueryElementAt(i, NS_GET_IID(nsIHandlerApp), getter_AddRefs(app)); + app->GetName(name); + app->GetDetailedDescription(detailedDescription); + happs.AppendElement(HandlerApp(name, detailedDescription)); + } + } + + nsTArray<nsCString> extensions; + + if (isMIMEInfo) { + nsCOMPtr<nsIUTF8StringEnumerator> extensionsIter; + mimeInfo->GetFileExtensions(getter_AddRefs(extensionsIter)); + if (extensionsIter) { + bool hasMore = false; + while (NS_SUCCEEDED(extensionsIter->HasMore(&hasMore)) && hasMore) { + nsAutoCString extension; + if (NS_SUCCEEDED(extensionsIter->GetNext(extension))) { + extensions.AppendElement(std::move(extension)); + } + } + } + } + + nsHandlerInfoAction action; + aInfo->GetPreferredAction(&action); + HandlerInfo info(type, isMIMEInfo, description, alwaysAskBeforeHandling, + std::move(extensions), happ, happs, action); + *aHandlerInfo = info; +} + +NS_IMETHODIMP RemoteHandlerApp::GetName(nsAString& aName) { + aName.Assign(mAppChild.name()); + return NS_OK; +} + +NS_IMETHODIMP RemoteHandlerApp::SetName(const nsAString& aName) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP RemoteHandlerApp::GetDetailedDescription( + nsAString& aDetailedDescription) { + aDetailedDescription.Assign(mAppChild.detailedDescription()); + return NS_OK; +} + +NS_IMETHODIMP RemoteHandlerApp::SetDetailedDescription( + const nsAString& aDetailedDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP RemoteHandlerApp::Equals(nsIHandlerApp* aHandlerApp, + bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP RemoteHandlerApp::LaunchWithURI( + nsIURI* aURI, BrowsingContext* aBrowsingContext) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMPL_ISUPPORTS(RemoteHandlerApp, nsIHandlerApp) + +static inline void CopyHanderInfoTonsIHandlerInfo( + const HandlerInfo& info, nsIHandlerInfo* aHandlerInfo) { + HandlerApp preferredApplicationHandler = info.preferredApplicationHandler(); + nsCOMPtr<nsIHandlerApp> preferredApp( + new RemoteHandlerApp(preferredApplicationHandler)); + aHandlerInfo->SetPreferredApplicationHandler(preferredApp); + nsCOMPtr<nsIMutableArray> possibleHandlers; + aHandlerInfo->GetPossibleApplicationHandlers( + getter_AddRefs(possibleHandlers)); + possibleHandlers->AppendElement(preferredApp); + + if (info.isMIMEInfo()) { + nsCOMPtr<nsIMIMEInfo> mimeInfo(do_QueryInterface(aHandlerInfo)); + MOZ_ASSERT(mimeInfo, + "parent and child don't agree on whether this is a MIME info"); + mimeInfo->SetFileExtensions(StringJoin(","_ns, info.extensions())); + } +} + +ContentHandlerService::~ContentHandlerService() {} + +NS_IMETHODIMP ContentHandlerService::AsyncInit() { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ContentHandlerService::Enumerate(nsISimpleEnumerator** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ContentHandlerService::FillHandlerInfo( + nsIHandlerInfo* aHandlerInfo, const nsACString& aOverrideType) { + HandlerInfo info, returnedInfo; + nsIHandlerInfoToHandlerInfo(aHandlerInfo, &info); + mHandlerServiceChild->SendFillHandlerInfo(info, nsCString(aOverrideType), + &returnedInfo); + CopyHanderInfoTonsIHandlerInfo(returnedInfo, aHandlerInfo); + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::GetMIMEInfoFromOS( + nsIHandlerInfo* aHandlerInfo, const nsACString& aMIMEType, + const nsACString& aExtension, bool* aFound) { + nsresult rv = NS_ERROR_FAILURE; + HandlerInfo returnedInfo; + if (!mHandlerServiceChild->SendGetMIMEInfoFromOS(nsCString(aMIMEType), + nsCString(aExtension), &rv, + &returnedInfo, aFound)) { + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + CopyHanderInfoTonsIHandlerInfo(returnedInfo, aHandlerInfo); + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::Store(nsIHandlerInfo* aHandlerInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ContentHandlerService::Exists(nsIHandlerInfo* aHandlerInfo, + bool* _retval) { + HandlerInfo info; + nsIHandlerInfoToHandlerInfo(aHandlerInfo, &info); + mHandlerServiceChild->SendExists(info, _retval); + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::Remove(nsIHandlerInfo* aHandlerInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ContentHandlerService::ExistsForProtocolOS(const nsACString& aProtocolScheme, + bool* aRetval) { + if (!mHandlerServiceChild->SendExistsForProtocolOS(nsCString(aProtocolScheme), + aRetval)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +ContentHandlerService::ExistsForProtocol(const nsACString& aProtocolScheme, + bool* aRetval) { + if (!mHandlerServiceChild->SendExistsForProtocol(nsCString(aProtocolScheme), + aRetval)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::GetTypeFromExtension( + const nsACString& aFileExtension, nsACString& _retval) { + nsCString* cachedType = nullptr; + if (!!mExtToTypeMap.Get(aFileExtension, &cachedType) && !!cachedType) { + _retval.Assign(*cachedType); + return NS_OK; + } + nsCString type; + mHandlerServiceChild->SendGetTypeFromExtension(nsCString(aFileExtension), + &type); + _retval.Assign(type); + mExtToTypeMap.Put(nsCString(aFileExtension), new nsCString(type)); + + return NS_OK; +} + +NS_IMETHODIMP ContentHandlerService::GetApplicationDescription( + const nsACString& aProtocolScheme, nsAString& aRetVal) { + nsresult rv = NS_ERROR_FAILURE; + nsAutoCString scheme(aProtocolScheme); + nsAutoString desc; + mHandlerServiceChild->SendGetApplicationDescription(scheme, &rv, &desc); + aRetVal.Assign(desc); + return rv; +} + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/ContentHandlerService.h b/uriloader/exthandler/ContentHandlerService.h new file mode 100644 index 0000000000..81d0c44148 --- /dev/null +++ b/uriloader/exthandler/ContentHandlerService.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef ContentHandlerService_h +#define ContentHandlerService_h + +#include "mozilla/dom/PHandlerService.h" +#include "nsIHandlerService.h" +#include "nsClassHashtable.h" +#include "nsIMIMEInfo.h" + +namespace mozilla { + +class HandlerServiceChild; + +namespace dom { + +class PHandlerServiceChild; + +class ContentHandlerService : public nsIHandlerService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERSERVICE + + ContentHandlerService(); + [[nodiscard]] nsresult Init(); + static void nsIHandlerInfoToHandlerInfo(nsIHandlerInfo* aInfo, + HandlerInfo* aHandlerInfo); + + private: + virtual ~ContentHandlerService(); + RefPtr<HandlerServiceChild> mHandlerServiceChild; + nsClassHashtable<nsCStringHashKey, nsCString> mExtToTypeMap; +}; + +class RemoteHandlerApp : public nsIHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + + explicit RemoteHandlerApp(HandlerApp aAppChild) : mAppChild(aAppChild) {} + + private: + virtual ~RemoteHandlerApp() {} + HandlerApp mAppChild; +}; + +} // namespace dom +} // namespace mozilla +#endif diff --git a/uriloader/exthandler/DBusHelpers.h b/uriloader/exthandler/DBusHelpers.h new file mode 100644 index 0000000000..4f4f64309d --- /dev/null +++ b/uriloader/exthandler/DBusHelpers.h @@ -0,0 +1,84 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_DBusHelpers_h +#define mozilla_DBusHelpers_h + +#include <dbus/dbus.h> +#include "mozilla/UniquePtr.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { + +template <> +struct RefPtrTraits<DBusMessage> { + static void AddRef(DBusMessage* aMessage) { + MOZ_ASSERT(aMessage); + dbus_message_ref(aMessage); + } + static void Release(DBusMessage* aMessage) { + MOZ_ASSERT(aMessage); + dbus_message_unref(aMessage); + } +}; + +template <> +struct RefPtrTraits<DBusPendingCall> { + static void AddRef(DBusPendingCall* aPendingCall) { + MOZ_ASSERT(aPendingCall); + dbus_pending_call_ref(aPendingCall); + } + static void Release(DBusPendingCall* aPendingCall) { + MOZ_ASSERT(aPendingCall); + dbus_pending_call_unref(aPendingCall); + } +}; + +/* + * |RefPtrTraits<DBusConnection>| specializes |RefPtrTraits<>| + * for managing |DBusConnection| with |RefPtr|. + * + * |RefPtrTraits<DBusConnection>| will _not_ close the DBus + * connection upon the final unref. The caller is responsible + * for closing the connection. + */ +template <> +struct RefPtrTraits<DBusConnection> { + static void AddRef(DBusConnection* aConnection) { + MOZ_ASSERT(aConnection); + dbus_connection_ref(aConnection); + } + static void Release(DBusConnection* aConnection) { + MOZ_ASSERT(aConnection); + dbus_connection_unref(aConnection); + } +}; + +/* + * |DBusConnectionDelete| is a deleter for managing instances + * of |DBusConnection| in |UniquePtr|. Upon destruction, it + * will close an open connection before unref'ing the data + * structure. + * + * Do not use |UniquePtr| with shared DBus connections. For + * shared connections, use |RefPtr|. + */ +class DBusConnectionDelete { + public: + constexpr DBusConnectionDelete() {} + + void operator()(DBusConnection* aConnection) const { + MOZ_ASSERT(aConnection); + if (dbus_connection_get_is_connected(aConnection)) { + dbus_connection_close(aConnection); + } + dbus_connection_unref(aConnection); + } +}; + +} // namespace mozilla + +#endif // mozilla_DBusHelpers_h diff --git a/uriloader/exthandler/ExternalHelperAppChild.cpp b/uriloader/exthandler/ExternalHelperAppChild.cpp new file mode 100644 index 0000000000..569d42e7a4 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppChild.cpp @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ExternalHelperAppChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "nsIInputStream.h" +#include "nsIRequest.h" +#include "nsIResumableChannel.h" +#include "nsIPropertyBag2.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS(ExternalHelperAppChild, nsIStreamListener, nsIRequestObserver) + +ExternalHelperAppChild::ExternalHelperAppChild() : mStatus(NS_OK) {} + +ExternalHelperAppChild::~ExternalHelperAppChild() {} + +//----------------------------------------------------------------------------- +// nsIStreamListener +//----------------------------------------------------------------------------- +NS_IMETHODIMP +ExternalHelperAppChild::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, uint64_t offset, + uint32_t count) { + if (NS_FAILED(mStatus)) return mStatus; + + static uint32_t const kCopyChunkSize = 128 * 1024; + uint32_t toRead = std::min<uint32_t>(count, kCopyChunkSize); + + nsCString data; + + while (count) { + nsresult rv = NS_ReadInputStreamToString(input, data, toRead); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!SendOnDataAvailable(data, offset, toRead))) { + return NS_ERROR_UNEXPECTED; + } + + count -= toRead; + offset += toRead; + toRead = std::min<uint32_t>(count, kCopyChunkSize); + } + + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////// +// nsIRequestObserver +////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +ExternalHelperAppChild::OnStartRequest(nsIRequest* request) { + nsresult rv = mHandler->OnStartRequest(request); + NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); + + nsCString entityID; + nsCOMPtr<nsIResumableChannel> resumable(do_QueryInterface(request)); + if (resumable) { + resumable->GetEntityID(entityID); + } + SendOnStartRequest(entityID); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppChild::OnStopRequest(nsIRequest* request, nsresult status) { + // mHandler can be null if we diverted the request to the parent + if (mHandler) { + nsresult rv = mHandler->OnStopRequest(request, status); + SendOnStopRequest(status); + NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); + } + + return NS_OK; +} + +mozilla::ipc::IPCResult ExternalHelperAppChild::RecvCancel( + const nsresult& aStatus) { + mStatus = aStatus; + return IPC_OK(); +} + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/ExternalHelperAppChild.h b/uriloader/exthandler/ExternalHelperAppChild.h new file mode 100644 index 0000000000..228d7e45b3 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppChild.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ExternalHelperAppChild_h +#define mozilla_dom_ExternalHelperAppChild_h + +#include "mozilla/dom/PExternalHelperAppChild.h" +#include "nsExternalHelperAppService.h" +#include "nsIStreamListener.h" + +class nsIDivertableChannel; + +namespace mozilla { +namespace dom { + +class BrowserChild; + +class ExternalHelperAppChild : public PExternalHelperAppChild, + public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + ExternalHelperAppChild(); + + // Give the listener a real nsExternalAppHandler to complete processing on + // the child. + void SetHandler(nsExternalAppHandler* handler) { mHandler = handler; } + + mozilla::ipc::IPCResult RecvCancel(const nsresult& aStatus); + + private: + virtual ~ExternalHelperAppChild(); + + RefPtr<nsExternalAppHandler> mHandler; + nsresult mStatus; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ExternalHelperAppChild_h diff --git a/uriloader/exthandler/ExternalHelperAppParent.cpp b/uriloader/exthandler/ExternalHelperAppParent.cpp new file mode 100644 index 0000000000..1a88e54df7 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppParent.cpp @@ -0,0 +1,446 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/DebugOnly.h" + +#include "ExternalHelperAppParent.h" +#include "nsIContent.h" +#include "nsCExternalHandlerService.h" +#include "nsIExternalHelperAppService.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/BrowserParent.h" +#include "nsStringStream.h" +#include "mozilla/ipc/URIUtils.h" +#include "nsNetUtil.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "nsQueryObject.h" + +#include "mozilla/Unused.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace dom { + +NS_IMPL_ISUPPORTS_INHERITED(ExternalHelperAppParent, nsHashPropertyBag, + nsIRequest, nsIChannel, nsIMultiPartChannel, + nsIPrivateBrowsingChannel, nsIResumableChannel, + nsIStreamListener, nsIExternalHelperAppParent) + +ExternalHelperAppParent::ExternalHelperAppParent( + nsIURI* uri, const int64_t& aContentLength, const bool& aWasFileChannel, + const nsCString& aContentDispositionHeader, + const uint32_t& aContentDispositionHint, + const nsString& aContentDispositionFilename) + : mURI(uri), + mPending(false), + mIPCClosed(false), + mLoadFlags(0), + mStatus(NS_OK), + mCanceled(false), + mContentLength(aContentLength), + mWasFileChannel(aWasFileChannel) { + mContentDispositionHeader = aContentDispositionHeader; + if (!mContentDispositionHeader.IsEmpty()) { + NS_GetFilenameFromDisposition(mContentDispositionFilename, + mContentDispositionHeader); + mContentDisposition = + NS_GetContentDispositionFromHeader(mContentDispositionHeader, this); + } else { + mContentDisposition = aContentDispositionHint; + mContentDispositionFilename = aContentDispositionFilename; + } +} + +void ExternalHelperAppParent::Init( + const Maybe<mozilla::net::LoadInfoArgs>& aLoadInfoArgs, + const nsCString& aMimeContentType, const bool& aForceSave, + nsIURI* aReferrer, BrowsingContext* aContext, + const bool& aShouldCloseWindow) { + mozilla::ipc::LoadInfoArgsToLoadInfo(aLoadInfoArgs, + getter_AddRefs(mLoadInfo)); + + nsCOMPtr<nsIExternalHelperAppService> helperAppService = + do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID); + NS_ASSERTION(helperAppService, "No Helper App Service!"); + + if (aReferrer) { + SetPropertyAsInterface(u"docshell.internalReferrer"_ns, aReferrer); + } + + if (aContext) { + WindowGlobalParent* parent = + aContext->Canonical()->GetCurrentWindowGlobal(); + if (parent) { + RefPtr<BrowserParent> browser = parent->GetBrowserParent(); + if (browser) { + bool isPrivate = false; + nsCOMPtr<nsILoadContext> loadContext = browser->GetLoadContext(); + loadContext->GetUsePrivateBrowsing(&isPrivate); + SetPrivate(isPrivate); + } + } + } + + helperAppService->CreateListener(aMimeContentType, this, aContext, aForceSave, + nullptr, getter_AddRefs(mListener)); + if (aShouldCloseWindow) { + RefPtr<nsExternalAppHandler> handler = do_QueryObject(mListener); + if (handler) { + handler->SetShouldCloseWindow(); + } + } +} + +void ExternalHelperAppParent::ActorDestroy(ActorDestroyReason why) { + mIPCClosed = true; +} + +void ExternalHelperAppParent::Delete() { + if (!mIPCClosed) { + Unused << Send__delete__(this); + } +} + +mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnStartRequest( + const nsCString& entityID) { + mEntityID = entityID; + mPending = true; + mStatus = mListener->OnStartRequest(this); + return IPC_OK(); +} + +mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnDataAvailable( + const nsCString& data, const uint64_t& offset, const uint32_t& count) { + if (NS_FAILED(mStatus)) { + return IPC_OK(); + } + + MOZ_ASSERT(mPending, "must be pending!"); + + nsCOMPtr<nsIInputStream> stringStream; + DebugOnly<nsresult> rv = NS_NewByteInputStream( + getter_AddRefs(stringStream), Span(data).To(count), NS_ASSIGNMENT_DEPEND); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create dependent string!"); + mStatus = mListener->OnDataAvailable(this, stringStream, offset, count); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult ExternalHelperAppParent::RecvOnStopRequest( + const nsresult& code) { + mPending = false; + mListener->OnStopRequest( + this, (NS_SUCCEEDED(code) && NS_FAILED(mStatus)) ? mStatus : code); + Delete(); + return IPC_OK(); +} + +// +// nsIStreamListener +// + +NS_IMETHODIMP +ExternalHelperAppParent::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, uint64_t offset, + uint32_t count) { + return mListener->OnDataAvailable(request, input, offset, count); +} + +NS_IMETHODIMP +ExternalHelperAppParent::OnStartRequest(nsIRequest* request) { + return mListener->OnStartRequest(request); +} + +NS_IMETHODIMP +ExternalHelperAppParent::OnStopRequest(nsIRequest* request, nsresult status) { + nsresult rv = mListener->OnStopRequest(request, status); + Delete(); + return rv; +} + +ExternalHelperAppParent::~ExternalHelperAppParent() {} + +// +// nsIRequest implementation... +// + +NS_IMETHODIMP +ExternalHelperAppParent::GetName(nsACString& aResult) { + if (!mURI) { + aResult.Truncate(); + return NS_ERROR_NOT_AVAILABLE; + } + mURI->GetAsciiSpec(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::IsPending(bool* aResult) { + *aResult = mPending; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetStatus(nsresult* aResult) { + *aResult = mStatus; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::Cancel(nsresult aStatus) { + mCanceled = true; + mStatus = aStatus; + Unused << SendCancel(aStatus); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::Suspend() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +ExternalHelperAppParent::Resume() { return NS_ERROR_NOT_IMPLEMENTED; } + +// +// nsIChannel implementation +// + +NS_IMETHODIMP +ExternalHelperAppParent::GetOriginalURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetOriginalURI(nsIURI* aURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::Open(nsIInputStream** aResult) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::AsyncOpen(nsIStreamListener* aListener) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetLoadGroup(nsILoadGroup** aLoadGroup) { + *aLoadGroup = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetOwner(nsISupports** aOwner) { + *aOwner = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetOwner(nsISupports* aOwner) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetLoadInfo(nsILoadInfo* aLoadInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + *aCallbacks = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetSecurityInfo(nsISupports** aSecurityInfo) { + *aSecurityInfo = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentType(nsACString& aContentType) { + aContentType.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentType(const nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentCharset(nsACString& aContentCharset) { + aContentCharset.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentCharset(const nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentDisposition(uint32_t* aContentDisposition) { + // NB: mContentDisposition may or may not be set to a non UINT32_MAX value in + // nsExternalHelperAppService::DoContentContentProcessHelper + if (mContentDispositionHeader.IsEmpty() && mContentDisposition == UINT32_MAX) + return NS_ERROR_NOT_AVAILABLE; + + *aContentDisposition = mContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentDisposition(uint32_t aContentDisposition) { + mContentDisposition = aContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + if (mContentDispositionFilename.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + aContentDispositionFilename = mContentDispositionFilename; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + mContentDispositionFilename = aContentDispositionFilename; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + if (mContentDispositionHeader.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + aContentDispositionHeader = mContentDispositionHeader; + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetContentLength(int64_t* aContentLength) { + if (mContentLength < 0) { + *aContentLength = -1; + } else { + *aContentLength = mContentLength; + } + return NS_OK; +} + +NS_IMETHODIMP +ExternalHelperAppParent::SetContentLength(int64_t aContentLength) { + mContentLength = aContentLength; + return NS_OK; +} + +// +// nsIResumableChannel implementation +// + +NS_IMETHODIMP +ExternalHelperAppParent::ResumeAt(uint64_t startPos, + const nsACString& entityID) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetEntityID(nsACString& aEntityID) { + aEntityID = mEntityID; + return NS_OK; +} + +// +// nsIMultiPartChannel implementation +// + +NS_IMETHODIMP +ExternalHelperAppParent::GetBaseChannel(nsIChannel** aChannel) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetPartID(uint32_t* aPartID) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ExternalHelperAppParent::GetIsLastPart(bool* aIsLastPart) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/ExternalHelperAppParent.h b/uriloader/exthandler/ExternalHelperAppParent.h new file mode 100644 index 0000000000..71ac04cee8 --- /dev/null +++ b/uriloader/exthandler/ExternalHelperAppParent.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/PExternalHelperAppParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "nsIChannel.h" +#include "nsIMultiPartChannel.h" +#include "nsIResumableChannel.h" +#include "nsIStreamListener.h" +#include "nsHashPropertyBag.h" +#include "mozilla/net/PrivateBrowsingChannel.h" + +namespace IPC { +class URI; +} // namespace IPC + +class nsExternalAppHandler; + +namespace mozilla { + +namespace net { +class PChannelDiverterParent; +} // namespace net + +namespace dom { + +#define NS_IEXTERNALHELPERAPPPARENT_IID \ + { \ + 0x127a01bc, 0x2a49, 0x46a8, { \ + 0x8c, 0x63, 0x4b, 0x5d, 0x3c, 0xa4, 0x07, 0x9c \ + } \ + } + +class nsIExternalHelperAppParent : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IEXTERNALHELPERAPPPARENT_IID) + + /** + * Returns true if this fake channel represented a file channel in the child. + */ + virtual bool WasFileChannel() = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIExternalHelperAppParent, + NS_IEXTERNALHELPERAPPPARENT_IID) + +class ContentParent; +class PBrowserParent; + +class ExternalHelperAppParent + : public PExternalHelperAppParent, + public nsHashPropertyBag, + public nsIChannel, + public nsIMultiPartChannel, + public nsIResumableChannel, + public nsIStreamListener, + public net::PrivateBrowsingChannel<ExternalHelperAppParent>, + public nsIExternalHelperAppParent { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIMULTIPARTCHANNEL + NS_DECL_NSIRESUMABLECHANNEL + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + mozilla::ipc::IPCResult RecvOnStartRequest( + const nsCString& entityID) override; + mozilla::ipc::IPCResult RecvOnDataAvailable(const nsCString& data, + const uint64_t& offset, + const uint32_t& count) override; + mozilla::ipc::IPCResult RecvOnStopRequest(const nsresult& code) override; + + bool WasFileChannel() override { return mWasFileChannel; } + + ExternalHelperAppParent(nsIURI* uri, const int64_t& contentLength, + const bool& wasFileChannel, + const nsCString& aContentDispositionHeader, + const uint32_t& aContentDispositionHint, + const nsString& aContentDispositionFilename); + void Init(const Maybe<mozilla::net::LoadInfoArgs>& aLoadInfoArgs, + const nsCString& aMimeContentType, const bool& aForceSave, + nsIURI* aReferrer, BrowsingContext* aContext, + const bool& aShouldCloseWindow); + + protected: + virtual ~ExternalHelperAppParent(); + + virtual void ActorDestroy(ActorDestroyReason why) override; + void Delete(); + + private: + RefPtr<nsIStreamListener> mListener; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsILoadInfo> mLoadInfo; + bool mPending; + bool mIPCClosed; + nsLoadFlags mLoadFlags; + nsresult mStatus; + bool mCanceled; + int64_t mContentLength; + bool mWasFileChannel; + uint32_t mContentDisposition; + nsString mContentDispositionFilename; + nsCString mContentDispositionHeader; + nsCString mEntityID; +}; + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/HandlerService.js b/uriloader/exthandler/HandlerService.js new file mode 100644 index 0000000000..085ddab31c --- /dev/null +++ b/uriloader/exthandler/HandlerService.js @@ -0,0 +1,675 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const TOPIC_PDFJS_HANDLER_CHANGED = "pdfjs:handlerChanged"; + +ChromeUtils.defineModuleGetter( + this, + "FileUtils", + "resource://gre/modules/FileUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "JSONFile", + "resource://gre/modules/JSONFile.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); + +function HandlerService() { + // Observe handlersvc-json-replace so we can switch to the datasource + Services.obs.addObserver(this, "handlersvc-json-replace", true); +} + +HandlerService.prototype = { + classID: Components.ID("{220cc253-b60f-41f6-b9cf-fdcb325f970f}"), + QueryInterface: ChromeUtils.generateQI([ + "nsISupportsWeakReference", + "nsIHandlerService", + "nsIObserver", + ]), + + __store: null, + get _store() { + if (!this.__store) { + this.__store = new JSONFile({ + path: PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "handlers.json" + ), + dataPostProcessor: this._dataPostProcessor.bind(this), + }); + } + + // Always call this even if this.__store was set, since it may have been + // set by asyncInit, which might not have completed yet. + this._ensureStoreInitialized(); + return this.__store; + }, + + __storeInitialized: false, + _ensureStoreInitialized() { + if (!this.__storeInitialized) { + this.__storeInitialized = true; + this.__store.ensureDataReady(); + + this._injectDefaultProtocolHandlersIfNeeded(); + this._migrateProtocolHandlersIfNeeded(); + + Services.obs.notifyObservers(null, "handlersvc-store-initialized"); + } + }, + + _dataPostProcessor(data) { + return data.defaultHandlersVersion + ? data + : { + defaultHandlersVersion: {}, + mimeTypes: {}, + schemes: {}, + }; + }, + + /** + * Injects new default protocol handlers if the version in the preferences is + * newer than the one in the data store. + */ + _injectDefaultProtocolHandlersIfNeeded() { + let prefsDefaultHandlersVersion; + try { + prefsDefaultHandlersVersion = Services.prefs.getComplexValue( + "gecko.handlerService.defaultHandlersVersion", + Ci.nsIPrefLocalizedString + ); + } catch (ex) { + if ( + ex instanceof Components.Exception && + ex.result == Cr.NS_ERROR_UNEXPECTED + ) { + // This platform does not have any default protocol handlers configured. + return; + } + throw ex; + } + + try { + prefsDefaultHandlersVersion = Number(prefsDefaultHandlersVersion.data); + let locale = Services.locale.appLocaleAsBCP47; + + let defaultHandlersVersion = + this._store.data.defaultHandlersVersion[locale] || 0; + if (defaultHandlersVersion < prefsDefaultHandlersVersion) { + this._injectDefaultProtocolHandlers(); + this._store.data.defaultHandlersVersion[ + locale + ] = prefsDefaultHandlersVersion; + // Now save the result: + this._store.saveSoon(); + } + } catch (ex) { + Cu.reportError(ex); + } + }, + + _injectDefaultProtocolHandlers() { + let schemesPrefBranch = Services.prefs.getBranch( + "gecko.handlerService.schemes." + ); + let schemePrefList = schemesPrefBranch.getChildList(""); + + let schemes = {}; + + // read all the scheme prefs into a hash + for (let schemePrefName of schemePrefList) { + let [scheme, handlerNumber, attribute] = schemePrefName.split("."); + + try { + let attrData = schemesPrefBranch.getComplexValue( + schemePrefName, + Ci.nsIPrefLocalizedString + ).data; + if (!(scheme in schemes)) { + schemes[scheme] = {}; + } + + if (!(handlerNumber in schemes[scheme])) { + schemes[scheme][handlerNumber] = {}; + } + + schemes[scheme][handlerNumber][attribute] = attrData; + } catch (ex) {} + } + + // Now drop any entries without a uriTemplate, or with a broken one. + // The Array.from calls ensure we can safely delete things without + // affecting the iterator. + for (let [scheme, handlerObject] of Array.from(Object.entries(schemes))) { + let handlers = Array.from(Object.entries(handlerObject)); + let validHandlers = 0; + for (let [key, obj] of handlers) { + if ( + !obj.uriTemplate || + !obj.uriTemplate.startsWith("https://") || + !obj.uriTemplate.toLowerCase().includes("%s") + ) { + delete handlerObject[key]; + } else { + validHandlers++; + } + } + if (!validHandlers) { + delete schemes[scheme]; + } + } + + // Now, we're going to cheat. Terribly. The idiologically correct way + // of implementing the following bit of code would be to fetch the + // handler info objects from the protocol service, manipulate those, + // and then store each of them. + // However, that's expensive. It causes us to talk to the OS about + // default apps, which causes the OS to go hit the disk. + // All we're trying to do is insert some web apps into the list. We + // don't care what's already in the file, we just want to do the + // equivalent of appending into the database. So let's just go do that: + for (let scheme of Object.keys(schemes)) { + let existingSchemeInfo = this._store.data.schemes[scheme]; + if (!existingSchemeInfo) { + // Haven't seen this scheme before. Default to asking which app the + // user wants to use: + existingSchemeInfo = { + // Signal to future readers that we didn't ask the OS anything. + // When the entry is first used, get the info from the OS. + stubEntry: true, + // The first item in the list is the preferred handler, and + // there isn't one, so we fill in null: + handlers: [null], + }; + this._store.data.schemes[scheme] = existingSchemeInfo; + } + let { handlers } = existingSchemeInfo; + for (let handlerNumber of Object.keys(schemes[scheme])) { + let newHandler = schemes[scheme][handlerNumber]; + // If there is already a handler registered with the same template + // URL, ignore the new one: + let matchingTemplate = handler => + handler && handler.uriTemplate == newHandler.uriTemplate; + if (!handlers.some(matchingTemplate)) { + handlers.push(newHandler); + } + } + } + }, + + /** + * Execute any migrations. Migrations are defined here for any changes or removals for + * existing handlers. Additions are still handled via the localized prefs infrastructure. + * + * This depends on the browser.handlers.migrations pref being set by migrateUI in + * nsBrowserGlue (for Fx Desktop) or similar mechanisms for other products. + * This is a comma-separated list of identifiers of migrations that need running. + * This avoids both re-running older migrations and keeping an additional + * pref around permanently. + */ + _migrateProtocolHandlersIfNeeded() { + const kMigrations = { + "30boxes": () => { + const k30BoxesRegex = /^https?:\/\/(?:www\.)?30boxes.com\/external\/widget/i; + let webcalHandler = gExternalProtocolService.getProtocolHandlerInfo( + "webcal" + ); + if (this.exists(webcalHandler)) { + this.fillHandlerInfo(webcalHandler, ""); + let shouldStore = false; + // First remove 30boxes from possible handlers. + let handlers = webcalHandler.possibleApplicationHandlers; + for (let i = handlers.length - 1; i >= 0; i--) { + let app = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if ( + app instanceof Ci.nsIWebHandlerApp && + k30BoxesRegex.test(app.uriTemplate) + ) { + shouldStore = true; + handlers.removeElementAt(i); + } + } + // Then remove as a preferred handler. + if (webcalHandler.preferredApplicationHandler) { + let app = webcalHandler.preferredApplicationHandler; + if ( + app instanceof Ci.nsIWebHandlerApp && + k30BoxesRegex.test(app.uriTemplate) + ) { + webcalHandler.preferredApplicationHandler = null; + shouldStore = true; + } + } + // Then store, if we changed anything. + if (shouldStore) { + this.store(webcalHandler); + } + } + }, + }; + let migrationsToRun = Services.prefs.getCharPref( + "browser.handlers.migrations", + "" + ); + migrationsToRun = migrationsToRun ? migrationsToRun.split(",") : []; + for (let migration of migrationsToRun) { + migration.trim(); + try { + kMigrations[migration](); + } catch (ex) { + Cu.reportError(ex); + } + } + + if (migrationsToRun.length) { + Services.prefs.clearUserPref("browser.handlers.migrations"); + } + }, + + _onDBChange() { + return (async () => { + if (this.__store) { + await this.__store.finalize(); + } + this.__store = null; + this.__storeInitialized = false; + })().catch(Cu.reportError); + }, + + // nsIObserver + observe(subject, topic, data) { + if (topic != "handlersvc-json-replace") { + return; + } + let promise = this._onDBChange(); + promise.then(() => { + Services.obs.notifyObservers(null, "handlersvc-json-replace-complete"); + }); + }, + + // nsIHandlerService + asyncInit() { + if (!this.__store) { + this.__store = new JSONFile({ + path: PathUtils.join( + Services.dirsvc.get("ProfD", Ci.nsIFile).path, + "handlers.json" + ), + dataPostProcessor: this._dataPostProcessor.bind(this), + }); + this.__store + .load() + .then(() => { + // __store can be null if we called _onDBChange in the mean time. + if (this.__store) { + this._ensureStoreInitialized(); + } + }) + .catch(Cu.reportError); + } + }, + + // nsIHandlerService + enumerate() { + let handlers = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + for (let type of Object.keys(this._store.data.mimeTypes)) { + let handler = gMIMEService.getFromTypeAndExtension(type, null); + handlers.appendElement(handler); + } + for (let type of Object.keys(this._store.data.schemes)) { + // nsIExternalProtocolService.getProtocolHandlerInfo can be expensive + // on Windows, so we return a proxy to delay retrieving the nsIHandlerInfo + // until one of its properties is accessed. + // + // Note: our caller still needs to yield periodically when iterating + // the enumerator and accessing handler properties to avoid monopolizing + // the main thread. + // + let handler = new Proxy( + { + QueryInterface: ChromeUtils.generateQI(["nsIHandlerInfo"]), + type, + get _handlerInfo() { + delete this._handlerInfo; + return (this._handlerInfo = gExternalProtocolService.getProtocolHandlerInfo( + type + )); + }, + }, + { + get(target, name) { + return target[name] || target._handlerInfo[name]; + }, + set(target, name, value) { + target._handlerInfo[name] = value; + }, + } + ); + handlers.appendElement(handler); + } + return handlers.enumerate(Ci.nsIHandlerInfo); + }, + + // nsIHandlerService + store(handlerInfo) { + let handlerList = this._getHandlerListByHandlerInfoType(handlerInfo); + + // Retrieve an existing entry if present, instead of creating a new one, so + // that we preserve unknown properties for forward compatibility. + let storedHandlerInfo = handlerList[handlerInfo.type]; + if (!storedHandlerInfo) { + storedHandlerInfo = {}; + handlerList[handlerInfo.type] = storedHandlerInfo; + } + + // Only a limited number of preferredAction values is allowed. + if ( + handlerInfo.preferredAction == Ci.nsIHandlerInfo.saveToDisk || + handlerInfo.preferredAction == Ci.nsIHandlerInfo.useSystemDefault || + handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally + ) { + storedHandlerInfo.action = handlerInfo.preferredAction; + } else { + storedHandlerInfo.action = Ci.nsIHandlerInfo.useHelperApp; + } + + if (handlerInfo.alwaysAskBeforeHandling) { + storedHandlerInfo.ask = true; + } else { + delete storedHandlerInfo.ask; + } + + // Build a list of unique nsIHandlerInfo instances to process later. + let handlers = []; + if (handlerInfo.preferredApplicationHandler) { + handlers.push(handlerInfo.preferredApplicationHandler); + } + for (let handler of handlerInfo.possibleApplicationHandlers.enumerate( + Ci.nsIHandlerApp + )) { + // If the caller stored duplicate handlers, we save them only once. + if (!handlers.some(h => h.equals(handler))) { + handlers.push(handler); + } + } + + // If any of the nsIHandlerInfo instances cannot be serialized, it is not + // included in the final list. The first element is always the preferred + // handler, or null if there is none. + let serializableHandlers = handlers + .map(h => this.handlerAppToSerializable(h)) + .filter(h => h); + if (serializableHandlers.length) { + if (!handlerInfo.preferredApplicationHandler) { + serializableHandlers.unshift(null); + } + storedHandlerInfo.handlers = serializableHandlers; + } else { + delete storedHandlerInfo.handlers; + } + + if (this._isMIMEInfo(handlerInfo)) { + let extensions = storedHandlerInfo.extensions || []; + for (let extension of handlerInfo.getFileExtensions()) { + extension = extension.toLowerCase(); + // If the caller stored duplicate extensions, we save them only once. + if (!extensions.includes(extension)) { + extensions.push(extension); + } + } + if (extensions.length) { + storedHandlerInfo.extensions = extensions; + } else { + delete storedHandlerInfo.extensions; + } + } + + // If we're saving *anything*, it stops being a stub: + delete storedHandlerInfo.stubEntry; + + this._store.saveSoon(); + + // Now notify PDF.js. This is hacky, but a lot better than expecting all + // the consumers to do it... + if (handlerInfo.type == "application/pdf") { + Services.obs.notifyObservers(null, TOPIC_PDFJS_HANDLER_CHANGED); + } + }, + + // nsIHandlerService + fillHandlerInfo(handlerInfo, overrideType) { + let type = overrideType || handlerInfo.type; + let storedHandlerInfo = this._getHandlerListByHandlerInfoType(handlerInfo)[ + type + ]; + if (!storedHandlerInfo) { + throw new Components.Exception( + "handlerSvc fillHandlerInfo: don't know this type", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + let isStub = !!storedHandlerInfo.stubEntry; + // In the normal case, this is not a stub, so we can just read stored info + // and write to the handlerInfo object we were passed. + if (!isStub) { + handlerInfo.preferredAction = storedHandlerInfo.action; + handlerInfo.alwaysAskBeforeHandling = !!storedHandlerInfo.ask; + } else { + // If we've got a stub, ensure the defaults are still set: + gExternalProtocolService.setProtocolHandlerDefaults( + handlerInfo, + handlerInfo.hasDefaultHandler + ); + if ( + handlerInfo.preferredAction == Ci.nsIHandlerInfo.alwaysAsk && + handlerInfo.alwaysAskBeforeHandling + ) { + // `store` will default to `useHelperApp` because `alwaysAsk` is + // not one of the 3 recognized options; for compatibility, do + // the same here. + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + } + } + // If it *is* a stub, don't override alwaysAskBeforeHandling or the + // preferred actions. Instead, just append the stored handlers, without + // overriding the preferred app, and then schedule a task to store proper + // info for this handler. + this._appendStoredHandlers(handlerInfo, storedHandlerInfo.handlers, isStub); + + if (this._isMIMEInfo(handlerInfo) && storedHandlerInfo.extensions) { + for (let extension of storedHandlerInfo.extensions) { + handlerInfo.appendExtension(extension); + } + } + }, + + /** + * Private method to inject stored handler information into an nsIHandlerInfo + * instance. + * @param handlerInfo the nsIHandlerInfo instance to write to + * @param storedHandlers the stored handlers + * @param keepPreferredApp whether to keep the handlerInfo's + * preferredApplicationHandler or override it + * (default: false, ie override it) + */ + _appendStoredHandlers(handlerInfo, storedHandlers, keepPreferredApp) { + // If the first item is not null, it is also the preferred handler. Since + // we cannot modify the stored array, use a boolean to keep track of this. + let isFirstItem = true; + for (let handler of storedHandlers || [null]) { + let handlerApp = this.handlerAppFromSerializable(handler || {}); + if (isFirstItem) { + isFirstItem = false; + // Do not overwrite the preferred app unless that's allowed + if (!keepPreferredApp) { + handlerInfo.preferredApplicationHandler = handlerApp; + } + } + if (handlerApp) { + handlerInfo.possibleApplicationHandlers.appendElement(handlerApp); + } + } + }, + + /** + * @param handler + * A nsIHandlerApp handler app + * @returns Serializable representation of a handler app object. + */ + handlerAppToSerializable(handler) { + if (handler instanceof Ci.nsILocalHandlerApp) { + return { + name: handler.name, + path: handler.executable.path, + }; + } else if (handler instanceof Ci.nsIWebHandlerApp) { + return { + name: handler.name, + uriTemplate: handler.uriTemplate, + }; + } else if (handler instanceof Ci.nsIDBusHandlerApp) { + return { + name: handler.name, + service: handler.service, + method: handler.method, + objectPath: handler.objectPath, + dBusInterface: handler.dBusInterface, + }; + } else if (handler instanceof Ci.nsIGIOMimeApp) { + return { + name: handler.name, + command: handler.command, + }; + } + // If the handler is an unknown handler type, return null. + // Android default application handler is the case. + return null; + }, + + /** + * @param handlerObj + * Serializable representation of a handler object. + * @returns {nsIHandlerApp} the handler app, if any; otherwise null + */ + handlerAppFromSerializable(handlerObj) { + let handlerApp; + if ("path" in handlerObj) { + try { + let file = new FileUtils.File(handlerObj.path); + if (!file.exists()) { + return null; + } + handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.executable = file; + } catch (ex) { + return null; + } + } else if ("uriTemplate" in handlerObj) { + handlerApp = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + handlerApp.uriTemplate = handlerObj.uriTemplate; + } else if ("service" in handlerObj) { + handlerApp = Cc[ + "@mozilla.org/uriloader/dbus-handler-app;1" + ].createInstance(Ci.nsIDBusHandlerApp); + handlerApp.service = handlerObj.service; + handlerApp.method = handlerObj.method; + handlerApp.objectPath = handlerObj.objectPath; + handlerApp.dBusInterface = handlerObj.dBusInterface; + } else if ("command" in handlerObj && "@mozilla.org/gio-service;1" in Cc) { + try { + handlerApp = Cc["@mozilla.org/gio-service;1"] + .getService(Ci.nsIGIOService) + .createAppFromCommand(handlerObj.command, handlerObj.name); + } catch (ex) { + return null; + } + } else { + return null; + } + + handlerApp.name = handlerObj.name; + return handlerApp; + }, + + /** + * The function returns a reference to the "mimeTypes" or "schemes" object + * based on which type of handlerInfo is provided. + */ + _getHandlerListByHandlerInfoType(handlerInfo) { + return this._isMIMEInfo(handlerInfo) + ? this._store.data.mimeTypes + : this._store.data.schemes; + }, + + /** + * Determines whether an nsIHandlerInfo instance represents a MIME type. + */ + _isMIMEInfo(handlerInfo) { + // We cannot rely only on the instanceof check because on Android both MIME + // types and protocols are instances of nsIMIMEInfo. We still do the check + // so that properties of nsIMIMEInfo become available to the callers. + return ( + handlerInfo instanceof Ci.nsIMIMEInfo && handlerInfo.type.includes("/") + ); + }, + + // nsIHandlerService + exists(handlerInfo) { + return ( + handlerInfo.type in this._getHandlerListByHandlerInfoType(handlerInfo) + ); + }, + + // nsIHandlerService + remove(handlerInfo) { + delete this._getHandlerListByHandlerInfoType(handlerInfo)[handlerInfo.type]; + this._store.saveSoon(); + }, + + // nsIHandlerService + getTypeFromExtension(fileExtension) { + let extension = fileExtension.toLowerCase(); + let mimeTypes = this._store.data.mimeTypes; + for (let type of Object.keys(mimeTypes)) { + if ( + mimeTypes[type].extensions && + mimeTypes[type].extensions.includes(extension) + ) { + return type; + } + } + return ""; + }, +}; + +this.NSGetFactory = ComponentUtils.generateNSGetFactory([HandlerService]); diff --git a/uriloader/exthandler/HandlerService.manifest b/uriloader/exthandler/HandlerService.manifest new file mode 100644 index 0000000000..854d13adab --- /dev/null +++ b/uriloader/exthandler/HandlerService.manifest @@ -0,0 +1,2 @@ +component {220cc253-b60f-41f6-b9cf-fdcb325f970f} HandlerService.js +contract @mozilla.org/uriloader/handler-service;1 {220cc253-b60f-41f6-b9cf-fdcb325f970f} process=main diff --git a/uriloader/exthandler/HandlerServiceChild.h b/uriloader/exthandler/HandlerServiceChild.h new file mode 100644 index 0000000000..5c58a3c230 --- /dev/null +++ b/uriloader/exthandler/HandlerServiceChild.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef handler_service_child_h +#define handler_service_child_h + +#include "mozilla/dom/PHandlerServiceChild.h" + +namespace mozilla { + +class HandlerServiceChild final : public mozilla::dom::PHandlerServiceChild { + public: + NS_INLINE_DECL_REFCOUNTING(HandlerServiceChild, final) + HandlerServiceChild() {} + + private: + virtual ~HandlerServiceChild() {} +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/exthandler/HandlerServiceParent.cpp b/uriloader/exthandler/HandlerServiceParent.cpp new file mode 100644 index 0000000000..8f73c7cb8c --- /dev/null +++ b/uriloader/exthandler/HandlerServiceParent.cpp @@ -0,0 +1,379 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ipc/ProtocolUtils.h" +#include "mozilla/Logging.h" +#include "HandlerServiceParent.h" +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "ContentHandlerService.h" +#include "nsStringEnumerator.h" +#ifdef MOZ_WIDGET_GTK +# include "unix/nsGNOMERegistry.h" +#endif + +using mozilla::dom::ContentHandlerService; +using mozilla::dom::HandlerApp; +using mozilla::dom::HandlerInfo; +using mozilla::dom::RemoteHandlerApp; + +namespace { + +class ProxyHandlerInfo final : public nsIHandlerInfo { + public: + explicit ProxyHandlerInfo(const HandlerInfo& aHandlerInfo); + NS_DECL_ISUPPORTS; + NS_DECL_NSIHANDLERINFO; + + nsTArray<nsCString>& Extensions() { return mHandlerInfo.extensions(); } + + protected: + ~ProxyHandlerInfo() {} + HandlerInfo mHandlerInfo; + nsHandlerInfoAction mPrefAction; + nsCOMPtr<nsIMutableArray> mPossibleApps; +}; + +NS_IMPL_ISUPPORTS(ProxyHandlerInfo, nsIHandlerInfo) + +ProxyHandlerInfo::ProxyHandlerInfo(const HandlerInfo& aHandlerInfo) + : mHandlerInfo(aHandlerInfo), + mPrefAction(nsIHandlerInfo::alwaysAsk), + mPossibleApps(do_CreateInstance(NS_ARRAY_CONTRACTID)) { + for (auto& happ : aHandlerInfo.possibleApplicationHandlers()) { + mPossibleApps->AppendElement(new RemoteHandlerApp(happ)); + } +} + +/* readonly attribute ACString type; */ +NS_IMETHODIMP ProxyHandlerInfo::GetType(nsACString& aType) { + aType.Assign(mHandlerInfo.type()); + return NS_OK; +} + +/* attribute AString description; */ +NS_IMETHODIMP ProxyHandlerInfo::GetDescription(nsAString& aDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP ProxyHandlerInfo::SetDescription(const nsAString& aDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* attribute nsIHandlerApp preferredApplicationHandler; */ +NS_IMETHODIMP ProxyHandlerInfo::GetPreferredApplicationHandler( + nsIHandlerApp** aPreferredApplicationHandler) { + *aPreferredApplicationHandler = + new RemoteHandlerApp(mHandlerInfo.preferredApplicationHandler()); + NS_IF_ADDREF(*aPreferredApplicationHandler); + return NS_OK; +} + +NS_IMETHODIMP ProxyHandlerInfo::SetPreferredApplicationHandler( + nsIHandlerApp* aApp) { + nsString name; + nsString detailedDescription; + if (aApp) { + aApp->GetName(name); + aApp->GetDetailedDescription(detailedDescription); + } + + mHandlerInfo.preferredApplicationHandler() = + HandlerApp(name, detailedDescription); + return NS_OK; +} + +/* readonly attribute nsIMutableArray possibleApplicationHandlers; */ +NS_IMETHODIMP ProxyHandlerInfo::GetPossibleApplicationHandlers( + nsIMutableArray** aPossibleApplicationHandlers) { + *aPossibleApplicationHandlers = mPossibleApps; + NS_IF_ADDREF(*aPossibleApplicationHandlers); + return NS_OK; +} + +/* readonly attribute boolean hasDefaultHandler; */ +NS_IMETHODIMP ProxyHandlerInfo::GetHasDefaultHandler(bool* aHasDefaultHandler) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* readonly attribute AString defaultDescription; */ +NS_IMETHODIMP ProxyHandlerInfo::GetDefaultDescription( + nsAString& aDefaultDescription) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void launchWithURI (in nsIURI aURI, + [optional] in BrowsingContext aBrowsingContext); */ +NS_IMETHODIMP ProxyHandlerInfo::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* attribute ProxyHandlerInfoAction preferredAction; */ +NS_IMETHODIMP ProxyHandlerInfo::GetPreferredAction( + nsHandlerInfoAction* aPreferredAction) { + *aPreferredAction = mPrefAction; + return NS_OK; +} +NS_IMETHODIMP ProxyHandlerInfo::SetPreferredAction( + nsHandlerInfoAction aPreferredAction) { + mHandlerInfo.preferredAction() = aPreferredAction; + mPrefAction = aPreferredAction; + return NS_OK; +} + +/* attribute boolean alwaysAskBeforeHandling; */ +NS_IMETHODIMP ProxyHandlerInfo::GetAlwaysAskBeforeHandling( + bool* aAlwaysAskBeforeHandling) { + *aAlwaysAskBeforeHandling = mHandlerInfo.alwaysAskBeforeHandling(); + return NS_OK; +} +NS_IMETHODIMP ProxyHandlerInfo::SetAlwaysAskBeforeHandling( + bool aAlwaysAskBeforeHandling) { + mHandlerInfo.alwaysAskBeforeHandling() = aAlwaysAskBeforeHandling; + return NS_OK; +} + +class ProxyMIMEInfo : public nsIMIMEInfo { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMIMEINFO + NS_FORWARD_NSIHANDLERINFO(mProxyHandlerInfo->); + + explicit ProxyMIMEInfo(const HandlerInfo& aHandlerInfo) + : mProxyHandlerInfo(new ProxyHandlerInfo(aHandlerInfo)) {} + + private: + virtual ~ProxyMIMEInfo() {} + RefPtr<ProxyHandlerInfo> mProxyHandlerInfo; + + protected: + /* additional members */ +}; + +NS_IMPL_ISUPPORTS(ProxyMIMEInfo, nsIMIMEInfo, nsIHandlerInfo) + +/* nsIUTF8StringEnumerator getFileExtensions (); */ +NS_IMETHODIMP ProxyMIMEInfo::GetFileExtensions( + nsIUTF8StringEnumerator** _retval) { + return NS_NewUTF8StringEnumerator(_retval, &mProxyHandlerInfo->Extensions(), + this); +} + +/* void setFileExtensions (in AUTF8String aExtensions); */ +NS_IMETHODIMP ProxyMIMEInfo::SetFileExtensions(const nsACString& aExtensions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean extensionExists (in AUTF8String aExtension); */ +NS_IMETHODIMP ProxyMIMEInfo::ExtensionExists(const nsACString& aExtension, + bool* _retval) { + *_retval = mProxyHandlerInfo->Extensions().Contains( + aExtension, nsCaseInsensitiveCStringArrayComparator()); + return NS_OK; +} + +/* void appendExtension (in AUTF8String aExtension); */ +NS_IMETHODIMP ProxyMIMEInfo::AppendExtension(const nsACString& aExtension) { + if (!aExtension.IsEmpty() && + !mProxyHandlerInfo->Extensions().Contains( + aExtension, nsCaseInsensitiveCStringArrayComparator())) { + mProxyHandlerInfo->Extensions().AppendElement(aExtension); + } + return NS_OK; +} + +/* attribute AUTF8String primaryExtension; */ +NS_IMETHODIMP ProxyMIMEInfo::GetPrimaryExtension( + nsACString& aPrimaryExtension) { + const auto& extensions = mProxyHandlerInfo->Extensions(); + if (extensions.IsEmpty()) { + aPrimaryExtension.Truncate(); + return NS_ERROR_FAILURE; + } + aPrimaryExtension = extensions[0]; + return NS_OK; +} + +NS_IMETHODIMP ProxyMIMEInfo::SetPrimaryExtension( + const nsACString& aPrimaryExtension) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* readonly attribute ACString MIMEType; */ +NS_IMETHODIMP ProxyMIMEInfo::GetMIMEType(nsACString& aMIMEType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean equals (in nsIMIMEInfo aMIMEInfo); */ +NS_IMETHODIMP ProxyMIMEInfo::Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* readonly attribute nsIArray possibleLocalHandlers; */ +NS_IMETHODIMP ProxyMIMEInfo::GetPossibleLocalHandlers( + nsIArray** aPossibleLocalHandlers) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void launchWithFile (in nsIFile aFile); */ +NS_IMETHODIMP ProxyMIMEInfo::LaunchWithFile(nsIFile* aFile) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean isCurrentAppOSDefault(); */ +NS_IMETHODIMP ProxyMIMEInfo::IsCurrentAppOSDefault(bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +static already_AddRefed<nsIHandlerInfo> WrapHandlerInfo( + const HandlerInfo& aHandlerInfo) { + nsCOMPtr<nsIHandlerInfo> info; + if (aHandlerInfo.isMIMEInfo()) { + info = new ProxyMIMEInfo(aHandlerInfo); + } else { + info = new ProxyHandlerInfo(aHandlerInfo); + } + return info.forget(); +} + +} // anonymous namespace + +HandlerServiceParent::HandlerServiceParent() {} + +HandlerServiceParent::~HandlerServiceParent() {} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvFillHandlerInfo( + const HandlerInfo& aHandlerInfoData, const nsCString& aOverrideType, + HandlerInfo* handlerInfoData) { + nsCOMPtr<nsIHandlerInfo> info(WrapHandlerInfo(aHandlerInfoData)); + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + handlerSvc->FillHandlerInfo(info, aOverrideType); + ContentHandlerService::nsIHandlerInfoToHandlerInfo(info, handlerInfoData); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvGetMIMEInfoFromOS( + const nsCString& aMIMEType, const nsCString& aExtension, nsresult* aRv, + HandlerInfo* aHandlerInfoData, bool* aFound) { + *aFound = false; + if (aMIMEType.Length() > MAX_MIMETYPE_LENGTH || + aExtension.Length() > MAX_EXT_LENGTH) { + *aRv = NS_OK; + return IPC_OK(); + } + + nsCOMPtr<nsIMIMEService> mimeService = + do_GetService(NS_MIMESERVICE_CONTRACTID, aRv); + if (NS_WARN_IF(NS_FAILED(*aRv))) { + return IPC_OK(); + } + + nsCOMPtr<nsIMIMEInfo> mimeInfo; + *aRv = mimeService->GetMIMEInfoFromOS(aMIMEType, aExtension, aFound, + getter_AddRefs(mimeInfo)); + if (NS_WARN_IF(NS_FAILED(*aRv))) { + return IPC_OK(); + } + + if (mimeInfo) { + ContentHandlerService::nsIHandlerInfoToHandlerInfo(mimeInfo, + aHandlerInfoData); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvExists( + const HandlerInfo& aHandlerInfo, bool* exists) { + nsCOMPtr<nsIHandlerInfo> info(WrapHandlerInfo(aHandlerInfo)); + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + handlerSvc->Exists(info, exists); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvExistsForProtocolOS( + const nsCString& aProtocolScheme, bool* aHandlerExists) { + if (aProtocolScheme.Length() > MAX_SCHEME_LENGTH) { + *aHandlerExists = false; + return IPC_OK(); + } +#ifdef MOZ_WIDGET_GTK + // Check the GNOME registry for a protocol handler + *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme.get()); +#else + *aHandlerExists = false; +#endif + return IPC_OK(); +} + +/* + * Check if a handler exists for the provided protocol. Check the datastore + * first and then fallback to checking the OS for a handler. + */ +mozilla::ipc::IPCResult HandlerServiceParent::RecvExistsForProtocol( + const nsCString& aProtocolScheme, bool* aHandlerExists) { + if (aProtocolScheme.Length() > MAX_SCHEME_LENGTH) { + *aHandlerExists = false; + return IPC_OK(); + } +#if defined(XP_MACOSX) + // Check the datastore and fallback to an OS check. + // ExternalProcotolHandlerExists() does the fallback. + nsresult rv; + nsCOMPtr<nsIExternalProtocolService> protoSvc = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aHandlerExists = false; + return IPC_OK(); + } + rv = protoSvc->ExternalProtocolHandlerExists(aProtocolScheme.get(), + aHandlerExists); + + if (NS_WARN_IF(NS_FAILED(rv))) { + *aHandlerExists = false; + } +#else + MOZ_RELEASE_ASSERT(false, "No implementation on this platform."); + *aHandlerExists = false; +#endif + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvGetTypeFromExtension( + const nsCString& aFileExtension, nsCString* type) { + if (aFileExtension.Length() > MAX_EXT_LENGTH) { + return IPC_OK(); + } + + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return IPC_OK(); + } + + rv = handlerSvc->GetTypeFromExtension(aFileExtension, *type); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult HandlerServiceParent::RecvGetApplicationDescription( + const nsCString& aScheme, nsresult* aRv, nsString* aDescription) { + if (aScheme.Length() > MAX_SCHEME_LENGTH) { + *aRv = NS_ERROR_NOT_AVAILABLE; + return IPC_OK(); + } + nsCOMPtr<nsIExternalProtocolService> protoSvc = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID); + NS_ASSERTION(protoSvc, "No Helper App Service!"); + *aRv = protoSvc->GetApplicationDescription(aScheme, *aDescription); + return IPC_OK(); +} + +void HandlerServiceParent::ActorDestroy(ActorDestroyReason aWhy) {} diff --git a/uriloader/exthandler/HandlerServiceParent.h b/uriloader/exthandler/HandlerServiceParent.h new file mode 100644 index 0000000000..e98e2f589e --- /dev/null +++ b/uriloader/exthandler/HandlerServiceParent.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef handler_service_parent_h +#define handler_service_parent_h + +#include "mozilla/dom/PHandlerServiceParent.h" +#include "nsIMIMEInfo.h" + +class nsIHandlerApp; + +class HandlerServiceParent final : public mozilla::dom::PHandlerServiceParent { + public: + HandlerServiceParent(); + NS_INLINE_DECL_REFCOUNTING(HandlerServiceParent, final) + + private: + virtual ~HandlerServiceParent(); + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvFillHandlerInfo( + const HandlerInfo& aHandlerInfoData, const nsCString& aOverrideType, + HandlerInfo* handlerInfoData) override; + + mozilla::ipc::IPCResult RecvGetMIMEInfoFromOS(const nsCString& aMIMEType, + const nsCString& aExtension, + nsresult* aRv, + HandlerInfo* aHandlerInfoData, + bool* aFound) override; + + mozilla::ipc::IPCResult RecvExists(const HandlerInfo& aHandlerInfo, + bool* exists) override; + + mozilla::ipc::IPCResult RecvGetTypeFromExtension( + const nsCString& aFileExtension, nsCString* type) override; + + mozilla::ipc::IPCResult RecvExistsForProtocolOS( + const nsCString& aProtocolScheme, bool* aHandlerExists) override; + + mozilla::ipc::IPCResult RecvExistsForProtocol( + const nsCString& aProtocolScheme, bool* aHandlerExists) override; + + mozilla::ipc::IPCResult RecvGetApplicationDescription( + const nsCString& aScheme, nsresult* aRv, nsString* aDescription) override; + + /* + * Limit the length of MIME types, filename extensions, and protocol + * schemes we'll consider. + */ + static const size_t MAX_MIMETYPE_LENGTH = 129; /* Per RFC 6838, type and + subtype should be limited + to 64 characters. We add + one more to account for + a '/' separator. */ + static const size_t MAX_EXT_LENGTH = 64; /* Arbitratily chosen to be + longer than any known + extension */ + static const size_t MAX_SCHEME_LENGTH = 1024; /* Arbitratily chosen to be + longer than any known + protocol scheme */ +}; + +#endif diff --git a/uriloader/exthandler/PExternalHelperApp.ipdl b/uriloader/exthandler/PExternalHelperApp.ipdl new file mode 100644 index 0000000000..b3d1e7c736 --- /dev/null +++ b/uriloader/exthandler/PExternalHelperApp.ipdl @@ -0,0 +1,28 @@ +/* vim: set ft=cpp: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBrowser; +include protocol PContent; + +namespace mozilla { +namespace dom { + +refcounted protocol PExternalHelperApp +{ + manager PContent; + +parent: + async OnStartRequest(nsCString entityID); + async OnDataAvailable(nsCString data, uint64_t offset, uint32_t count); + async OnStopRequest(nsresult code); + +child: + async Cancel(nsresult aStatus); + async __delete__(); +}; + + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/PHandlerService.ipdl b/uriloader/exthandler/PHandlerService.ipdl new file mode 100644 index 0000000000..981c1839c4 --- /dev/null +++ b/uriloader/exthandler/PHandlerService.ipdl @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PContent; + +namespace mozilla { +namespace dom { + +struct HandlerApp { + nsString name; + nsString detailedDescription; +}; + +struct HandlerInfo { + nsCString type; + bool isMIMEInfo; + nsString description; + bool alwaysAskBeforeHandling; + nsCString[] extensions; + HandlerApp preferredApplicationHandler; + HandlerApp[] possibleApplicationHandlers; + long preferredAction; +}; + +sync refcounted protocol PHandlerService +{ + manager PContent; + +parent: + sync FillHandlerInfo(HandlerInfo aHandlerInfoData, + nsCString aOverrideType) + returns (HandlerInfo handlerInfoData); + + /* + * Check if an OS handler exists for the given protocol scheme. + */ + sync ExistsForProtocolOS(nsCString aProtocolScheme) + returns (bool exists); + + /* + * Check if a handler exists for the given protocol scheme. Check + * the datastore first and then fallback to an OS handler check. + */ + sync ExistsForProtocol(nsCString aProtocolScheme) + returns (bool exists); + + sync Exists(HandlerInfo aHandlerInfo) + returns (bool exists); + sync GetTypeFromExtension(nsCString aFileExtension) + returns (nsCString type); + sync GetMIMEInfoFromOS(nsCString aMIMEType, nsCString aExtension) + returns (nsresult rv, HandlerInfo handlerInfoData, bool found); + sync GetApplicationDescription(nsCString aScheme) + returns (nsresult rv, nsString description); + async __delete__(); +}; + + +} // namespace dom +} // namespace mozilla diff --git a/uriloader/exthandler/WebHandlerApp.jsm b/uriloader/exthandler/WebHandlerApp.jsm new file mode 100644 index 0000000000..bac75230f3 --- /dev/null +++ b/uriloader/exthandler/WebHandlerApp.jsm @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); + +function nsWebHandlerApp() {} + +nsWebHandlerApp.prototype = { + classDescription: "A web handler for protocols and content", + classID: Components.ID("8b1ae382-51a9-4972-b930-56977a57919d"), + contractID: "@mozilla.org/uriloader/web-handler-app;1", + QueryInterface: ChromeUtils.generateQI(["nsIWebHandlerApp", "nsIHandlerApp"]), + + _name: null, + _detailedDescription: null, + _uriTemplate: null, + + // nsIHandlerApp + + get name() { + return this._name; + }, + + set name(aName) { + this._name = aName; + }, + + get detailedDescription() { + return this._detailedDescription; + }, + + set detailedDescription(aDesc) { + this._detailedDescription = aDesc; + }, + + equals(aHandlerApp) { + if (!aHandlerApp) { + throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER); + } + + if ( + aHandlerApp instanceof Ci.nsIWebHandlerApp && + aHandlerApp.uriTemplate && + this.uriTemplate && + aHandlerApp.uriTemplate == this.uriTemplate + ) { + return true; + } + return false; + }, + + launchWithURI(aURI, aBrowsingContext) { + // XXX need to strip passwd & username from URI to handle, as per the + // WhatWG HTML5 draft. nsSimpleURL, which is what we're going to get, + // can't do this directly. Ideally, we'd fix nsStandardURL to make it + // possible to turn off all of its quirks handling, and use that... + + // encode the URI to be handled + var escapedUriSpecToHandle = encodeURIComponent(aURI.spec); + + // insert the encoded URI and create the object version. + var uriSpecToSend = this.uriTemplate.replace("%s", escapedUriSpecToHandle); + var uriToSend = Services.io.newURI(uriSpecToSend); + + let policy = WebExtensionPolicy.getByURI(uriToSend); + let privateAllowed = !policy || policy.privateBrowsingAllowed; + + // If we're in a frame, check if we're a built-in scheme, in which case, + // override the target browsingcontext. It's not a good idea to try to + // load mail clients or other apps with potential for logged in data into + // iframes, and in any case it's unlikely to work due to framing + // restrictions employed by the target site. + if (aBrowsingContext && aBrowsingContext != aBrowsingContext.top) { + let { scheme } = aURI; + if (!scheme.startsWith("web+") && !scheme.startsWith("ext+")) { + aBrowsingContext = null; + } + } + + // if we have a context, use the URI loader to load there + if (aBrowsingContext) { + if (aBrowsingContext.usePrivateBrowsing && !privateAllowed) { + throw Components.Exception( + "Extension not allowed in private windows.", + Cr.NS_ERROR_FILE_NOT_FOUND + ); + } + + let triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + Services.tm.dispatchToMainThread(() => + aBrowsingContext.loadURI(uriSpecToSend, { triggeringPrincipal }) + ); + return; + } + + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // If this is an extension handler, check private browsing access. + if (!privateAllowed && PrivateBrowsingUtils.isWindowPrivate(win)) { + throw Components.Exception( + "Extension not allowed in private windows.", + Cr.NS_ERROR_FILE_NOT_FOUND + ); + } + + // If we get an exception, there are several possible reasons why: + // a) this gecko embedding doesn't provide an nsIBrowserDOMWindow + // implementation (i.e. doesn't support browser-style functionality), + // so we need to kick the URL out to the OS default browser. This is + // the subject of bug 394479. + // b) this embedding does provide an nsIBrowserDOMWindow impl, but + // there doesn't happen to be a browser window open at the moment; one + // should be opened. It's not clear whether this situation will really + // ever occur in real life. If it does, the only API that I can find + // that seems reasonably likely to work for most embedders is the + // command line handler. + // c) something else went wrong + // + // It's not clear how one would differentiate between the three cases + // above, so for now we don't catch the exception. + + // openURI + win.browserDOMWindow.openURI( + uriToSend, + null, // no window.opener + Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, + Ci.nsIBrowserDOMWindow.OPEN_NEW, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }, + + // nsIWebHandlerApp + + get uriTemplate() { + return this._uriTemplate; + }, + + set uriTemplate(aURITemplate) { + this._uriTemplate = aURITemplate; + }, +}; + +var EXPORTED_SYMBOLS = ["nsWebHandlerApp"]; diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.cpp b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp new file mode 100644 index 0000000000..de9f6be509 --- /dev/null +++ b/uriloader/exthandler/android/nsAndroidHandlerApp.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/java/GeckoAppShellWrappers.h" +#include "nsAndroidHandlerApp.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsAndroidHandlerApp, nsIHandlerApp, nsISharingHandlerApp) + +nsAndroidHandlerApp::nsAndroidHandlerApp(const nsAString& aName, + const nsAString& aDescription, + const nsAString& aPackageName, + const nsAString& aClassName, + const nsACString& aMimeType, + const nsAString& aAction) + : mName(aName), + mDescription(aDescription), + mPackageName(aPackageName), + mClassName(aClassName), + mMimeType(aMimeType), + mAction(aAction) {} + +nsAndroidHandlerApp::~nsAndroidHandlerApp() {} + +NS_IMETHODIMP +nsAndroidHandlerApp::GetName(nsAString& aName) { + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::SetName(const nsAString& aName) { + mName.Assign(aName); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::GetDetailedDescription(nsAString& aDescription) { + aDescription.Assign(mDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::SetDetailedDescription(const nsAString& aDescription) { + mDescription.Assign(aDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* aRetval) { + *aRetval = false; + if (!aHandlerApp) { + return NS_OK; + } + + nsAutoString name; + nsAutoString detailedDescription; + aHandlerApp->GetName(name); + aHandlerApp->GetDetailedDescription(detailedDescription); + + *aRetval = name.Equals(mName) && detailedDescription.Equals(mDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + nsCString uriSpec; + aURI->GetSpec(uriSpec); + return java::GeckoAppShell::OpenUriExternal(uriSpec, mMimeType, mPackageName, + mClassName, mAction, u""_ns) + ? NS_OK + : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsAndroidHandlerApp::Share(const nsAString& data, const nsAString& title) { + return java::GeckoAppShell::OpenUriExternal(data, mMimeType, mPackageName, + mClassName, mAction, u""_ns) + ? NS_OK + : NS_ERROR_FAILURE; +} diff --git a/uriloader/exthandler/android/nsAndroidHandlerApp.h b/uriloader/exthandler/android/nsAndroidHandlerApp.h new file mode 100644 index 0000000000..dab4042fd8 --- /dev/null +++ b/uriloader/exthandler/android/nsAndroidHandlerApp.h @@ -0,0 +1,33 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAndroidHandlerApp_h +#define nsAndroidHandlerApp_h + +#include "nsMIMEInfoImpl.h" +#include "nsISharingHandlerApp.h" + +class nsAndroidHandlerApp : public nsISharingHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + NS_DECL_NSISHARINGHANDLERAPP + + nsAndroidHandlerApp(const nsAString& aName, const nsAString& aDescription, + const nsAString& aPackageName, + const nsAString& aClassName, const nsACString& aMimeType, + const nsAString& aAction); + + private: + virtual ~nsAndroidHandlerApp(); + + nsString mName; + nsString mDescription; + nsString mPackageName; + nsString mClassName; + nsCString mMimeType; + nsString mAction; +}; +#endif diff --git a/uriloader/exthandler/android/nsExternalURLHandlerService.cpp b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp new file mode 100644 index 0000000000..60ccbbdfe9 --- /dev/null +++ b/uriloader/exthandler/android/nsExternalURLHandlerService.cpp @@ -0,0 +1,21 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsExternalURLHandlerService.h" +#include "nsMIMEInfoAndroid.h" + +NS_IMPL_ISUPPORTS(nsExternalURLHandlerService, nsIExternalURLHandlerService) + +nsExternalURLHandlerService::nsExternalURLHandlerService() {} + +nsExternalURLHandlerService::~nsExternalURLHandlerService() {} + +NS_IMETHODIMP +nsExternalURLHandlerService::GetURLHandlerInfoFromOS(nsIURI* aURL, bool* found, + nsIHandlerInfo** info) { + // We don't want to get protocol handlers from the OS in GV; the app + // should take care of that in NavigationDelegate.onLoadRequest(). + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/android/nsExternalURLHandlerService.h b/uriloader/exthandler/android/nsExternalURLHandlerService.h new file mode 100644 index 0000000000..18389c5286 --- /dev/null +++ b/uriloader/exthandler/android/nsExternalURLHandlerService.h @@ -0,0 +1,22 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSEXTERNALURLHANDLERSERVICE_H +#define NSEXTERNALURLHANDLERSERVICE_H + +#include "nsIExternalURLHandlerService.h" + +class nsExternalURLHandlerService final : public nsIExternalURLHandlerService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALURLHANDLERSERVICE + + nsExternalURLHandlerService(); + + private: + ~nsExternalURLHandlerService(); +}; + +#endif // NSEXTERNALURLHANDLERSERVICE_H diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp new file mode 100644 index 0000000000..6d361d32c2 --- /dev/null +++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.cpp @@ -0,0 +1,414 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "nsMIMEInfoAndroid.h" +#include "AndroidBridge.h" +#include "nsAndroidHandlerApp.h" +#include "nsArrayUtils.h" +#include "nsISupportsUtils.h" +#include "nsStringEnumerator.h" +#include "nsNetUtil.h" +#include "mozilla/Utf8.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsMIMEInfoAndroid, nsIMIMEInfo, nsIHandlerInfo) + +NS_IMETHODIMP +nsMIMEInfoAndroid::LaunchDefaultWithFile(nsIFile* aFile) { + return LaunchWithFile(aFile); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::LoadUriInternal(nsIURI* aURI) { + nsCString uriSpec; + aURI->GetSpec(uriSpec); + + nsCString uriScheme; + aURI->GetScheme(uriScheme); + + nsAutoString mimeType; + if (mType.Equals(uriScheme) || mType.Equals(uriSpec)) { + mimeType.Truncate(); + } else { + CopyUTF8toUTF16(mType, mimeType); + } + + if (java::GeckoAppShell::OpenUriExternal(NS_ConvertUTF8toUTF16(uriSpec), + mimeType, u""_ns, u""_ns, u""_ns, + u""_ns)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +bool nsMIMEInfoAndroid::GetMimeInfoForMimeType(const nsACString& aMimeType, + nsMIMEInfoAndroid** aMimeInfo) { + RefPtr<nsMIMEInfoAndroid> info = new nsMIMEInfoAndroid(aMimeType); + mozilla::AndroidBridge* bridge = mozilla::AndroidBridge::Bridge(); + // we don't have access to the bridge, so just assume we can handle + // the mime type for now and let the system deal with it + if (!bridge) { + info.forget(aMimeInfo); + return false; + } + + nsIHandlerApp* systemDefault = nullptr; + + if (!IsUtf8(aMimeType)) return false; + + NS_ConvertUTF8toUTF16 mimeType(aMimeType); + + bridge->GetHandlersForMimeType(mimeType, info->mHandlerApps, &systemDefault); + + if (systemDefault) info->mPrefApp = systemDefault; + + nsAutoCString fileExt; + bridge->GetExtensionFromMimeType(aMimeType, fileExt); + info->SetPrimaryExtension(fileExt); + + uint32_t len; + info->mHandlerApps->GetLength(&len); + if (len == 1) { + info.forget(aMimeInfo); + return false; + } + + info.forget(aMimeInfo); + return true; +} + +bool nsMIMEInfoAndroid::GetMimeInfoForFileExt(const nsACString& aFileExt, + nsMIMEInfoAndroid** aMimeInfo) { + nsCString mimeType; + if (mozilla::AndroidBridge::Bridge()) + mozilla::AndroidBridge::Bridge()->GetMimeTypeFromExtensions(aFileExt, + mimeType); + + // "*/*" means that the bridge didn't know. + if (mimeType.Equals(nsDependentCString("*/*"), + nsCaseInsensitiveCStringComparator)) + return false; + + bool found = GetMimeInfoForMimeType(mimeType, aMimeInfo); + (*aMimeInfo)->SetPrimaryExtension(aFileExt); + return found; +} + +/** + * Returns MIME info for the aURL, which may contain the whole URL or only a + * protocol + */ +nsresult nsMIMEInfoAndroid::GetMimeInfoForURL(const nsACString& aURL, + bool* found, + nsIHandlerInfo** info) { + nsMIMEInfoAndroid* mimeinfo = new nsMIMEInfoAndroid(aURL); + NS_ADDREF(*info = mimeinfo); + *found = true; + + mozilla::AndroidBridge* bridge = mozilla::AndroidBridge::Bridge(); + if (!bridge) { + // we don't have access to the bridge, so just assume we can handle + // the protocol for now and let the system deal with it + return NS_OK; + } + + nsIHandlerApp* systemDefault = nullptr; + bridge->GetHandlersForURL(NS_ConvertUTF8toUTF16(aURL), mimeinfo->mHandlerApps, + &systemDefault); + + if (systemDefault) mimeinfo->mPrefApp = systemDefault; + + nsAutoCString fileExt; + nsAutoCString mimeType; + mimeinfo->GetType(mimeType); + bridge->GetExtensionFromMimeType(mimeType, fileExt); + mimeinfo->SetPrimaryExtension(fileExt); + + uint32_t len; + mimeinfo->mHandlerApps->GetLength(&len); + if (len == 1) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + *found = false; + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetType(nsACString& aType) { + aType.Assign(mType); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetDescription(nsAString& aDesc) { + aDesc.Assign(mDescription); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetDescription(const nsAString& aDesc) { + mDescription.Assign(aDesc); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPreferredApplicationHandler(nsIHandlerApp** aApp) { + *aApp = mPrefApp; + NS_IF_ADDREF(*aApp); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetPreferredApplicationHandler(nsIHandlerApp* aApp) { + mPrefApp = aApp; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPossibleApplicationHandlers( + nsIMutableArray** aHandlerApps) { + if (!mHandlerApps) mHandlerApps = do_CreateInstance(NS_ARRAY_CONTRACTID); + + if (!mHandlerApps) return NS_ERROR_OUT_OF_MEMORY; + + *aHandlerApps = mHandlerApps; + NS_IF_ADDREF(*aHandlerApps); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetHasDefaultHandler(bool* aHasDefault) { + uint32_t len; + *aHasDefault = false; + if (!mHandlerApps) return NS_OK; + + if (NS_FAILED(mHandlerApps->GetLength(&len))) return NS_OK; + + if (len == 0) return NS_OK; + + *aHasDefault = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetDefaultDescription(nsAString& aDesc) { + aDesc.Assign(u""_ns); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + return mPrefApp->LaunchWithURI(aURI, aBrowsingContext); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPreferredAction(nsHandlerInfoAction* aPrefAction) { + *aPrefAction = mPrefAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetPreferredAction(nsHandlerInfoAction aPrefAction) { + mPrefAction = aPrefAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetAlwaysAskBeforeHandling(bool* aAlwaysAsk) { + *aAlwaysAsk = mAlwaysAsk; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetAlwaysAskBeforeHandling(bool aAlwaysAsk) { + mAlwaysAsk = aAlwaysAsk; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetFileExtensions(nsIUTF8StringEnumerator** aResult) { + return NS_NewUTF8StringEnumerator(aResult, &mExtensions, this); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetFileExtensions(const nsACString& aExtensions) { + mExtensions.Clear(); + nsACString::const_iterator start, end; + aExtensions.BeginReading(start); + aExtensions.EndReading(end); + while (start != end) { + nsACString::const_iterator cursor = start; + mozilla::Unused << FindCharInReadable(',', cursor, end); + AddUniqueExtension(Substring(start, cursor)); + // If a comma was found, skip it for the next search. + start = cursor != end ? ++cursor : cursor; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::ExtensionExists(const nsACString& aExtension, + bool* aRetVal) { + NS_ASSERTION(!aExtension.IsEmpty(), "no extension"); + + nsCString mimeType; + if (mozilla::AndroidBridge::Bridge()) { + mozilla::AndroidBridge::Bridge()->GetMimeTypeFromExtensions(aExtension, + mimeType); + } + + // "*/*" means the bridge didn't find anything (i.e., extension doesn't + // exist). + *aRetVal = !mimeType.Equals(nsDependentCString("*/*"), + nsCaseInsensitiveCStringComparator); + return NS_OK; +} + +void nsMIMEInfoAndroid::AddUniqueExtension(const nsACString& aExtension) { + if (!aExtension.IsEmpty() && + !mExtensions.Contains(aExtension, + nsCaseInsensitiveCStringArrayComparator())) { + mExtensions.AppendElement(aExtension); + } +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::AppendExtension(const nsACString& aExtension) { + MOZ_ASSERT(!aExtension.IsEmpty(), "No extension"); + AddUniqueExtension(aExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPrimaryExtension(nsACString& aPrimaryExtension) { + if (!mExtensions.Length()) { + aPrimaryExtension.Truncate(); + return NS_ERROR_NOT_INITIALIZED; + } + aPrimaryExtension = mExtensions[0]; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::SetPrimaryExtension(const nsACString& aExtension) { + if (MOZ_UNLIKELY(aExtension.IsEmpty())) { + // Don't assert since Java may return an empty extension for unknown types. + return NS_ERROR_INVALID_ARG; + } + int32_t i = mExtensions.IndexOf(aExtension, 0, + nsCaseInsensitiveCStringArrayComparator()); + if (i != -1) { + mExtensions.RemoveElementAt(i); + } + mExtensions.InsertElementAt(0, aExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetMIMEType(nsACString& aMIMEType) { + aMIMEType.Assign(mType); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::Equals(nsIMIMEInfo* aMIMEInfo, bool* aRetVal) { + if (!aMIMEInfo) return NS_ERROR_NULL_POINTER; + + nsAutoCString type; + nsresult rv = aMIMEInfo->GetMIMEType(type); + if (NS_FAILED(rv)) return rv; + + *aRetVal = mType.Equals(type); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::GetPossibleLocalHandlers(nsIArray** aPossibleLocalHandlers) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::LaunchWithFile(nsIFile* aFile) { + nsCOMPtr<nsIURI> uri; + NS_NewFileURI(getter_AddRefs(uri), aFile); + return LoadUriInternal(uri); +} + +NS_IMETHODIMP +nsMIMEInfoAndroid::IsCurrentAppOSDefault(bool* aRetVal) { + // FIXME: this should in theory be meaningfully implemented. However, android + // implements its own version of nsIHandlerApp instances which internally + // have package and class names - but do not expose those. So to meaningfully + // compare the handler app would require access to those and knowing what + // our own package/class names are, and it's not clear how to do that. + // It also seems less important to do this right on Android, given that + // Android UI normally limits what apps you can associate with what files, so + // it shouldn't be possible to get into the same kind of loop as on desktop. + *aRetVal = false; + return NS_OK; +} + +nsMIMEInfoAndroid::nsMIMEInfoAndroid(const nsACString& aMIMEType) + : mType(aMIMEType), + mAlwaysAsk(true), + mPrefAction(nsIMIMEInfo::useHelperApp) { + mPrefApp = new nsMIMEInfoAndroid::SystemChooser(this); + nsresult rv; + mHandlerApps = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + mHandlerApps->AppendElement(mPrefApp); +} + +#define SYSTEMCHOOSER_NAME u"Android chooser" +#define SYSTEMCHOOSER_DESCRIPTION \ + u"Android's default handler application chooser" + +NS_IMPL_ISUPPORTS(nsMIMEInfoAndroid::SystemChooser, nsIHandlerApp) + +nsresult nsMIMEInfoAndroid::SystemChooser::GetName(nsAString& aName) { + aName.AssignLiteral(SYSTEMCHOOSER_NAME); + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::SetName(const nsAString&) { + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::GetDetailedDescription( + nsAString& aDesc) { + aDesc.AssignLiteral(SYSTEMCHOOSER_DESCRIPTION); + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::SetDetailedDescription( + const nsAString&) { + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::Equals(nsIHandlerApp* aHandlerApp, + bool* aRetVal) { + *aRetVal = false; + if (!aHandlerApp) { + return NS_OK; + } + + nsAutoString name; + nsAutoString detailedDescription; + aHandlerApp->GetName(name); + aHandlerApp->GetDetailedDescription(detailedDescription); + + *aRetVal = name.Equals(SYSTEMCHOOSER_NAME) && + detailedDescription.Equals(SYSTEMCHOOSER_DESCRIPTION); + return NS_OK; +} + +nsresult nsMIMEInfoAndroid::SystemChooser::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext*) { + return mOuter->LoadUriInternal(aURI); +} diff --git a/uriloader/exthandler/android/nsMIMEInfoAndroid.h b/uriloader/exthandler/android/nsMIMEInfoAndroid.h new file mode 100644 index 0000000000..bdac8f97f9 --- /dev/null +++ b/uriloader/exthandler/android/nsMIMEInfoAndroid.h @@ -0,0 +1,62 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoAndroid_h +#define nsMIMEInfoAndroid_h + +#include "nsMIMEInfoImpl.h" +#include "nsIMutableArray.h" +#include "nsAndroidHandlerApp.h" + +class nsMIMEInfoAndroid final : public nsIMIMEInfo { + public: + [[nodiscard]] static bool GetMimeInfoForMimeType( + const nsACString& aMimeType, nsMIMEInfoAndroid** aMimeInfo); + [[nodiscard]] static bool GetMimeInfoForFileExt( + const nsACString& aFileExt, nsMIMEInfoAndroid** aMimeInfo); + + [[nodiscard]] static nsresult GetMimeInfoForURL(const nsACString& aURL, + bool* found, + nsIHandlerInfo** info); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMIMEINFO + NS_DECL_NSIHANDLERINFO + + explicit nsMIMEInfoAndroid(const nsACString& aMIMEType); + + private: + ~nsMIMEInfoAndroid() {} + + /** + * Internal helper to avoid adding duplicates. + */ + void AddUniqueExtension(const nsACString& aExtension); + + [[nodiscard]] virtual nsresult LaunchDefaultWithFile(nsIFile* aFile); + [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI); + nsCOMPtr<nsIMutableArray> mHandlerApps; + nsCString mType; + nsTArray<nsCString> mExtensions; + bool mAlwaysAsk; + nsHandlerInfoAction mPrefAction; + nsString mDescription; + nsCOMPtr<nsIHandlerApp> mPrefApp; + + public: + class SystemChooser final : public nsIHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + explicit SystemChooser(nsMIMEInfoAndroid* aOuter) : mOuter(aOuter) {} + + private: + ~SystemChooser() {} + + nsMIMEInfoAndroid* mOuter; + }; +}; + +#endif /* nsMIMEInfoAndroid_h */ diff --git a/uriloader/exthandler/android/nsOSHelperAppService.cpp b/uriloader/exthandler/android/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..9849b66075 --- /dev/null +++ b/uriloader/exthandler/android/nsOSHelperAppService.cpp @@ -0,0 +1,70 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" +#include "nsMIMEInfoAndroid.h" +#include "AndroidBridge.h" + +nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() {} + +nsOSHelperAppService::~nsOSHelperAppService() {} + +nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + RefPtr<nsMIMEInfoAndroid> mimeInfo; + *aFound = false; + if (!aMIMEType.IsEmpty()) + *aFound = nsMIMEInfoAndroid::GetMimeInfoForMimeType( + aMIMEType, getter_AddRefs(mimeInfo)); + if (!*aFound) + *aFound = nsMIMEInfoAndroid::GetMimeInfoForFileExt( + aFileExt, getter_AddRefs(mimeInfo)); + + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + if (!*aFound) mimeInfo = new nsMIMEInfoAndroid(aMIMEType); + + mimeInfo.forget(aMIMEInfo); + return NS_OK; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aScheme, + bool* aExists) { + // Support any URI barring a couple schemes we use in testing; let the + // app decide what to do with them. + nsAutoCString scheme(aScheme); + *aExists = + !scheme.Equals("unsupported"_ns) && !scheme.Equals("unknownextproto"_ns); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsOSHelperAppService::GetProtocolHandlerInfoFromOS( + const nsACString& aScheme, bool* found, nsIHandlerInfo** info) { + // We don't want to get protocol handlers from the OS in GV; the app + // should take care of that in NavigationDelegate.onLoadRequest(). + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsIHandlerApp* nsOSHelperAppService::CreateAndroidHandlerApp( + const nsAString& aName, const nsAString& aDescription, + const nsAString& aPackageName, const nsAString& aClassName, + const nsACString& aMimeType, const nsAString& aAction) { + return new nsAndroidHandlerApp(aName, aDescription, aPackageName, aClassName, + aMimeType, aAction); +} diff --git a/uriloader/exthandler/android/nsOSHelperAppService.h b/uriloader/exthandler/android/nsOSHelperAppService.h new file mode 100644 index 0000000000..a333c4bcd3 --- /dev/null +++ b/uriloader/exthandler/android/nsOSHelperAppService.h @@ -0,0 +1,38 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h +#define nsOSHelperAppService_h + +#include "nsCExternalHandlerService.h" +#include "nsExternalHelperAppService.h" + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + nsresult GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + + [[nodiscard]] nsresult OSProtocolHandlerExists(const char* aScheme, + bool* aExists) override; + + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + static nsIHandlerApp* CreateAndroidHandlerApp( + const nsAString& aName, const nsAString& aDescription, + const nsAString& aPackageName, const nsAString& aClassName, + const nsACString& aMimeType, const nsAString& aAction = u""_ns); +}; + +#endif /* nsOSHelperAppService_h */ diff --git a/uriloader/exthandler/components.conf b/uriloader/exthandler/components.conf new file mode 100644 index 0000000000..1379c13ffd --- /dev/null +++ b/uriloader/exthandler/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{8b1ae382-51a9-4972-b930-56977a57919d}', + 'contract_ids': ['@mozilla.org/uriloader/web-handler-app;1'], + 'jsm': 'resource://gre/modules/WebHandlerApp.jsm', + 'constructor': 'nsWebHandlerApp', + }, +] diff --git a/uriloader/exthandler/docs/index.rst b/uriloader/exthandler/docs/index.rst new file mode 100644 index 0000000000..40088ab6d3 --- /dev/null +++ b/uriloader/exthandler/docs/index.rst @@ -0,0 +1,76 @@ +.. _external_helper_app_service: + +External Helper App Service +=========================== + +The external helper app service is responsible for deciding how to handle an +attempt to load come content that cannot be loaded by the browser itself. + +Part of this involves using the Handler Service which manages the users +preferences for what to do by default with different content. + +When a Link is Clicked +---------------------- + +When a link in a page is clicked (or a form submitted) ``nsDocShell`` tests +whether the target protocol can be loaded by the browser itself, this is based +on the preferences under ``network.protocol-handler``. When the browser cannot +load the protocol it calls into ``nsExternalHelperAppService::LoadURI``. + +Some validation checks are performed but ultimateley we look for a registered +protocol handler. First the OS is queried for an app registration for the +protocol and then the handler server is asked to fill in any user settings from +the internal database. If there were no settings from the handler service then +some defaults are applied in ``nsExternalHelperAppService::SetProtocolHandlerDefaults``. + +If there is a default handler app chosen and the settings say to use it without +asking then that happens. If not a dialog s shown asking the user what they +want to do. + +During a Load +------------- + +When content is already being loaded the :ref:`URI Loader Service <uri_loader_service>` +determines whether the browser can handle the content or not. If not it calls +into the external helper app server through ``nsExternalHelperAppService::DoContent``. + +The content type of the loading content is retrieved from the channel. A file +extension is also generated using the Content-Disposition header or if the load +is not a HTTP POST request the file extension is generated from the requested URL. + +We then query the MIME Service for an nsIMIMEInfo to find information about +apps that can handle the content type or file extension based on OS and user +settings, :ref:`see below <mime_service>` for further details. The result is +used to create a ``nsExternalAppHandler`` which is then used as a stream listener +for the content. + +The MIME info object contains settings that control whether to prompt the user +before doing anything and what the default action should be. If we need to ask +the user then a dialog is shown offering to let users cancel the load, save the +content to disk or send it to a registered application handler. + +Assuming the load isn't canceled the content is streamed to disk using a background +file saver with a target ``nsITransfer``. The ``nsITransfer`` is responsible for +showing the download in the UI. + +If the user opted to open the file with an application then once the transfer is +complete then ``nsIMIMEInfo::LaunchWithFile`` is used to +`launch the application <https://searchfox.org/mozilla-central/search?q=nsIMIMEInfo%3A%3ALaunchWithFile&path=>`_. + +.. _mime_service: + +MIME Service +------------ + +The MIME service is responsible for getting an ``nsIMIMEInfo`` object for a +content type or file extension: + +1. Fills out an ``nsIMIMEInfo`` based on OS provided information. This is platform + specific but should try to find the default application registered to handle + the content. +2. Ask the handler service to fill out the ``nsIMIMEInfo`` with information held + in browser settings. This will not overwrite a any application found from + the OS. +3. If one has not been found already then try to find a type description from + a `lookup table <https://searchfox.org/mozilla-central/search?q=extraMimeEntries[]&path=>`_ + or just by appending " File" to the file extension. diff --git a/uriloader/exthandler/mac/nsDecodeAppleFile.cpp b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp new file mode 100644 index 0000000000..29c1adf941 --- /dev/null +++ b/uriloader/exthandler/mac/nsDecodeAppleFile.cpp @@ -0,0 +1,357 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsDecodeAppleFile.h" +#include "nsCRT.h" + +NS_IMPL_ADDREF(nsDecodeAppleFile) +NS_IMPL_RELEASE(nsDecodeAppleFile) + +NS_INTERFACE_MAP_BEGIN(nsDecodeAppleFile) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStream) + NS_INTERFACE_MAP_ENTRY(nsIOutputStream) +NS_INTERFACE_MAP_END + +nsDecodeAppleFile::nsDecodeAppleFile() { + m_state = parseHeaders; + m_dataBufferLength = 0; + m_dataBuffer = (unsigned char*)malloc(MAX_BUFFERSIZE); + m_entries = nullptr; + m_rfRefNum = -1; + m_totalDataForkWritten = 0; + m_totalResourceForkWritten = 0; + m_headerOk = false; + + m_comment[0] = 0; + memset(&m_dates, 0, sizeof(m_dates)); + memset(&m_finderInfo, 0, sizeof(m_dates)); + memset(&m_finderExtraInfo, 0, sizeof(m_dates)); +} + +nsDecodeAppleFile::~nsDecodeAppleFile() { + free(m_dataBuffer); + m_dataBuffer = nullptr; + if (m_entries) delete[] m_entries; +} + +NS_IMETHODIMP nsDecodeAppleFile::Initialize(nsIOutputStream* output, + nsIFile* file) { + m_output = output; + + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(file); + macFile->GetTargetFSSpec(&m_fsFileSpec); + + m_offset = 0; + m_dataForkOffset = 0; + + return NS_OK; +} + +NS_IMETHODIMP nsDecodeAppleFile::Close(void) { + nsresult rv; + rv = m_output->Close(); + + int32_t i; + + if (m_rfRefNum != -1) FSClose(m_rfRefNum); + + /* Check if the file is complete and if it's the case, write file attributes + */ + if (m_headerOk) { + bool dataOk = true; /* It's ok if the file doesn't have a datafork, + therefore set it to true by default. */ + if (m_headers.magic == APPLESINGLE_MAGIC) { + for (i = 0; i < m_headers.entriesCount; i++) + if (ENT_DFORK == m_entries[i].id) { + dataOk = (bool)(m_totalDataForkWritten == m_entries[i].length); + break; + } + } + + bool resourceOk = FALSE; + for (i = 0; i < m_headers.entriesCount; i++) + if (ENT_RFORK == m_entries[i].id) { + resourceOk = (bool)(m_totalResourceForkWritten == m_entries[i].length); + break; + } + + if (dataOk && resourceOk) { + HFileInfo* fpb; + CInfoPBRec cipbr; + + fpb = (HFileInfo*)&cipbr; + fpb->ioVRefNum = m_fsFileSpec.vRefNum; + fpb->ioDirID = m_fsFileSpec.parID; + fpb->ioNamePtr = m_fsFileSpec.name; + fpb->ioFDirIndex = 0; + PBGetCatInfoSync(&cipbr); + + /* set finder info */ + memcpy(&fpb->ioFlFndrInfo, &m_finderInfo, sizeof(FInfo)); + memcpy(&fpb->ioFlXFndrInfo, &m_finderExtraInfo, sizeof(FXInfo)); + fpb->ioFlFndrInfo.fdFlags &= + 0xfc00; /* clear flags maintained by finder */ + + /* set file dates */ + fpb->ioFlCrDat = m_dates.create - CONVERT_TIME; + fpb->ioFlMdDat = m_dates.modify - CONVERT_TIME; + fpb->ioFlBkDat = m_dates.backup - CONVERT_TIME; + + /* update file info */ + fpb->ioDirID = fpb->ioFlParID; + PBSetCatInfoSync(&cipbr); + + /* set comment */ + IOParam vinfo; + GetVolParmsInfoBuffer vp; + DTPBRec dtp; + + memset((void*)&vinfo, 0, sizeof(vinfo)); + vinfo.ioVRefNum = fpb->ioVRefNum; + vinfo.ioBuffer = (Ptr)&vp; + vinfo.ioReqCount = sizeof(vp); + if (PBHGetVolParmsSync((HParmBlkPtr)&vinfo) == noErr && + ((vp.vMAttrib >> bHasDesktopMgr) & 1)) { + memset((void*)&dtp, 0, sizeof(dtp)); + dtp.ioVRefNum = fpb->ioVRefNum; + if (PBDTGetPath(&dtp) == noErr) { + dtp.ioDTBuffer = (Ptr)&m_comment[1]; + dtp.ioNamePtr = fpb->ioNamePtr; + dtp.ioDirID = fpb->ioDirID; + dtp.ioDTReqCount = m_comment[0]; + if (PBDTSetCommentSync(&dtp) == noErr) PBDTFlushSync(&dtp); + } + } + } + } + + return rv; +} + +NS_IMETHODIMP nsDecodeAppleFile::Flush(void) { return m_output->Flush(); } + +NS_IMETHODIMP nsDecodeAppleFile::WriteFrom(nsIInputStream* inStr, + uint32_t count, uint32_t* _retval) { + return m_output->WriteFrom(inStr, count, _retval); +} + +NS_IMETHODIMP nsDecodeAppleFile::WriteSegments(nsReadSegmentFun reader, + void* closure, uint32_t count, + uint32_t* _retval) { + return m_output->WriteSegments(reader, closure, count, _retval); +} + +NS_IMETHODIMP nsDecodeAppleFile::IsNonBlocking(bool* aNonBlocking) { + return m_output->IsNonBlocking(aNonBlocking); +} + +NS_IMETHODIMP nsDecodeAppleFile::Write(const char* buffer, uint32_t bufferSize, + uint32_t* writeCount) { + /* WARNING: to simplify my life, I presume that I should get all appledouble + headers in the first block, else I would have to implement a buffer */ + + const char* buffPtr = buffer; + uint32_t dataCount; + int32_t i; + nsresult rv = NS_OK; + + *writeCount = 0; + + while (bufferSize > 0 && NS_SUCCEEDED(rv)) { + switch (m_state) { + case parseHeaders: + dataCount = sizeof(ap_header) - m_dataBufferLength; + if (dataCount > bufferSize) dataCount = bufferSize; + memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount); + m_dataBufferLength += dataCount; + + if (m_dataBufferLength == sizeof(ap_header)) { + memcpy(&m_headers, m_dataBuffer, sizeof(ap_header)); + + /* Check header to be sure we are dealing with the right kind of data, + * else just write it to the data fork. */ + if ((m_headers.magic == APPLEDOUBLE_MAGIC || + m_headers.magic == APPLESINGLE_MAGIC) && + m_headers.version == VERSION && m_headers.entriesCount) { + /* Just to be sure, the filler must contains only 0 */ + for (i = 0; i < 4 && m_headers.fill[i] == 0L; i++) + ; + if (i == 4) m_state = parseEntries; + } + m_dataBufferLength = 0; + + if (m_state == parseHeaders) { + dataCount = 0; + m_state = parseWriteThrough; + } + } + break; + + case parseEntries: + if (!m_entries) { + m_entries = new ap_entry[m_headers.entriesCount]; + if (!m_entries) return NS_ERROR_OUT_OF_MEMORY; + } + uint32_t entriesSize = sizeof(ap_entry) * m_headers.entriesCount; + dataCount = entriesSize - m_dataBufferLength; + if (dataCount > bufferSize) dataCount = bufferSize; + memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount); + m_dataBufferLength += dataCount; + + if (m_dataBufferLength == entriesSize) { + for (i = 0; i < m_headers.entriesCount; i++) { + memcpy(&m_entries[i], &m_dataBuffer[i * sizeof(ap_entry)], + sizeof(ap_entry)); + if (m_headers.magic == APPLEDOUBLE_MAGIC) { + uint32_t offset = m_entries[i].offset + m_entries[i].length; + if (offset > m_dataForkOffset) m_dataForkOffset = offset; + } + } + m_headerOk = true; + m_state = parseLookupPart; + } + break; + + case parseLookupPart: + /* which part are we parsing? */ + m_currentPartID = -1; + for (i = 0; i < m_headers.entriesCount; i++) + if (m_offset == m_entries[i].offset && m_entries[i].length) { + m_currentPartID = m_entries[i].id; + m_currentPartLength = m_entries[i].length; + m_currentPartCount = 0; + + switch (m_currentPartID) { + case ENT_DFORK: + m_state = parseDataFork; + break; + case ENT_RFORK: + m_state = parseResourceFork; + break; + + case ENT_COMMENT: + case ENT_DATES: + case ENT_FINFO: + m_dataBufferLength = 0; + m_state = parsePart; + break; + + default: + m_state = parseSkipPart; + break; + } + break; + } + + if (m_currentPartID == -1) { + /* maybe is the datafork of an appledouble file? */ + if (m_offset == m_dataForkOffset) { + m_currentPartID = ENT_DFORK; + m_currentPartLength = -1; + m_currentPartCount = 0; + m_state = parseDataFork; + } else + dataCount = 1; + } + break; + + case parsePart: + dataCount = m_currentPartLength - m_dataBufferLength; + if (dataCount > bufferSize) dataCount = bufferSize; + memcpy(&m_dataBuffer[m_dataBufferLength], buffPtr, dataCount); + m_dataBufferLength += dataCount; + + if (m_dataBufferLength == m_currentPartLength) { + switch (m_currentPartID) { + case ENT_COMMENT: + m_comment[0] = + m_currentPartLength > 255 ? 255 : m_currentPartLength; + memcpy(&m_comment[1], buffPtr, m_comment[0]); + break; + case ENT_DATES: + if (m_currentPartLength == sizeof(m_dates)) + memcpy(&m_dates, buffPtr, m_currentPartLength); + break; + case ENT_FINFO: + if (m_currentPartLength == + (sizeof(m_finderInfo) + sizeof(m_finderExtraInfo))) { + memcpy(&m_finderInfo, buffPtr, sizeof(m_finderInfo)); + memcpy(&m_finderExtraInfo, buffPtr + sizeof(m_finderInfo), + sizeof(m_finderExtraInfo)); + } + break; + } + m_state = parseLookupPart; + } + break; + + case parseSkipPart: + dataCount = m_currentPartLength - m_currentPartCount; + if (dataCount > bufferSize) + dataCount = bufferSize; + else + m_state = parseLookupPart; + break; + + case parseDataFork: + if (m_headers.magic == APPLEDOUBLE_MAGIC) + dataCount = bufferSize; + else { + dataCount = m_currentPartLength - m_currentPartCount; + if (dataCount > bufferSize) + dataCount = bufferSize; + else + m_state = parseLookupPart; + } + + if (m_output) { + uint32_t writeCount; + rv = m_output->Write((const char*)buffPtr, dataCount, &writeCount); + if (dataCount != writeCount) rv = NS_ERROR_FAILURE; + m_totalDataForkWritten += dataCount; + } + + break; + + case parseResourceFork: + dataCount = m_currentPartLength - m_currentPartCount; + if (dataCount > bufferSize) + dataCount = bufferSize; + else + m_state = parseLookupPart; + + if (m_rfRefNum == -1) { + if (noErr != FSpOpenRF(&m_fsFileSpec, fsWrPerm, &m_rfRefNum)) + return NS_ERROR_FAILURE; + } + + long count = dataCount; + if (noErr != FSWrite(m_rfRefNum, &count, buffPtr) || count != dataCount) + return NS_ERROR_FAILURE; + m_totalResourceForkWritten += dataCount; + break; + + case parseWriteThrough: + dataCount = bufferSize; + if (m_output) { + uint32_t writeCount; + rv = m_output->Write((const char*)buffPtr, dataCount, &writeCount); + if (dataCount != writeCount) rv = NS_ERROR_FAILURE; + } + break; + } + + if (dataCount) { + *writeCount += dataCount; + bufferSize -= dataCount; + buffPtr += dataCount; + m_currentPartCount += dataCount; + m_offset += dataCount; + dataCount = 0; + } + } + + return rv; +} diff --git a/uriloader/exthandler/mac/nsDecodeAppleFile.h b/uriloader/exthandler/mac/nsDecodeAppleFile.h new file mode 100644 index 0000000000..22279a8a49 --- /dev/null +++ b/uriloader/exthandler/mac/nsDecodeAppleFile.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDecodeAppleFile_h__ +#define nsDecodeAppleFile_h__ + +#include "nscore.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" + +/* +** applefile definitions used +*/ +#if PRAGMA_STRUCT_ALIGN +# pragma options align = mac68k +#endif + +#define APPLESINGLE_MAGIC 0x00051600L +#define APPLEDOUBLE_MAGIC 0x00051607L +#define VERSION 0x00020000 + +#define NUM_ENTRIES 6 + +#define ENT_DFORK 1L +#define ENT_RFORK 2L +#define ENT_NAME 3L +#define ENT_COMMENT 4L +#define ENT_DATES 8L +#define ENT_FINFO 9L + +#define CONVERT_TIME 1265437696L + +/* +** data type used in the header decoder. +*/ +typedef struct ap_header { + int32_t magic; + int32_t version; + int32_t fill[4]; + int16_t entriesCount; + +} ap_header; + +typedef struct ap_entry { + int32_t id; + int32_t offset; + int32_t length; + +} ap_entry; + +typedef struct ap_dates { + int32_t create, modify, backup, access; + +} ap_dates; + +#if PRAGMA_STRUCT_ALIGN +# pragma options align = reset +#endif + +/* +**Error codes +*/ +enum { errADNotEnoughData = -12099, errADNotSupported, errADBadVersion }; + +class nsDecodeAppleFile : public nsIOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + + nsDecodeAppleFile(); + virtual ~nsDecodeAppleFile(); + + [[nodiscard]] nsresult Initialize(nsIOutputStream* output, nsIFile* file); + + private: +#define MAX_BUFFERSIZE 1024 + enum ParserState { + parseHeaders, + parseEntries, + parseLookupPart, + parsePart, + parseSkipPart, + parseDataFork, + parseResourceFork, + parseWriteThrough + }; + + nsCOMPtr<nsIOutputStream> m_output; + FSSpec m_fsFileSpec; + SInt16 m_rfRefNum; + + unsigned char* m_dataBuffer; + int32_t m_dataBufferLength; + ParserState m_state; + ap_header m_headers; + ap_entry* m_entries; + int32_t m_offset; + int32_t m_dataForkOffset; + int32_t m_totalDataForkWritten; + int32_t m_totalResourceForkWritten; + bool m_headerOk; + + int32_t m_currentPartID; + int32_t m_currentPartLength; + int32_t m_currentPartCount; + + Str255 m_comment; + ap_dates m_dates; + FInfo m_finderInfo; + FXInfo m_finderExtraInfo; +}; + +#endif diff --git a/uriloader/exthandler/mac/nsLocalHandlerAppMac.h b/uriloader/exthandler/mac/nsLocalHandlerAppMac.h new file mode 100644 index 0000000000..7c77091b42 --- /dev/null +++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.h @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSLOCALHANDLERAPPMAC_H_ +#define NSLOCALHANDLERAPPMAC_H_ + +#include "nsLocalHandlerApp.h" + +class nsLocalHandlerAppMac : public nsLocalHandlerApp { + public: + nsLocalHandlerAppMac() {} + + nsLocalHandlerAppMac(const char16_t* aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + nsLocalHandlerAppMac(const nsAString& aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + virtual ~nsLocalHandlerAppMac() {} + + NS_IMETHOD LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override; + NS_IMETHOD GetName(nsAString& aName) override; +}; + +#endif /*NSLOCALHANDLERAPPMAC_H_*/ diff --git a/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm new file mode 100644 index 0000000000..7cb09a6622 --- /dev/null +++ b/uriloader/exthandler/mac/nsLocalHandlerAppMac.mm @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <CoreFoundation/CoreFoundation.h> +#import <ApplicationServices/ApplicationServices.h> + +#include "nsObjCExceptions.h" +#include "nsLocalHandlerAppMac.h" +#include "nsILocalFileMac.h" +#include "nsIURI.h" + +// We override this to make sure app bundles display their pretty name (without .app suffix) +NS_IMETHODIMP nsLocalHandlerAppMac::GetName(nsAString& aName) { + if (mExecutable) { + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(mExecutable); + if (macFile) { + bool isPackage; + (void)macFile->IsPackage(&isPackage); + if (isPackage) return macFile->GetBundleDisplayName(aName); + } + } + + return nsLocalHandlerApp::GetName(aName); +} + +/** + * mostly copy/pasted from nsMacShellService.cpp (which is in browser/, + * so we can't depend on it here). This code probably really wants to live + * somewhere more central (see bug 389922). + */ +NS_IMETHODIMP +nsLocalHandlerAppMac::LaunchWithURI(nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv; + nsCOMPtr<nsILocalFileMac> lfm(do_QueryInterface(mExecutable, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + CFURLRef appURL; + rv = lfm->GetCFURL(&appURL); + if (NS_FAILED(rv)) return rv; + + nsAutoCString uriSpec; + aURI->GetAsciiSpec(uriSpec); + + const UInt8* uriString = reinterpret_cast<const UInt8*>(uriSpec.get()); + CFURLRef uri = + ::CFURLCreateWithBytes(NULL, uriString, uriSpec.Length(), kCFStringEncodingUTF8, NULL); + if (!uri) { + ::CFRelease(appURL); + return NS_ERROR_OUT_OF_MEMORY; + } + + CFArrayRef uris = ::CFArrayCreate(NULL, reinterpret_cast<const void**>(&uri), 1, NULL); + if (!uris) { + ::CFRelease(uri); + ::CFRelease(appURL); + return NS_ERROR_OUT_OF_MEMORY; + } + + LSLaunchURLSpec launchSpec; + launchSpec.appURL = appURL; + launchSpec.itemURLs = uris; + launchSpec.passThruParams = NULL; + launchSpec.launchFlags = kLSLaunchDefaults; + launchSpec.asyncRefCon = NULL; + + OSErr err = ::LSOpenFromURLSpec(&launchSpec, NULL); + + ::CFRelease(uris); + ::CFRelease(uri); + ::CFRelease(appURL); + + return err != noErr ? NS_ERROR_FAILURE : NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/uriloader/exthandler/mac/nsMIMEInfoMac.h b/uriloader/exthandler/mac/nsMIMEInfoMac.h new file mode 100644 index 0000000000..2b87601573 --- /dev/null +++ b/uriloader/exthandler/mac/nsMIMEInfoMac.h @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoMac_h_ +#define nsMIMEInfoMac_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoMac : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoMac(const char* aMIMEType = "") + : nsMIMEInfoImpl(aMIMEType) {} + explicit nsMIMEInfoMac(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoMac(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + + protected: + [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI) override; +#ifdef DEBUG + [[nodiscard]] virtual nsresult LaunchDefaultWithFile( + nsIFile* aFile) override { + MOZ_ASSERT_UNREACHABLE("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override; +}; + +#endif diff --git a/uriloader/exthandler/mac/nsMIMEInfoMac.mm b/uriloader/exthandler/mac/nsMIMEInfoMac.mm new file mode 100644 index 0000000000..ab4db322af --- /dev/null +++ b/uriloader/exthandler/mac/nsMIMEInfoMac.mm @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <ApplicationServices/ApplicationServices.h> + +#include "nsComponentManagerUtils.h" +#include "nsObjCExceptions.h" +#include "nsMIMEInfoMac.h" +#include "nsILocalFileMac.h" + +// We override this to make sure app bundles display their pretty name (without .app suffix) +NS_IMETHODIMP nsMIMEInfoMac::GetDefaultDescription(nsAString& aDefaultDescription) { + if (mDefaultApplication) { + nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(mDefaultApplication); + if (macFile) { + bool isPackage; + (void)macFile->IsPackage(&isPackage); + if (isPackage) return macFile->GetBundleDisplayName(aDefaultDescription); + } + } + + return nsMIMEInfoImpl::GetDefaultDescription(aDefaultDescription); +} + +NS_IMETHODIMP +nsMIMEInfoMac::LaunchWithFile(nsIFile* aFile) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsCOMPtr<nsIFile> application; + nsresult rv; + + NS_ASSERTION(mClass == eMIMEInfo, "only MIME infos are currently allowed" + "to pass content by value"); + + if (mPreferredAction == useHelperApp) { + // we don't yet support passing content by value (rather than reference) + // to web apps. at some point, we will probably want to. + nsCOMPtr<nsILocalHandlerApp> localHandlerApp = do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = localHandlerApp->GetExecutable(getter_AddRefs(application)); + NS_ENSURE_SUCCESS(rv, rv); + + } else if (mPreferredAction == useSystemDefault) { + application = mDefaultApplication; + } else + return NS_ERROR_INVALID_ARG; + + // if we've already got an app, just QI so we have the launchWithDoc method + nsCOMPtr<nsILocalFileMac> app; + if (application) { + app = do_QueryInterface(application, &rv); + if (NS_FAILED(rv)) return rv; + } else { + // otherwise ask LaunchServices for an app directly + nsCOMPtr<nsILocalFileMac> tempFile = do_QueryInterface(aFile, &rv); + if (NS_FAILED(rv)) return rv; + + FSRef tempFileRef; + tempFile->GetFSRef(&tempFileRef); + + FSRef appFSRef; + if (::LSGetApplicationForItem(&tempFileRef, kLSRolesAll, &appFSRef, nullptr) == noErr) { + app = (do_CreateInstance("@mozilla.org/file/local;1")); + if (!app) return NS_ERROR_FAILURE; + app->InitWithFSRef(&appFSRef); + } else { + return NS_ERROR_FAILURE; + } + } + return app->LaunchWithDoc(aFile, false); + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsMIMEInfoMac::LoadUriInternal(nsIURI* aURI) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + NS_ENSURE_ARG_POINTER(aURI); + + nsresult rv = NS_ERROR_FAILURE; + + nsAutoCString uri; + aURI->GetSpec(uri); + if (!uri.IsEmpty()) { + CFURLRef myURLRef = ::CFURLCreateWithBytes(kCFAllocatorDefault, (const UInt8*)uri.get(), + strlen(uri.get()), kCFStringEncodingUTF8, NULL); + if (myURLRef) { + OSStatus status = ::LSOpenCFURLRef(myURLRef, NULL); + if (status == noErr) rv = NS_OK; + ::CFRelease(myURLRef); + } + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} diff --git a/uriloader/exthandler/mac/nsOSHelperAppService.h b/uriloader/exthandler/mac/nsOSHelperAppService.h new file mode 100644 index 0000000000..aa97fee5b4 --- /dev/null +++ b/uriloader/exthandler/mac/nsOSHelperAppService.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. This is the Mac version. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsIMimeInfo; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + nsresult GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + [[nodiscard]] nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + [[nodiscard]] nsresult OSProtocolHandlerExists(const char* aScheme, + bool* aHandlerExists) override; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/mac/nsOSHelperAppService.mm b/uriloader/exthandler/mac/nsOSHelperAppService.mm new file mode 100644 index 0000000000..5585fb67d2 --- /dev/null +++ b/uriloader/exthandler/mac/nsOSHelperAppService.mm @@ -0,0 +1,591 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <sys/stat.h> +#include "mozilla/net/NeckoCommon.h" +#include "nsComponentManagerUtils.h" +#include "nsOSHelperAppService.h" +#include "nsObjCExceptions.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIFile.h" +#include "nsILocalFileMac.h" +#include "nsMimeTypes.h" +#include "nsMemory.h" +#include "nsCRT.h" +#include "nsMIMEInfoMac.h" +#include "nsEmbedCID.h" + +#import <CoreFoundation/CoreFoundation.h> +#import <ApplicationServices/ApplicationServices.h> + +// chrome URL's +#define HELPERAPPLAUNCHER_BUNDLE_URL "chrome://global/locale/helperAppLauncher.properties" +#define BRAND_BUNDLE_URL "chrome://branding/locale/brand.properties" + +nsresult GetDefaultBundleURL(const nsACString& aScheme, CFURLRef* aBundleURL) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + CFStringRef schemeCFString = ::CFStringCreateWithBytes( + kCFAllocatorDefault, (const UInt8*)PromiseFlatCString(aScheme).get(), aScheme.Length(), + kCFStringEncodingUTF8, false); + + if (schemeCFString) { + CFStringRef lookupCFString = + ::CFStringCreateWithFormat(NULL, NULL, CFSTR("%@:"), schemeCFString); + + if (lookupCFString) { + CFURLRef lookupCFURL = ::CFURLCreateWithString(NULL, lookupCFString, NULL); + + if (lookupCFURL) { + if (@available(macOS 10.10, *)) { + *aBundleURL = ::LSCopyDefaultApplicationURLForURL(lookupCFURL, kLSRolesAll, NULL); + if (*aBundleURL) { + rv = NS_OK; + } + } else { + OSStatus theErr = ::LSGetApplicationForURL(lookupCFURL, kLSRolesAll, NULL, aBundleURL); + if (theErr == noErr && *aBundleURL) { + rv = NS_OK; + } + } + + ::CFRelease(lookupCFURL); + } + + ::CFRelease(lookupCFString); + } + + ::CFRelease(schemeCFString); + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +using mozilla::LogLevel; + +/* This is an undocumented interface (in the Foundation framework) that has + * been stable since at least 10.2.8 and is still present on SnowLeopard. + * Furthermore WebKit has three public methods (in WebKitSystemInterface.h) + * that are thin wrappers around this interface's last three methods. So + * it's unlikely to change anytime soon. Now that we're no longer using + * Internet Config Services, this is the only way to look up a MIME type + * from an extension, or vice versa. + */ +@class NSURLFileTypeMappingsInternal; + +@interface NSURLFileTypeMappings : NSObject { + NSURLFileTypeMappingsInternal* _internal; +} + ++ (NSURLFileTypeMappings*)sharedMappings; +- (NSString*)MIMETypeForExtension:(NSString*)aString; +- (NSString*)preferredExtensionForMIMEType:(NSString*)aString; +- (NSArray*)extensionsForMIMEType:(NSString*)aString; +@end + +nsOSHelperAppService::~nsOSHelperAppService() {} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) { + // CFStringCreateWithBytes() can fail even if we're not out of memory -- + // for example if the 'bytes' parameter is something very weird (like + // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's + // specified in the 'encoding' parameter. See bug 548719. + CFStringRef schemeString = + ::CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)aProtocolScheme, + strlen(aProtocolScheme), kCFStringEncodingUTF8, false); + if (schemeString) { + // LSCopyDefaultHandlerForURLScheme() can fail to find the default handler + // for aProtocolScheme when it's never been explicitly set (using + // LSSetDefaultHandlerForURLScheme()). For example, Safari is the default + // handler for the "http" scheme on a newly installed copy of OS X. But + // this (presumably) wasn't done using LSSetDefaultHandlerForURLScheme(), + // so LSCopyDefaultHandlerForURLScheme() will fail to find Safari. To get + // around this we use LSCopyAllHandlersForURLScheme() instead -- which seems + // never to fail. + // http://lists.apple.com/archives/Carbon-dev/2007/May/msg00349.html + // http://www.realsoftware.com/listarchives/realbasic-nug/2008-02/msg00119.html + CFArrayRef handlerArray = ::LSCopyAllHandlersForURLScheme(schemeString); + *aHandlerExists = !!handlerArray; + if (handlerArray) ::CFRelease(handlerArray); + ::CFRelease(schemeString); + } else { + *aHandlerExists = false; + } + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + CFURLRef handlerBundleURL; + rv = GetDefaultBundleURL(aScheme, &handlerBundleURL); + + if (NS_SUCCEEDED(rv) && handlerBundleURL) { + CFBundleRef handlerBundle = CFBundleCreate(NULL, handlerBundleURL); + if (!handlerBundle) { + ::CFRelease(handlerBundleURL); + return NS_ERROR_OUT_OF_MEMORY; + } + + // Get the human-readable name of the bundle + CFStringRef bundleName = + (CFStringRef)::CFBundleGetValueForInfoDictionaryKey(handlerBundle, kCFBundleNameKey); + + if (bundleName) { + AutoTArray<UniChar, 255> buffer; + CFIndex bundleNameLength = ::CFStringGetLength(bundleName); + buffer.SetLength(bundleNameLength); + ::CFStringGetCharacters(bundleName, CFRangeMake(0, bundleNameLength), buffer.Elements()); + _retval.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), bundleNameLength); + rv = NS_OK; + } + ::CFRelease(handlerBundle); + ::CFRelease(handlerBundleURL); + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + CFURLRef handlerBundleURL; + rv = GetDefaultBundleURL(aScheme, &handlerBundleURL); + if (NS_SUCCEEDED(rv) && handlerBundleURL) { + // Ensure we don't accidentally return success if we can't get an app bundle. + rv = NS_ERROR_NOT_AVAILABLE; + CFBundleRef appBundle = ::CFBundleGetMainBundle(); + if (appBundle) { + CFURLRef selfURL = ::CFBundleCopyBundleURL(appBundle); + *_retval = ::CFEqual(selfURL, handlerBundleURL); + rv = NS_OK; + ::CFRelease(selfURL); + } + ::CFRelease(handlerBundleURL); + } + + return rv; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath, + nsIFile** aFile) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; + + nsresult rv; + nsCOMPtr<nsILocalFileMac> localFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + CFURLRef pathAsCFURL; + CFStringRef pathAsCFString = ::CFStringCreateWithCharacters( + NULL, reinterpret_cast<const UniChar*>(aPlatformAppPath), NS_strlen(aPlatformAppPath)); + if (!pathAsCFString) return NS_ERROR_OUT_OF_MEMORY; + + if (::CFStringGetCharacterAtIndex(pathAsCFString, 0) == '/') { + // we have a Posix path + pathAsCFURL = + ::CFURLCreateWithFileSystemPath(nullptr, pathAsCFString, kCFURLPOSIXPathStyle, false); + if (!pathAsCFURL) { + ::CFRelease(pathAsCFString); + return NS_ERROR_OUT_OF_MEMORY; + } + } else { + // if it doesn't start with a / it's not an absolute Posix path + // let's check if it's a HFS path left over from old preferences + + // If it starts with a ':' char, it's not an absolute HFS path + // so bail for that, and also if it's empty + if (::CFStringGetLength(pathAsCFString) == 0 || + ::CFStringGetCharacterAtIndex(pathAsCFString, 0) == ':') { + ::CFRelease(pathAsCFString); + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + } + + pathAsCFURL = + ::CFURLCreateWithFileSystemPath(nullptr, pathAsCFString, kCFURLHFSPathStyle, false); + if (!pathAsCFURL) { + ::CFRelease(pathAsCFString); + return NS_ERROR_OUT_OF_MEMORY; + } + } + + rv = localFile->InitWithCFURL(pathAsCFURL); + ::CFRelease(pathAsCFString); + ::CFRelease(pathAsCFURL); + if (NS_FAILED(rv)) return rv; + *aFile = localFile; + NS_IF_ADDREF(*aFile); + + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +// Returns the MIME types an application bundle explicitly claims to handle. +// Returns NULL if aAppRef doesn't explicitly claim to handle any MIME types. +// If the return value is non-NULL, the caller is responsible for freeing it. +// This isn't necessarily the same as the MIME types the application bundle +// is registered to handle in the Launch Services database. (For example +// the Preview application is normally registered to handle the application/pdf +// MIME type, even though it doesn't explicitly claim to handle *any* MIME +// types in its Info.plist. This is probably because Preview does explicitly +// claim to handle the com.adobe.pdf UTI, and Launch Services somehow +// translates this into a claim to support the application/pdf MIME type. +// Launch Services doesn't provide any APIs (documented or undocumented) to +// query which MIME types a given application is registered to handle. So any +// app that wants this information (e.g. the Default Apps pref pane) needs to +// iterate through the entire Launch Services database -- a process which can +// take several seconds.) +static CFArrayRef GetMIMETypesHandledByApp(FSRef* aAppRef) { + CFURLRef appURL = ::CFURLCreateFromFSRef(kCFAllocatorDefault, aAppRef); + if (!appURL) { + return NULL; + } + CFDictionaryRef infoDict = ::CFBundleCopyInfoDictionaryForURL(appURL); + ::CFRelease(appURL); + if (!infoDict) { + return NULL; + } + CFTypeRef cfObject = ::CFDictionaryGetValue(infoDict, CFSTR("CFBundleDocumentTypes")); + if (!cfObject || (::CFGetTypeID(cfObject) != ::CFArrayGetTypeID())) { + ::CFRelease(infoDict); + return NULL; + } + + CFArrayRef docTypes = static_cast<CFArrayRef>(cfObject); + CFIndex docTypesCount = ::CFArrayGetCount(docTypes); + if (docTypesCount == 0) { + ::CFRelease(infoDict); + return NULL; + } + + CFMutableArrayRef mimeTypes = + ::CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + for (CFIndex i = 0; i < docTypesCount; ++i) { + cfObject = ::CFArrayGetValueAtIndex(docTypes, i); + if (!cfObject || (::CFGetTypeID(cfObject) != ::CFDictionaryGetTypeID())) { + continue; + } + CFDictionaryRef typeDict = static_cast<CFDictionaryRef>(cfObject); + + // When this key is present (on OS X 10.5 and later), its contents + // take precedence over CFBundleTypeMIMETypes (and CFBundleTypeExtensions + // and CFBundleTypeOSTypes). + cfObject = ::CFDictionaryGetValue(typeDict, CFSTR("LSItemContentTypes")); + if (cfObject && (::CFGetTypeID(cfObject) == ::CFArrayGetTypeID())) { + continue; + } + + cfObject = ::CFDictionaryGetValue(typeDict, CFSTR("CFBundleTypeMIMETypes")); + if (!cfObject || (::CFGetTypeID(cfObject) != ::CFArrayGetTypeID())) { + continue; + } + CFArrayRef mimeTypeHolder = static_cast<CFArrayRef>(cfObject); + CFArrayAppendArray(mimeTypes, mimeTypeHolder, + ::CFRangeMake(0, ::CFArrayGetCount(mimeTypeHolder))); + } + + ::CFRelease(infoDict); + if (!::CFArrayGetCount(mimeTypes)) { + ::CFRelease(mimeTypes); + mimeTypes = NULL; + } + return mimeTypes; +} + +nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSNULL; + MOZ_ASSERT(XRE_IsParentProcess()); + + *aFound = false; + + const nsCString& flatType = PromiseFlatCString(aMIMEType); + const nsCString& flatExt = PromiseFlatCString(aFileExt); + + MOZ_LOG(mLog, LogLevel::Debug, + ("Mac: HelperAppService lookup for type '%s' ext '%s'\n", flatType.get(), flatExt.get())); + + // Create a Mac-specific MIME info so we can use Mac-specific members. + RefPtr<nsMIMEInfoMac> mimeInfoMac = new nsMIMEInfoMac(aMIMEType); + + NSAutoreleasePool* localPool = [[NSAutoreleasePool alloc] init]; + + OSStatus err; + bool haveAppForType = false; + bool haveAppForExt = false; + bool typeIsOctetStream = false; + bool typeAppIsDefault = false; + bool extAppIsDefault = false; + FSRef typeAppFSRef; + FSRef extAppFSRef; + + CFStringRef cfMIMEType = NULL; + + if (!aMIMEType.IsEmpty()) { + typeIsOctetStream = aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM); + CFURLRef appURL = NULL; + // CFStringCreateWithCString() can fail even if we're not out of memory -- + // for example if the 'cStr' parameter is something very weird (like + // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's + // specified in the 'encoding' parameter. See bug 548719. + cfMIMEType = ::CFStringCreateWithCString(NULL, flatType.get(), kCFStringEncodingUTF8); + if (cfMIMEType) { + err = ::LSCopyApplicationForMIMEType(cfMIMEType, kLSRolesAll, &appURL); + if ((err == noErr) && appURL && ::CFURLGetFSRef(appURL, &typeAppFSRef)) { + haveAppForType = true; + MOZ_LOG(mLog, LogLevel::Debug, + ("LSCopyApplicationForMIMEType found a default application\n")); + } + if (appURL) { + ::CFRelease(appURL); + } + } + } + if (!aFileExt.IsEmpty()) { + // CFStringCreateWithCString() can fail even if we're not out of memory -- + // for example if the 'cStr' parameter is something very weird (like + // "\xFF\xFF~"), or possibly if it can't be interpreted as using what's + // specified in the 'encoding' parameter. See bug 548719. + CFStringRef cfExt = ::CFStringCreateWithCString(NULL, flatExt.get(), kCFStringEncodingUTF8); + if (cfExt) { + err = ::LSGetApplicationForInfo(kLSUnknownType, kLSUnknownCreator, cfExt, kLSRolesAll, + &extAppFSRef, nullptr); + if (err == noErr) { + haveAppForExt = true; + MOZ_LOG(mLog, LogLevel::Debug, ("LSGetApplicationForInfo found a default application\n")); + } + ::CFRelease(cfExt); + } + } + + if (haveAppForType && haveAppForExt) { + // Do aMIMEType and aFileExt match? + if (::FSCompareFSRefs((const FSRef*)&typeAppFSRef, (const FSRef*)&extAppFSRef) == noErr) { + typeAppIsDefault = true; + *aFound = true; + } + } else if (haveAppForType) { + // If aFileExt isn't empty, it doesn't match aMIMEType. + if (aFileExt.IsEmpty()) { + typeAppIsDefault = true; + *aFound = true; + } + } + + // If we have an app for the extension, and either don't have one for the + // type, or the type is application/octet-stream (ie "binary blob"), rely + // on the file extension. + if ((!haveAppForType || (!*aFound && typeIsOctetStream)) && haveAppForExt) { + // If aMIMEType isn't empty, it doesn't match aFileExt, which should mean + // that we haven't found a matching app. But make an exception for an app + // that also explicitly claims to handle aMIMEType, or which doesn't claim + // to handle any MIME types. This helps work around the following Apple + // design flaw: + // + // Launch Services is somewhat unreliable about registering Apple apps to + // handle MIME types. Probably this is because Apple has officially + // deprecated support for MIME types (in favor of UTIs). As a result, + // most of Apple's own apps don't explicitly claim to handle any MIME + // types (instead they claim to handle one or more UTIs). So Launch + // Services must contain logic to translate support for a given UTI into + // support for one or more MIME types, and it doesn't always do this + // correctly. For example DiskImageMounter isn't (by default) registered + // to handle the application/x-apple-diskimage MIME type. See bug 675356. + // + // Apple has also deprecated support for file extensions, and Apple apps + // also don't register to handle them. But for some reason Launch Services + // is (apparently) better about translating support for a given UTI into + // support for one or more file extensions. It's not at all clear why. + if (aMIMEType.IsEmpty() || typeIsOctetStream) { + extAppIsDefault = true; + *aFound = true; + } else { + CFArrayRef extAppMIMETypes = GetMIMETypesHandledByApp(&extAppFSRef); + if (extAppMIMETypes) { + if (cfMIMEType) { + if (::CFArrayContainsValue(extAppMIMETypes, + ::CFRangeMake(0, ::CFArrayGetCount(extAppMIMETypes)), + cfMIMEType)) { + extAppIsDefault = true; + *aFound = true; + } + } + ::CFRelease(extAppMIMETypes); + } else { + extAppIsDefault = true; + *aFound = true; + } + } + } + + if (cfMIMEType) { + ::CFRelease(cfMIMEType); + } + + if (aMIMEType.IsEmpty()) { + if (haveAppForExt) { + // If aMIMEType is empty and we've found a default app for aFileExt, try + // to get the MIME type from aFileExt. (It might also be worth doing + // this when aMIMEType isn't empty but haveAppForType is false -- but + // the doc for this method says that if we have a MIME type (in + // aMIMEType), we need to give it preference.) + NSURLFileTypeMappings* map = [NSURLFileTypeMappings sharedMappings]; + NSString* extStr = [NSString stringWithCString:flatExt.get() encoding:NSASCIIStringEncoding]; + NSString* typeStr = map ? [map MIMETypeForExtension:extStr] : NULL; + if (typeStr) { + nsAutoCString mimeType; + mimeType.Assign((char*)[typeStr cStringUsingEncoding:NSASCIIStringEncoding]); + mimeInfoMac->SetMIMEType(mimeType); + haveAppForType = true; + } else { + // Sometimes the OS won't give us a MIME type for an extension that's + // registered with Launch Services and has a default app: For example + // Real Player registers itself for the "ogg" extension and for the + // audio/x-ogg and application/x-ogg MIME types, but + // MIMETypeForExtension returns nil for the "ogg" extension even on + // systems where Real Player is installed. This is probably an Apple + // bug. But bad things happen if we return an nsIMIMEInfo structure + // with an empty MIME type and set *aFound to true. So in this + // case we need to set it to false here. + haveAppForExt = false; + extAppIsDefault = false; + *aFound = false; + } + } else { + // Otherwise set the MIME type to a reasonable fallback. + mimeInfoMac->SetMIMEType(nsLiteralCString(APPLICATION_OCTET_STREAM)); + } + } + + if (typeAppIsDefault || extAppIsDefault) { + if (haveAppForExt) mimeInfoMac->AppendExtension(aFileExt); + + nsresult rv; + nsCOMPtr<nsILocalFileMac> app(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + [localPool release]; + return rv; + } + + CFStringRef cfAppName = NULL; + if (typeAppIsDefault) { + app->InitWithFSRef(&typeAppFSRef); + ::LSCopyItemAttribute((const FSRef*)&typeAppFSRef, kLSRolesAll, kLSItemDisplayName, + (CFTypeRef*)&cfAppName); + } else { + app->InitWithFSRef(&extAppFSRef); + ::LSCopyItemAttribute((const FSRef*)&extAppFSRef, kLSRolesAll, kLSItemDisplayName, + (CFTypeRef*)&cfAppName); + } + if (cfAppName) { + AutoTArray<UniChar, 255> buffer; + CFIndex appNameLength = ::CFStringGetLength(cfAppName); + buffer.SetLength(appNameLength); + ::CFStringGetCharacters(cfAppName, CFRangeMake(0, appNameLength), buffer.Elements()); + nsAutoString appName; + appName.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), appNameLength); + mimeInfoMac->SetDefaultDescription(appName); + ::CFRelease(cfAppName); + } + + mimeInfoMac->SetDefaultApplication(app); + mimeInfoMac->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + } else { + mimeInfoMac->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + nsAutoCString mimeType; + mimeInfoMac->GetMIMEType(mimeType); + if (*aFound && !mimeType.IsEmpty()) { + // If we have a MIME type, make sure its extension list is included in our + // list. + NSURLFileTypeMappings* map = [NSURLFileTypeMappings sharedMappings]; + NSString* typeStr = [NSString stringWithCString:mimeType.get() encoding:NSASCIIStringEncoding]; + NSArray* extensionsList = map ? [map extensionsForMIMEType:typeStr] : NULL; + if (extensionsList) { + for (NSString* extension in extensionsList) { + nsAutoCString ext; + ext.Assign((char*)[extension cStringUsingEncoding:NSASCIIStringEncoding]); + mimeInfoMac->AppendExtension(ext); + } + } + + CFStringRef cfType = ::CFStringCreateWithCString(NULL, mimeType.get(), kCFStringEncodingUTF8); + if (cfType) { + CFStringRef cfTypeDesc = NULL; + if (::LSCopyKindStringForMIMEType(cfType, &cfTypeDesc) == noErr) { + AutoTArray<UniChar, 255> buffer; + CFIndex typeDescLength = ::CFStringGetLength(cfTypeDesc); + buffer.SetLength(typeDescLength); + ::CFStringGetCharacters(cfTypeDesc, CFRangeMake(0, typeDescLength), buffer.Elements()); + nsAutoString typeDesc; + typeDesc.Assign(reinterpret_cast<char16_t*>(buffer.Elements()), typeDescLength); + mimeInfoMac->SetDescription(typeDesc); + } + if (cfTypeDesc) { + ::CFRelease(cfTypeDesc); + } + ::CFRelease(cfType); + } + } + + MOZ_LOG(mLog, LogLevel::Debug, ("OS gave us: type '%s' found '%i'\n", mimeType.get(), *aFound)); + + [localPool release]; + mimeInfoMac.forget(aMIMEInfo); + return NS_OK; + + NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoMac* handlerInfo = new nsMIMEInfoMac(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + // As a workaround for the OS X problem described in bug 1391186, don't + // attempt to get/set the application description from the child process. + if (!mozilla::net::IsNeckoChild()) { + nsAutoString desc; + rv = GetApplicationDescription(aScheme, desc); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "GetApplicationDescription failed"); + handlerInfo->SetDefaultDescription(desc); + } + + return NS_OK; +} diff --git a/uriloader/exthandler/moz.build b/uriloader/exthandler/moz.build new file mode 100644 index 0000000000..0a668f9922 --- /dev/null +++ b/uriloader/exthandler/moz.build @@ -0,0 +1,152 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SPHINX_TREES["/uriloader/exthandler"] = "docs" + +TEST_DIRS += ["tests"] + +XPIDL_SOURCES += [ + "nsCExternalHandlerService.idl", + "nsIContentDispatchChooser.idl", + "nsIExternalHelperAppService.idl", + "nsIExternalProtocolService.idl", + "nsIExternalURLHandlerService.idl", + "nsIHandlerService.idl", + "nsIHelperAppLauncherDialog.idl", + "nsISharingHandlerApp.idl", +] + +XPIDL_MODULE = "exthandler" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + osdir = "win" + LOCAL_INCLUDES += ["win"] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + osdir = "mac" +elif CONFIG["MOZ_WIDGET_TOOLKIT"] in ("android", "uikit"): + osdir = CONFIG["MOZ_WIDGET_TOOLKIT"] +else: + osdir = "unix" + +EXPORTS += [osdir + "/nsOSHelperAppService.h"] + +EXPORTS += [ + "ContentHandlerService.h", + "nsExternalHelperAppService.h", + "nsMIMEInfoChild.h", + "nsOSHelperAppServiceChild.h", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + EXPORTS += [ + "%s/%s" % (osdir, f) + for f in [ + "nsExternalURLHandlerService.h", + ] + ] + +EXPORTS += [ + "nsLocalHandlerApp.h", +] + +EXPORTS.mozilla.dom += [ + "ExternalHelperAppChild.h", + "ExternalHelperAppParent.h", +] + +UNIFIED_SOURCES += [ + "ContentHandlerService.cpp", + "ExternalHelperAppChild.cpp", + "ExternalHelperAppParent.cpp", + "HandlerServiceParent.cpp", + "nsExternalHelperAppService.cpp", + "nsExternalProtocolHandler.cpp", + "nsLocalHandlerApp.cpp", + "nsMIMEInfoImpl.cpp", + "nsOSHelperAppServiceChild.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + UNIFIED_SOURCES += [ + "mac/nsLocalHandlerAppMac.mm", + "mac/nsMIMEInfoMac.mm", + "mac/nsOSHelperAppService.mm", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "uikit": + UNIFIED_SOURCES += [ + "uikit/nsLocalHandlerAppUIKit.mm", + "uikit/nsMIMEInfoUIKit.mm", + "uikit/nsOSHelperAppService.mm", + ] +else: + # These files can't be built in unified mode because they redefine LOG. + SOURCES += [ + osdir + "/nsOSHelperAppService.cpp", + ] + if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + UNIFIED_SOURCES += [ + "unix/nsGNOMERegistry.cpp", + "unix/nsMIMEInfoUnix.cpp", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "android": + UNIFIED_SOURCES += [ + "android/nsAndroidHandlerApp.cpp", + "android/nsExternalURLHandlerService.cpp", + "android/nsMIMEInfoAndroid.cpp", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + UNIFIED_SOURCES += [ + "win/nsMIMEInfoWin.cpp", + ] + +if CONFIG["MOZ_ENABLE_DBUS"]: + UNIFIED_SOURCES += [ + "nsDBusHandlerApp.cpp", + ] + EXPORTS.mozilla += [ + "DBusHelpers.h", + ] + +EXTRA_COMPONENTS += [ + "HandlerService.js", + "HandlerService.manifest", +] + +EXTRA_JS_MODULES += [ + "WebHandlerApp.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +IPDL_SOURCES += [ + "PExternalHelperApp.ipdl", + "PHandlerService.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", + "/dom/ipc", + "/netwerk/base", + "/netwerk/protocol/http", +] + +if CONFIG["MOZ_ENABLE_DBUS"]: + CXXFLAGS += CONFIG["TK_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_DBUS_CFLAGS"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["TK_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] diff --git a/uriloader/exthandler/nsCExternalHandlerService.idl b/uriloader/exthandler/nsCExternalHandlerService.idl new file mode 100644 index 0000000000..bdf59e9ae6 --- /dev/null +++ b/uriloader/exthandler/nsCExternalHandlerService.idl @@ -0,0 +1,33 @@ +/* -*- Mode: IDL; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIExternalHelperAppService.idl" + +/* +nsCExternalHelperApp implements: +------------------------- +nsIExternalHelperAppService +*/ + +%{ C++ + +#define NS_EXTERNALHELPERAPPSERVICE_CONTRACTID \ +"@mozilla.org/uriloader/external-helper-app-service;1" + +#define NS_HANDLERSERVICE_CONTRACTID \ +"@mozilla.org/uriloader/handler-service;1" + +#define NS_EXTERNALPROTOCOLSERVICE_CONTRACTID \ +"@mozilla.org/uriloader/external-protocol-service;1" + +#define NS_MIMESERVICE_CONTRACTID \ +"@mozilla.org/mime;1" + +#define NS_LOCALHANDLERAPP_CONTRACTID \ +"@mozilla.org/uriloader/local-handler-app;1" + +%} + diff --git a/uriloader/exthandler/nsContentHandlerApp.h b/uriloader/exthandler/nsContentHandlerApp.h new file mode 100644 index 0000000000..f372d8735e --- /dev/null +++ b/uriloader/exthandler/nsContentHandlerApp.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsContentHandlerAppImpl_h__ +#define __nsContentHandlerAppImpl_h__ + +#include <contentaction/contentaction.h> +#include "nsString.h" +#include "nsIMIMEInfo.h" + +class nsContentHandlerApp : public nsIHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + + nsContentHandlerApp(nsString aName, nsCString aType, + ContentAction::Action& aAction); + virtual ~nsContentHandlerApp() {} + + protected: + nsString mName; + nsCString mType; + nsString mDetailedDescription; + + ContentAction::Action mAction; +}; +#endif diff --git a/uriloader/exthandler/nsDBusHandlerApp.cpp b/uriloader/exthandler/nsDBusHandlerApp.cpp new file mode 100644 index 0000000000..6155a1a951 --- /dev/null +++ b/uriloader/exthandler/nsDBusHandlerApp.cpp @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <dbus/dbus.h> +#include "mozilla/Components.h" +#include "mozilla/DBusHelpers.h" +#include "nsDBusHandlerApp.h" +#include "nsIURI.h" +#include "nsIClassInfoImpl.h" +#include "nsCOMPtr.h" +#include "nsCExternalHandlerService.h" + +using namespace mozilla; + +// XXX why does nsMIMEInfoImpl have a threadsafe nsISupports? do we need one +// here too? +NS_IMPL_CLASSINFO(nsDBusHandlerApp, nullptr, 0, + components::DBusHandlerApp::CID()) +NS_IMPL_ISUPPORTS_CI(nsDBusHandlerApp, nsIDBusHandlerApp, nsIHandlerApp) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIHandlerApp + +NS_IMETHODIMP nsDBusHandlerApp::GetName(nsAString& aName) { + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetName(const nsAString& aName) { + mName.Assign(aName); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetDetailedDescription( + const nsAString& aDescription) { + mDetailedDescription.Assign(aDescription); + + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetDetailedDescription( + nsAString& aDescription) { + aDescription.Assign(mDetailedDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsDBusHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* _retval) { + NS_ENSURE_ARG_POINTER(aHandlerApp); + + // If the handler app isn't a dbus handler app, then it's not the same app. + nsCOMPtr<nsIDBusHandlerApp> dbusHandlerApp = do_QueryInterface(aHandlerApp); + if (!dbusHandlerApp) { + *_retval = false; + return NS_OK; + } + nsAutoCString service; + nsAutoCString method; + + nsresult rv = dbusHandlerApp->GetService(service); + if (NS_FAILED(rv)) { + *_retval = false; + return NS_OK; + } + rv = dbusHandlerApp->GetMethod(method); + if (NS_FAILED(rv)) { + *_retval = false; + return NS_OK; + } + + *_retval = service.Equals(mService) && method.Equals(mMethod); + return NS_OK; +} + +NS_IMETHODIMP +nsDBusHandlerApp::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + nsAutoCString spec; + nsresult rv = aURI->GetAsciiSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + const char* uri = spec.get(); + + DBusError err; + dbus_error_init(&err); + + mozilla::UniquePtr<DBusConnection, mozilla::DBusConnectionDelete> connection( + dbus_bus_get_private(DBUS_BUS_SESSION, &err)); + + if (dbus_error_is_set(&err)) { + dbus_error_free(&err); + return NS_ERROR_FAILURE; + } + if (nullptr == connection) { + return NS_ERROR_FAILURE; + } + dbus_connection_set_exit_on_disconnect(connection.get(), false); + + RefPtr<DBusMessage> msg = + already_AddRefed<DBusMessage>(dbus_message_new_method_call( + mService.get(), mObjpath.get(), mInterface.get(), mMethod.get())); + + if (!msg) { + return NS_ERROR_FAILURE; + } + dbus_message_set_no_reply(msg, true); + + DBusMessageIter iter; + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &uri); + + if (dbus_connection_send(connection.get(), msg, nullptr)) { + dbus_connection_flush(connection.get()); + } else { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIDBusHandlerApp + +NS_IMETHODIMP nsDBusHandlerApp::GetService(nsACString& aService) { + aService.Assign(mService); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetService(const nsACString& aService) { + mService.Assign(aService); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetMethod(nsACString& aMethod) { + aMethod.Assign(mMethod); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetMethod(const nsACString& aMethod) { + mMethod.Assign(aMethod); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetDBusInterface(nsACString& aInterface) { + aInterface.Assign(mInterface); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetDBusInterface(const nsACString& aInterface) { + mInterface.Assign(aInterface); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::GetObjectPath(nsACString& aObjpath) { + aObjpath.Assign(mObjpath); + return NS_OK; +} + +NS_IMETHODIMP nsDBusHandlerApp::SetObjectPath(const nsACString& aObjpath) { + mObjpath.Assign(aObjpath); + return NS_OK; +} diff --git a/uriloader/exthandler/nsDBusHandlerApp.h b/uriloader/exthandler/nsDBusHandlerApp.h new file mode 100644 index 0000000000..f45f917446 --- /dev/null +++ b/uriloader/exthandler/nsDBusHandlerApp.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsDBusHandlerAppImpl_h__ +#define __nsDBusHandlerAppImpl_h__ + +#include "nsString.h" +#include "nsIMIMEInfo.h" + +class nsDBusHandlerApp : public nsIDBusHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + NS_DECL_NSIDBUSHANDLERAPP + + nsDBusHandlerApp() {} + + protected: + virtual ~nsDBusHandlerApp() {} + + nsString mName; + nsString mDetailedDescription; + nsCString mService; + nsCString mMethod; + nsCString mInterface; + nsCString mObjpath; +}; +#endif diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp new file mode 100644 index 0000000000..b78eac71d5 --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp @@ -0,0 +1,3096 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "base/basictypes.h" + +/* This must occur *after* base/basictypes.h to avoid typedefs conflicts. */ +#include "mozilla/ArrayUtils.h" +#include "mozilla/Base64.h" +#include "mozilla/ResultExtensions.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/StaticPrefs_security.h" +#include "nsXULAppAPI.h" + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIChannel.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsICategoryManager.h" +#include "nsDependentSubstring.h" +#include "nsString.h" +#include "nsUnicharUtils.h" +#include "nsIStringEnumerator.h" +#include "nsMemory.h" +#include "nsIStreamListener.h" +#include "nsIMIMEService.h" +#include "nsILoadGroup.h" +#include "nsIWebProgressListener.h" +#include "nsITransfer.h" +#include "nsReadableUtils.h" +#include "nsIRequest.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIInterfaceRequestor.h" +#include "nsThreadUtils.h" +#include "nsIMutableArray.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsOSHelperAppService.h" +#include "nsOSHelperAppServiceChild.h" +#include "nsContentSecurityUtils.h" + +// used to access our datastore of user-configured helper applications +#include "nsIHandlerService.h" +#include "nsIMIMEInfo.h" +#include "nsIHelperAppLauncherDialog.h" +#include "nsIContentDispatchChooser.h" +#include "nsNetUtil.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsIIOService.h" +#include "nsNetCID.h" + +#include "nsIApplicationReputation.h" + +#include "nsDSURIContentListener.h" +#include "nsMimeTypes.h" +// used for header disposition information. +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIEncodedChannel.h" +#include "nsIMultiPartChannel.h" +#include "nsIFileChannel.h" +#include "nsIObserverService.h" // so we can be a profile change observer +#include "nsIPropertyBag2.h" // for the 64-bit content length + +#ifdef XP_MACOSX +# include "nsILocalFileMac.h" +#endif + +#include "nsPluginHost.h" +#include "nsEscape.h" + +#include "nsIStringBundle.h" // XXX needed to localize error msgs +#include "nsIPrompt.h" + +#include "nsITextToSubURI.h" // to unescape the filename + +#include "nsDocShellCID.h" + +#include "nsCRT.h" +#include "nsLocalHandlerApp.h" + +#include "nsIRandomGenerator.h" + +#include "ContentChild.h" +#include "nsXULAppAPI.h" +#include "nsPIDOMWindow.h" +#include "ExternalHelperAppChild.h" + +#ifdef XP_WIN +# include "nsWindowsHelpers.h" +#endif + +#include "mozilla/Components.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla; +using namespace mozilla::ipc; +using namespace mozilla::dom; + +// Download Folder location constants +#define NS_PREF_DOWNLOAD_DIR "browser.download.dir" +#define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList" +enum { + NS_FOLDER_VALUE_DESKTOP = 0, + NS_FOLDER_VALUE_DOWNLOADS = 1, + NS_FOLDER_VALUE_CUSTOM = 2 +}; + +LazyLogModule nsExternalHelperAppService::mLog("HelperAppService"); + +// Using level 3 here because the OSHelperAppServices use a log level +// of LogLevel::Debug (4), and we want less detailed output here +// Using 3 instead of LogLevel::Warning because we don't output warnings +#undef LOG +#define LOG(args) \ + MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args) +#define LOG_ENABLED() \ + MOZ_LOG_TEST(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info) + +static const char NEVER_ASK_FOR_SAVE_TO_DISK_PREF[] = + "browser.helperApps.neverAsk.saveToDisk"; +static const char NEVER_ASK_FOR_OPEN_FILE_PREF[] = + "browser.helperApps.neverAsk.openFile"; + +// Helper functions for Content-Disposition headers + +/** + * Given a URI fragment, unescape it + * @param aFragment The string to unescape + * @param aURI The URI from which this fragment is taken. Only its character set + * will be used. + * @param aResult [out] Unescaped string. + */ +static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI, + nsAString& aResult) { + // We need the unescaper + nsresult rv; + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return textToSubURI->UnEscapeURIForUI(aFragment, aResult); +} + +/** + * UTF-8 version of UnescapeFragment. + * @param aFragment The string to unescape + * @param aURI The URI from which this fragment is taken. Only its character set + * will be used. + * @param aResult [out] Unescaped string, UTF-8 encoded. + * @note It is safe to pass the same string for aFragment and aResult. + * @note When this function fails, aResult will not be modified. + */ +static nsresult UnescapeFragment(const nsACString& aFragment, nsIURI* aURI, + nsACString& aResult) { + nsAutoString result; + nsresult rv = UnescapeFragment(aFragment, aURI, result); + if (NS_SUCCEEDED(rv)) CopyUTF16toUTF8(result, aResult); + return rv; +} + +/** + * Given a channel, returns the filename and extension the channel has. + * This uses the URL and other sources (nsIMultiPartChannel). + * Also gives back whether the channel requested external handling (i.e. + * whether Content-Disposition: attachment was sent) + * @param aChannel The channel to extract the filename/extension from + * @param aFileName [out] Reference to the string where the filename should be + * stored. Empty if it could not be retrieved. + * WARNING - this filename may contain characters which the OS does not + * allow as part of filenames! + * @param aExtension [out] Reference to the string where the extension should + * be stored. Empty if it could not be retrieved. Stored in UTF-8. + * @param aAllowURLExtension (optional) Get the extension from the URL if no + * Content-Disposition header is present. Default is true. + * @retval true The server sent Content-Disposition:attachment or equivalent + * @retval false Content-Disposition: inline or no content-disposition header + * was sent. + */ +static bool GetFilenameAndExtensionFromChannel(nsIChannel* aChannel, + nsString& aFileName, + nsCString& aExtension, + bool aAllowURLExtension = true) { + aExtension.Truncate(); + /* + * If the channel is an http or part of a multipart channel and we + * have a content disposition header set, then use the file name + * suggested there as the preferred file name to SUGGEST to the + * user. we shouldn't actually use that without their + * permission... otherwise just use our temp file + */ + bool handleExternally = false; + uint32_t disp; + nsresult rv = aChannel->GetContentDisposition(&disp); + bool gotFileNameFromURI = false; + if (NS_SUCCEEDED(rv)) { + aChannel->GetContentDispositionFilename(aFileName); + if (disp == nsIChannel::DISPOSITION_ATTACHMENT) handleExternally = true; + } + + // If the disposition header didn't work, try the filename from nsIURL + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsIURL> url(do_QueryInterface(uri)); + if (url && aFileName.IsEmpty()) { + if (aAllowURLExtension) { + url->GetFileExtension(aExtension); + UnescapeFragment(aExtension, url, aExtension); + + // Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + // In case the aExtension consisted only of the dot, the code below will + // extract an aExtension from the filename + aExtension.Trim(".", false); + } + + // try to extract the file name from the url and use that as a first pass as + // the leaf name of our temp file... + nsAutoCString leafName; + url->GetFileName(leafName); + if (!leafName.IsEmpty()) { + gotFileNameFromURI = true; + rv = UnescapeFragment(leafName, url, aFileName); + if (NS_FAILED(rv)) { + CopyUTF8toUTF16(leafName, aFileName); // use escaped name + } + } + } + + // If we have a filename and no extension, remove trailing dots from the + // filename and extract the extension if that is possible. + if (aExtension.IsEmpty() && !aFileName.IsEmpty()) { + // Windows ignores terminating dots. So we have to as well, so + // that our security checks do "the right thing" + aFileName.Trim(".", false); + // We can get an extension if the filename is from a header, or if getting + // it from the URL was allowed. + bool canGetExtensionFromFilename = + !gotFileNameFromURI || aAllowURLExtension; + // ... , or if the mimetype is meaningless and we have nothing to go on: + if (!canGetExtensionFromFilename) { + nsAutoCString contentType; + if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) { + canGetExtensionFromFilename = + contentType.EqualsIgnoreCase(APPLICATION_OCTET_STREAM) || + contentType.EqualsIgnoreCase("binary/octet-stream") || + contentType.EqualsIgnoreCase("application/x-msdownload"); + } + } + + if (canGetExtensionFromFilename) { + // XXX RFindCharInReadable!! + nsAutoString fileNameStr(aFileName); + int32_t idx = fileNameStr.RFindChar(char16_t('.')); + if (idx != kNotFound) + CopyUTF16toUTF8(StringTail(fileNameStr, fileNameStr.Length() - idx - 1), + aExtension); + } + } + + return handleExternally; +} + +/** + * Obtains the directory to use. This tends to vary per platform, and + * needs to be consistent throughout our codepaths. For platforms where + * helper apps use the downloads directory, this should be kept in + * sync with DownloadIntegration.jsm. + * + * Optionally skip availability of the directory and storage. + */ +static nsresult GetDownloadDirectory(nsIFile** _directory, + bool aSkipChecks = false) { + nsCOMPtr<nsIFile> dir; +#ifdef XP_MACOSX + // On OS X, we first try to get the users download location, if it's set. + switch (Preferences::GetInt(NS_PREF_DOWNLOAD_FOLDERLIST, -1)) { + case NS_FOLDER_VALUE_DESKTOP: + (void)NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(dir)); + break; + case NS_FOLDER_VALUE_CUSTOM: { + Preferences::GetComplex(NS_PREF_DOWNLOAD_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(dir)); + if (!dir) break; + + // If we're not checking for availability we're done. + if (aSkipChecks) { + dir.forget(_directory); + return NS_OK; + } + + // We have the directory, and now we need to make sure it exists + bool dirExists = false; + (void)dir->Exists(&dirExists); + if (dirExists) break; + + nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_FAILED(rv)) { + dir = nullptr; + break; + } + } break; + case NS_FOLDER_VALUE_DOWNLOADS: + // This is just the OS default location, so fall out + break; + } + + if (!dir) { + // If not, we default to the OS X default download location. + nsresult rv = NS_GetSpecialDirectory(NS_OSX_DEFAULT_DOWNLOAD_DIR, + getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + } +#elif defined(ANDROID) + return NS_ERROR_FAILURE; +#else + // On all other platforms, we default to the systems temporary directory. + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + +# if defined(XP_UNIX) + // Ensuring that only the current user can read the file names we end up + // creating. Note that Creating directories with specified permission only + // supported on Unix platform right now. That's why above if exists. + + uint32_t permissions; + rv = dir->GetPermissions(&permissions); + NS_ENSURE_SUCCESS(rv, rv); + + if (permissions != PR_IRWXU) { + const char* userName = PR_GetEnv("USERNAME"); + if (!userName || !*userName) { + userName = PR_GetEnv("USER"); + } + if (!userName || !*userName) { + userName = PR_GetEnv("LOGNAME"); + } + if (!userName || !*userName) { + userName = "mozillaUser"; + } + + nsAutoString userDir; + userDir.AssignLiteral("mozilla_"); + userDir.AppendASCII(userName); + userDir.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_'); + + int counter = 0; + bool pathExists; + nsCOMPtr<nsIFile> finalPath; + + while (true) { + nsAutoString countedUserDir(userDir); + countedUserDir.AppendInt(counter, 10); + dir->Clone(getter_AddRefs(finalPath)); + finalPath->Append(countedUserDir); + + rv = finalPath->Exists(&pathExists); + NS_ENSURE_SUCCESS(rv, rv); + + if (pathExists) { + // If this path has the right permissions, use it. + rv = finalPath->GetPermissions(&permissions); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensuring the path is writable by the current user. + bool isWritable; + rv = finalPath->IsWritable(&isWritable); + NS_ENSURE_SUCCESS(rv, rv); + + if (permissions == PR_IRWXU && isWritable) { + dir = finalPath; + break; + } + } + + rv = finalPath->Create(nsIFile::DIRECTORY_TYPE, PR_IRWXU); + if (NS_SUCCEEDED(rv)) { + dir = finalPath; + break; + } else if (rv != NS_ERROR_FILE_ALREADY_EXISTS) { + // Unexpected error. + return rv; + } + + counter++; + } + } + +# endif +#endif + + NS_ASSERTION(dir, "Somehow we didn't get a download directory!"); + dir.forget(_directory); + return NS_OK; +} + +/** + * Structure for storing extension->type mappings. + * @see defaultMimeEntries + */ +struct nsDefaultMimeTypeEntry { + const char* mMimeType; + const char* mFileExtension; +}; + +/** + * Default extension->mimetype mappings. These are not overridable. + * If you add types here, make sure they are lowercase, or you'll regret it. + */ +static const nsDefaultMimeTypeEntry defaultMimeEntries[] = { + // The following are those extensions that we're asked about during startup, + // sorted by order used + {IMAGE_GIF, "gif"}, + {TEXT_XML, "xml"}, + {APPLICATION_RDF, "rdf"}, + {IMAGE_PNG, "png"}, + // -- end extensions used during startup + {TEXT_CSS, "css"}, + {IMAGE_JPEG, "jpeg"}, + {IMAGE_JPEG, "jpg"}, + {IMAGE_SVG_XML, "svg"}, + {TEXT_HTML, "html"}, + {TEXT_HTML, "htm"}, + {APPLICATION_XPINSTALL, "xpi"}, + {"application/xhtml+xml", "xhtml"}, + {"application/xhtml+xml", "xht"}, + {TEXT_PLAIN, "txt"}, + {APPLICATION_JSON, "json"}, + {APPLICATION_XJAVASCRIPT, "js"}, + {APPLICATION_XJAVASCRIPT, "jsm"}, + {VIDEO_OGG, "ogv"}, + {VIDEO_OGG, "ogg"}, + {APPLICATION_OGG, "ogg"}, + {AUDIO_OGG, "oga"}, + {AUDIO_OGG, "opus"}, + {APPLICATION_PDF, "pdf"}, + {VIDEO_WEBM, "webm"}, + {AUDIO_WEBM, "webm"}, + {IMAGE_ICO, "ico"}, + {TEXT_PLAIN, "properties"}, + {TEXT_PLAIN, "locale"}, + {TEXT_PLAIN, "ftl"}, +#if defined(MOZ_WMF) + {VIDEO_MP4, "mp4"}, + {AUDIO_MP4, "m4a"}, + {AUDIO_MP3, "mp3"}, +#endif +#ifdef MOZ_RAW + {VIDEO_RAW, "yuv"} +#endif +}; + +/** + * This is a small private struct used to help us initialize some + * default mime types. + */ +struct nsExtraMimeTypeEntry { + const char* mMimeType; + const char* mFileExtensions; + const char* mDescription; +}; + +/** + * This table lists all of the 'extra' content types that we can deduce from + * particular file extensions. These entries also ensure that we provide a good + * descriptive name when we encounter files with these content types and/or + * extensions. These can be overridden by user helper app prefs. If you add + * types here, make sure they are lowercase, or you'll regret it. + */ +static const nsExtraMimeTypeEntry extraMimeEntries[] = { +#if defined(XP_MACOSX) // don't define .bin on the mac...use internet config to + // look that up... + {APPLICATION_OCTET_STREAM, "exe,com", "Binary File"}, +#else + {APPLICATION_OCTET_STREAM, "exe,com,bin", "Binary File"}, +#endif + {APPLICATION_GZIP2, "gz", "gzip"}, + {"application/x-arj", "arj", "ARJ file"}, + {"application/rtf", "rtf", "Rich Text Format File"}, + {APPLICATION_ZIP, "zip", "ZIP Archive"}, + {APPLICATION_XPINSTALL, "xpi", "XPInstall Install"}, + {APPLICATION_PDF, "pdf", "Portable Document Format"}, + {APPLICATION_POSTSCRIPT, "ps,eps,ai", "Postscript File"}, + {APPLICATION_XJAVASCRIPT, "js", "Javascript Source File"}, + {APPLICATION_XJAVASCRIPT, "jsm,mjs", "Javascript Module Source File"}, +#ifdef MOZ_WIDGET_ANDROID + {"application/vnd.android.package-archive", "apk", "Android Package"}, +#endif + + // OpenDocument formats + {"application/vnd.oasis.opendocument.text", "odt", "OpenDocument Text"}, + {"application/vnd.oasis.opendocument.presentation", "odp", + "OpenDocument Presentation"}, + {"application/vnd.oasis.opendocument.spreadsheet", "ods", + "OpenDocument Spreadsheet"}, + {"application/vnd.oasis.opendocument.graphics", "odg", + "OpenDocument Graphics"}, + + // Legacy Microsoft Office + {"application/msword", "doc", "Microsoft Word"}, + {"application/vnd.ms-powerpoint", "ppt", "Microsoft PowerPoint"}, + {"application/vnd.ms-excel", "xls", "Microsoft Excel"}, + + // Office Open XML + {"application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "docx", "Microsoft Word (Open XML)"}, + {"application/" + "vnd.openxmlformats-officedocument.presentationml.presentation", + "pptx", "Microsoft PowerPoint (Open XML)"}, + {"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsx", "Microsoft Excel (Open XML)"}, + + // Note: if you add new image types, please also update the list in + // contentAreaUtils.js to match. + {IMAGE_ART, "art", "ART Image"}, + {IMAGE_BMP, "bmp", "BMP Image"}, + {IMAGE_GIF, "gif", "GIF Image"}, + {IMAGE_ICO, "ico,cur", "ICO Image"}, + {IMAGE_JPEG, "jpg,jpeg,jfif,pjpeg,pjp", "JPEG Image"}, + {IMAGE_PNG, "png", "PNG Image"}, + {IMAGE_APNG, "apng", "APNG Image"}, + {IMAGE_TIFF, "tiff,tif", "TIFF Image"}, + {IMAGE_XBM, "xbm", "XBM Image"}, + {IMAGE_SVG_XML, "svg", "Scalable Vector Graphics"}, + {IMAGE_WEBP, "webp", "WebP Image"}, + {IMAGE_AVIF, "avif", "AV1 Image File"}, + + {MESSAGE_RFC822, "eml", "RFC-822 data"}, + {TEXT_PLAIN, "txt,text", "Text File"}, + {APPLICATION_JSON, "json", "JavaScript Object Notation"}, + {TEXT_VTT, "vtt", "Web Video Text Tracks"}, + {TEXT_CACHE_MANIFEST, "appcache", "Application Cache Manifest"}, + {TEXT_HTML, "html,htm,shtml,ehtml", "HyperText Markup Language"}, + {"application/xhtml+xml", "xhtml,xht", + "Extensible HyperText Markup Language"}, + {APPLICATION_MATHML_XML, "mml", "Mathematical Markup Language"}, + {APPLICATION_RDF, "rdf", "Resource Description Framework"}, + {"text/csv", "csv", "CSV File"}, + {TEXT_XML, "xml,xsl,xbl", "Extensible Markup Language"}, + {TEXT_CSS, "css", "Style Sheet"}, + {TEXT_VCARD, "vcf,vcard", "Contact Information"}, + {TEXT_CALENDAR, "ics", "iCalendar"}, + {VIDEO_OGG, "ogv", "Ogg Video"}, + {VIDEO_OGG, "ogg", "Ogg Video"}, + {APPLICATION_OGG, "ogg", "Ogg Video"}, + {AUDIO_OGG, "oga", "Ogg Audio"}, + {AUDIO_OGG, "opus", "Opus Audio"}, + {VIDEO_WEBM, "webm", "Web Media Video"}, + {AUDIO_WEBM, "webm", "Web Media Audio"}, + {AUDIO_MP3, "mp3", "MPEG Audio"}, + {VIDEO_MP4, "mp4", "MPEG-4 Video"}, + {AUDIO_MP4, "m4a", "MPEG-4 Audio"}, + {VIDEO_RAW, "yuv", "Raw YUV Video"}, + {AUDIO_WAV, "wav", "Waveform Audio"}, + {VIDEO_3GPP, "3gpp,3gp", "3GPP Video"}, + {VIDEO_3GPP2, "3g2", "3GPP2 Video"}, + {AUDIO_AAC, "aac", "AAC Audio"}, + {AUDIO_FLAC, "flac", "FLAC Audio"}, + {AUDIO_MIDI, "mid", "Standard MIDI Audio"}, + {APPLICATION_WASM, "wasm", "WebAssembly Module"}}; + +static const nsDefaultMimeTypeEntry sForbiddenPrimaryExtensions[] = { + {IMAGE_JPEG, "jfif"}}; + +/** + * File extensions for which decoding should be disabled. + * NOTE: These MUST be lower-case and ASCII. + */ +static const nsDefaultMimeTypeEntry nonDecodableExtensions[] = { + {APPLICATION_GZIP, "gz"}, + {APPLICATION_GZIP, "tgz"}, + {APPLICATION_ZIP, "zip"}, + {APPLICATION_COMPRESS, "z"}, + {APPLICATION_GZIP, "svgz"}}; + +/** + * Mimetypes for which we enforce using a known extension. + * + * In addition to this list, we do this for all audio/, video/ and + * image/ mimetypes. + */ +static const char* forcedExtensionMimetypes[] = { + // OpenDocument formats + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.graphics", + + // Legacy Microsoft Office + "application/msword", "application/vnd.ms-powerpoint", + "application/vnd.ms-excel", + + // Office Open XML + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + + // Note: zip and json mimetypes are commonly used with a variety of + // extensions; don't add them here. It's a similar story for text/xml, + // but slightly worse because we can use it when sniffing for a mimetype + // if one hasn't been provided, so don't re-add that here either. + + APPLICATION_PDF, + + APPLICATION_OGG, + + APPLICATION_WASM, + + TEXT_CALENDAR, TEXT_CSS, TEXT_VCARD}; + +/** + * Primary extensions of types whose descriptions should be overwritten. + * This extension is concatenated with "ExtHandlerDescription" to look up the + * description in unknownContentType.properties. + * NOTE: These MUST be lower-case and ASCII. + */ +static const char* descriptionOverwriteExtensions[] = { + "avif", "pdf", "svg", "webp", "xml", +}; + +static StaticRefPtr<nsExternalHelperAppService> sExtHelperAppSvcSingleton; + +/** + * On Mac child processes, return an nsOSHelperAppServiceChild for remoting + * OS calls to the parent process. On all other platforms use + * nsOSHelperAppService. + */ +/* static */ +already_AddRefed<nsExternalHelperAppService> +nsExternalHelperAppService::GetSingleton() { + if (!sExtHelperAppSvcSingleton) { +#ifdef XP_MACOSX + if (XRE_IsParentProcess()) { + sExtHelperAppSvcSingleton = new nsOSHelperAppService(); + } else { + sExtHelperAppSvcSingleton = new nsOSHelperAppServiceChild(); + } +#else + sExtHelperAppSvcSingleton = new nsOSHelperAppService(); +#endif /* XP_MACOSX */ + ClearOnShutdown(&sExtHelperAppSvcSingleton); + } + + return do_AddRef(sExtHelperAppSvcSingleton); +} + +NS_IMPL_ISUPPORTS(nsExternalHelperAppService, nsIExternalHelperAppService, + nsPIExternalAppLauncher, nsIExternalProtocolService, + nsIMIMEService, nsIObserver, nsISupportsWeakReference) + +nsExternalHelperAppService::nsExternalHelperAppService() {} +nsresult nsExternalHelperAppService::Init() { + // Add an observer for profile change + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) return NS_ERROR_FAILURE; + + nsresult rv = obs->AddObserver(this, "profile-before-change", true); + NS_ENSURE_SUCCESS(rv, rv); + return obs->AddObserver(this, "last-pb-context-exited", true); +} + +nsExternalHelperAppService::~nsExternalHelperAppService() {} + +nsresult nsExternalHelperAppService::DoContentContentProcessHelper( + const nsACString& aMimeContentType, nsIRequest* aRequest, + BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + // We need to get a hold of a ContentChild so that we can begin forwarding + // this data to the parent. In the HTTP case, this is unfortunate, since + // we're actually passing data from parent->child->parent wastefully, but + // the Right Fix will eventually be to short-circuit those channels on the + // parent side based on some sort of subscription concept. + using mozilla::dom::ContentChild; + using mozilla::dom::ExternalHelperAppChild; + ContentChild* child = ContentChild::GetSingleton(); + if (!child) { + return NS_ERROR_FAILURE; + } + + nsCString disp; + nsCOMPtr<nsIURI> uri; + int64_t contentLength = -1; + bool wasFileChannel = false; + uint32_t contentDisposition = -1; + nsAutoString fileName; + nsCOMPtr<nsILoadInfo> loadInfo; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + channel->GetContentDispositionHeader(disp); + loadInfo = channel->LoadInfo(); + + nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(aRequest)); + wasFileChannel = fileChan != nullptr; + } + + nsCOMPtr<nsIURI> referrer; + NS_GetReferrerFromChannel(channel, getter_AddRefs(referrer)); + + Maybe<mozilla::net::LoadInfoArgs> loadInfoArgs; + MOZ_ALWAYS_SUCCEEDS(LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs)); + + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(aRequest)); + // Determine whether a new window was opened specifically for this request + bool shouldCloseWindow = false; + if (props) { + props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, + &shouldCloseWindow); + } + + // Now we build a protocol for forwarding our data to the parent. The + // protocol will act as a listener on the child-side and create a "real" + // helperAppService listener on the parent-side, via another call to + // DoContent. + RefPtr<ExternalHelperAppChild> childListener = new ExternalHelperAppChild(); + MOZ_ALWAYS_TRUE(child->SendPExternalHelperAppConstructor( + childListener, uri, loadInfoArgs, nsCString(aMimeContentType), disp, + contentDisposition, fileName, aForceSave, contentLength, wasFileChannel, + referrer, aContentContext, shouldCloseWindow)); + + NS_ADDREF(*aStreamListener = childListener); + + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + + RefPtr<nsExternalAppHandler> handler = + new nsExternalAppHandler(nullptr, ""_ns, aContentContext, aWindowContext, + this, fileName, reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + childListener->SetHandler(handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::CreateListener( + const nsACString& aMimeContentType, nsIRequest* aRequest, + BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + MOZ_ASSERT(!XRE_IsContentProcess()); + + nsAutoString fileName; + nsAutoCString fileExtension; + uint32_t reason = nsIHelperAppLauncherDialog::REASON_CANTHANDLE; + uint32_t contentDisposition = -1; + + // Get the file extension and name that we will need later + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsIURI> uri; + int64_t contentLength = -1; + if (channel) { + channel->GetURI(getter_AddRefs(uri)); + channel->GetContentLength(&contentLength); + channel->GetContentDisposition(&contentDisposition); + channel->GetContentDispositionFilename(fileName); + + // Check if we have a POST request, in which case we don't want to use + // the url's extension + bool allowURLExt = !net::ChannelIsPost(channel); + + // Check if we had a query string - we don't want to check the URL + // extension if a query is present in the URI + // If we already know we don't want to check the URL extension, don't + // bother checking the query + if (uri && allowURLExt) { + nsCOMPtr<nsIURL> url = do_QueryInterface(uri); + + if (url) { + nsAutoCString query; + + // We only care about the query for HTTP and HTTPS URLs + if (uri->SchemeIs("http") || uri->SchemeIs("https")) { + url->GetQuery(query); + } + + // Only get the extension if the query is empty; if it isn't, then the + // extension likely belongs to a cgi script and isn't helpful + allowURLExt = query.IsEmpty(); + } + } + // Extract name & extension + bool isAttachment = GetFilenameAndExtensionFromChannel( + channel, fileName, fileExtension, allowURLExt); + LOG(("Found extension '%s' (filename is '%s', handling attachment: %i)", + fileExtension.get(), NS_ConvertUTF16toUTF8(fileName).get(), + isAttachment)); + if (isAttachment) { + reason = nsIHelperAppLauncherDialog::REASON_SERVERREQUEST; + } + } + + LOG(("HelperAppService::DoContent: mime '%s', extension '%s'\n", + PromiseFlatCString(aMimeContentType).get(), fileExtension.get())); + + // We get the mime service here even though we're the default implementation + // of it, so it's possible to override only the mime service and not need to + // reimplement the whole external helper app service itself. + nsCOMPtr<nsIMIMEService> mimeSvc(do_GetService(NS_MIMESERVICE_CONTRACTID)); + NS_ENSURE_TRUE(mimeSvc, NS_ERROR_FAILURE); + + // Try to find a mime object by looking at the mime type/extension + nsCOMPtr<nsIMIMEInfo> mimeInfo; + if (aMimeContentType.Equals(APPLICATION_GUESS_FROM_EXT, + nsCaseInsensitiveCStringComparator)) { + nsAutoCString mimeType; + if (!fileExtension.IsEmpty()) { + mimeSvc->GetFromTypeAndExtension(""_ns, fileExtension, + getter_AddRefs(mimeInfo)); + if (mimeInfo) { + mimeInfo->GetMIMEType(mimeType); + + LOG(("OS-Provided mime type '%s' for extension '%s'\n", mimeType.get(), + fileExtension.get())); + } + } + + if (fileExtension.IsEmpty() || mimeType.IsEmpty()) { + // Extension lookup gave us no useful match + mimeSvc->GetFromTypeAndExtension( + nsLiteralCString(APPLICATION_OCTET_STREAM), fileExtension, + getter_AddRefs(mimeInfo)); + mimeType.AssignLiteral(APPLICATION_OCTET_STREAM); + } + + if (channel) { + channel->SetContentType(mimeType); + } + + // Don't overwrite SERVERREQUEST + if (reason == nsIHelperAppLauncherDialog::REASON_CANTHANDLE) { + reason = nsIHelperAppLauncherDialog::REASON_TYPESNIFFED; + } + } else { + mimeSvc->GetFromTypeAndExtension(aMimeContentType, fileExtension, + getter_AddRefs(mimeInfo)); + } + LOG(("Type/Ext lookup found 0x%p\n", mimeInfo.get())); + + // No mimeinfo -> we can't continue. probably OOM. + if (!mimeInfo) { + return NS_ERROR_OUT_OF_MEMORY; + } + + *aStreamListener = nullptr; + // We want the mimeInfo's primary extension to pass it to + // nsExternalAppHandler + nsAutoCString buf; + mimeInfo->GetPrimaryExtension(buf); + + // NB: ExternalHelperAppParent depends on this listener always being an + // nsExternalAppHandler. If this changes, make sure to update that code. + nsExternalAppHandler* handler = + new nsExternalAppHandler(mimeInfo, buf, aContentContext, aWindowContext, + this, fileName, reason, aForceSave); + if (!handler) { + return NS_ERROR_OUT_OF_MEMORY; + } + + NS_ADDREF(*aStreamListener = handler); + return NS_OK; +} + +NS_IMETHODIMP nsExternalHelperAppService::DoContent( + const nsACString& aMimeContentType, nsIRequest* aRequest, + nsIInterfaceRequestor* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + // Scripted interface requestors cannot return an instance of the + // (non-scriptable) nsPIDOMWindowOuter or nsPIDOMWindowInner interfaces, so + // get to the window via `nsIDOMWindow`. Unfortunately, at that point we + // don't know whether the thing we got is an inner or outer window, so have to + // work with either one. + RefPtr<BrowsingContext> bc; + nsCOMPtr<nsIDOMWindow> domWindow = do_GetInterface(aContentContext); + if (nsCOMPtr<nsPIDOMWindowOuter> outerWindow = do_QueryInterface(domWindow)) { + bc = outerWindow->GetBrowsingContext(); + } else if (nsCOMPtr<nsPIDOMWindowInner> innerWindow = + do_QueryInterface(domWindow)) { + bc = innerWindow->GetBrowsingContext(); + } + + if (XRE_IsContentProcess()) { + return DoContentContentProcessHelper(aMimeContentType, aRequest, bc, + aForceSave, aWindowContext, + aStreamListener); + } + + nsresult rv = CreateListener(aMimeContentType, aRequest, bc, aForceSave, + aWindowContext, aStreamListener); + return rv; +} + +NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension( + const nsACString& aExtension, const nsACString& aEncodingType, + bool* aApplyDecoding) { + *aApplyDecoding = true; + uint32_t i; + for (i = 0; i < ArrayLength(nonDecodableExtensions); ++i) { + if (aExtension.LowerCaseEqualsASCII( + nonDecodableExtensions[i].mFileExtension) && + aEncodingType.LowerCaseEqualsASCII( + nonDecodableExtensions[i].mMimeType)) { + *aApplyDecoding = false; + break; + } + } + return NS_OK; +} + +nsresult nsExternalHelperAppService::GetFileTokenForPath( + const char16_t* aPlatformAppPath, nsIFile** aFile) { + nsDependentString platformAppPath(aPlatformAppPath); + // First, check if we have an absolute path + nsIFile* localFile = nullptr; + nsresult rv = NS_NewLocalFile(platformAppPath, true, &localFile); + if (NS_SUCCEEDED(rv)) { + *aFile = localFile; + bool exists; + if (NS_FAILED((*aFile)->Exists(&exists)) || !exists) { + NS_RELEASE(*aFile); + return NS_ERROR_FILE_NOT_FOUND; + } + return NS_OK; + } + + // Second, check if file exists in mozilla program directory + rv = NS_GetSpecialDirectory(NS_XPCOM_CURRENT_PROCESS_DIR, aFile); + if (NS_SUCCEEDED(rv)) { + rv = (*aFile)->Append(platformAppPath); + if (NS_SUCCEEDED(rv)) { + bool exists = false; + rv = (*aFile)->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) return NS_OK; + } + NS_RELEASE(*aFile); + } + + return NS_ERROR_NOT_AVAILABLE; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// begin external protocol service default implementation... +////////////////////////////////////////////////////////////////////////////////////////////////////// +NS_IMETHODIMP nsExternalHelperAppService::ExternalProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsCOMPtr<nsIHandlerInfo> handlerInfo; + nsresult rv = GetProtocolHandlerInfo(nsDependentCString(aProtocolScheme), + getter_AddRefs(handlerInfo)); + if (NS_SUCCEEDED(rv)) { + // See if we have any known possible handler apps for this + nsCOMPtr<nsIMutableArray> possibleHandlers; + handlerInfo->GetPossibleApplicationHandlers( + getter_AddRefs(possibleHandlers)); + + uint32_t length; + possibleHandlers->GetLength(&length); + if (length) { + *aHandlerExists = true; + return NS_OK; + } + } + + // if not, fall back on an os-based handler + return OSProtocolHandlerExists(aProtocolScheme, aHandlerExists); +} + +NS_IMETHODIMP nsExternalHelperAppService::IsExposedProtocol( + const char* aProtocolScheme, bool* aResult) { + // check the per protocol setting first. it always takes precedence. + // if not set, then use the global setting. + + nsAutoCString prefName("network.protocol-handler.expose."); + prefName += aProtocolScheme; + bool val; + if (NS_SUCCEEDED(Preferences::GetBool(prefName.get(), &val))) { + *aResult = val; + return NS_OK; + } + + // by default, no protocol is exposed. i.e., by default all link clicks must + // go through the external protocol service. most applications override this + // default behavior. + *aResult = Preferences::GetBool("network.protocol-handler.expose-all", false); + + return NS_OK; +} + +static const char kExternalProtocolPrefPrefix[] = + "network.protocol-handler.external."; +static const char kExternalProtocolDefaultPref[] = + "network.protocol-handler.external-default"; + +NS_IMETHODIMP +nsExternalHelperAppService::LoadURI(nsIURI* aURI, + nsIPrincipal* aTriggeringPrincipal, + BrowsingContext* aBrowsingContext) { + NS_ENSURE_ARG_POINTER(aURI); + + if (XRE_IsContentProcess()) { + mozilla::dom::ContentChild::GetSingleton()->SendLoadURIExternal( + aURI, aTriggeringPrincipal, aBrowsingContext); + return NS_OK; + } + + nsAutoCString spec; + aURI->GetSpec(spec); + + if (spec.Find("%00") != -1) return NS_ERROR_MALFORMED_URI; + + spec.ReplaceSubstring("\"", "%22"); + spec.ReplaceSubstring("`", "%60"); + + nsCOMPtr<nsIIOService> ios(do_GetIOService()); + nsCOMPtr<nsIURI> uri; + nsresult rv = ios->NewURI(spec, nullptr, nullptr, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString scheme; + uri->GetScheme(scheme); + if (scheme.IsEmpty()) return NS_OK; // must have a scheme + + // Deny load if the prefs say to do so + nsAutoCString externalPref(kExternalProtocolPrefPrefix); + externalPref += scheme; + bool allowLoad = false; + if (NS_FAILED(Preferences::GetBool(externalPref.get(), &allowLoad))) { + // no scheme-specific value, check the default + if (NS_FAILED( + Preferences::GetBool(kExternalProtocolDefaultPref, &allowLoad))) { + return NS_OK; // missing default pref + } + } + + if (!allowLoad) { + return NS_OK; // explicitly denied + } + + // Now check if the principal is allowed to access the navigated context. + // We allow navigating subframes, even if not same-origin - non-external + // links can always navigate everywhere, so this is a minor additional + // restriction, only aiming to prevent some types of spoofing attacks + // from otherwise disjoint browsingcontext trees. + if (aBrowsingContext && aTriggeringPrincipal && + !StaticPrefs::security_allow_disjointed_external_uri_loads() && + // Add-on principals are always allowed: + !BasePrincipal::Cast(aTriggeringPrincipal)->AddonPolicy() && + // As is chrome code: + !aTriggeringPrincipal->IsSystemPrincipal()) { + RefPtr<BrowsingContext> bc = aBrowsingContext; + WindowGlobalParent* wgp = bc->Canonical()->GetCurrentWindowGlobal(); + bool foundAccessibleFrame = false; + + // Also allow this load if the target is a toplevel BC and contains a + // non-web-controlled about:blank document + if (bc->IsTop() && !bc->HadOriginalOpener() && wgp) { + RefPtr<nsIURI> uri = wgp->GetDocumentURI(); + foundAccessibleFrame = + uri && uri->GetSpecOrDefault().EqualsLiteral("about:blank"); + } + + while (!foundAccessibleFrame) { + if (wgp) { + foundAccessibleFrame = + aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal()); + } + // We have to get the parent via the bc, because there may not + // be a window global for the innermost bc; see bug 1650162. + BrowsingContext* parent = bc->GetParent(); + if (!parent) { + break; + } + bc = parent; + wgp = parent->Canonical()->GetCurrentWindowGlobal(); + } + + if (!foundAccessibleFrame) { + // See if this navigation could have come from a subframe. + nsTArray<RefPtr<BrowsingContext>> contexts; + aBrowsingContext->GetAllBrowsingContextsInSubtree(contexts); + for (const auto& kid : contexts) { + wgp = kid->Canonical()->GetCurrentWindowGlobal(); + if (wgp && aTriggeringPrincipal->Subsumes(wgp->DocumentPrincipal())) { + foundAccessibleFrame = true; + break; + } + } + } + + if (!foundAccessibleFrame) { + return NS_OK; // deny the load. + } + } + + nsCOMPtr<nsIHandlerInfo> handler; + rv = GetProtocolHandlerInfo(scheme, getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIContentDispatchChooser> chooser = + do_CreateInstance("@mozilla.org/content-dispatch-chooser;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return chooser->HandleURI(handler, uri, aTriggeringPrincipal, + aBrowsingContext); +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// Methods related to deleting temporary files on exit +////////////////////////////////////////////////////////////////////////////////////////////////////// + +/* static */ +nsresult nsExternalHelperAppService::DeleteTemporaryFileHelper( + nsIFile* aTemporaryFile, nsCOMArray<nsIFile>& aFileList) { + bool isFile = false; + + // as a safety measure, make sure the nsIFile is really a file and not a + // directory object. + aTemporaryFile->IsFile(&isFile); + if (!isFile) return NS_OK; + + aFileList.AppendObject(aTemporaryFile); + + return NS_OK; +} + +NS_IMETHODIMP +nsExternalHelperAppService::DeleteTemporaryFileOnExit(nsIFile* aTemporaryFile) { + return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryFilesList); +} + +NS_IMETHODIMP +nsExternalHelperAppService::DeleteTemporaryPrivateFileWhenPossible( + nsIFile* aTemporaryFile) { + return DeleteTemporaryFileHelper(aTemporaryFile, mTemporaryPrivateFilesList); +} + +void nsExternalHelperAppService::ExpungeTemporaryFilesHelper( + nsCOMArray<nsIFile>& fileList) { + int32_t numEntries = fileList.Count(); + nsIFile* localFile; + for (int32_t index = 0; index < numEntries; index++) { + localFile = fileList[index]; + if (localFile) { + // First make the file writable, since the temp file is probably readonly. + localFile->SetPermissions(0600); + localFile->Remove(false); + } + } + + fileList.Clear(); +} + +void nsExternalHelperAppService::ExpungeTemporaryFiles() { + ExpungeTemporaryFilesHelper(mTemporaryFilesList); +} + +void nsExternalHelperAppService::ExpungeTemporaryPrivateFiles() { + ExpungeTemporaryFilesHelper(mTemporaryPrivateFilesList); +} + +static const char kExternalWarningPrefPrefix[] = + "network.protocol-handler.warn-external."; +static const char kExternalWarningDefaultPref[] = + "network.protocol-handler.warn-external-default"; + +NS_IMETHODIMP +nsExternalHelperAppService::GetProtocolHandlerInfo( + const nsACString& aScheme, nsIHandlerInfo** aHandlerInfo) { + // XXX enterprise customers should be able to turn this support off with a + // single master pref (maybe use one of the "exposed" prefs here?) + + bool exists; + nsresult rv = GetProtocolHandlerInfoFromOS(aScheme, &exists, aHandlerInfo); + if (NS_FAILED(rv)) { + // Either it knows nothing, or we ran out of memory + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void)handlerSvc->Exists(*aHandlerInfo, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*aHandlerInfo, ""_ns); + if (NS_SUCCEEDED(rv)) return NS_OK; + } + } + + return SetProtocolHandlerDefaults(*aHandlerInfo, exists); +} + +NS_IMETHODIMP +nsExternalHelperAppService::SetProtocolHandlerDefaults( + nsIHandlerInfo* aHandlerInfo, bool aOSHandlerExists) { + // this type isn't in our database, so we've only got an OS default handler, + // if one exists + + if (aOSHandlerExists) { + // we've got a default, so use it + aHandlerInfo->SetPreferredAction(nsIHandlerInfo::useSystemDefault); + + // whether or not to ask the user depends on the warning preference + nsAutoCString scheme; + aHandlerInfo->GetType(scheme); + + nsAutoCString warningPref(kExternalWarningPrefPrefix); + warningPref += scheme; + bool warn; + if (NS_FAILED(Preferences::GetBool(warningPref.get(), &warn))) { + // no scheme-specific value, check the default + warn = Preferences::GetBool(kExternalWarningDefaultPref, true); + } + aHandlerInfo->SetAlwaysAskBeforeHandling(warn); + } else { + // If no OS default existed, we set the preferred action to alwaysAsk. + // This really means not initialized (i.e. there's no available handler) + // to all the code... + aHandlerInfo->SetPreferredAction(nsIHandlerInfo::alwaysAsk); + } + + return NS_OK; +} + +// XPCOM profile change observer +NS_IMETHODIMP +nsExternalHelperAppService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* someData) { + if (!strcmp(aTopic, "profile-before-change")) { + ExpungeTemporaryFiles(); + } else if (!strcmp(aTopic, "last-pb-context-exited")) { + ExpungeTemporaryPrivateFiles(); + } + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////// +// begin external app handler implementation +////////////////////////////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ADDREF(nsExternalAppHandler) +NS_IMPL_RELEASE(nsExternalAppHandler) + +NS_INTERFACE_MAP_BEGIN(nsExternalAppHandler) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIHelperAppLauncher) + NS_INTERFACE_MAP_ENTRY(nsICancelable) + NS_INTERFACE_MAP_ENTRY(nsIBackgroundFileSaverObserver) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsExternalAppHandler) +NS_INTERFACE_MAP_END + +nsExternalAppHandler::nsExternalAppHandler( + nsIMIMEInfo* aMIMEInfo, const nsACString& aTempFileExtension, + BrowsingContext* aBrowsingContext, nsIInterfaceRequestor* aWindowContext, + nsExternalHelperAppService* aExtProtSvc, + const nsAString& aSuggestedFilename, uint32_t aReason, bool aForceSave) + : mMimeInfo(aMIMEInfo), + mBrowsingContext(aBrowsingContext), + mWindowContext(aWindowContext), + mSuggestedFileName(aSuggestedFilename), + mForceSave(aForceSave), + mCanceled(false), + mStopRequestIssued(false), + mIsFileChannel(false), + mShouldCloseWindow(false), + mHandleInternally(false), + mReason(aReason), + mTempFileIsExecutable(false), + mTimeDownloadStarted(0), + mContentLength(-1), + mProgress(0), + mSaver(nullptr), + mDialogProgressListener(nullptr), + mTransfer(nullptr), + mRequest(nullptr), + mExtProtSvc(aExtProtSvc) { + // make sure the extention includes the '.' + if (!aTempFileExtension.IsEmpty() && aTempFileExtension.First() != '.') + mTempFileExtension = char16_t('.'); + AppendUTF8toUTF16(aTempFileExtension, mTempFileExtension); + + // Get mSuggestedFileName's current file extension. + nsAutoString originalFileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos != kNotFound) { + mSuggestedFileName.Right(originalFileExt, + mSuggestedFileName.Length() - pos); + } + + // replace platform specific path separator and illegal characters to avoid + // any confusion. + // Try to keep the use of spaces or underscores in sync with the Downloads + // code sanitization in DownloadPaths.jsm + mSuggestedFileName.ReplaceChar(KNOWN_PATH_SEPARATORS, '_'); + mSuggestedFileName.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' '); + mSuggestedFileName.ReplaceChar(char16_t(0), '_'); + mTempFileExtension.ReplaceChar(KNOWN_PATH_SEPARATORS, '_'); + mTempFileExtension.ReplaceChar(FILE_ILLEGAL_CHARACTERS, ' '); + + // Remove unsafe bidi characters which might have spoofing implications (bug + // 511521). + const char16_t unsafeBidiCharacters[] = { + char16_t(0x061c), // Arabic Letter Mark + char16_t(0x200e), // Left-to-Right Mark + char16_t(0x200f), // Right-to-Left Mark + char16_t(0x202a), // Left-to-Right Embedding + char16_t(0x202b), // Right-to-Left Embedding + char16_t(0x202c), // Pop Directional Formatting + char16_t(0x202d), // Left-to-Right Override + char16_t(0x202e), // Right-to-Left Override + char16_t(0x2066), // Left-to-Right Isolate + char16_t(0x2067), // Right-to-Left Isolate + char16_t(0x2068), // First Strong Isolate + char16_t(0x2069), // Pop Directional Isolate + char16_t(0)}; + mSuggestedFileName.ReplaceChar(unsafeBidiCharacters, '_'); + mTempFileExtension.ReplaceChar(unsafeBidiCharacters, '_'); + + // Remove trailing or leading spaces that we may have generated while + // sanitizing. + mSuggestedFileName.CompressWhitespace(); + mTempFileExtension.CompressWhitespace(); + + EnsureCorrectExtension(originalFileExt); + + mBufferSize = Preferences::GetUint("network.buffer.cache.size", 4096); +} + +nsExternalAppHandler::~nsExternalAppHandler() { + MOZ_ASSERT(!mSaver, "Saver should hold a reference to us until deleted"); +} + +bool nsExternalAppHandler::ShouldForceExtension(const nsString& aFileExt) { + nsAutoCString MIMEType; + if (!mMimeInfo || NS_FAILED(mMimeInfo->GetMIMEType(MIMEType))) { + return false; + } + + bool canForce = StringBeginsWith(MIMEType, "image/"_ns) || + StringBeginsWith(MIMEType, "audio/"_ns) || + StringBeginsWith(MIMEType, "video/"_ns); + + if (!canForce && + StaticPrefs::browser_download_sanitize_non_media_extensions()) { + for (const char* mime : forcedExtensionMimetypes) { + if (MIMEType.Equals(mime)) { + canForce = true; + break; + } + } + } + if (!canForce) { + return false; + } + + // If we get here, we know for sure the mimetype allows us to overwrite the + // existing extension, if it's wrong. Return whether the extension is wrong: + + bool knownExtension = false; + // Note that aFileExt is either empty or consists of an extension + // *including the dot* which we remove for ExtensionExists(). + return ( + aFileExt.IsEmpty() || aFileExt.EqualsLiteral(".") || + (NS_SUCCEEDED(mMimeInfo->ExtensionExists( + Substring(NS_ConvertUTF16toUTF8(aFileExt), 1), &knownExtension)) && + !knownExtension)); +} + +void nsExternalAppHandler::EnsureCorrectExtension(const nsString& aFileExt) { + // If we don't have an extension (which will include the .), + // just short-circuit. + if (mTempFileExtension.Length() <= 1) { + return; + } + + // After removing trailing whitespaces from the name, if we have a + // temp file extension, there are broadly 2 cases where we want to + // replace the extension. + // First, if the file extension contains invalid characters. + // Second, for document type mimetypes, if the extension is either + // missing or not valid for this mimetype. + bool replaceExtension = + (aFileExt.FindCharInSet(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS) != + kNotFound) || + ShouldForceExtension(aFileExt); + + if (replaceExtension) { + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos != kNotFound) { + mSuggestedFileName = + Substring(mSuggestedFileName, 0, pos) + mTempFileExtension; + } else { + mSuggestedFileName.Append(mTempFileExtension); + } + } + + /* + * Ensure we don't double-append the file extension if it matches: + */ + if (replaceExtension || + aFileExt.Equals(mTempFileExtension, nsCaseInsensitiveStringComparator)) { + // Matches -> mTempFileExtension can be empty + mTempFileExtension.Truncate(); + } +} + +void nsExternalAppHandler::DidDivertRequest(nsIRequest* request) { + MOZ_ASSERT(XRE_IsContentProcess(), "in child process"); + // Remove our request from the child loadGroup + RetargetLoadNotifications(request); +} + +NS_IMETHODIMP nsExternalAppHandler::SetWebProgressListener( + nsIWebProgressListener2* aWebProgressListener) { + // This is always called by nsHelperDlg.js. Go ahead and register the + // progress listener. At this point, we don't have mTransfer. + mDialogProgressListener = aWebProgressListener; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTargetFile(nsIFile** aTarget) { + if (mFinalFileDestination) + *aTarget = mFinalFileDestination; + else + *aTarget = mTempFile; + + NS_IF_ADDREF(*aTarget); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTargetFileIsExecutable(bool* aExec) { + // Use the real target if it's been set + if (mFinalFileDestination) return mFinalFileDestination->IsExecutable(aExec); + + // Otherwise, use the stored executable-ness of the temporary + *aExec = mTempFileIsExecutable; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetTimeDownloadStarted(PRTime* aTime) { + *aTime = mTimeDownloadStarted; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetContentLength(int64_t* aContentLength) { + *aContentLength = mContentLength; + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetBrowsingContextId( + uint64_t* aBrowsingContextId) { + *aBrowsingContextId = mBrowsingContext->Id(); + return NS_OK; +} + +void nsExternalAppHandler::RetargetLoadNotifications(nsIRequest* request) { + // we are going to run the downloading of the helper app in our own little + // docloader / load group context. so go ahead and force the creation of a + // load group and doc loader for us to use... + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + if (!aChannel) return; + + bool isPrivate = NS_UsePrivateBrowsing(aChannel); + + nsCOMPtr<nsILoadGroup> oldLoadGroup; + aChannel->GetLoadGroup(getter_AddRefs(oldLoadGroup)); + + if (oldLoadGroup) { + oldLoadGroup->RemoveRequest(request, nullptr, NS_BINDING_RETARGETED); + } + + aChannel->SetLoadGroup(nullptr); + aChannel->SetNotificationCallbacks(nullptr); + + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(aChannel); + if (pbChannel) { + pbChannel->SetPrivate(isPrivate); + } +} + +nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { + // First we need to try to get the destination directory for the temporary + // file. + nsresult rv = GetDownloadDirectory(getter_AddRefs(mTempFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // At this point, we do not have a filename for the temp file. For security + // purposes, this cannot be predictable, so we must use a cryptographic + // quality PRNG to generate one. + // We will request raw random bytes, and transform that to a base64 string, + // as all characters from the base64 set are acceptable for filenames. For + // each three bytes of random data, we will get four bytes of ASCII. Request + // a bit more, to be safe, and truncate to the length we want in the end. + + const uint32_t wantedFileNameLength = 8; + const uint32_t requiredBytesLength = + static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3); + + nsCOMPtr<nsIRandomGenerator> rg = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint8_t* buffer; + rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString tempLeafName; + nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer), + requiredBytesLength); + rv = Base64Encode(randomData, tempLeafName); + free(buffer); + buffer = nullptr; + NS_ENSURE_SUCCESS(rv, rv); + + tempLeafName.Truncate(wantedFileNameLength); + + // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need + // to replace illegal characters -- notably '/' + tempLeafName.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + + // now append our extension. + nsAutoCString ext; + mMimeInfo->GetPrimaryExtension(ext); + if (!ext.IsEmpty()) { + ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + if (ext.First() != '.') tempLeafName.Append('.'); + tempLeafName.Append(ext); + } + + // We need to temporarily create a dummy file with the correct + // file extension to determine the executable-ness, so do this before adding + // the extra .part extension. + nsCOMPtr<nsIFile> dummyFile; + rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dummyFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the file name without .part + rv = dummyFile->Append(NS_ConvertUTF8toUTF16(tempLeafName)); + NS_ENSURE_SUCCESS(rv, rv); + rv = dummyFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + // Store executable-ness then delete + dummyFile->IsExecutable(&mTempFileIsExecutable); + dummyFile->Remove(false); + + // Add an additional .part to prevent the OS from running this file in the + // default application. + tempLeafName.AppendLiteral(".part"); + + rv = mTempFile->Append(NS_ConvertUTF8toUTF16(tempLeafName)); + // make this file unique!!! + NS_ENSURE_SUCCESS(rv, rv); + rv = mTempFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + // Now save the temp leaf name, minus the ".part" bit, so we can use it later. + // This is a bit broken in the case when createUnique actually had to append + // some numbers, because then we now have a filename like foo.bar-1.part and + // we'll end up with foo.bar-1.bar as our final filename if we end up using + // this. But the other options are all bad too.... Ideally we'd have a way + // to tell createUnique to put its unique marker before the extension that + // comes before ".part" or something. + rv = mTempFile->GetLeafName(mTempLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(StringEndsWith(mTempLeafName, u".part"_ns), + NS_ERROR_UNEXPECTED); + + // Strip off the ".part" from mTempLeafName + mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1); + + MOZ_ASSERT(!mSaver, "Output file initialization called more than once!"); + mSaver = + do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mSaver->SetObserver(this); + if (NS_FAILED(rv)) { + mSaver = nullptr; + return rv; + } + + rv = mSaver->EnableSha256(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mSaver->EnableSignatureInfo(); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("Enabled hashing and signature verification")); + + rv = mSaver->SetTarget(mTempFile, false); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +void nsExternalAppHandler::MaybeApplyDecodingForExtension( + nsIRequest* aRequest) { + MOZ_ASSERT(aRequest); + + nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aRequest); + if (!encChannel) { + return; + } + + // Turn off content encoding conversions if needed + bool applyConversion = true; + + // First, check to see if conversion is already disabled. If so, we + // have nothing to do here. + encChannel->GetApplyConversion(&applyConversion); + if (!applyConversion) { + return; + } + + nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(mSourceUrl)); + if (sourceURL) { + nsAutoCString extension; + sourceURL->GetFileExtension(extension); + if (!extension.IsEmpty()) { + nsCOMPtr<nsIUTF8StringEnumerator> encEnum; + encChannel->GetContentEncodings(getter_AddRefs(encEnum)); + if (encEnum) { + bool hasMore; + nsresult rv = encEnum->HasMore(&hasMore); + if (NS_SUCCEEDED(rv) && hasMore) { + nsAutoCString encType; + rv = encEnum->GetNext(encType); + if (NS_SUCCEEDED(rv) && !encType.IsEmpty()) { + MOZ_ASSERT(mExtProtSvc); + mExtProtSvc->ApplyDecodingForExtension(extension, encType, + &applyConversion); + } + } + } + } + } + + encChannel->SetApplyConversion(applyConversion); +} + +already_AddRefed<nsIInterfaceRequestor> +nsExternalAppHandler::GetDialogParent() { + nsCOMPtr<nsIInterfaceRequestor> dialogParent = mWindowContext; + + if (!dialogParent && mBrowsingContext) { + dialogParent = do_QueryInterface(mBrowsingContext->GetDOMWindow()); + } + if (!dialogParent && mBrowsingContext && XRE_IsParentProcess()) { + RefPtr<Element> element = mBrowsingContext->Top()->GetEmbedderElement(); + if (element) { + dialogParent = do_QueryInterface(element->OwnerDoc()->GetWindow()); + } + } + return dialogParent.forget(); +} + +NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { + MOZ_ASSERT(request, "OnStartRequest without request?"); + + // Set mTimeDownloadStarted here as the download has already started and + // we want to record the start time before showing the filepicker. + mTimeDownloadStarted = PR_Now(); + + mRequest = request; + + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + + nsresult rv; + nsAutoCString MIMEType; + if (mMimeInfo) { + mMimeInfo->GetMIMEType(MIMEType); + } + // Now get the URI + if (aChannel) { + aChannel->GetURI(getter_AddRefs(mSourceUrl)); + } + + mDownloadClassification = + nsContentSecurityUtils::ClassifyDownload(aChannel, MIMEType); + + if (mDownloadClassification == nsITransfer::DOWNLOAD_FORBIDDEN) { + // If the download is rated as forbidden, + // cancel the request so no ui knows about this. + mCanceled = true; + request->Cancel(NS_ERROR_ABORT); + return NS_OK; + } + + nsCOMPtr<nsIFileChannel> fileChan(do_QueryInterface(request)); + mIsFileChannel = fileChan != nullptr; + if (!mIsFileChannel) { + // It's possible that this request came from the child process and the + // file channel actually lives there. If this returns true, then our + // mSourceUrl will be an nsIFileURL anyway. + nsCOMPtr<dom::nsIExternalHelperAppParent> parent( + do_QueryInterface(request)); + mIsFileChannel = parent && parent->WasFileChannel(); + } + + // Get content length + if (aChannel) { + aChannel->GetContentLength(&mContentLength); + } + + if (mBrowsingContext) { + mMaybeCloseWindowHelper = new MaybeCloseWindowHelper(mBrowsingContext); + mMaybeCloseWindowHelper->SetShouldCloseWindow(mShouldCloseWindow); + nsCOMPtr<nsIPropertyBag2> props(do_QueryInterface(request, &rv)); + // Determine whether a new window was opened specifically for this request + if (props) { + bool tmp = false; + if (NS_SUCCEEDED( + props->GetPropertyAsBool(u"docshell.newWindowTarget"_ns, &tmp))) { + mMaybeCloseWindowHelper->SetShouldCloseWindow(tmp); + } + } + } + + // retarget all load notifications to our docloader instead of the original + // window's docloader... + RetargetLoadNotifications(request); + + // Close the underlying DOMWindow if it was opened specifically for the + // download. We don't run this in the content process, since we have + // an instance running in the parent as well, which will handle this + // if needed. + if (!XRE_IsContentProcess() && mMaybeCloseWindowHelper) { + mBrowsingContext = mMaybeCloseWindowHelper->MaybeCloseWindow(); + } + + // In an IPC setting, we're allowing the child process, here, to make + // decisions about decoding the channel (e.g. decompression). It will + // still forward the decoded (uncompressed) data back to the parent. + // Con: Uncompressed data means more IPC overhead. + // Pros: ExternalHelperAppParent doesn't need to implement nsIEncodedChannel. + // Parent process doesn't need to expect CPU time on decompression. + MaybeApplyDecodingForExtension(aChannel); + + // At this point, the child process has done everything it can usefully do + // for OnStartRequest. + if (XRE_IsContentProcess()) { + return NS_OK; + } + + rv = SetUpTempFile(aChannel); + if (NS_FAILED(rv)) { + nsresult transferError = rv; + + rv = CreateFailedTransfer(); + if (NS_FAILED(rv)) { + LOG( + ("Failed to create transfer to report failure." + "Will fallback to prompter!")); + } + + mCanceled = true; + request->Cancel(transferError); + + nsAutoString path; + if (mTempFile) mTempFile->GetPath(path); + + SendStatusChange(kWriteError, transferError, request, path); + + return NS_OK; + } + + // Inform channel it is open on behalf of a download to throttle it during + // page loads and prevent its caching. + nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(aChannel); + if (httpInternal) { + rv = httpInternal->SetChannelIsForDownload(true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (mSourceUrl->SchemeIs("data")) { + // In case we're downloading a data:// uri + // we don't want to apply AllowTopLevelNavigationToDataURI. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + loadInfo->SetForceAllowDataURI(true); + } + + // now that the temp file is set up, find out if we need to invoke a dialog + // asking the user what they want us to do with this content... + + // We can get here for three reasons: "can't handle", "sniffed type", or + // "server sent content-disposition:attachment". In the first case we want + // to honor the user's "always ask" pref; in the other two cases we want to + // honor it only if the default action is "save". Opening attachments in + // helper apps by default breaks some websites (especially if the attachment + // is one part of a multipart document). Opening sniffed content in helper + // apps by default introduces security holes that we'd rather not have. + + // So let's find out whether the user wants to be prompted. If he does not, + // check mReason and the preferred action to see what we should do. + + bool alwaysAsk = true; + mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); + if (alwaysAsk) { + // But we *don't* ask if this mimeInfo didn't come from + // our user configuration datastore and the user has said + // at some point in the distant past that they don't + // want to be asked. The latter fact would have been + // stored in pref strings back in the old days. + + bool mimeTypeIsInDatastore = false; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + handlerSvc->Exists(mMimeInfo, &mimeTypeIsInDatastore); + } + if (!handlerSvc || !mimeTypeIsInDatastore) { + if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_SAVE_TO_DISK_PREF, + MIMEType.get())) { + // Don't need to ask after all. + alwaysAsk = false; + // Make sure action matches pref (save to disk). + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } else if (!GetNeverAskFlagFromPref(NEVER_ASK_FOR_OPEN_FILE_PREF, + MIMEType.get())) { + // Don't need to ask after all. + alwaysAsk = false; + } + } + } else if (MIMEType.EqualsLiteral("text/plain")) { + nsAutoCString ext; + mMimeInfo->GetPrimaryExtension(ext); + // If people are sending us ApplicationReputation-eligible files with + // text/plain mimetypes, enforce asking the user what to do. + if (!ext.IsEmpty()) { + nsAutoCString dummyFileName("f"); + if (ext.First() != '.') { + dummyFileName.Append("."); + } + ext.ReplaceChar(KNOWN_PATH_SEPARATORS FILE_ILLEGAL_CHARACTERS, '_'); + nsCOMPtr<nsIApplicationReputationService> appRep = + components::ApplicationReputation::Service(); + appRep->IsBinary(dummyFileName + ext, &alwaysAsk); + } + } + + int32_t action = nsIMIMEInfo::saveToDisk; + mMimeInfo->GetPreferredAction(&action); + + // OK, now check why we're here + if (!alwaysAsk && mReason != nsIHelperAppLauncherDialog::REASON_CANTHANDLE) { + // Force asking if we're not saving. See comment back when we fetched the + // alwaysAsk boolean for details. + alwaysAsk = (action != nsIMIMEInfo::saveToDisk); + } + + // If we're not asking, check we actually know what to do: + if (!alwaysAsk) { + alwaysAsk = action != nsIMIMEInfo::saveToDisk && + action != nsIMIMEInfo::useHelperApp && + action != nsIMIMEInfo::useSystemDefault; + } + + // if we were told that we _must_ save to disk without asking, all the stuff + // before this is irrelevant; override it + if (mForceSave) { + alwaysAsk = false; + action = nsIMIMEInfo::saveToDisk; + } + + if (alwaysAsk) { + // Display the dialog + mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // this will create a reference cycle (the dialog holds a reference to us as + // nsIHelperAppLauncher), which will be broken in Cancel or CreateTransfer. + nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent(); + rv = mDialog->Show(this, dialogParent, mReason); + + // what do we do if the dialog failed? I guess we should call Cancel and + // abort the load.... + } else { + // We need to do the save/open immediately, then. +#ifdef XP_WIN + /* We need to see whether the file we've got here could be + * executable. If it could, we had better not try to open it! + * We can skip this check, though, if we have a setting to open in a + * helper app. + * This code mirrors the code in + * nsExternalAppHandler::LaunchWithApplication so that what we + * test here is as close as possible to what will really be + * happening if we decide to execute + */ + nsCOMPtr<nsIHandlerApp> prefApp; + mMimeInfo->GetPreferredApplicationHandler(getter_AddRefs(prefApp)); + if (action != nsIMIMEInfo::useHelperApp || !prefApp) { + nsCOMPtr<nsIFile> fileToTest; + GetTargetFile(getter_AddRefs(fileToTest)); + if (fileToTest) { + bool isExecutable; + rv = fileToTest->IsExecutable(&isExecutable); + if (NS_FAILED(rv) || + isExecutable) { // checking NS_FAILED, because paranoia is good + action = nsIMIMEInfo::saveToDisk; + } + } else { // Paranoia is good here too, though this really should not + // happen + NS_WARNING( + "GetDownloadInfo returned a null file after the temp file has been " + "set up! "); + action = nsIMIMEInfo::saveToDisk; + } + } + +#endif + if (action == nsIMIMEInfo::useHelperApp || + action == nsIMIMEInfo::useSystemDefault) { + rv = LaunchWithApplication(mHandleInternally); + } else { + rv = PromptForSaveDestination(); + } + } + return NS_OK; +} + +// Convert error info into proper message text and send OnStatusChange +// notification to the dialog progress listener or nsITransfer implementation. +void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv, + nsIRequest* aRequest, + const nsString& path) { + const char* msgId = nullptr; + switch (rv) { + case NS_ERROR_OUT_OF_MEMORY: + // No memory + msgId = "noMemory"; + break; + + case NS_ERROR_FILE_DISK_FULL: + case NS_ERROR_FILE_NO_DEVICE_SPACE: + // Out of space on target volume. + msgId = "diskFull"; + break; + + case NS_ERROR_FILE_READ_ONLY: + // Attempt to write to read/only file. + msgId = "readOnly"; + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + if (type == kWriteError) { + // Attempt to write without sufficient permissions. +#if defined(ANDROID) + // On Android this means the SD card is present but + // unavailable (read-only). + msgId = "SDAccessErrorCardReadOnly"; +#else + msgId = "accessError"; +#endif + } else { + msgId = "launchError"; + } + break; + + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST: + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + // Helper app not found, let's verify this happened on launch + if (type == kLaunchError) { + msgId = "helperAppNotFound"; + break; + } +#if defined(ANDROID) + else if (type == kWriteError) { + // On Android this means the SD card is missing (not in + // SD slot). + msgId = "SDAccessErrorCardMissing"; + break; + } +#endif + [[fallthrough]]; + + default: + // Generic read/write/launch error message. + switch (type) { + case kReadError: + msgId = "readError"; + break; + case kWriteError: + msgId = "writeError"; + break; + case kLaunchError: + msgId = "launchError"; + break; + } + break; + } + + MOZ_LOG( + nsExternalHelperAppService::mLog, LogLevel::Error, + ("Error: %s, type=%i, listener=0x%p, transfer=0x%p, rv=0x%08" PRIX32 "\n", + msgId, type, mDialogProgressListener.get(), mTransfer.get(), + static_cast<uint32_t>(rv))); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + (" path='%s'\n", NS_ConvertUTF16toUTF8(path).get())); + + // Get properties file bundle and extract status string. + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::services::GetStringBundleService(); + if (stringService) { + nsCOMPtr<nsIStringBundle> bundle; + if (NS_SUCCEEDED(stringService->CreateBundle( + "chrome://global/locale/nsWebBrowserPersist.properties", + getter_AddRefs(bundle)))) { + nsAutoString msgText; + AutoTArray<nsString, 1> strings = {path}; + if (NS_SUCCEEDED(bundle->FormatStringFromName(msgId, strings, msgText))) { + if (mDialogProgressListener) { + // We have a listener, let it handle the error. + mDialogProgressListener->OnStatusChange( + nullptr, (type == kReadError) ? aRequest : nullptr, rv, + msgText.get()); + } else if (mTransfer) { + mTransfer->OnStatusChange(nullptr, + (type == kReadError) ? aRequest : nullptr, + rv, msgText.get()); + } else if (XRE_IsParentProcess()) { + // We don't have a listener. Simply show the alert ourselves. + nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent(); + nsresult qiRv; + nsCOMPtr<nsIPrompt> prompter(do_GetInterface(dialogParent, &qiRv)); + nsAutoString title; + bundle->FormatStringFromName("title", strings, title); + + MOZ_LOG( + nsExternalHelperAppService::mLog, LogLevel::Debug, + ("mBrowsingContext=0x%p, prompter=0x%p, qi rv=0x%08" PRIX32 + ", title='%s', msg='%s'", + mBrowsingContext.get(), prompter.get(), + static_cast<uint32_t>(qiRv), NS_ConvertUTF16toUTF8(title).get(), + NS_ConvertUTF16toUTF8(msgText).get())); + + // If we didn't have a prompter we will try and get a window + // instead, get it's docshell and use it to alert the user. + if (!prompter) { + nsCOMPtr<nsPIDOMWindowOuter> window(do_GetInterface(dialogParent)); + if (!window || !window->GetDocShell()) { + return; + } + + prompter = do_GetInterface(window->GetDocShell(), &qiRv); + + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Debug, + ("No prompter from mBrowsingContext, using DocShell, " + "window=0x%p, docShell=0x%p, " + "prompter=0x%p, qi rv=0x%08" PRIX32, + window.get(), window->GetDocShell(), prompter.get(), + static_cast<uint32_t>(qiRv))); + + // If we still don't have a prompter, there's nothing else we + // can do so just return. + if (!prompter) { + MOZ_LOG(nsExternalHelperAppService::mLog, LogLevel::Error, + ("No prompter from DocShell, no way to alert user")); + return; + } + } + + // We should always have a prompter at this point. + prompter->Alert(title.get(), msgText.get()); + } + } + } + } +} + +NS_IMETHODIMP +nsExternalAppHandler::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + nsresult rv = NS_OK; + // first, check to see if we've been canceled.... + if (mCanceled || !mSaver) { + // then go cancel our underlying channel too + return request->Cancel(NS_BINDING_ABORTED); + } + + // read the data out of the stream and write it to the temp file. + if (count > 0) { + mProgress += count; + + nsCOMPtr<nsIStreamListener> saver = do_QueryInterface(mSaver); + rv = saver->OnDataAvailable(request, inStr, sourceOffset, count); + if (NS_SUCCEEDED(rv)) { + // Send progress notification. + if (mTransfer) { + mTransfer->OnProgressChange64(nullptr, request, mProgress, + mContentLength, mProgress, + mContentLength); + } + } else { + // An error occurred, notify listener. + nsAutoString tempFilePath; + if (mTempFile) { + mTempFile->GetPath(tempFilePath); + } + SendStatusChange(kReadError, rv, request, tempFilePath); + + // Cancel the download. + Cancel(rv); + } + } + return rv; +} + +NS_IMETHODIMP nsExternalAppHandler::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + LOG( + ("nsExternalAppHandler::OnStopRequest\n" + " mCanceled=%d, mTransfer=0x%p, aStatus=0x%08" PRIX32 "\n", + mCanceled, mTransfer.get(), static_cast<uint32_t>(aStatus))); + + mStopRequestIssued = true; + + // Cancel if the request did not complete successfully. + if (!mCanceled && NS_FAILED(aStatus)) { + // Send error notification. + nsAutoString tempFilePath; + if (mTempFile) mTempFile->GetPath(tempFilePath); + SendStatusChange(kReadError, aStatus, request, tempFilePath); + + Cancel(aStatus); + } + + // first, check to see if we've been canceled.... + if (mCanceled || !mSaver) { + return NS_OK; + } + + return mSaver->Finish(NS_OK); +} + +NS_IMETHODIMP +nsExternalAppHandler::OnTargetChange(nsIBackgroundFileSaver* aSaver, + nsIFile* aTarget) { + return NS_OK; +} + +NS_IMETHODIMP +nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, + nsresult aStatus) { + LOG( + ("nsExternalAppHandler::OnSaveComplete\n" + " aSaver=0x%p, aStatus=0x%08" PRIX32 ", mCanceled=%d, mTransfer=0x%p\n", + aSaver, static_cast<uint32_t>(aStatus), mCanceled, mTransfer.get())); + + if (!mCanceled) { + // Save the hash and signature information + (void)mSaver->GetSha256Hash(mHash); + (void)mSaver->GetSignatureInfo(mSignatureInfo); + + // Free the reference that the saver keeps on us, even if we couldn't get + // the hash. + mSaver = nullptr; + + // Save the redirect information. + nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + nsresult rv = NS_OK; + nsCOMPtr<nsIMutableArray> redirectChain = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("nsExternalAppHandler: Got %zu redirects\n", + loadInfo->RedirectChain().Length())); + for (nsIRedirectHistoryEntry* entry : loadInfo->RedirectChain()) { + redirectChain->AppendElement(entry); + } + mRedirects = redirectChain; + } + + if (NS_FAILED(aStatus)) { + nsAutoString path; + mTempFile->GetPath(path); + + // It may happen when e10s is enabled that there will be no transfer + // object available to communicate status as expected by the system. + // Let's try and create a temporary transfer object to take care of this + // for us, we'll fall back to using the prompt service if we absolutely + // have to. + if (!mTransfer) { + // We don't care if this fails. + CreateFailedTransfer(); + } + + SendStatusChange(kWriteError, aStatus, nullptr, path); + if (!mCanceled) Cancel(aStatus); + return NS_OK; + } + } + + // Notify the transfer object that we are done if the user has chosen an + // action. If the user hasn't chosen an action, the progress listener + // (nsITransfer) will be notified in CreateTransfer. + if (mTransfer) { + NotifyTransfer(aStatus); + } + + return NS_OK; +} + +void nsExternalAppHandler::NotifyTransfer(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread(), "Must notify on main thread"); + MOZ_ASSERT(mTransfer, "We must have an nsITransfer"); + + LOG(("Notifying progress listener")); + + if (NS_SUCCEEDED(aStatus)) { + (void)mTransfer->SetSha256Hash(mHash); + (void)mTransfer->SetSignatureInfo(mSignatureInfo); + (void)mTransfer->SetRedirects(mRedirects); + (void)mTransfer->OnProgressChange64( + nullptr, nullptr, mProgress, mContentLength, mProgress, mContentLength); + } + + (void)mTransfer->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_NETWORK, + aStatus); + + // This nsITransfer object holds a reference to us (we are its observer), so + // we need to release the reference to break a reference cycle (and therefore + // to prevent leaking). We do this even if the previous calls failed. + mTransfer = nullptr; +} + +NS_IMETHODIMP nsExternalAppHandler::GetMIMEInfo(nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = mMimeInfo; + NS_ADDREF(*aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetSource(nsIURI** aSourceURI) { + NS_ENSURE_ARG(aSourceURI); + *aSourceURI = mSourceUrl; + NS_IF_ADDREF(*aSourceURI); + return NS_OK; +} + +NS_IMETHODIMP nsExternalAppHandler::GetSuggestedFileName( + nsAString& aSuggestedFileName) { + aSuggestedFileName = mSuggestedFileName; + return NS_OK; +} + +nsresult nsExternalAppHandler::CreateTransfer() { + LOG(("nsExternalAppHandler::CreateTransfer")); + + MOZ_ASSERT(NS_IsMainThread(), "Must create transfer on main thread"); + // We are back from the helper app dialog (where the user chooses to save or + // open), but we aren't done processing the load. in this case, throw up a + // progress dialog so the user can see what's going on. + // Also, release our reference to mDialog. We don't need it anymore, and we + // need to break the reference cycle. + mDialog = nullptr; + if (!mDialogProgressListener) { + NS_WARNING("The dialog should nullify the dialog progress listener"); + } + // In case of a non acceptable download, we need to cancel the request and + // pass a FailedTransfer for the Download UI. + if (mDownloadClassification != nsITransfer::DOWNLOAD_ACCEPTABLE) { + mCanceled = true; + mRequest->Cancel(NS_ERROR_ABORT); + return CreateFailedTransfer(); + } + nsresult rv; + + // We must be able to create an nsITransfer object. If not, it doesn't matter + // much that we can't launch the helper application or save to disk. Work on + // a local copy rather than mTransfer until we know we succeeded, to make it + // clearer that this function is re-entrant. + nsCOMPtr<nsITransfer> transfer = + do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Initialize the download + nsCOMPtr<nsIURI> target; + rv = NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest); + if (mBrowsingContext) { + rv = transfer->InitWithBrowsingContext( + mSourceUrl, target, u""_ns, mMimeInfo, mTimeDownloadStarted, mTempFile, + this, channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, mBrowsingContext, mHandleInternally); + } else { + rv = transfer->Init(mSourceUrl, target, u""_ns, mMimeInfo, + mTimeDownloadStarted, mTempFile, this, + channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification); + } + + NS_ENSURE_SUCCESS(rv, rv); + + // If we were cancelled since creating the transfer, just return. It is + // always ok to return NS_OK if we are cancelled. Callers of this function + // must call Cancel if CreateTransfer fails, but there's no need to cancel + // twice. + if (mCanceled) { + return NS_OK; + } + rv = transfer->OnStateChange(nullptr, mRequest, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_REQUEST | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); + NS_ENSURE_SUCCESS(rv, rv); + + if (mCanceled) { + return NS_OK; + } + + mRequest = nullptr; + // Finally, save the transfer to mTransfer. + mTransfer = transfer; + transfer = nullptr; + + // While we were bringing up the progress dialog, we actually finished + // processing the url. If that's the case then mStopRequestIssued will be + // true and OnSaveComplete has been called. + if (mStopRequestIssued && !mSaver && mTransfer) { + NotifyTransfer(NS_OK); + } + + return rv; +} + +nsresult nsExternalAppHandler::CreateFailedTransfer() { + nsresult rv; + nsCOMPtr<nsITransfer> transfer = + do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // If we don't have a download directory we're kinda screwed but it's OK + // we'll still report the error via the prompter. + nsCOMPtr<nsIFile> pseudoFile; + rv = GetDownloadDirectory(getter_AddRefs(pseudoFile), true); + NS_ENSURE_SUCCESS(rv, rv); + + // Append the default suggested filename. If the user restarts the transfer + // we will re-trigger a filename check anyway to ensure that it is unique. + rv = pseudoFile->Append(mSuggestedFileName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> pseudoTarget; + rv = NS_NewFileURI(getter_AddRefs(pseudoTarget), pseudoFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(mRequest); + if (mBrowsingContext) { + rv = transfer->InitWithBrowsingContext( + mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, mTimeDownloadStarted, + nullptr, this, channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification, mBrowsingContext, mHandleInternally); + } else { + rv = transfer->Init(mSourceUrl, pseudoTarget, u""_ns, mMimeInfo, + mTimeDownloadStarted, nullptr, this, + channel && NS_UsePrivateBrowsing(channel), + mDownloadClassification); + } + NS_ENSURE_SUCCESS(rv, rv); + + // Our failed transfer is ready. + mTransfer = std::move(transfer); + + return NS_OK; +} + +nsresult nsExternalAppHandler::SaveDestinationAvailable(nsIFile* aFile) { + if (aFile) + ContinueSave(aFile); + else + Cancel(NS_BINDING_ABORTED); + + return NS_OK; +} + +void nsExternalAppHandler::RequestSaveDestination( + const nsString& aDefaultFile, const nsString& aFileExtension) { + // Display the dialog + // XXX Convert to use file picker? No, then embeddors could not do any sort of + // "AutoDownload" w/o showing a prompt + nsresult rv = NS_OK; + if (!mDialog) { + // Get helper app launcher dialog. + mDialog = do_CreateInstance(NS_HELPERAPPLAUNCHERDLG_CONTRACTID, &rv); + if (rv != NS_OK) { + Cancel(NS_BINDING_ABORTED); + return; + } + } + + // we want to explicitly unescape aDefaultFile b4 passing into the dialog. we + // can't unescape it because the dialog is implemented by a JS component which + // doesn't have a window so no unescape routine is defined... + + // Now, be sure to keep |this| alive, and the dialog + // If we don't do this, users that close the helper app dialog while the file + // picker is up would cause Cancel() to be called, and the dialog would be + // released, which would release this object too, which would crash. + // See Bug 249143 + RefPtr<nsExternalAppHandler> kungFuDeathGrip(this); + nsCOMPtr<nsIHelperAppLauncherDialog> dlg(mDialog); + nsCOMPtr<nsIInterfaceRequestor> dialogParent = GetDialogParent(); + + rv = dlg->PromptForSaveToFileAsync(this, dialogParent, aDefaultFile.get(), + aFileExtension.get(), mForceSave); + if (NS_FAILED(rv)) { + Cancel(NS_BINDING_ABORTED); + } +} + +// PromptForSaveDestination should only be called by the helper app dialog which +// allows the user to say launch with application or save to disk. +NS_IMETHODIMP nsExternalAppHandler::PromptForSaveDestination() { + if (mCanceled) return NS_OK; + + mMimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + + if (mSuggestedFileName.IsEmpty()) { + RequestSaveDestination(mTempLeafName, mTempFileExtension); + } else { + nsAutoString fileExt; + int32_t pos = mSuggestedFileName.RFindChar('.'); + if (pos >= 0) { + mSuggestedFileName.Right(fileExt, mSuggestedFileName.Length() - pos); + } + if (fileExt.IsEmpty()) { + fileExt = mTempFileExtension; + } + + RequestSaveDestination(mSuggestedFileName, fileExt); + } + + return NS_OK; +} +nsresult nsExternalAppHandler::ContinueSave(nsIFile* aNewFileLocation) { + if (mCanceled) return NS_OK; + + MOZ_ASSERT(aNewFileLocation, "Must be called with a non-null file"); + + nsresult rv = NS_OK; + nsCOMPtr<nsIFile> fileToUse = aNewFileLocation; + mFinalFileDestination = fileToUse; + + // Move what we have in the final directory, but append .part + // to it, to indicate that it's unfinished. Do not call SetTarget on the + // saver if we are done (Finish has been called) but OnSaverComplete has not + // been called. + if (mFinalFileDestination && mSaver && !mStopRequestIssued) { + nsCOMPtr<nsIFile> movedFile; + mFinalFileDestination->Clone(getter_AddRefs(movedFile)); + if (movedFile) { + // Get the old leaf name and append .part to it + nsAutoString name; + mFinalFileDestination->GetLeafName(name); + name.AppendLiteral(".part"); + movedFile->SetLeafName(name); + + rv = mSaver->SetTarget(movedFile, true); + if (NS_FAILED(rv)) { + nsAutoString path; + mTempFile->GetPath(path); + SendStatusChange(kWriteError, rv, nullptr, path); + Cancel(rv); + return NS_OK; + } + + mTempFile = movedFile; + } + } + + // The helper app dialog has told us what to do and we have a final file + // destination. + rv = CreateTransfer(); + // If we fail to create the transfer, Cancel. + if (NS_FAILED(rv)) { + Cancel(rv); + return rv; + } + + return NS_OK; +} + +// LaunchWithApplication should only be called by the helper app dialog which +// allows the user to say launch with application or save to disk. +NS_IMETHODIMP nsExternalAppHandler::LaunchWithApplication( + bool aHandleInternally) { + if (mCanceled) return NS_OK; + + mHandleInternally = aHandleInternally; + + // Now check if the file is local, in which case we won't bother with saving + // it to a temporary directory and just launch it from where it is + nsCOMPtr<nsIFileURL> fileUrl(do_QueryInterface(mSourceUrl)); + if (fileUrl && mIsFileChannel) { + Cancel(NS_BINDING_ABORTED); + nsCOMPtr<nsIFile> file; + nsresult rv = fileUrl->GetFile(getter_AddRefs(file)); + + if (NS_SUCCEEDED(rv)) { + rv = mMimeInfo->LaunchWithFile(file); + if (NS_SUCCEEDED(rv)) return NS_OK; + } + nsAutoString path; + if (file) file->GetPath(path); + // If we get here, an error happened + SendStatusChange(kLaunchError, rv, nullptr, path); + return rv; + } + + // Now that the user has elected to launch the downloaded file with a helper + // app, we're justified in removing the 'salted' name. We'll rename to what + // was specified in mSuggestedFileName after the download is done prior to + // launching the helper app. So that any existing file of that name won't be + // overwritten we call CreateUnique(). Also note that we use the same + // directory as originally downloaded so the download can be renamed in place + // later. + nsCOMPtr<nsIFile> fileToUse; + (void)GetDownloadDirectory(getter_AddRefs(fileToUse)); + + if (mSuggestedFileName.IsEmpty()) { + // Keep using the leafname of the temp file, since we're just starting a + // helper + mSuggestedFileName = mTempLeafName; + } + +#ifdef XP_WIN + fileToUse->Append(mSuggestedFileName + mTempFileExtension); +#else + fileToUse->Append(mSuggestedFileName); +#endif + + nsresult rv = fileToUse->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + if (NS_SUCCEEDED(rv)) { + mFinalFileDestination = fileToUse; + // launch the progress window now that the user has picked the desired + // action. + rv = CreateTransfer(); + if (NS_FAILED(rv)) { + Cancel(rv); + } + } else { + // Cancel the download and report an error. We do not want to end up in + // a state where it appears that we have a normal download that is + // pointing to a file that we did not actually create. + nsAutoString path; + mTempFile->GetPath(path); + SendStatusChange(kWriteError, rv, nullptr, path); + Cancel(rv); + } + return rv; +} + +NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { + NS_ENSURE_ARG(NS_FAILED(aReason)); + + if (mCanceled) { + return NS_OK; + } + mCanceled = true; + + if (mSaver) { + // We are still writing to the target file. Give the saver a chance to + // close the target file, then notify the transfer object if necessary in + // the OnSaveComplete callback. + mSaver->Finish(aReason); + mSaver = nullptr; + } else { + if (mStopRequestIssued && mTempFile) { + // This branch can only happen when the user cancels the helper app dialog + // when the request has completed. The temp file has to be removed here, + // because mSaver has been released at that time with the temp file left. + (void)mTempFile->Remove(false); + } + + // Notify the transfer object that the download has been canceled, if the + // user has already chosen an action and we didn't notify already. + if (mTransfer) { + NotifyTransfer(aReason); + } + } + + // Break our reference cycle with the helper app dialog (set up in + // OnStartRequest) + mDialog = nullptr; + + mRequest = nullptr; + + // Release the listener, to break the reference cycle with it (we are the + // observer of the listener). + mDialogProgressListener = nullptr; + + return NS_OK; +} + +bool nsExternalAppHandler::GetNeverAskFlagFromPref(const char* prefName, + const char* aContentType) { + // Search the obsolete pref strings. + nsAutoCString prefCString; + Preferences::GetCString(prefName, prefCString); + if (prefCString.IsEmpty()) { + // Default is true, if not found in the pref string. + return true; + } + + NS_UnescapeURL(prefCString); + nsACString::const_iterator start, end; + prefCString.BeginReading(start); + prefCString.EndReading(end); + return !CaseInsensitiveFindInReadable(nsDependentCString(aContentType), start, + end); +} + +NS_IMETHODIMP +nsExternalAppHandler::GetName(nsACString& aName) { + aName.AssignLiteral("nsExternalAppHandler"); + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The following section contains our nsIMIMEService implementation and related +// methods. +// +////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// nsIMIMEService methods +NS_IMETHODIMP nsExternalHelperAppService::GetFromTypeAndExtension( + const nsACString& aMIMEType, const nsACString& aFileExt, + nsIMIMEInfo** _retval) { + MOZ_ASSERT(!aMIMEType.IsEmpty() || !aFileExt.IsEmpty(), + "Give me something to work with"); + MOZ_DIAGNOSTIC_ASSERT(aFileExt.FindChar('\0') == kNotFound, + "The extension should never contain null characters"); + LOG(("Getting mimeinfo from type '%s' ext '%s'\n", + PromiseFlatCString(aMIMEType).get(), + PromiseFlatCString(aFileExt).get())); + + *_retval = nullptr; + + // OK... we need a type. Get one. + nsAutoCString typeToUse(aMIMEType); + if (typeToUse.IsEmpty()) { + nsresult rv = GetTypeFromExtension(aFileExt, typeToUse); + if (NS_FAILED(rv)) return NS_ERROR_NOT_AVAILABLE; + } + + // We promise to only send lower case mime types to the OS + ToLowerCase(typeToUse); + + // First, ask the OS for a mime info + bool found; + nsresult rv = GetMIMEInfoFromOS(typeToUse, aFileExt, &found, _retval); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + LOG(("OS gave back 0x%p - found: %i\n", *_retval, found)); + // If we got no mimeinfo, something went wrong. Probably lack of memory. + if (!*_retval) return NS_ERROR_OUT_OF_MEMORY; + + // The handler service can make up for bad mime types by checking the file + // extension. If the mime type is known (in extras or in the handler + // service), we stop it doing so by flipping this bool to true. + bool trustMIMEType = false; + + // Check extras - not everything we support will be known by the OS store, + // unfortunately, and it may even miss some extensions that we know should + // be accepted. We only do this for non-octet-stream mimetypes, because + // our information for octet-stream would lead to us trying to open all such + // files as Binary file with exe, com or bin extension regardless of the + // real extension. + if (!typeToUse.Equals(APPLICATION_OCTET_STREAM, + nsCaseInsensitiveCStringComparator)) { + rv = FillMIMEInfoForMimeTypeFromExtras(typeToUse, !found, *_retval); + LOG(("Searched extras (by type), rv 0x%08" PRIX32 "\n", + static_cast<uint32_t>(rv))); + trustMIMEType = NS_SUCCEEDED(rv); + found = found || NS_SUCCEEDED(rv); + } + + // Now, let's see if we can find something in our datastore. + // This will not overwrite the OS information that interests us + // (i.e. default application, default app. description) + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + bool hasHandler = false; + (void)handlerSvc->Exists(*_retval, &hasHandler); + if (hasHandler) { + rv = handlerSvc->FillHandlerInfo(*_retval, ""_ns); + LOG(("Data source: Via type: retval 0x%08" PRIx32 "\n", + static_cast<uint32_t>(rv))); + trustMIMEType = trustMIMEType || NS_SUCCEEDED(rv); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + found = found || NS_SUCCEEDED(rv); + } + + // If we still haven't found anything, try finding a match for + // an extension in extras first: + if (!found && !aFileExt.IsEmpty()) { + rv = FillMIMEInfoForExtensionFromExtras(aFileExt, *_retval); + LOG(("Searched extras (by ext), rv 0x%08" PRIX32 "\n", + static_cast<uint32_t>(rv))); + } + + // Then check the handler service - but only do so if we really do not know + // the mimetype. This avoids overwriting good mimetype info with bad file + // extension info. + if ((!found || !trustMIMEType) && handlerSvc && !aFileExt.IsEmpty()) { + nsAutoCString overrideType; + rv = handlerSvc->GetTypeFromExtension(aFileExt, overrideType); + if (NS_SUCCEEDED(rv) && !overrideType.IsEmpty()) { + // We can't check handlerSvc->Exists() here, because we have a + // overideType. That's ok, it just results in some console noise. + // (If there's no handler for the override type, it throws) + rv = handlerSvc->FillHandlerInfo(*_retval, overrideType); + LOG(("Data source: Via ext: retval 0x%08" PRIx32 "\n", + static_cast<uint32_t>(rv))); + found = found || NS_SUCCEEDED(rv); + } + } + + // If we still don't have a match, at least set the file description + // to `${aFileExt} File` if it's empty: + if (!found && !aFileExt.IsEmpty()) { + // XXXzpao This should probably be localized + nsAutoCString desc(aFileExt); + desc.AppendLiteral(" File"); + (*_retval)->SetDescription(NS_ConvertASCIItoUTF16(desc)); + LOG(("Falling back to 'File' file description\n")); + } + + // Sometimes, OSes give us bad data. We have a set of forbidden extensions + // for some MIME types. If the primary extension is forbidden, + // overwrite it with a known-good one. See bug 1571247 for context. + nsAutoCString primaryExtension; + (*_retval)->GetPrimaryExtension(primaryExtension); + if (!primaryExtension.EqualsIgnoreCase(PromiseFlatCString(aFileExt).get())) { + if (MaybeReplacePrimaryExtension(primaryExtension, *_retval)) { + (*_retval)->GetPrimaryExtension(primaryExtension); + } + } + + // Finally, check if we got a file extension and if yes, if it is an + // extension on the mimeinfo, in which case we want it to be the primary one + if (!aFileExt.IsEmpty()) { + bool matches = false; + (*_retval)->ExtensionExists(aFileExt, &matches); + LOG(("Extension '%s' matches mime info: %i\n", + PromiseFlatCString(aFileExt).get(), matches)); + if (matches) { + nsAutoCString fileExt; + ToLowerCase(aFileExt, fileExt); + (*_retval)->SetPrimaryExtension(fileExt); + primaryExtension = fileExt; + } + } + + // Overwrite with a generic description if the primary extension for the + // type is in our list; these are file formats supported by Firefox and + // we don't want other brands positioning themselves as the sole viewer + // for a system. + if (!primaryExtension.IsEmpty()) { + for (const char* ext : descriptionOverwriteExtensions) { + if (primaryExtension.Equals(ext)) { + nsCOMPtr<nsIStringBundleService> bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIStringBundle> unknownContentTypeBundle; + rv = bundleService->CreateBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties", + getter_AddRefs(unknownContentTypeBundle)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString stringName(ext); + stringName.AppendLiteral("ExtHandlerDescription"); + nsAutoString handlerDescription; + rv = unknownContentTypeBundle->GetStringFromName(stringName.get(), + handlerDescription); + if (NS_SUCCEEDED(rv)) { + (*_retval)->SetDescription(handlerDescription); + } + } + break; + } + } + } + + if (LOG_ENABLED()) { + nsAutoCString type; + (*_retval)->GetMIMEType(type); + + LOG(("MIME Info Summary: Type '%s', Primary Ext '%s'\n", type.get(), + primaryExtension.get())); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsExternalHelperAppService::GetTypeFromExtension(const nsACString& aFileExt, + nsACString& aContentType) { + // OK. We want to try the following sources of mimetype information, in this + // order: + // 1. defaultMimeEntries array + // 2. OS-provided information + // 3. our "extras" array + // 4. Information from plugins + // 5. The "ext-to-type-mapping" category + // Note that, we are intentionally not looking at the handler service, because + // that can be affected by websites, which leads to undesired behavior. + + // Early return if called with an empty extension parameter + if (aFileExt.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // First of all, check our default entries + for (auto& entry : defaultMimeEntries) { + if (aFileExt.LowerCaseEqualsASCII(entry.mFileExtension)) { + aContentType = entry.mMimeType; + return NS_OK; + } + } + + // Ask OS. + if (GetMIMETypeFromOSForExtension(aFileExt, aContentType)) { + return NS_OK; + } + + // Check extras array. + bool found = GetTypeFromExtras(aFileExt, aContentType); + if (found) { + return NS_OK; + } + + // Try the plugins + RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst(); + if (pluginHost && + pluginHost->HavePluginForExtension(aFileExt, aContentType)) { + return NS_OK; + } + + // Let's see if an extension added something + nsCOMPtr<nsICategoryManager> catMan( + do_GetService("@mozilla.org/categorymanager;1")); + if (catMan) { + // The extension in the category entry is always stored as lowercase + nsAutoCString lowercaseFileExt(aFileExt); + ToLowerCase(lowercaseFileExt); + // Read the MIME type from the category entry, if available + nsCString type; + nsresult rv = + catMan->GetCategoryEntry("ext-to-type-mapping", lowercaseFileExt, type); + if (NS_SUCCEEDED(rv)) { + aContentType = type; + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExternalHelperAppService::GetPrimaryExtension( + const nsACString& aMIMEType, const nsACString& aFileExt, + nsACString& _retval) { + NS_ENSURE_ARG(!aMIMEType.IsEmpty()); + + nsCOMPtr<nsIMIMEInfo> mi; + nsresult rv = + GetFromTypeAndExtension(aMIMEType, aFileExt, getter_AddRefs(mi)); + if (NS_FAILED(rv)) return rv; + + return mi->GetPrimaryExtension(_retval); +} + +NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromURI( + nsIURI* aURI, nsACString& aContentType) { + NS_ENSURE_ARG_POINTER(aURI); + nsresult rv = NS_ERROR_NOT_AVAILABLE; + aContentType.Truncate(); + + // First look for a file to use. If we have one, we just use that. + nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI); + if (fileUrl) { + nsCOMPtr<nsIFile> file; + rv = fileUrl->GetFile(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + rv = GetTypeFromFile(file, aContentType); + if (NS_SUCCEEDED(rv)) { + // we got something! + return rv; + } + } + } + + // Now try to get an nsIURL so we don't have to do our own parsing + nsCOMPtr<nsIURL> url = do_QueryInterface(aURI); + if (url) { + nsAutoCString ext; + rv = url->GetFileExtension(ext); + if (NS_FAILED(rv)) return rv; + if (ext.IsEmpty()) return NS_ERROR_NOT_AVAILABLE; + + UnescapeFragment(ext, url, ext); + + return GetTypeFromExtension(ext, aContentType); + } + + // no url, let's give the raw spec a shot + nsAutoCString specStr; + rv = aURI->GetSpec(specStr); + if (NS_FAILED(rv)) return rv; + UnescapeFragment(specStr, aURI, specStr); + + // find the file extension (if any) + int32_t extLoc = specStr.RFindChar('.'); + int32_t specLength = specStr.Length(); + if (-1 != extLoc && extLoc != specLength - 1 && + // nothing over 20 chars long can be sanely considered an + // extension.... Dat dere would be just data. + specLength - extLoc < 20) { + return GetTypeFromExtension(Substring(specStr, extLoc + 1), aContentType); + } + + // We found no information; say so. + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExternalHelperAppService::GetTypeFromFile( + nsIFile* aFile, nsACString& aContentType) { + NS_ENSURE_ARG_POINTER(aFile); + nsresult rv; + + // Get the Extension + nsAutoString fileName; + rv = aFile->GetLeafName(fileName); + if (NS_FAILED(rv)) return rv; + + nsAutoCString fileExt; + if (!fileName.IsEmpty()) { + int32_t len = fileName.Length(); + for (int32_t i = len; i >= 0; i--) { + if (fileName[i] == char16_t('.')) { + CopyUTF16toUTF8(Substring(fileName, i + 1), fileExt); + break; + } + } + } + + if (fileExt.IsEmpty()) return NS_ERROR_FAILURE; + + return GetTypeFromExtension(fileExt, aContentType); +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForMimeTypeFromExtras( + const nsACString& aContentType, bool aOverwriteDescription, + nsIMIMEInfo* aMIMEInfo) { + NS_ENSURE_ARG(aMIMEInfo); + + NS_ENSURE_ARG(!aContentType.IsEmpty()); + + // Look for default entry with matching mime type. + nsAutoCString MIMEType(aContentType); + ToLowerCase(MIMEType); + for (auto entry : extraMimeEntries) { + if (MIMEType.Equals(entry.mMimeType)) { + // This is the one. Set attributes appropriately. + nsDependentCString extensions(entry.mFileExtensions); + nsACString::const_iterator start, end; + extensions.BeginReading(start); + extensions.EndReading(end); + while (start != end) { + nsACString::const_iterator cursor = start; + mozilla::Unused << FindCharInReadable(',', cursor, end); + aMIMEInfo->AppendExtension(Substring(start, cursor)); + // If a comma was found, skip it for the next search. + start = cursor != end ? ++cursor : cursor; + } + + nsAutoString desc; + aMIMEInfo->GetDescription(desc); + if (aOverwriteDescription || desc.IsEmpty()) { + aMIMEInfo->SetDescription(NS_ConvertASCIItoUTF16(entry.mDescription)); + } + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsExternalHelperAppService::FillMIMEInfoForExtensionFromExtras( + const nsACString& aExtension, nsIMIMEInfo* aMIMEInfo) { + nsAutoCString type; + bool found = GetTypeFromExtras(aExtension, type); + if (!found) return NS_ERROR_NOT_AVAILABLE; + return FillMIMEInfoForMimeTypeFromExtras(type, true, aMIMEInfo); +} + +bool nsExternalHelperAppService::MaybeReplacePrimaryExtension( + const nsACString& aPrimaryExtension, nsIMIMEInfo* aMIMEInfo) { + for (const auto& entry : sForbiddenPrimaryExtensions) { + if (aPrimaryExtension.LowerCaseEqualsASCII(entry.mFileExtension)) { + nsDependentCString mime(entry.mMimeType); + for (const auto& extraEntry : extraMimeEntries) { + if (mime.LowerCaseEqualsASCII(extraEntry.mMimeType)) { + nsDependentCString goodExts(extraEntry.mFileExtensions); + int32_t commaPos = goodExts.FindChar(','); + commaPos = commaPos == kNotFound ? goodExts.Length() : commaPos; + auto goodExt = Substring(goodExts, 0, commaPos); + aMIMEInfo->SetPrimaryExtension(goodExt); + return true; + } + } + } + } + return false; +} + +bool nsExternalHelperAppService::GetTypeFromExtras(const nsACString& aExtension, + nsACString& aMIMEType) { + NS_ASSERTION(!aExtension.IsEmpty(), "Empty aExtension parameter!"); + + // Look for default entry with matching extension. + nsDependentCString::const_iterator start, end, iter; + int32_t numEntries = ArrayLength(extraMimeEntries); + for (int32_t index = 0; index < numEntries; index++) { + nsDependentCString extList(extraMimeEntries[index].mFileExtensions); + extList.BeginReading(start); + extList.EndReading(end); + iter = start; + while (start != end) { + FindCharInReadable(',', iter, end); + if (Substring(start, iter) + .Equals(aExtension, nsCaseInsensitiveCStringComparator)) { + aMIMEType = extraMimeEntries[index].mMimeType; + return true; + } + if (iter != end) { + ++iter; + } + start = iter; + } + } + + return false; +} + +bool nsExternalHelperAppService::GetMIMETypeFromOSForExtension( + const nsACString& aExtension, nsACString& aMIMEType) { + bool found = false; + nsCOMPtr<nsIMIMEInfo> mimeInfo; + nsresult rv = + GetMIMEInfoFromOS(""_ns, aExtension, &found, getter_AddRefs(mimeInfo)); + return NS_SUCCEEDED(rv) && found && mimeInfo && + NS_SUCCEEDED(mimeInfo->GetMIMEType(aMIMEType)); +} + +nsresult nsExternalHelperAppService::GetMIMEInfoFromOS( + const nsACString& aMIMEType, const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = nullptr; + *aFound = false; + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h new file mode 100644 index 0000000000..8c87587e9e --- /dev/null +++ b/uriloader/exthandler/nsExternalHelperAppService.h @@ -0,0 +1,512 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsExternalHelperAppService_h__ +#define nsExternalHelperAppService_h__ + +#include "mozilla/Logging.h" +#include "prtime.h" + +#include "nsIExternalHelperAppService.h" +#include "nsIExternalProtocolService.h" +#include "nsIWebProgressListener2.h" +#include "nsIHelperAppLauncherDialog.h" + +#include "nsIMIMEInfo.h" +#include "nsIMIMEService.h" +#include "nsINamed.h" +#include "nsIStreamListener.h" +#include "nsIFile.h" +#include "nsString.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIChannel.h" +#include "nsIBackgroundFileSaver.h" + +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsCOMArray.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsExternalAppHandler; +class nsIMIMEInfo; +class nsITransfer; +class nsIPrincipal; +class MaybeCloseWindowHelper; + +#define EXTERNAL_APP_HANDLER_IID \ + { \ + 0x50eb7479, 0x71ff, 0x4ef8, { \ + 0xb3, 0x1e, 0x3b, 0x59, 0xc8, 0xab, 0xb9, 0x24 \ + } \ + } + +/** + * The helper app service. Responsible for handling content that Mozilla + * itself can not handle + * Note that this is an abstract class - we depend on appropriate subclassing + * on a per-OS basis to implement some methods. + */ +class nsExternalHelperAppService : public nsIExternalHelperAppService, + public nsPIExternalAppLauncher, + public nsIExternalProtocolService, + public nsIMIMEService, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALHELPERAPPSERVICE + NS_DECL_NSPIEXTERNALAPPLAUNCHER + NS_DECL_NSIMIMESERVICE + NS_DECL_NSIOBSERVER + + nsExternalHelperAppService(); + + /** + * Initializes internal state. Will be called automatically when + * this service is first instantiated. + */ + [[nodiscard]] nsresult Init(); + + /** + * nsIExternalProtocolService methods that we provide in this class. Other + * methods should be implemented by per-OS subclasses. + */ + NS_IMETHOD ExternalProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + NS_IMETHOD IsExposedProtocol(const char* aProtocolScheme, + bool* aResult) override; + NS_IMETHOD GetProtocolHandlerInfo(const nsACString& aScheme, + nsIHandlerInfo** aHandlerInfo) override; + NS_IMETHOD LoadURI(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal, + mozilla::dom::BrowsingContext* aBrowsingContext) override; + NS_IMETHOD SetProtocolHandlerDefaults(nsIHandlerInfo* aHandlerInfo, + bool aOSHandlerExists) override; + + /** + * Given a string identifying an application, create an nsIFile representing + * it. This function should look in $PATH for the application. + * The base class implementation will first try to interpret platformAppPath + * as an absolute path, and if that fails it will look for a file next to the + * mozilla executable. Subclasses can override this method if they want a + * different behaviour. + * @param platformAppPath A platform specific path to an application that we + * got out of the rdf data source. This can be a mac + * file spec, a unix path or a windows path depending + * on the platform + * @param aFile [out] An nsIFile representation of that platform + * application path. + */ + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile); + + NS_IMETHOD OSProtocolHandlerExists(const char* aScheme, bool* aExists) = 0; + + /** + * Given an extension, get a MIME type string. If not overridden by + * the OS-specific nsOSHelperAppService, will call into GetMIMEInfoFromOS + * with an empty mimetype. + * @return true if we successfully found a mimetype. + */ + virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension, + nsACString& aMIMEType); + + static already_AddRefed<nsExternalHelperAppService> GetSingleton(); + + protected: + virtual ~nsExternalHelperAppService(); + + /** + * Searches the "extra" array of MIMEInfo objects for an object + * with a specific type. If found, it will modify the passed-in + * MIMEInfo. Otherwise, it will return an error and the MIMEInfo + * will be untouched. + * @param aContentType The type to search for. + * @param aOverwriteDescription Whether to overwrite the description + * @param aMIMEInfo [inout] The mime info, if found + */ + nsresult FillMIMEInfoForMimeTypeFromExtras(const nsACString& aContentType, + bool aOverwriteDescription, + nsIMIMEInfo* aMIMEInfo); + /** + * Searches the "extra" array of MIMEInfo objects for an object + * with a specific extension. + * + * Does not change the MIME Type of the MIME Info. + * + * @see FillMIMEInfoForMimeTypeFromExtras + */ + nsresult FillMIMEInfoForExtensionFromExtras(const nsACString& aExtension, + nsIMIMEInfo* aMIMEInfo); + + /** + * Replace the primary extension of the mimeinfo object if it's in our + * list of forbidden extensions. This fixes up broken information + * provided to us by the OS. + */ + bool MaybeReplacePrimaryExtension(const nsACString& aPrimaryExtension, + nsIMIMEInfo* aMIMEInfo); + + /** + * Searches the "extra" array for a MIME type, and gets its extension. + * @param aExtension The extension to search for + * @param aMIMEType [out] The found MIME type. + * @return true if the extension was found, false otherwise. + */ + bool GetTypeFromExtras(const nsACString& aExtension, nsACString& aMIMEType); + + /** + * Logging Module. Usage: set MOZ_LOG=HelperAppService:level, where level + * should be 2 for errors, 3 for debug messages from the cross- platform + * nsExternalHelperAppService, and 4 for os-specific debug messages. + */ + static mozilla::LazyLogModule mLog; + + // friend, so that it can access the nspr log module. + friend class nsExternalAppHandler; + + /** + * Helper function for ExpungeTemporaryFiles and ExpungeTemporaryPrivateFiles + */ + static void ExpungeTemporaryFilesHelper(nsCOMArray<nsIFile>& fileList); + /** + * Helper function for DeleteTemporaryFileOnExit and + * DeleteTemporaryPrivateFileWhenPossible + */ + static nsresult DeleteTemporaryFileHelper(nsIFile* aTemporaryFile, + nsCOMArray<nsIFile>& aFileList); + /** + * Functions related to the tempory file cleanup service provided by + * nsExternalHelperAppService + */ + void ExpungeTemporaryFiles(); + /** + * Functions related to the tempory file cleanup service provided by + * nsExternalHelperAppService (for the temporary files added during + * the private browsing mode) + */ + void ExpungeTemporaryPrivateFiles(); + + /** + * Array for the files that should be deleted + */ + nsCOMArray<nsIFile> mTemporaryFilesList; + /** + * Array for the files that should be deleted (for the temporary files + * added during the private browsing mode) + */ + nsCOMArray<nsIFile> mTemporaryPrivateFilesList; + + private: + nsresult DoContentContentProcessHelper( + const nsACString& aMimeContentType, nsIRequest* aRequest, + mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener); +}; + +/** + * An external app handler is just a small little class that presents itself as + * a nsIStreamListener. It saves the incoming data into a temp file. The handler + * is bound to an application when it is created. When it receives an + * OnStopRequest it launches the application using the temp file it has + * stored the data into. We create a handler every time we have to process + * data using a helper app. + */ +class nsExternalAppHandler final : public nsIStreamListener, + public nsIHelperAppLauncher, + public nsIBackgroundFileSaverObserver, + public nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIHELPERAPPLAUNCHER + NS_DECL_NSICANCELABLE + NS_DECL_NSIBACKGROUNDFILESAVEROBSERVER + NS_DECL_NSINAMED + + NS_DECLARE_STATIC_IID_ACCESSOR(EXTERNAL_APP_HANDLER_IID) + + /** + * @param aMIMEInfo MIMEInfo object, representing the type of the + * content that should be handled + * @param aFileExtension The extension we need to append to our temp file, + * INCLUDING the ".". e.g. .mp3 + * @param aContentContext dom Window context, as passed to DoContent. + * @param aWindowContext Top level window context used in dialog parenting, + * as passed to DoContent. This parameter may be null, + * in which case dialogs will be parented to + * aContentContext. + * @param mExtProtSvc nsExternalHelperAppService on creation + * @param aFileName The filename to use + * @param aReason A constant from nsIHelperAppLauncherDialog + * indicating why the request is handled by a helper app. + */ + nsExternalAppHandler(nsIMIMEInfo* aMIMEInfo, const nsACString& aFileExtension, + mozilla::dom::BrowsingContext* aBrowsingContext, + nsIInterfaceRequestor* aWindowContext, + nsExternalHelperAppService* aExtProtSvc, + const nsAString& aFilename, uint32_t aReason, + bool aForceSave); + + /** + * Clean up after the request was diverted to the parent process. + */ + void DidDivertRequest(nsIRequest* request); + + /** + * Apply content conversions if needed. + */ + void MaybeApplyDecodingForExtension(nsIRequest* request); + + void SetShouldCloseWindow() { mShouldCloseWindow = true; } + + protected: + ~nsExternalAppHandler(); + + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsIURI> mSourceUrl; + nsString mTempFileExtension; + nsString mTempLeafName; + + /** + * The MIME Info for this load. Will never be null. + */ + nsCOMPtr<nsIMIMEInfo> mMimeInfo; + + /** + * The BrowsingContext associated with this request to handle content. + */ + RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext; + + /** + * If set, the parent window helper app dialogs and file pickers + * should use in parenting. If null, we use mContentContext. + */ + nsCOMPtr<nsIInterfaceRequestor> mWindowContext; + + /** + * Used to close the window on a timer, to avoid any exceptions that are + * thrown if we try to close the window before it's fully loaded. + */ + RefPtr<MaybeCloseWindowHelper> mMaybeCloseWindowHelper; + + /** + * The following field is set if we were processing an http channel that had + * a content disposition header which specified the SUGGESTED file name we + * should present to the user in the save to disk dialog. + */ + nsString mSuggestedFileName; + + /** + * If set, this handler should forcibly save the file to disk regardless of + * MIME info settings or anything else, without ever popping up the + * unknown content type handling dialog. + */ + bool mForceSave; + + /** + * The canceled flag is set if the user canceled the launching of this + * application before we finished saving the data to a temp file. + */ + bool mCanceled; + + /** + * True if a stop request has been issued. + */ + bool mStopRequestIssued; + + bool mIsFileChannel; + + /** + * True if the ExternalHelperAppChild told us that we should close the window + * if we handle the content as a download. + */ + bool mShouldCloseWindow; + + /** + * True if the file should be handled internally. + */ + bool mHandleInternally; + + /** + * One of the REASON_ constants from nsIHelperAppLauncherDialog. Indicates the + * reason the dialog was shown (unknown content type, server requested it, + * etc). + */ + uint32_t mReason; + + /** + * Indicates if the nsContentSecurityUtils rate this download as + * acceptable, potentialy unwanted or illigal request. + * + */ + int32_t mDownloadClassification; + + /** + * Track the executable-ness of the temporary file. + */ + bool mTempFileIsExecutable; + + PRTime mTimeDownloadStarted; + int64_t mContentLength; + int64_t mProgress; /**< Number of bytes received (for sending progress + notifications). */ + + /** + * When we are told to save the temp file to disk (in a more permament + * location) before we are done writing the content to a temp file, then + * we need to remember the final destination until we are ready to use it. + */ + nsCOMPtr<nsIFile> mFinalFileDestination; + + uint32_t mBufferSize; + + /** + * This object handles saving the data received from the network to a + * temporary location first, and then move the file to its final location, + * doing all the input/output on a background thread. + */ + nsCOMPtr<nsIBackgroundFileSaver> mSaver; + + /** + * Stores the SHA-256 hash associated with the file that we downloaded. + */ + nsAutoCString mHash; + /** + * Stores the signature information of the downloaded file in an nsTArray of + * nsTArray of Array of bytes. If the file is unsigned this will be + * empty. + */ + nsTArray<nsTArray<nsTArray<uint8_t>>> mSignatureInfo; + /** + * Stores the redirect information associated with the channel. + */ + nsCOMPtr<nsIArray> mRedirects; + /** + * Get the dialog parent: the parent window that we can attach + * a dialog to when prompting the user for a download. + */ + already_AddRefed<nsIInterfaceRequestor> GetDialogParent(); + /** + * Creates the temporary file for the download and an output stream for it. + * Upon successful return, both mTempFile and mSaver will be valid. + */ + nsresult SetUpTempFile(nsIChannel* aChannel); + /** + * When we download a helper app, we are going to retarget all load + * notifications into our own docloader and load group instead of + * using the window which initiated the load....RetargetLoadNotifications + * contains that information... + */ + void RetargetLoadNotifications(nsIRequest* request); + /** + * Once the user tells us how they want to dispose of the content + * create an nsITransfer so they know what's going on. If this fails, the + * caller MUST call Cancel. + */ + nsresult CreateTransfer(); + + /** + * If we fail to create the necessary temporary file to initiate a transfer + * we will report the failure by creating a failed nsITransfer. + */ + nsresult CreateFailedTransfer(); + + /* + * The following two functions are part of the split of SaveToDisk + * to make it async, and works as following: + * + * SaveToDisk -------> RequestSaveDestination + * . + * . + * v + * ContinueSave <------- SaveDestinationAvailable + */ + + /** + * This is called by SaveToDisk to decide what's the final + * file destination chosen by the user or by auto-download settings. + */ + void RequestSaveDestination(const nsString& aDefaultFile, + const nsString& aDefaultFileExt); + + /** + * When SaveToDisk is called, it possibly delegates to RequestSaveDestination + * to decide the file destination. ContinueSave must then be called when + * the final destination is finally known. + * @param aFile The file that was chosen as the final destination. + * Must not be null. + */ + nsresult ContinueSave(nsIFile* aFile); + + /** + * Notify our nsITransfer object that we are done with the download. This is + * always called after the target file has been closed. + * + * @param aStatus + * NS_OK for success, or a failure code if the download failed. + * A partially downloaded file may still be available in this case. + */ + void NotifyTransfer(nsresult aStatus); + + /** + * Helper routine that searches a pref string for a given mime type + */ + bool GetNeverAskFlagFromPref(const char* prefName, const char* aContentType); + + /** + * Helper routine that checks whether we should enforce an extension + * for this file. + */ + bool ShouldForceExtension(const nsString& aFileExt); + + /** + * Helper routine to ensure that mSuggestedFileName ends in the correct + * extension, in case the original extension contains invalid characters + * or if this download is for a mimetype where we enforce using a specific + * extension (image/, video/, and audio/ based mimetypes, and a few specific + * document types). + * + * It also ensure that mTempFileExtension only contains an extension + * when it is different from mSuggestedFileName's extension. + */ + void EnsureCorrectExtension(const nsString& aFileExt); + + typedef enum { kReadError, kWriteError, kLaunchError } ErrorType; + /** + * Utility function to send proper error notification to web progress listener + */ + void SendStatusChange(ErrorType type, nsresult aStatus, nsIRequest* aRequest, + const nsString& path); + + /** + * Set in nsHelperDlgApp.js. This is always null after the user has chosen an + * action. + */ + nsCOMPtr<nsIWebProgressListener2> mDialogProgressListener; + /** + * Set once the user has chosen an action. This is null after the download + * has been canceled or completes. + */ + nsCOMPtr<nsITransfer> mTransfer; + + nsCOMPtr<nsIHelperAppLauncherDialog> mDialog; + + /** + + * The request that's being loaded. Initialized in OnStartRequest. + * Nulled out in OnStopRequest or once we know what we're doing + * with the data, whichever happens later. + */ + nsCOMPtr<nsIRequest> mRequest; + + RefPtr<nsExternalHelperAppService> mExtProtSvc; +}; +NS_DEFINE_STATIC_IID_ACCESSOR(nsExternalAppHandler, EXTERNAL_APP_HANDLER_IID) + +#endif // nsExternalHelperAppService_h__ diff --git a/uriloader/exthandler/nsExternalProtocolHandler.cpp b/uriloader/exthandler/nsExternalProtocolHandler.cpp new file mode 100644 index 0000000000..30b806d7c2 --- /dev/null +++ b/uriloader/exthandler/nsExternalProtocolHandler.cpp @@ -0,0 +1,545 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/BasePrincipal.h" +#include "nsIURI.h" +#include "nsExternalProtocolHandler.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsNetUtil.h" +#include "nsContentSecurityManager.h" +#include "nsExternalHelperAppService.h" + +// used to dispatch urls to default protocol handlers +#include "nsCExternalHandlerService.h" +#include "nsIExternalProtocolService.h" +#include "nsIChildChannel.h" +#include "nsIParentChannel.h" + +class nsILoadInfo; + +//////////////////////////////////////////////////////////////////////// +// a stub channel implemenation which will map calls to AsyncRead and +// OpenInputStream to calls in the OS for loading the url. +//////////////////////////////////////////////////////////////////////// + +class nsExtProtocolChannel : public nsIChannel, + public nsIChildChannel, + public nsIParentChannel { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUEST + NS_DECL_NSICHILDCHANNEL + NS_DECL_NSIPARENTCHANNEL + + nsExtProtocolChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo); + + private: + virtual ~nsExtProtocolChannel(); + + nsresult OpenURL(); + void Finish(nsresult aResult); + + nsCOMPtr<nsIURI> mUrl; + nsCOMPtr<nsIURI> mOriginalURI; + nsresult mStatus; + nsLoadFlags mLoadFlags; + bool mWasOpened; + bool mCanceled; + // Set true (as a result of ConnectParent invoked from child process) + // when this channel is on the parent process and is being used as + // a redirect target channel. It turns AsyncOpen into a no-op since + // we do it on the child. + bool mConnectedParent; + + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsCOMPtr<nsILoadInfo> mLoadInfo; + nsCOMPtr<nsIStreamListener> mListener; +}; + +NS_IMPL_ADDREF(nsExtProtocolChannel) +NS_IMPL_RELEASE(nsExtProtocolChannel) + +NS_INTERFACE_MAP_BEGIN(nsExtProtocolChannel) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChildChannel) + NS_INTERFACE_MAP_ENTRY(nsIParentChannel) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) +NS_INTERFACE_MAP_END + +nsExtProtocolChannel::nsExtProtocolChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mUrl(aURI), + mOriginalURI(aURI), + mStatus(NS_OK), + mLoadFlags(nsIRequest::LOAD_NORMAL), + mWasOpened(false), + mCanceled(false), + mConnectedParent(false), + mLoadInfo(aLoadInfo) {} + +nsExtProtocolChannel::~nsExtProtocolChannel() {} + +NS_IMETHODIMP nsExtProtocolChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + NS_IF_ADDREF(*aLoadGroup = mLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + mLoadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + NS_IF_ADDREF(*aCallbacks = mCallbacks); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) { + mCallbacks = aCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsExtProtocolChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + *aSecurityInfo = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetOriginalURI(nsIURI** aURI) { + NS_ADDREF(*aURI = mOriginalURI); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetOriginalURI(nsIURI* aURI) { + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetURI(nsIURI** aURI) { + *aURI = mUrl; + NS_IF_ADDREF(*aURI); + return NS_OK; +} + +nsresult nsExtProtocolChannel::OpenURL() { + nsresult rv = NS_ERROR_FAILURE; + nsCOMPtr<nsIExternalProtocolService> extProtService( + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID)); + + if (extProtService) { +#ifdef DEBUG + nsAutoCString urlScheme; + mUrl->GetScheme(urlScheme); + bool haveHandler = false; + extProtService->ExternalProtocolHandlerExists(urlScheme.get(), + &haveHandler); + NS_ASSERTION(haveHandler, + "Why do we have a channel for this url if we don't support " + "the protocol?"); +#endif + + RefPtr<mozilla::dom::BrowsingContext> ctx; + rv = mLoadInfo->GetTargetBrowsingContext(getter_AddRefs(ctx)); + if (NS_FAILED(rv)) { + goto finish; + } + + RefPtr<nsIPrincipal> principal = mLoadInfo->TriggeringPrincipal(); + rv = extProtService->LoadURI(mUrl, principal, ctx); + + if (NS_SUCCEEDED(rv) && mListener) { + mStatus = NS_ERROR_NO_CONTENT; + + RefPtr<nsExtProtocolChannel> self = this; + nsCOMPtr<nsIStreamListener> listener = mListener; + MessageLoop::current()->PostTask(NS_NewRunnableFunction( + "nsExtProtocolChannel::OpenURL", [self, listener]() { + listener->OnStartRequest(self); + listener->OnStopRequest(self, self->mStatus); + })); + } + } + +finish: + mCallbacks = nullptr; + mListener = nullptr; + return rv; +} + +NS_IMETHODIMP nsExtProtocolChannel::Open(nsIInputStream** aStream) { + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + return OpenURL(); +} + +NS_IMETHODIMP nsExtProtocolChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_FAILED(rv)) { + mCallbacks = nullptr; + return rv; + } + + if (mConnectedParent) { + return NS_OK; + } + + MOZ_ASSERT( + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); + + mWasOpened = true; + mListener = listener; + + return OpenURL(); +} + +NS_IMETHODIMP nsExtProtocolChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsExtProtocolChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsExtProtocolChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentType(nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentType( + const nsACString& aContentType) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentCharset( + nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentCharset( + const nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentDisposition( + uint32_t* aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentDisposition( + uint32_t aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetContentLength(int64_t* aContentLength) { + *aContentLength = -1; + return NS_OK; +} + +NS_IMETHODIMP +nsExtProtocolChannel::SetContentLength(int64_t aContentLength) { + MOZ_ASSERT_UNREACHABLE("SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetOwner(nsISupports** aPrincipal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetOwner(nsISupports* aPrincipal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// From nsIRequest +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsExtProtocolChannel::GetName(nsACString& result) { + return mUrl->GetSpec(result); +} + +NS_IMETHODIMP nsExtProtocolChannel::IsPending(bool* result) { + *result = false; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetStatus(nsresult* status) { + *status = mStatus; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Cancel(nsresult status) { + if (NS_SUCCEEDED(mStatus)) { + mStatus = status; + } + mCanceled = true; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Suspend() { + MOZ_ASSERT_UNREACHABLE("Suspend"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::Resume() { + MOZ_ASSERT_UNREACHABLE("Resume"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +/////////////////////////////////////////////////////////////////////// +// From nsIChildChannel +////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsExtProtocolChannel::ConnectParent(uint32_t registrarId) { + mozilla::dom::ContentChild::GetSingleton() + ->SendExtProtocolChannelConnectParent(registrarId); + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::CompleteRedirectSetup( + nsIStreamListener* listener) { + // For redirects to external protocols we AsyncOpen on the child + // (not the parent) because child channel has the right docshell + // (which is needed for the select dialog). + return AsyncOpen(listener); +} + +/////////////////////////////////////////////////////////////////////// +// From nsIParentChannel (derives from nsIStreamListener) +////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsExtProtocolChannel::SetParentListener( + mozilla::net::ParentChannelListener* aListener) { + // This is called as part of the connect parent operation from + // ContentParent::RecvExtProtocolChannelConnectParent. Setting + // this flag tells this channel to not proceed and makes AsyncOpen + // just no-op. Actual operation will happen from the child process + // via CompleteRedirectSetup call on the child channel. + mConnectedParent = true; + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetClassifierMatchedInfo( + const nsACString& aList, const nsACString& aProvider, + const nsACString& aFullHash) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::SetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::NotifyClassificationFlags( + uint32_t aClassificationFlags, bool aIsThirdParty) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::NotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::Delete() { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP nsExtProtocolChannel::GetRemoteType(nsACString& aRemoteType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnStartRequest(nsIRequest* aRequest) { + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + MOZ_ASSERT(NS_FAILED(aStatusCode)); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP nsExtProtocolChannel::OnDataAvailable( + nsIRequest* aRequest, nsIInputStream* aInputStream, uint64_t aOffset, + uint32_t aCount) { + // no data is expected + MOZ_CRASH("No data expected from external protocol channel"); + return NS_ERROR_UNEXPECTED; +} + +/////////////////////////////////////////////////////////////////////// +// the default protocol handler implementation +////////////////////////////////////////////////////////////////////// + +nsExternalProtocolHandler::nsExternalProtocolHandler() { + m_schemeName = "default"; +} + +nsExternalProtocolHandler::~nsExternalProtocolHandler() {} + +NS_IMPL_ADDREF(nsExternalProtocolHandler) +NS_IMPL_RELEASE(nsExternalProtocolHandler) + +NS_INTERFACE_MAP_BEGIN(nsExternalProtocolHandler) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIProtocolHandler) + NS_INTERFACE_MAP_ENTRY(nsIProtocolHandler) + NS_INTERFACE_MAP_ENTRY(nsIExternalProtocolHandler) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP nsExternalProtocolHandler::GetScheme(nsACString& aScheme) { + aScheme = m_schemeName; + return NS_OK; +} + +NS_IMETHODIMP nsExternalProtocolHandler::GetDefaultPort(int32_t* aDefaultPort) { + *aDefaultPort = 0; + return NS_OK; +} + +NS_IMETHODIMP +nsExternalProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} +// returns TRUE if the OS can handle this protocol scheme and false otherwise. +bool nsExternalProtocolHandler::HaveExternalProtocolHandler(nsIURI* aURI) { + MOZ_ASSERT(aURI); + nsAutoCString scheme; + aURI->GetScheme(scheme); + + nsCOMPtr<nsIExternalProtocolService> extProtSvc( + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID)); + if (!extProtSvc) { + return false; + } + + bool haveHandler = false; + extProtSvc->ExternalProtocolHandlerExists(scheme.get(), &haveHandler); + return haveHandler; +} + +NS_IMETHODIMP nsExternalProtocolHandler::GetProtocolFlags(uint32_t* aUritype) { + // Make it norelative since it is a simple uri + *aUritype = URI_NORELATIVE | URI_NOAUTH | URI_LOADABLE_BY_ANYONE | + URI_NON_PERSISTABLE | URI_DOES_NOT_RETURN_DATA; + return NS_OK; +} + +NS_IMETHODIMP +nsExternalProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** aRetval) { + NS_ENSURE_TRUE(aURI, NS_ERROR_UNKNOWN_PROTOCOL); + NS_ENSURE_TRUE(aRetval, NS_ERROR_UNKNOWN_PROTOCOL); + + // Only try to return a channel if we have a protocol handler for the url. + // nsOSHelperAppService::LoadUriInternal relies on this to check trustedness + // for some platforms at least. (win uses ::ShellExecute and unix uses + // gnome_url_show.) + if (!HaveExternalProtocolHandler(aURI)) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + nsCOMPtr<nsIChannel> channel = new nsExtProtocolChannel(aURI, aLoadInfo); + channel.forget(aRetval); + return NS_OK; +} + +/////////////////////////////////////////////////////////////////////// +// External protocol handler interface implementation +////////////////////////////////////////////////////////////////////// +NS_IMETHODIMP nsExternalProtocolHandler::ExternalAppExistsForScheme( + const nsACString& aScheme, bool* _retval) { + nsCOMPtr<nsIExternalProtocolService> extProtSvc( + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID)); + if (extProtSvc) + return extProtSvc->ExternalProtocolHandlerExists( + PromiseFlatCString(aScheme).get(), _retval); + + // In case we don't have external protocol service. + *_retval = false; + return NS_OK; +} diff --git a/uriloader/exthandler/nsExternalProtocolHandler.h b/uriloader/exthandler/nsExternalProtocolHandler.h new file mode 100644 index 0000000000..373223bc39 --- /dev/null +++ b/uriloader/exthandler/nsExternalProtocolHandler.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsExternalProtocolHandler_h___ +#define nsExternalProtocolHandler_h___ + +#include "nsIExternalProtocolHandler.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsIURI; + +// protocol handlers need to support weak references if we want the netlib +// nsIOService to cache them. +class nsExternalProtocolHandler final : public nsIExternalProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIEXTERNALPROTOCOLHANDLER + + nsExternalProtocolHandler(); + + protected: + ~nsExternalProtocolHandler(); + + // helper function + bool HaveExternalProtocolHandler(nsIURI* aURI); + nsCString m_schemeName; +}; + +#endif // nsExternalProtocolHandler_h___ diff --git a/uriloader/exthandler/nsIContentDispatchChooser.idl b/uriloader/exthandler/nsIContentDispatchChooser.idl new file mode 100644 index 0000000000..d12c1aa907 --- /dev/null +++ b/uriloader/exthandler/nsIContentDispatchChooser.idl @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIHandlerInfo; +interface nsIPrincipal; +interface nsIURI; +webidl BrowsingContext; + +/** + * This is used to ask a user what they would like to do with a given piece of + * content. + */ +[scriptable, uuid(456ca3b2-02be-4f97-89a2-08c08d3ad88f)] +interface nsIContentDispatchChooser : nsISupports { + /** + * Opens the handler associated with the given resource. + * If the caller does not have permission or no handler is set, we ask the + * user to grant permission and pick a handler. + * + * @param aHander + * The interface describing the details of how this content should or + * can be handled. + * @param aURI + * The URI of the resource that we are asking about. + * @param aTriggeringPrincipal + * The principal making the request. + * @param aBrowsingContext + * The browsing context where the load should happen. + */ + void handleURI(in nsIHandlerInfo aHandler, + in nsIURI aURI, + in nsIPrincipal aTriggeringPrincipal, + in BrowsingContext aBrowsingContext); +}; + diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl new file mode 100644 index 0000000000..657e15bc07 --- /dev/null +++ b/uriloader/exthandler/nsIExternalHelperAppService.idl @@ -0,0 +1,179 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsICancelable.idl" + +interface nsIURI; +interface nsIRequest; +interface nsIStreamListener; +interface nsIFile; +interface nsIMIMEInfo; +interface nsIWebProgressListener2; +interface nsIInterfaceRequestor; +webidl BrowsingContext; + +/** + * The external helper app service is used for finding and launching + * platform specific external applications for a given mime content type. + */ +[scriptable, uuid(1E4F3AE1-B737-431F-A95D-31FA8DA70199)] +interface nsIExternalHelperAppService : nsISupports +{ + /** + * Binds an external helper application to a stream listener. The caller + * should pump data into the returned stream listener. When the OnStopRequest + * is issued, the stream listener implementation will launch the helper app + * with this data. + * @param aMimeContentType The content type of the incoming data + * @param aRequest The request corresponding to the incoming data + * @param aContentContext Used in processing content document refresh + * headers after target content is downloaded. + * @param aForceSave True to always save this content to disk, regardless of + * nsIMIMEInfo and other such influences. + * @param aWindowContext Used in parenting helper app dialogs, usually + * points to the parent browser window. This parameter may be null, + * in which case dialogs will be parented to aContentContext. + * @return A nsIStreamListener which the caller should pump the data into. + */ + nsIStreamListener doContent (in ACString aMimeContentType, + in nsIRequest aRequest, + in nsIInterfaceRequestor aContentContext, + in boolean aForceSave, + [optional] in nsIInterfaceRequestor aWindowContext); + + /** + * Binds an external helper application to a stream listener. The caller + * should pump data into the returned stream listener. When the OnStopRequest + * is issued, the stream listener implementation will launch the helper app + * with this data. + * Replaces doContent for native code, and uses BrowsingContext. + * + * @param aMimeContentType The content type of the incoming data + * @param aRequest The request corresponding to the incoming data + * @param aContentContext The BrowsingContext that the request was initiated + * by. Used for closing the window if we opened one specifically for this download. + * @param aForceSave True to always save this content to disk, regardless of + * nsIMIMEInfo and other such influences. + * @param aWindowContext Used in parenting helper app dialogs, usually + * points to the parent browser window. This parameter may be null, + * in which case dialogs will be parented to aContentContext. + * @return A nsIStreamListener which the caller should pump the data into. + */ + nsIStreamListener createListener (in ACString aMimeContentType, + in nsIRequest aRequest, + in BrowsingContext aContentContext, + in boolean aForceSave, + [optional] in nsIInterfaceRequestor aWindowContext); + + /** + * Returns true if data from a URL with this extension combination + * is to be decoded from aEncodingType prior to saving or passing + * off to helper apps, false otherwise. + */ + boolean applyDecodingForExtension(in AUTF8String aExtension, + in ACString aEncodingType); + +}; + +/** + * This is a private interface shared between external app handlers and the platform specific + * external helper app service + */ +[scriptable, uuid(6613e2e7-feab-4e3a-bb1f-b03200d544ec)] +interface nsPIExternalAppLauncher : nsISupports +{ + /** + * mscott --> eventually I should move this into a new service so other + * consumers can add temporary files they want deleted on exit. + * @param aTemporaryFile A temporary file we should delete on exit. + */ + void deleteTemporaryFileOnExit(in nsIFile aTemporaryFile); + /** + * Delete a temporary file created inside private browsing mode when + * the private browsing mode has ended. + */ + void deleteTemporaryPrivateFileWhenPossible(in nsIFile aTemporaryFile); +}; + +/** + * A helper app launcher is a small object created to handle the launching + * of an external application. + * + * Note that cancelling the load via the nsICancelable interface will release + * the reference to the launcher dialog. + */ +[scriptable, uuid(acf2a516-7d7f-4771-8b22-6c4a559c088e)] +interface nsIHelperAppLauncher : nsICancelable +{ + /** + * The mime info object associated with the content type this helper app + * launcher is currently attempting to load + */ + readonly attribute nsIMIMEInfo MIMEInfo; + + /** + * The source uri + */ + readonly attribute nsIURI source; + + /** + * The suggested name for this file + */ + readonly attribute AString suggestedFileName; + + /** + * Saves the final destination of the file. + * NOTE: This will release the reference to the nsIHelperAppLauncherDialog. + */ + void promptForSaveDestination(); + + /** + * Tell the launcher that we will want to open the file. + * NOTE: This will release the reference to the nsIHelperAppLauncherDialog. + * @param aHandleInternally TRUE if we should handle opening this internally. + */ + void launchWithApplication(in boolean aHandleInternally); + + /** + * Callback invoked by nsIHelperAppLauncherDialog::promptForSaveToFileAsync + * after the user has chosen a file through the File Picker (or dismissed it). + * @param aFile The file that was chosen by the user (or null if dialog was dismissed). + */ + void saveDestinationAvailable(in nsIFile aFile); + + /** + * The following methods are used by the progress dialog to get or set + * information on the current helper app launcher download. + * This reference will be released when the download is finished (after the + * listener receives the STATE_STOP notification). + */ + void setWebProgressListener(in nsIWebProgressListener2 aWebProgressListener); + + /** + * The file we are saving to + */ + readonly attribute nsIFile targetFile; + + /** + * The executable-ness of the target file + */ + readonly attribute boolean targetFileIsExecutable; + + /** + * Time when the download started + */ + readonly attribute PRTime timeDownloadStarted; + + /** + * The download content length, or -1 if the length is not available. + */ + readonly attribute int64_t contentLength; + + /** + * The browsingContext ID of the launcher's source + */ + readonly attribute uint64_t browsingContextId; +}; diff --git a/uriloader/exthandler/nsIExternalProtocolService.idl b/uriloader/exthandler/nsIExternalProtocolService.idl new file mode 100644 index 0000000000..66d459c491 --- /dev/null +++ b/uriloader/exthandler/nsIExternalProtocolService.idl @@ -0,0 +1,140 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIURI; +interface nsIFile; +interface nsIPrincipal; +interface nsIInterfaceRequestor; +interface nsIHandlerInfo; + +webidl BrowsingContext; + +/** + * The external protocol service is used for finding and launching + * web handlers (a la registerProtocolHandler in the HTML5 draft) or + * platform-specific applications for handling particular protocols. + * + * You can ask the external protocol service if it has an external + * handler for a given protocol scheme. And you can ask it to load + * the url using the default handler. + */ +[scriptable, uuid(70f93b7a-3ec6-4bcb-b093-92d9984c9f83)] +interface nsIExternalProtocolService : nsISupports +{ + /** + * Check whether a handler for a specific protocol exists. Specifically, + * this looks to see whether there are any known possible application handlers + * in either the nsIHandlerService datastore or registered with the OS. + * + * @param aProtocolScheme The scheme from a url: http, ftp, mailto, etc. + * + * @return true if we have a handler and false otherwise. + * + * XXX shouldn't aProtocolScheme be an ACString like nsIURI::scheme? + */ + boolean externalProtocolHandlerExists(in string aProtocolScheme); + + /** + * Check whether a handler for a specific protocol is "exposed" as a visible + * feature of the current application. + * + * An exposed protocol handler is one that can be used in all contexts. A + * non-exposed protocol handler is one that can only be used internally by the + * application. For example, a non-exposed protocol would not be loaded by the + * application in response to a link click or a X-remote openURL command. + * Instead, it would be deferred to the system's external protocol handler. + * XXX shouldn't aProtocolScheme be an ACString like nsIURI::scheme? + */ + boolean isExposedProtocol(in string aProtocolScheme); + + /** + * Retrieve the handler for the given protocol. If neither the application + * nor the OS knows about a handler for the protocol, the object this method + * returns will represent a default handler for unknown content. + * + * @param aProtocolScheme the scheme from a URL: http, ftp, mailto, etc. + * + * Note: aProtocolScheme should not include a trailing colon, which is part + * of the URI syntax, not part of the scheme itself (i.e. pass "mailto" not + * "mailto:"). + * + * @return the handler, if any; otherwise a default handler + */ + nsIHandlerInfo getProtocolHandlerInfo(in ACString aProtocolScheme); + + /** + * Given a scheme, looks up the protocol info from the OS. This should be + * overridden by each OS's implementation. + * + * @param aScheme The protocol scheme we are looking for. + * @param aFound Was an OS default handler for this scheme found? + * @return An nsIHanderInfo for the protocol. + */ + nsIHandlerInfo getProtocolHandlerInfoFromOS(in ACString aProtocolScheme, + out boolean aFound); + + /** + * Set some sane defaults for a protocol handler object. + * + * @param aHandlerInfo nsIHandlerInfo object, as returned by + * getProtocolHandlerInfoFromOS + * @param aOSHandlerExists was the object above created for an extant + * OS default handler? This is generally the + * value of the aFound out param from + * getProtocolHandlerInfoFromOS. + */ + void setProtocolHandlerDefaults(in nsIHandlerInfo aHandlerInfo, + in boolean aOSHandlerExists); + + /** + * Used to load a URI via an external application. Might prompt the user for + * permission to load the external application. + * + * @param aURI + * The URI to load + * + * @param aTriggeringPrincipal + * The principal triggering this load. + * + * @param aBrowsingContext + * The context to parent the dialog against, and, if a web handler + * is chosen, it is loaded in this window as well. This parameter + * may be ultimately passed nsIURILoader.openURI in the case of a + * web handler, and aWindowContext is null or not present, web + * handlers will fail. We need to do better than that; bug 394483 + * filed in order to track. + * + * @note Embedders that do not expose the http protocol should not currently + * use web-based protocol handlers, as handoff won't work correctly + * (bug 394479). + */ + void loadURI(in nsIURI aURI, + [optional] in nsIPrincipal aTriggeringPrincipal, + [optional] in BrowsingContext aBrowsingContext); + + /** + * Gets a human-readable description for the application responsible for + * handling a specific protocol. + * + * @param aScheme The scheme to look up. For example, "mms". + * + * @throw NS_ERROR_NOT_IMPLEMENTED + * If getting descriptions for protocol helpers is not supported + * @throw NS_ERROR_NOT_AVAILABLE + * If no protocol helper exists for this scheme, or if it is not + * possible to get a description for it. + */ + AString getApplicationDescription(in AUTF8String aScheme); + + /** + * Check if this app is registered as the OS default for a given scheme. + * + * @param aScheme The scheme to look up. For example, "mms". + */ + bool isCurrentAppOSDefaultForProtocol(in AUTF8String aScheme); +}; diff --git a/uriloader/exthandler/nsIExternalURLHandlerService.idl b/uriloader/exthandler/nsIExternalURLHandlerService.idl new file mode 100644 index 0000000000..3573497591 --- /dev/null +++ b/uriloader/exthandler/nsIExternalURLHandlerService.idl @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIMIMEInfo.idl" + +/** + * The external URL handler service is used for finding + * platform-specific applications for handling particular URLs. + */ + +[scriptable, uuid(56c5c7d3-6fd3-43f8-9429-4397e111453a)] +interface nsIExternalURLHandlerService : nsISupports +{ + /** + * Given a URL, looks up the handler info from the OS. This should be + * overridden by each OS's implementation. + * + * @param aURL The URL we are looking for. + * @param aFound Was an OS default handler for this URL found? + * @return An nsIHanderInfo for the protocol. + */ + nsIHandlerInfo getURLHandlerInfoFromOS(in nsIURI aURL, + out boolean aFound); + +}; diff --git a/uriloader/exthandler/nsIHandlerService.idl b/uriloader/exthandler/nsIHandlerService.idl new file mode 100644 index 0000000000..34519f57cd --- /dev/null +++ b/uriloader/exthandler/nsIHandlerService.idl @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIHandlerInfo; +interface nsISimpleEnumerator; + +[scriptable, uuid(53f0ad17-ec62-46a1-adbc-efccc06babcd)] +interface nsIHandlerService : nsISupports +{ + /** + * Asynchronously performs any IO that the nsIHandlerService needs to do + * before it can be of use. + */ + void asyncInit(); + + /** + * Retrieve a list of all handlers in the datastore. This list is not + * guaranteed to be in any particular order, and callers should not assume + * it will remain in the same order in the future. + * + * @returns a list of all handlers in the datastore + */ + nsISimpleEnumerator enumerate(); + + /** + * Fill a handler info object with information from the datastore. + * + * Note: because of the way the external helper app service currently mixes + * OS and user handler info in the same handler info object, this method + * takes an existing handler info object (probably retrieved from the OS) + * and "fills it in" with information from the datastore, overriding any + * existing properties on the object with properties from the datastore. + * + * Ultimately, however, we're going to separate OS and user handler info + * into separate objects, at which point this method should be renamed to + * something like "get" or "retrieve", take a class and type (or perhaps + * a type whose class can be determined by querying the type, for example + * an nsIContentType which is also an nsIMIMEType or an nsIProtocolScheme), + * and return a handler info object representing only the user info. + * + * Note: if you specify an override type, then the service will fill in + * the handler info object with information about that type instead of + * the type specified by the object's nsIHandlerInfo::type attribute. + * + * This is useful when you are trying to retrieve information about a MIME + * type that doesn't exist in the datastore, but you have a file extension + * for that type, and nsIHandlerService::getTypeFromExtension returns another + * MIME type that does exist in the datastore and can handle that extension. + * + * For example, the user clicks on a link, and the content has a MIME type + * that isn't in the datastore, but the link has a file extension, and that + * extension is associated with another MIME type in the datastore (perhaps + * an unofficial MIME type preceded an official one, like with image/x-png + * and image/png). + * + * In that situation, you can call this method to fill in the handler info + * object with information about that other type by passing the other type + * as the aOverrideType parameter. + * + * @param aHandlerInfo the handler info object + * @param aOverrideType a type to use instead of aHandlerInfo::type + * + * Note: if there is no information in the datastore about this type, + * this method throws NS_ERROR_NOT_AVAILABLE. Callers are encouraged to + * check exists() before calling fillHandlerInfo(), to prevent spamming the + * console with XPCOM exception errors. + */ + void fillHandlerInfo(in nsIHandlerInfo aHandlerInfo, + in ACString aOverrideType); + + /** + * Save the preferred action, preferred handler, possible handlers, and + * always ask properties of the given handler info object to the datastore. + * Updates an existing record or creates a new one if necessary. + * + * Note: if preferred action is undefined or invalid, then we assume + * the default value nsIHandlerInfo::useHelperApp. + * + * @param aHandlerInfo the handler info object + */ + void store(in nsIHandlerInfo aHandlerInfo); + + /** + * Whether or not a record for the given handler info object exists + * in the datastore. If the datastore is corrupt (or some other error + * is caught in the implementation), false will be returned. + * + * @param aHandlerInfo a handler info object + * + * @returns whether or not a record exists + */ + boolean exists(in nsIHandlerInfo aHandlerInfo); + + /** + * Remove the given handler info object from the datastore. Deletes all + * records associated with the object, including the preferred handler, info, + * and type records plus the entry in the list of types, if they exist. + * Otherwise, it does nothing and does not return an error. + * + * @param aHandlerInfo the handler info object + */ + void remove(in nsIHandlerInfo aHandlerInfo); + + /** + * Get the MIME type mapped to the given file extension in the datastore. + * + * XXX If we ever support extension -> protocol scheme mappings, then this + * method should work for those as well. + * + * Note: in general, you should use nsIMIMEService::getTypeFromExtension + * to get a MIME type from a file extension, as that method checks a variety + * of other sources besides just the datastore. Use this only when you want + * to specifically get only the mapping available in the datastore. + * + * @param aFileExtension the file extension + * + * @returns the MIME type, if any; otherwise returns an empty string (""). + */ + ACString getTypeFromExtension(in ACString aFileExtension); + + /** + * Whether or not there is a handler known to the OS for the + * specified protocol type. + * + * @param aProtocolScheme scheme to check for support + * + * @returns whether or not a handler exists + */ + boolean existsForProtocolOS(in ACString aProtocolScheme); + + /** + * Whether or not there is a handler in the datastore or OS for + * the specified protocol type. If there is no handler in the datastore, + * falls back to a check for an OS handler. + * + * @param aProtocolScheme scheme to check for support + * + * @returns whether or not a handler exists + */ + boolean existsForProtocol(in ACString aProtocolScheme); + + /* + * Fill in a handler info object using information from the OS, taking into + * account the MIME type and file extension. When the OS handler + * for the MIME type and extension match, |aFound| is returned as true. If + * either the MIME type or extension is the empty string and a handler is + * found, |aFound| is returned as true. + */ + void getMIMEInfoFromOS(in nsIHandlerInfo aHandlerInfo, + in ACString aMIMEType, + in ACString aExtension, + out bool aFound); + + /* + * Get a description for the application responsible for handling + * the provided protocol. + */ + AString getApplicationDescription(in ACString aProtocolScheme); +}; diff --git a/uriloader/exthandler/nsIHelperAppLauncherDialog.idl b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl new file mode 100644 index 0000000000..9a7f76b7da --- /dev/null +++ b/uriloader/exthandler/nsIHelperAppLauncherDialog.idl @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIHelperAppLauncher; +interface nsIFile; +interface nsIInterfaceRequestor; + +/** + * This interface is used to display a confirmation dialog before + * launching a "helper app" to handle content not handled by + * Mozilla. + * + * Usage: Clients (of which there is one: the nsIExternalHelperAppService + * implementation in mozilla/uriloader/exthandler) create an instance of + * this interface (using the contract ID) and then call the show() method. + * + * The dialog is shown non-modally. The implementation of the dialog + * will access methods of the nsIHelperAppLauncher passed in to show() + * in order to cause a "save to disk" or "open using" action. + */ +[scriptable, uuid(bfc739f3-8d75-4034-a6f8-1039a5996bad)] +interface nsIHelperAppLauncherDialog : nsISupports { + /** + * This request is passed to the helper app dialog because Gecko can not + * handle content of this type. + */ + const unsigned long REASON_CANTHANDLE = 0; + + /** + * The server requested external handling. + */ + const unsigned long REASON_SERVERREQUEST = 1; + + /** + * Gecko detected that the type sent by the server (e.g. text/plain) does + * not match the actual type. + */ + const unsigned long REASON_TYPESNIFFED = 2; + + /** + * Show confirmation dialog for launching application (or "save to + * disk") for content specified by aLauncher. + * + * @param aLauncher + * A nsIHelperAppLauncher to be invoked when a file is selected. + * @param aWindowContext + * Window associated with action. + * @param aReason + * One of the constants from above. It indicates why the dialog is + * shown. Implementors should treat unknown reasons like + * REASON_CANTHANDLE. + */ + void show(in nsIHelperAppLauncher aLauncher, + in nsIInterfaceRequestor aWindowContext, + in unsigned long aReason); + + /** + * Async invoke a save-to-file dialog instead of the full fledged helper app + * dialog. When the file is chosen (or the dialog is closed), the callback + * in aLauncher (aLauncher.saveDestinationAvailable) is called with the + * selected file. + * + * @param aLauncher + * A nsIHelperAppLauncher to be invoked when a file is selected. + * @param aWindowContext + * Window associated with action. + * @param aDefaultFileName + * Default file name to provide (can be null) + * @param aSuggestedFileExtension + * Sugested file extension + * @param aForcePrompt + * Set to true to force prompting the user for thet file + * name/location, otherwise perferences may control if the user is + * prompted. + */ + void promptForSaveToFileAsync(in nsIHelperAppLauncher aLauncher, + in nsIInterfaceRequestor aWindowContext, + in wstring aDefaultFileName, + in wstring aSuggestedFileExtension, + in boolean aForcePrompt); +}; + + +%{C++ +#define NS_HELPERAPPLAUNCHERDLG_CONTRACTID "@mozilla.org/helperapplauncherdialog;1" +%} diff --git a/uriloader/exthandler/nsISharingHandlerApp.idl b/uriloader/exthandler/nsISharingHandlerApp.idl new file mode 100644 index 0000000000..14bdee1884 --- /dev/null +++ b/uriloader/exthandler/nsISharingHandlerApp.idl @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIMIMEInfo.idl" + +[scriptable, uuid(7111f769-53ec-41fd-b314-613661d5b6ba)] +interface nsISharingHandlerApp : nsIHandlerApp +{ + void share(in AString data, [optional] in AString title); +}; + diff --git a/uriloader/exthandler/nsLocalHandlerApp.cpp b/uriloader/exthandler/nsLocalHandlerApp.cpp new file mode 100644 index 0000000000..f1a9a779cf --- /dev/null +++ b/uriloader/exthandler/nsLocalHandlerApp.cpp @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsLocalHandlerApp.h" +#include "nsIURI.h" +#include "nsIProcess.h" + +// XXX why does nsMIMEInfoImpl have a threadsafe nsISupports? do we need one +// here too? +NS_IMPL_ISUPPORTS(nsLocalHandlerApp, nsILocalHandlerApp, nsIHandlerApp) + +//////////////////////////////////////////////////////////////////////////////// +//// nsIHandlerApp + +NS_IMETHODIMP nsLocalHandlerApp::GetName(nsAString& aName) { + if (mName.IsEmpty() && mExecutable) { + // Don't want to cache this, just in case someone resets the app + // without changing the description.... + mExecutable->GetLeafName(aName); + } else { + aName.Assign(mName); + } + + return NS_OK; +} + +NS_IMETHODIMP nsLocalHandlerApp::SetName(const nsAString& aName) { + mName.Assign(aName); + + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::SetDetailedDescription(const nsAString& aDescription) { + mDetailedDescription.Assign(aDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::GetDetailedDescription(nsAString& aDescription) { + aDescription.Assign(mDetailedDescription); + + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::Equals(nsIHandlerApp* aHandlerApp, bool* _retval) { + NS_ENSURE_ARG_POINTER(aHandlerApp); + + *_retval = false; + + // If the handler app isn't a local handler app, then it's not the same app. + nsCOMPtr<nsILocalHandlerApp> localHandlerApp = do_QueryInterface(aHandlerApp); + if (!localHandlerApp) return NS_OK; + + // If either handler app doesn't have an executable, then they aren't + // the same app. + nsCOMPtr<nsIFile> executable; + nsresult rv = localHandlerApp->GetExecutable(getter_AddRefs(executable)); + if (NS_FAILED(rv)) return rv; + + // Equality for two empty nsIHandlerApp + if (!executable && !mExecutable) { + *_retval = true; + return NS_OK; + } + + // At least one is set so they are not equal + if (!mExecutable || !executable) return NS_OK; + + // Check the command line parameter list lengths + uint32_t len; + localHandlerApp->GetParameterCount(&len); + if (mParameters.Length() != len) return NS_OK; + + // Check the command line params lists + for (uint32_t idx = 0; idx < mParameters.Length(); idx++) { + nsAutoString param; + if (NS_FAILED(localHandlerApp->GetParameter(idx, param)) || + !param.Equals(mParameters[idx])) + return NS_OK; + } + + return executable->Equals(mExecutable, _retval); +} + +NS_IMETHODIMP +nsLocalHandlerApp::LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) { + // pass the entire URI to the handler. + nsAutoCString spec; + aURI->GetAsciiSpec(spec); + return LaunchWithIProcess(spec); +} + +nsresult nsLocalHandlerApp::LaunchWithIProcess(const nsCString& aArg) { + nsresult rv; + nsCOMPtr<nsIProcess> process = do_CreateInstance(NS_PROCESS_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + if (NS_FAILED(rv = process->Init(mExecutable))) return rv; + + const char* string = aArg.get(); + + return process->Run(false, &string, 1); +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsILocalHandlerApp + +NS_IMETHODIMP +nsLocalHandlerApp::GetExecutable(nsIFile** aExecutable) { + NS_IF_ADDREF(*aExecutable = mExecutable); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::SetExecutable(nsIFile* aExecutable) { + mExecutable = aExecutable; + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::GetParameterCount(uint32_t* aParameterCount) { + *aParameterCount = mParameters.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::ClearParameters() { + mParameters.Clear(); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::AppendParameter(const nsAString& aParam) { + mParameters.AppendElement(aParam); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::GetParameter(uint32_t parameterIndex, nsAString& _retval) { + if (mParameters.Length() <= parameterIndex) return NS_ERROR_INVALID_ARG; + + _retval.Assign(mParameters[parameterIndex]); + return NS_OK; +} + +NS_IMETHODIMP +nsLocalHandlerApp::ParameterExists(const nsAString& aParam, bool* _retval) { + *_retval = mParameters.Contains(aParam); + return NS_OK; +} diff --git a/uriloader/exthandler/nsLocalHandlerApp.h b/uriloader/exthandler/nsLocalHandlerApp.h new file mode 100644 index 0000000000..3ea8e3e4fc --- /dev/null +++ b/uriloader/exthandler/nsLocalHandlerApp.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsLocalHandlerAppImpl_h__ +#define __nsLocalHandlerAppImpl_h__ + +#include "nsString.h" +#include "nsIMIMEInfo.h" +#include "nsIFile.h" +#include "nsTArray.h" + +class nsLocalHandlerApp : public nsILocalHandlerApp { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHANDLERAPP + NS_DECL_NSILOCALHANDLERAPP + + nsLocalHandlerApp() {} + + nsLocalHandlerApp(const char16_t* aName, nsIFile* aExecutable) + : mName(aName), mExecutable(aExecutable) {} + + nsLocalHandlerApp(const nsAString& aName, nsIFile* aExecutable) + : mName(aName), mExecutable(aExecutable) {} + + protected: + virtual ~nsLocalHandlerApp() {} + + nsString mName; + nsString mDetailedDescription; + nsTArray<nsString> mParameters; + nsCOMPtr<nsIFile> mExecutable; + + /** + * Launches this application with a single argument (typically either + * a file path or a URI spec). This is meant as a helper method for + * implementations of (e.g.) LaunchWithURI. + * + * @param aApp The application to launch (may not be null) + * @param aArg The argument to pass on the command line + */ + nsresult LaunchWithIProcess(const nsCString& aArg); +}; + +// any platforms that need a platform-specific class instead of just +// using nsLocalHandlerApp need to add an include and a typedef here. +#ifdef XP_MACOSX +# ifndef NSLOCALHANDLERAPPMAC_H_ +# include "mac/nsLocalHandlerAppMac.h" +typedef nsLocalHandlerAppMac PlatformLocalHandlerApp_t; +# endif +#else +typedef nsLocalHandlerApp PlatformLocalHandlerApp_t; +#endif + +#endif // __nsLocalHandlerAppImpl_h__ diff --git a/uriloader/exthandler/nsMIMEInfoChild.h b/uriloader/exthandler/nsMIMEInfoChild.h new file mode 100644 index 0000000000..707f19aa67 --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoChild.h @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoChild_h +#define nsMIMEInfoChild_h + +#include "nsMIMEInfoImpl.h" + +/* + * A platform-generic nsMIMEInfo implementation to be used in child process + * generic code that needs a MIMEInfo with limited functionality. + */ +class nsChildProcessMIMEInfo : public nsMIMEInfoImpl { + public: + explicit nsChildProcessMIMEInfo(const char* aMIMEType = "") + : nsMIMEInfoImpl(aMIMEType) {} + + explicit nsChildProcessMIMEInfo(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + + nsChildProcessMIMEInfo(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; + + NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; + + protected: + [[nodiscard]] virtual nsresult LoadUriInternal(nsIURI* aURI) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; + +#ifdef DEBUG + [[nodiscard]] virtual nsresult LaunchDefaultWithFile( + nsIFile* aFile) override { + return NS_ERROR_UNEXPECTED; + } +#endif + [[nodiscard]] static nsresult OpenApplicationWithURI(nsIFile* aApplication, + const nsCString& aURI) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override { + return NS_ERROR_NOT_IMPLEMENTED; + }; +}; + +#endif diff --git a/uriloader/exthandler/nsMIMEInfoImpl.cpp b/uriloader/exthandler/nsMIMEInfoImpl.cpp new file mode 100644 index 0000000000..3800d41d57 --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoImpl.cpp @@ -0,0 +1,467 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoImpl.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsStringEnumerator.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsEscape.h" +#include "nsCURILoader.h" +#include "nsCExternalHandlerService.h" +#include "nsIExternalProtocolService.h" +#include "mozilla/StaticPtr.h" + +static bool sInitializedOurData = false; +StaticRefPtr<nsIFile> sOurAppFile; + +static already_AddRefed<nsIFile> GetCanonicalExecutable(nsIFile* aFile) { + nsCOMPtr<nsIFile> binary = aFile; +#ifdef XP_MACOSX + nsAutoString leafName; + if (binary) { + binary->GetLeafName(leafName); + } + while (binary && !StringEndsWith(leafName, u".app"_ns)) { + nsCOMPtr<nsIFile> parent; + binary->GetParent(getter_AddRefs(parent)); + binary = parent; + if (binary) { + binary->GetLeafName(leafName); + } + } +#endif + return binary.forget(); +} + +static void EnsureAppDetailsAvailable() { + if (sInitializedOurData) { + return; + } + sInitializedOurData = true; + nsCOMPtr<nsIFile> binary; + XRE_GetBinaryPath(getter_AddRefs(binary)); + sOurAppFile = GetCanonicalExecutable(binary); + ClearOnShutdown(&sOurAppFile); +} + +// nsISupports methods +NS_IMPL_ADDREF(nsMIMEInfoBase) +NS_IMPL_RELEASE(nsMIMEInfoBase) + +NS_INTERFACE_MAP_BEGIN(nsMIMEInfoBase) + NS_INTERFACE_MAP_ENTRY(nsIHandlerInfo) + // This is only an nsIMIMEInfo if it's a MIME handler. + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIMIMEInfo, mClass == eMIMEInfo) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIHandlerInfo) +NS_INTERFACE_MAP_END + +// nsMIMEInfoImpl methods + +// Constructors for a MIME handler. +nsMIMEInfoBase::nsMIMEInfoBase(const char* aMIMEType) + : mSchemeOrType(aMIMEType), + mClass(eMIMEInfo), + mPreferredAction(nsIMIMEInfo::saveToDisk), + mAlwaysAskBeforeHandling(true) {} + +nsMIMEInfoBase::nsMIMEInfoBase(const nsACString& aMIMEType) + : mSchemeOrType(aMIMEType), + mClass(eMIMEInfo), + mPreferredAction(nsIMIMEInfo::saveToDisk), + mAlwaysAskBeforeHandling(true) {} + +// Constructor for a handler that lets the caller specify whether this is a +// MIME handler or a protocol handler. In the long run, these will be distinct +// classes (f.e. nsMIMEInfo and nsProtocolInfo), but for now we reuse this class +// for both and distinguish between the two kinds of handlers via the aClass +// argument to this method, which can be either eMIMEInfo or eProtocolInfo. +nsMIMEInfoBase::nsMIMEInfoBase(const nsACString& aType, HandlerClass aClass) + : mSchemeOrType(aType), + mClass(aClass), + mPreferredAction(nsIMIMEInfo::saveToDisk), + mAlwaysAskBeforeHandling(true) {} + +nsMIMEInfoBase::~nsMIMEInfoBase() {} + +NS_IMETHODIMP +nsMIMEInfoBase::GetFileExtensions(nsIUTF8StringEnumerator** aResult) { + return NS_NewUTF8StringEnumerator(aResult, &mExtensions, this); +} + +NS_IMETHODIMP +nsMIMEInfoBase::ExtensionExists(const nsACString& aExtension, bool* _retval) { + MOZ_ASSERT(!aExtension.IsEmpty(), "no extension"); + *_retval = mExtensions.Contains(aExtension, + nsCaseInsensitiveCStringArrayComparator()); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPrimaryExtension(nsACString& _retval) { + if (!mExtensions.Length()) { + _retval.Truncate(); + return NS_ERROR_NOT_INITIALIZED; + } + _retval = mExtensions[0]; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetPrimaryExtension(const nsACString& aExtension) { + if (MOZ_UNLIKELY(aExtension.IsEmpty())) { + MOZ_ASSERT(false, "No extension"); + return NS_ERROR_INVALID_ARG; + } + int32_t i = mExtensions.IndexOf(aExtension, 0, + nsCaseInsensitiveCStringArrayComparator()); + if (i != -1) { + mExtensions.RemoveElementAt(i); + } + mExtensions.InsertElementAt(0, aExtension); + return NS_OK; +} + +void nsMIMEInfoBase::AddUniqueExtension(const nsACString& aExtension) { + if (!aExtension.IsEmpty() && + !mExtensions.Contains(aExtension, + nsCaseInsensitiveCStringArrayComparator())) { + mExtensions.AppendElement(aExtension); + } +} + +NS_IMETHODIMP +nsMIMEInfoBase::AppendExtension(const nsACString& aExtension) { + MOZ_ASSERT(!aExtension.IsEmpty(), "No extension"); + AddUniqueExtension(aExtension); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetType(nsACString& aType) { + if (mSchemeOrType.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + aType = mSchemeOrType; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetMIMEType(nsACString& aMIMEType) { + if (mSchemeOrType.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + aMIMEType = mSchemeOrType; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetDescription(nsAString& aDescription) { + aDescription = mDescription; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetDescription(const nsAString& aDescription) { + mDescription = aDescription; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) { + if (!aMIMEInfo) return NS_ERROR_NULL_POINTER; + + nsAutoCString type; + nsresult rv = aMIMEInfo->GetMIMEType(type); + if (NS_FAILED(rv)) return rv; + + *_retval = mSchemeOrType.Equals(type); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetFileExtensions(const nsACString& aExtensions) { + mExtensions.Clear(); + nsACString::const_iterator start, end; + aExtensions.BeginReading(start); + aExtensions.EndReading(end); + while (start != end) { + nsACString::const_iterator cursor = start; + mozilla::Unused << FindCharInReadable(',', cursor, end); + AddUniqueExtension(Substring(start, cursor)); + // If a comma was found, skip it for the next search. + start = cursor != end ? ++cursor : cursor; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetDefaultDescription(nsAString& aDefaultDescription) { + aDefaultDescription = mDefaultAppDescription; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPreferredApplicationHandler( + nsIHandlerApp** aPreferredAppHandler) { + *aPreferredAppHandler = mPreferredApplication; + NS_IF_ADDREF(*aPreferredAppHandler); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetPreferredApplicationHandler( + nsIHandlerApp* aPreferredAppHandler) { + mPreferredApplication = aPreferredAppHandler; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPossibleApplicationHandlers( + nsIMutableArray** aPossibleAppHandlers) { + if (!mPossibleApplications) + mPossibleApplications = do_CreateInstance(NS_ARRAY_CONTRACTID); + + if (!mPossibleApplications) return NS_ERROR_OUT_OF_MEMORY; + + *aPossibleAppHandlers = mPossibleApplications; + NS_IF_ADDREF(*aPossibleAppHandlers); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPreferredAction(nsHandlerInfoAction* aPreferredAction) { + *aPreferredAction = mPreferredAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetPreferredAction(nsHandlerInfoAction aPreferredAction) { + mPreferredAction = aPreferredAction; + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetAlwaysAskBeforeHandling(bool* aAlwaysAsk) { + *aAlwaysAsk = mAlwaysAskBeforeHandling; + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::SetAlwaysAskBeforeHandling(bool aAlwaysAsk) { + mAlwaysAskBeforeHandling = aAlwaysAsk; + return NS_OK; +} + +/* static */ +nsresult nsMIMEInfoBase::GetLocalFileFromURI(nsIURI* aURI, nsIFile** aFile) { + nsresult rv; + + nsCOMPtr<nsIFileURL> fileUrl = do_QueryInterface(aURI, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIFile> file; + rv = fileUrl->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + file.forget(aFile); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoBase::LaunchWithFile(nsIFile* aFile) { + nsresult rv; + + // it doesn't make any sense to call this on protocol handlers + NS_ASSERTION(mClass == eMIMEInfo, + "nsMIMEInfoBase should have mClass == eMIMEInfo"); + + if (mPreferredAction == useSystemDefault) { + return LaunchDefaultWithFile(aFile); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND; + + // at the moment, we only know how to hand files off to local handlers + nsCOMPtr<nsILocalHandlerApp> localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + return LaunchWithIProcess(executable, aFile->NativePath()); + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMIMEInfoBase::LaunchWithURI(nsIURI* aURI, BrowsingContext* aBrowsingContext) { + // This is only being called with protocol handlers + NS_ASSERTION(mClass == eProtocolInfo, + "nsMIMEInfoBase should be a protocol handler"); + + if (mPreferredAction == useSystemDefault) { + // First, ensure we're not accidentally going to call ourselves. + // That'd lead to an infinite loop (see bug 215554). + nsCOMPtr<nsIExternalProtocolService> extProtService = + do_GetService(NS_EXTERNALPROTOCOLSERVICE_CONTRACTID); + if (!extProtService) { + return NS_ERROR_FAILURE; + } + nsAutoCString scheme; + aURI->GetScheme(scheme); + bool isDefault = false; + nsresult rv = + extProtService->IsCurrentAppOSDefaultForProtocol(scheme, &isDefault); + if (NS_SUCCEEDED(rv) && isDefault) { + // Lie. This will trip the handler service into showing a dialog asking + // what the user wants. + return NS_ERROR_FILE_NOT_FOUND; + } + return LoadUriInternal(aURI); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND; + + EnsureAppDetailsAvailable(); + nsCOMPtr<nsILocalHandlerApp> localPreferredHandler = + do_QueryInterface(mPreferredApplication); + if (localPreferredHandler) { + nsCOMPtr<nsIFile> executable; + localPreferredHandler->GetExecutable(getter_AddRefs(executable)); + executable = GetCanonicalExecutable(executable); + bool isOurExecutable = false; + if (!executable || + NS_FAILED(executable->Equals(sOurAppFile, &isOurExecutable)) || + isOurExecutable) { + // Lie. This will trip the handler service into showing a dialog asking + // what the user wants. + return NS_ERROR_FILE_NOT_FOUND; + } + } + return mPreferredApplication->LaunchWithURI(aURI, aBrowsingContext); + } + + return NS_ERROR_INVALID_ARG; +} + +void nsMIMEInfoBase::CopyBasicDataTo(nsMIMEInfoBase* aOther) { + aOther->mSchemeOrType = mSchemeOrType; + aOther->mDefaultAppDescription = mDefaultAppDescription; + aOther->mExtensions = mExtensions.Clone(); +} + +/* static */ +already_AddRefed<nsIProcess> nsMIMEInfoBase::InitProcess(nsIFile* aApp, + nsresult* aResult) { + NS_ASSERTION(aApp, "Unexpected null pointer, fix caller"); + + nsCOMPtr<nsIProcess> process = + do_CreateInstance(NS_PROCESS_CONTRACTID, aResult); + if (NS_FAILED(*aResult)) return nullptr; + + *aResult = process->Init(aApp); + if (NS_FAILED(*aResult)) return nullptr; + + return process.forget(); +} + +/* static */ +nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp, + const nsCString& aArg) { + nsresult rv; + nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv); + if (NS_FAILED(rv)) return rv; + + const char* string = aArg.get(); + + return process->Run(false, &string, 1); +} + +/* static */ +nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp, + const nsString& aArg) { + nsresult rv; + nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv); + if (NS_FAILED(rv)) return rv; + + const char16_t* string = aArg.get(); + + return process->Runw(false, &string, 1); +} + +/* static */ +nsresult nsMIMEInfoBase::LaunchWithIProcess(nsIFile* aApp, const int aArgc, + const char16_t** aArgv) { + nsresult rv; + nsCOMPtr<nsIProcess> process = InitProcess(aApp, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + return process->Runw(false, aArgv, aArgc); +} + +// nsMIMEInfoImpl implementation +NS_IMETHODIMP +nsMIMEInfoImpl::GetDefaultDescription(nsAString& aDefaultDescription) { + if (mDefaultAppDescription.IsEmpty() && mDefaultApplication) { + // Don't want to cache this, just in case someone resets the app + // without changing the description.... + mDefaultApplication->GetLeafName(aDefaultDescription); + } else { + aDefaultDescription = mDefaultAppDescription; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoImpl::GetHasDefaultHandler(bool* _retval) { + *_retval = !mDefaultAppDescription.IsEmpty(); + if (mDefaultApplication) { + bool exists; + *_retval = NS_SUCCEEDED(mDefaultApplication->Exists(&exists)) && exists; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoImpl::IsCurrentAppOSDefault(bool* _retval) { + *_retval = false; + if (mDefaultApplication) { + // Determine if the default executable is our executable. + EnsureAppDetailsAvailable(); + bool isSame = false; + nsresult rv = mDefaultApplication->Equals(sOurAppFile, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + } + return NS_OK; +} + +nsresult nsMIMEInfoImpl::LaunchDefaultWithFile(nsIFile* aFile) { + if (!mDefaultApplication) return NS_ERROR_FILE_NOT_FOUND; + + return LaunchWithIProcess(mDefaultApplication, aFile->NativePath()); +} + +NS_IMETHODIMP +nsMIMEInfoBase::GetPossibleLocalHandlers(nsIArray** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/nsMIMEInfoImpl.h b/uriloader/exthandler/nsMIMEInfoImpl.h new file mode 100644 index 0000000000..f2fd792efb --- /dev/null +++ b/uriloader/exthandler/nsMIMEInfoImpl.h @@ -0,0 +1,213 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef __nsmimeinfoimpl_h___ +#define __nsmimeinfoimpl_h___ + +#include "nsIMIMEInfo.h" +#include "nsAtom.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIMutableArray.h" +#include "nsIFile.h" +#include "nsCOMPtr.h" +#include "nsIURI.h" +#include "nsIProcess.h" +#include "mozilla/dom/BrowsingContext.h" + +/** + * UTF8 moz-icon URI string for the default handler application's icon, if + * available. + */ +#define PROPERTY_DEFAULT_APP_ICON_URL "defaultApplicationIconURL" +/** + * UTF8 moz-icon URI string for the user's preferred handler application's + * icon, if available. + */ +#define PROPERTY_CUSTOM_APP_ICON_URL "customApplicationIconURL" + +/** + * Basic implementation of nsIMIMEInfo. Incomplete - it is meant to be + * subclassed, and GetHasDefaultHandler as well as LaunchDefaultWithFile need to + * be implemented. + */ +class nsMIMEInfoBase : public nsIMIMEInfo { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + // I'd use NS_DECL_NSIMIMEINFO, but I don't want GetHasDefaultHandler + NS_IMETHOD GetFileExtensions(nsIUTF8StringEnumerator** _retval) override; + NS_IMETHOD SetFileExtensions(const nsACString& aExtensions) override; + NS_IMETHOD ExtensionExists(const nsACString& aExtension, + bool* _retval) override; + NS_IMETHOD AppendExtension(const nsACString& aExtension) override; + NS_IMETHOD GetPrimaryExtension(nsACString& aPrimaryExtension) override; + NS_IMETHOD SetPrimaryExtension(const nsACString& aPrimaryExtension) override; + NS_IMETHOD GetType(nsACString& aType) override; + NS_IMETHOD GetMIMEType(nsACString& aMIMEType) override; + NS_IMETHOD GetDescription(nsAString& aDescription) override; + NS_IMETHOD SetDescription(const nsAString& aDescription) override; + NS_IMETHOD Equals(nsIMIMEInfo* aMIMEInfo, bool* _retval) override; + NS_IMETHOD GetPreferredApplicationHandler( + nsIHandlerApp** aPreferredAppHandler) override; + NS_IMETHOD SetPreferredApplicationHandler( + nsIHandlerApp* aPreferredAppHandler) override; + NS_IMETHOD GetPossibleApplicationHandlers( + nsIMutableArray** aPossibleAppHandlers) override; + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override; + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + NS_IMETHOD LaunchWithURI( + nsIURI* aURI, mozilla::dom::BrowsingContext* aBrowsingContext) override; + NS_IMETHOD GetPreferredAction(nsHandlerInfoAction* aPreferredAction) override; + NS_IMETHOD SetPreferredAction(nsHandlerInfoAction aPreferredAction) override; + NS_IMETHOD GetAlwaysAskBeforeHandling( + bool* aAlwaysAskBeforeHandling) override; + NS_IMETHOD SetAlwaysAskBeforeHandling(bool aAlwaysAskBeforeHandling) override; + NS_IMETHOD GetPossibleLocalHandlers(nsIArray** _retval) override; + + enum HandlerClass { eMIMEInfo, eProtocolInfo }; + + // nsMIMEInfoBase methods + explicit nsMIMEInfoBase(const char* aMIMEType = ""); + explicit nsMIMEInfoBase(const nsACString& aMIMEType); + nsMIMEInfoBase(const nsACString& aType, HandlerClass aClass); + + void SetMIMEType(const nsACString& aMIMEType) { mSchemeOrType = aMIMEType; } + + void SetDefaultDescription(const nsString& aDesc) { + mDefaultAppDescription = aDesc; + } + + /** + * Copies basic data of this MIME Info Implementation to the given other + * MIME Info. The data consists of the MIME Type, the (default) description, + * the MacOS type and creator, and the extension list (this object's + * extension list will replace aOther's list, not append to it). This + * function also ensures that aOther's primary extension will be the same as + * the one of this object. + */ + void CopyBasicDataTo(nsMIMEInfoBase* aOther); + + /** + * Return whether this MIMEInfo has any extensions + */ + bool HasExtensions() const { return mExtensions.Length() != 0; } + + protected: + virtual ~nsMIMEInfoBase(); // must be virtual, as the the base class's + // Release should call the subclass's destructor + + /** + * Launch the default application for the given file. + * For even more control over the launching, override launchWithFile. + * Also see the comment about nsIMIMEInfo in general, above. + * + * @param aFile The file that should be opened + */ + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) = 0; + + /** + * Loads the URI with the OS default app. + * + * @param aURI The URI to pass off to the OS. + */ + virtual nsresult LoadUriInternal(nsIURI* aURI) = 0; + + static already_AddRefed<nsIProcess> InitProcess(nsIFile* aApp, + nsresult* aResult); + + /** + * This method can be used to launch the file or URI with a single + * argument (typically either a file path or a URI spec). This is + * meant as a helper method for implementations of + * LaunchWithURI/LaunchDefaultWithFile. + * + * @param aApp The application to launch (may not be null) + * @param aArg The argument to pass on the command line + */ + static nsresult LaunchWithIProcess(nsIFile* aApp, const nsCString& aArg); + static nsresult LaunchWithIProcess(nsIFile* aApp, const nsString& aArg); + static nsresult LaunchWithIProcess(nsIFile* aApp, const int aArgc, + const char16_t** aArgv); + + /** + * Given a file: nsIURI, return the associated nsIFile + * + * @param aURI the file: URI in question + * @param aFile the associated nsIFile (out param) + */ + static nsresult GetLocalFileFromURI(nsIURI* aURI, nsIFile** aFile); + + /** + * Internal helper to avoid adding duplicates. + */ + void AddUniqueExtension(const nsACString& aExtension); + + // member variables + nsTArray<nsCString> + mExtensions; ///< array of file extensions associated w/ this MIME obj + nsString mDescription; ///< human readable description + nsCString mSchemeOrType; + HandlerClass mClass; + nsCOMPtr<nsIHandlerApp> mPreferredApplication; + nsCOMPtr<nsIMutableArray> mPossibleApplications; + nsHandlerInfoAction + mPreferredAction; ///< preferred action to associate with this type + nsString mPreferredAppDescription; + nsString mDefaultAppDescription; + bool mAlwaysAskBeforeHandling; +}; + +/** + * This is a complete implementation of nsIMIMEInfo, and contains all necessary + * methods. However, depending on your platform you may want to use a different + * way of launching applications. This class stores the default application in a + * member variable and provides a function for setting it. For local + * applications, launching is done using nsIProcess, native path of the file to + * open as first argument. + */ +class nsMIMEInfoImpl : public nsMIMEInfoBase { + public: + explicit nsMIMEInfoImpl(const char* aMIMEType = "") + : nsMIMEInfoBase(aMIMEType) {} + explicit nsMIMEInfoImpl(const nsACString& aMIMEType) + : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoImpl(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoBase(aType, aClass) {} + virtual ~nsMIMEInfoImpl() {} + + // nsIMIMEInfo methods + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + NS_IMETHOD GetDefaultDescription(nsAString& aDefaultDescription) override; + NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override; + + // additional methods + /** + * Sets the default application. Supposed to be only called by the OS Helper + * App Services; the default application is immutable after it is first set. + */ + void SetDefaultApplication(nsIFile* aApp) { + if (!mDefaultApplication) mDefaultApplication = aApp; + } + + protected: + // nsMIMEInfoBase methods + /** + * The base class implementation is to use LaunchWithIProcess in combination + * with mDefaultApplication. Subclasses can override that behaviour. + */ + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override; + + /** + * Loads the URI with the OS default app. This should be overridden by each + * OS's implementation. + */ + virtual nsresult LoadUriInternal(nsIURI* aURI) override = 0; + + nsCOMPtr<nsIFile> + mDefaultApplication; ///< default application associated with this type. +}; + +#endif //__nsmimeinfoimpl_h___ diff --git a/uriloader/exthandler/nsOSHelperAppServiceChild.cpp b/uriloader/exthandler/nsOSHelperAppServiceChild.cpp new file mode 100644 index 0000000000..1e1442fac6 --- /dev/null +++ b/uriloader/exthandler/nsOSHelperAppServiceChild.cpp @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <sys/stat.h> +#include "mozilla/Logging.h" +#include "mozilla/net/NeckoCommon.h" +#include "nsOSHelperAppServiceChild.h" +#include "nsMIMEInfoChild.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsIFile.h" +#include "nsIHandlerService.h" +#include "nsMimeTypes.h" +#include "nsMIMEInfoImpl.h" +#include "nsMemory.h" +#include "nsCRT.h" +#include "nsEmbedCID.h" + +#undef LOG +#define LOG(args) \ + MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info, args) +#undef LOG_ERR +#define LOG_ERR(args) \ + MOZ_LOG(nsExternalHelperAppService::mLog, mozilla::LogLevel::Error, args) +#undef LOG_ENABLED +#define LOG_ENABLED() \ + MOZ_LOG_TEST(nsExternalHelperAppService::mLog, mozilla::LogLevel::Info) + +nsresult nsOSHelperAppServiceChild::ExternalProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG_ERR(("nsOSHelperAppServiceChild error: no handler service")); + return rv; + } + + nsAutoCString scheme(aProtocolScheme); + *aHandlerExists = false; + rv = handlerSvc->ExistsForProtocol(scheme, aHandlerExists); + LOG( + ("nsOSHelperAppServiceChild::ExternalProtocolHandlerExists(): " + "protocol: %s, result: %" PRId32, + aProtocolScheme, static_cast<uint32_t>(rv))); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + return rv; +} + +nsresult nsOSHelperAppServiceChild::OSProtocolHandlerExists(const char* aScheme, + bool* aExists) { + // Use ExternalProtocolHandlerExists() which includes the + // OS-level check and remotes the call to the parent process. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::GetApplicationDescription(const nsACString& aScheme, + nsAString& aRetVal) { + nsresult rv; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG_ERR(("nsOSHelperAppServiceChild error: no handler service")); + return rv; + } + + rv = handlerSvc->GetApplicationDescription(aScheme, aRetVal); + LOG( + ("nsOSHelperAppServiceChild::GetApplicationDescription(): " + "scheme: %s, result: %" PRId32 ", description: %s", + PromiseFlatCString(aScheme).get(), static_cast<uint32_t>(rv), + NS_ConvertUTF16toUTF8(aRetVal).get())); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + return rv; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + RefPtr<nsChildProcessMIMEInfo> mimeInfo = + new nsChildProcessMIMEInfo(aMIMEType); + + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID); + if (handlerSvc) { + nsresult rv = + handlerSvc->GetMIMEInfoFromOS(mimeInfo, aMIMEType, aFileExt, aFound); + LOG( + ("nsOSHelperAppServiceChild::GetMIMEInfoFromOS(): " + "MIME type: %s, extension: %s, result: %" PRId32, + PromiseFlatCString(aMIMEType).get(), + PromiseFlatCString(aFileExt).get(), static_cast<uint32_t>(rv))); + mozilla::Unused << NS_WARN_IF(NS_FAILED(rv)); + if (NS_FAILED(rv)) { + return rv; + } + } else { + LOG_ERR(("nsOSHelperAppServiceChild error: no handler service")); + *aFound = false; + } + + mimeInfo.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::GetProtocolHandlerInfoFromOS( + const nsACString& aScheme, bool* aFound, nsIHandlerInfo** aRetVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppServiceChild::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* aRetVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsOSHelperAppServiceChild::GetFileTokenForPath( + const char16_t* platformAppPath, nsIFile** aFile) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/nsOSHelperAppServiceChild.h b/uriloader/exthandler/nsOSHelperAppServiceChild.h new file mode 100644 index 0000000000..f37caf010d --- /dev/null +++ b/uriloader/exthandler/nsOSHelperAppServiceChild.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppServiceChild_h__ +#define nsOSHelperAppServiceChild_h__ + +#include "nsExternalHelperAppService.h" + +class nsIMIMEInfo; + +/* + * Provides a generic implementation of the nsExternalHelperAppService + * platform-specific methods by remoting calls to the parent process. + * Only provides implementations for the methods needed in unprivileged + * child processes. + */ +class nsOSHelperAppServiceChild : public nsExternalHelperAppService { + public: + nsOSHelperAppServiceChild() : nsExternalHelperAppService(){}; + virtual ~nsOSHelperAppServiceChild() = default; + + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + NS_IMETHOD ExternalProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + + NS_IMETHOD OSProtocolHandlerExists(const char* aScheme, + bool* aExists) override; + + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& aRetVal) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; +}; + +#endif // nsOSHelperAppServiceChild_h__ diff --git a/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm b/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm new file mode 100644 index 0000000000..72c551d654 --- /dev/null +++ b/uriloader/exthandler/tests/HandlerServiceTestUtils.jsm @@ -0,0 +1,241 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Shared functions for tests related to invoking external handler applications. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["HandlerServiceTestUtils"]; + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gHandlerService", + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService" +); + +var HandlerServiceTestUtils = { + /** + * Retrieves the names of all the MIME types and protocols configured in the + * handler service instance currently under testing. + * + * @return Array of strings like "example/type" or "example-scheme", sorted + * alphabetically regardless of category. + */ + getAllHandlerInfoTypes() { + return Array.from(gHandlerService.enumerate(), info => info.type).sort(); + }, + + /** + * Retrieves all the configured handlers for MIME types and protocols. + * + * @note The nsIHandlerInfo instances returned by the "enumerate" method + * cannot be used for testing because they incorporate information from + * the operating system and also from the default nsIHandlerService + * instance, independently from what instance is under testing. + * + * @return Array of nsIHandlerInfo instances sorted by their "type" property. + */ + getAllHandlerInfos() { + return this.getAllHandlerInfoTypes().map(type => this.getHandlerInfo(type)); + }, + + /** + * Retrieves an nsIHandlerInfo for the given MIME type or protocol, which + * incorporates information from the operating system and also from the + * handler service instance currently under testing. + * + * @note If the handler service instance currently under testing is not the + * default one and the requested type is a MIME type, the returned + * nsIHandlerInfo will include information from the default + * nsIHandlerService instance. This cannot be avoided easily because the + * getMIMEInfoFromOS method is not exposed to JavaScript. + * + * @param type + * MIME type or scheme name of the nsIHandlerInfo to retrieve. + * + * @return The populated nsIHandlerInfo instance. + */ + getHandlerInfo(type) { + if (type.includes("/")) { + // We have to use the getFromTypeAndExtension method because we don't have + // access to getMIMEInfoFromOS. This means that we have to reset the data + // that may have been imported from the default nsIHandlerService instance + // and is not overwritten by fillHandlerInfo later. + let handlerInfo = gMIMEService.getFromTypeAndExtension(type, null); + if (AppConstants.platform == "android") { + // On Android, the first handler application is always the internal one. + while (handlerInfo.possibleApplicationHandlers.length > 1) { + handlerInfo.possibleApplicationHandlers.removeElementAt(1); + } + } else { + handlerInfo.possibleApplicationHandlers.clear(); + } + handlerInfo.setFileExtensions(""); + // Populate the object from the handler service instance under testing. + if (gHandlerService.exists(handlerInfo)) { + gHandlerService.fillHandlerInfo(handlerInfo, ""); + } + return handlerInfo; + } + + // Populate the protocol information from the handler service instance under + // testing, like the nsIExternalProtocolService::GetProtocolHandlerInfo + // method does on the default nsIHandlerService instance. + let osDefaultHandlerFound = {}; + let handlerInfo = gExternalProtocolService.getProtocolHandlerInfoFromOS( + type, + osDefaultHandlerFound + ); + if (gHandlerService.exists(handlerInfo)) { + gHandlerService.fillHandlerInfo(handlerInfo, ""); + } else { + gExternalProtocolService.setProtocolHandlerDefaults( + handlerInfo, + osDefaultHandlerFound.value + ); + } + return handlerInfo; + }, + + /** + * Creates an nsIHandlerInfo for the given MIME type or protocol, initialized + * to the default values for the current platform. + * + * @note For this method to work, the specified MIME type or protocol must not + * be configured in the default handler service instance or the one + * under testing, and must not be registered in the operating system. + * + * @param type + * MIME type or scheme name of the nsIHandlerInfo to create. + * + * @return The blank nsIHandlerInfo instance. + */ + getBlankHandlerInfo(type) { + let handlerInfo = this.getHandlerInfo(type); + + let preferredAction, preferredApplicationHandler; + if (AppConstants.platform == "android") { + // On Android, the default preferredAction for MIME types is useHelperApp. + // For protocols, we always behave as if an operating system provided + // handler existed, and as such we initialize them to useSystemDefault. + // This is because the AndroidBridge is not available in xpcshell tests. + preferredAction = type.includes("/") + ? Ci.nsIHandlerInfo.useHelperApp + : Ci.nsIHandlerInfo.useSystemDefault; + // On Android, the default handler application is always the internal one. + preferredApplicationHandler = { + name: "Android chooser", + }; + } else { + // On Desktop, the default preferredAction for MIME types is saveToDisk, + // while for protocols it is alwaysAsk. + preferredAction = type.includes("/") + ? Ci.nsIHandlerInfo.saveToDisk + : Ci.nsIHandlerInfo.alwaysAsk; + preferredApplicationHandler = null; + } + + this.assertHandlerInfoMatches(handlerInfo, { + type, + preferredAction, + alwaysAskBeforeHandling: true, + preferredApplicationHandler, + }); + return handlerInfo; + }, + + /** + * Checks whether an nsIHandlerInfo instance matches the provided object. + */ + assertHandlerInfoMatches(handlerInfo, expected) { + let expectedInterface = expected.type.includes("/") + ? Ci.nsIMIMEInfo + : Ci.nsIHandlerInfo; + Assert.ok(handlerInfo instanceof expectedInterface); + Assert.equal(handlerInfo.type, expected.type); + + if (!expected.preferredActionOSDependent) { + Assert.equal(handlerInfo.preferredAction, expected.preferredAction); + Assert.equal( + handlerInfo.alwaysAskBeforeHandling, + expected.alwaysAskBeforeHandling + ); + } + + if (expectedInterface == Ci.nsIMIMEInfo) { + let fileExtensionsEnumerator = handlerInfo.getFileExtensions(); + for (let expectedFileExtension of expected.fileExtensions || []) { + Assert.equal(fileExtensionsEnumerator.getNext(), expectedFileExtension); + } + Assert.ok(!fileExtensionsEnumerator.hasMore()); + } + + if (expected.preferredApplicationHandler) { + this.assertHandlerAppMatches( + handlerInfo.preferredApplicationHandler, + expected.preferredApplicationHandler + ); + } else { + Assert.equal(handlerInfo.preferredApplicationHandler, null); + } + + let handlerAppsArrayEnumerator = handlerInfo.possibleApplicationHandlers.enumerate(); + if (AppConstants.platform == "android") { + // On Android, the first handler application is always the internal one. + this.assertHandlerAppMatches(handlerAppsArrayEnumerator.getNext(), { + name: "Android chooser", + }); + } + for (let expectedHandlerApp of expected.possibleApplicationHandlers || []) { + this.assertHandlerAppMatches( + handlerAppsArrayEnumerator.getNext(), + expectedHandlerApp + ); + } + Assert.ok(!handlerAppsArrayEnumerator.hasMoreElements()); + }, + + /** + * Checks whether an nsIHandlerApp instance matches the provided object. + */ + assertHandlerAppMatches(handlerApp, expected) { + Assert.ok(handlerApp instanceof Ci.nsIHandlerApp); + Assert.equal(handlerApp.name, expected.name); + if (expected.executable) { + Assert.ok(handlerApp instanceof Ci.nsILocalHandlerApp); + Assert.ok(expected.executable.equals(handlerApp.executable)); + } else if (expected.uriTemplate) { + Assert.ok(handlerApp instanceof Ci.nsIWebHandlerApp); + Assert.equal(handlerApp.uriTemplate, expected.uriTemplate); + } else if (expected.service) { + Assert.ok(handlerApp instanceof Ci.nsIDBusHandlerApp); + Assert.equal(handlerApp.service, expected.service); + Assert.equal(handlerApp.method, expected.method); + Assert.equal(handlerApp.dBusInterface, expected.dBusInterface); + Assert.equal(handlerApp.objectPath, expected.objectPath); + } + }, +}; diff --git a/uriloader/exthandler/tests/WriteArgument.cpp b/uriloader/exthandler/tests/WriteArgument.cpp new file mode 100644 index 0000000000..603965150e --- /dev/null +++ b/uriloader/exthandler/tests/WriteArgument.cpp @@ -0,0 +1,20 @@ +#include <stdio.h> +#include "prenv.h" + +int main(int argc, char* argv[]) { + if (argc != 2) return 1; + + const char* value = PR_GetEnv("WRITE_ARGUMENT_FILE"); + + if (!value) return 2; + + FILE* outfile = fopen(value, "w"); + if (!outfile) return 3; + + // We only need to write out the first argument (no newline). + fputs(argv[argc - 1], outfile); + + fclose(outfile); + + return 0; +} diff --git a/uriloader/exthandler/tests/mochitest/.eslintrc.js b/uriloader/exthandler/tests/mochitest/.eslintrc.js new file mode 100644 index 0000000000..7612459de1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test", "plugin:mozilla/mochitest-test"], +}; diff --git a/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js new file mode 100644 index 0000000000..d08d72b048 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/HelperAppLauncherDialog_chromeScript.js @@ -0,0 +1,38 @@ +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); + +const HELPERAPP_DIALOG_CONTRACT = "@mozilla.org/helperapplauncherdialog;1"; +const HELPERAPP_DIALOG_CID = Components.ID( + Cc[HELPERAPP_DIALOG_CONTRACT].number +); + +const FAKE_CID = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator) + .generateUUID(); +/* eslint-env mozilla/frame-script */ +function HelperAppLauncherDialog() {} +HelperAppLauncherDialog.prototype = { + show(aLauncher, aWindowContext, aReason) { + sendAsyncMessage("suggestedFileName", aLauncher.suggestedFileName); + }, + QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]), +}; + +var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +registrar.registerFactory( + FAKE_CID, + "", + HELPERAPP_DIALOG_CONTRACT, + ComponentUtils._getFactory(HelperAppLauncherDialog) +); + +addMessageListener("unregister", function() { + registrar.registerFactory( + HELPERAPP_DIALOG_CID, + "", + HELPERAPP_DIALOG_CONTRACT, + null + ); + sendAsyncMessage("unregistered"); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser.ini b/uriloader/exthandler/tests/mochitest/browser.ini new file mode 100644 index 0000000000..0d5b20bef8 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser.ini @@ -0,0 +1,51 @@ +[DEFAULT] +head = head.js +support-files = + protocolHandler.html + +[browser_auto_close_window.js] +run-if = e10s # test relies on e10s behavior +support-files = + download_page.html + download.bin + download.sjs +[browser_download_always_ask_preferred_app.js] +[browser_download_privatebrowsing.js] +[browser_download_open_with_internal_handler.js] +support-files = + file_pdf_application_pdf.pdf + file_pdf_application_pdf.pdf^headers^ + file_pdf_application_unknown.pdf + file_pdf_application_unknown.pdf^headers^ + file_pdf_binary_octet_stream.pdf + file_pdf_binary_octet_stream.pdf^headers^ + file_txt_attachment_test.txt + file_txt_attachment_test.txt^headers^ + file_xml_attachment_binary_octet_stream.xml + file_xml_attachment_binary_octet_stream.xml^headers^ + file_xml_attachment_test.xml + file_xml_attachment_test.xml^headers^ +[browser_download_urlescape.js] +support-files = + file_with@@funny_name.png + file_with@@funny_name.png^headers^ + file_with[funny_name.webm + file_with[funny_name.webm^headers^ +[browser_extension_correction.js] +support-files = + file_as.exe + file_as.exe^headers^ +[browser_open_internal_choice_persistence.js] +support-files = + file_pdf_application_pdf.pdf + file_pdf_application_pdf.pdf^headers^ +[browser_protocol_ask_dialog.js] +support-files = + file_nested_protocol_request.html +[browser_first_prompt_not_blocked_without_user_interaction.js] +support-files = + file_external_protocol_iframe.html +[browser_protocol_ask_dialog_permission.js] +[browser_protocolhandler_loop.js] +[browser_remember_download_option.js] +[browser_web_protocol_handlers.js] diff --git a/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js new file mode 100644 index 0000000000..2e1d17e139 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_auto_close_window.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { ComponentUtils } = ChromeUtils.import( + "resource://gre/modules/ComponentUtils.jsm" +); + +const ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const PAGE_URL = ROOT + "download_page.html"; +const SJS_URL = ROOT + "download.sjs"; + +const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1"; +const HELPERAPP_DIALOG_CID = Components.ID( + Cc[HELPERAPP_DIALOG_CONTRACT_ID].number +); +const MOCK_HELPERAPP_DIALOG_CID = Components.ID( + "{2f372b6f-56c9-46d5-af0d-9f09bb69860c}" +); + +let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); +let curDialogResolve = null; + +function HelperAppLauncherDialog() {} + +HelperAppLauncherDialog.prototype = { + show(aLauncher, aWindowContext, aReason) { + ok(true, "Showing the helper app dialog"); + curDialogResolve(aWindowContext); + executeSoon(() => { + aLauncher.cancel(Cr.NS_ERROR_ABORT); + }); + }, + QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]), +}; + +function promiseHelperAppDialog() { + return new Promise(resolve => { + curDialogResolve = resolve; + }); +} + +let mockHelperAppService; + +add_task(async function setup() { + // Replace the real helper app dialog with our own. + mockHelperAppService = ComponentUtils._getFactory(HelperAppLauncherDialog); + registrar.registerFactory( + MOCK_HELPERAPP_DIALOG_CID, + "", + HELPERAPP_DIALOG_CONTRACT_ID, + mockHelperAppService + ); +}); + +add_task(async function simple_navigation() { + // Tests that simple navigation gives us the right windowContext (that is, + // the window that we're using). + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#regular_load", + {}, + browser + ); + let windowContext = await dialogAppeared; + + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + }); +}); + +// Given a browser pointing to download_page.html, clicks on the link that +// opens with target="_blank" (i.e. a new tab) and ensures that we +// automatically open and close that tab. +async function testNewTab(browser) { + let dialogAppeared = promiseHelperAppDialog(); + let tabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ).then(event => { + return [event.target, BrowserTestUtils.waitForTabClosing(event.target)]; + }); + + await BrowserTestUtils.synthesizeMouseAtCenter("#target_blank", {}, browser); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + let [tab, closingPromise] = await tabOpened; + await closingPromise; + is(tab.linkedBrowser, null, "tab was opened and closed"); +} + +add_task(async function target_blank() { + // Tests that a link with target=_blank opens a new tab and closes it, + // returning the window that we're using for navigation. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + await testNewTab(browser); + }); +}); + +add_task(async function target_blank_no_opener() { + // Tests that a link with target=_blank and no opener opens a new tab + // and closes it, returning the window that we're using for navigation. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let tabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ).then(event => { + return [event.target, BrowserTestUtils.waitForTabClosing(event.target)]; + }); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#target_blank_no_opener", + {}, + browser + ); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + let [tab, closingPromise] = await tabOpened; + await closingPromise; + is(tab.linkedBrowser, null, "tab was opened and closed"); + }); +}); + +add_task(async function open_in_new_tab_no_opener() { + // Tests that a link with target=_blank and no opener opens a new tab + // and closes it, returning the window that we're using for navigation. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let tabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ).then(event => { + return [event.target, BrowserTestUtils.waitForTabClosing(event.target)]; + }); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#open_in_new_tab_no_opener", + {}, + browser + ); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + let [tab, closingPromise] = await tabOpened; + await closingPromise; + is(tab.linkedBrowser, null, "tab was opened and closed"); + }); +}); + +add_task(async function new_window() { + // Tests that a link that forces us to open a new window (by specifying a + // width and a height in window.open) opens a new window for the load, + // realizes that we need to close that window and returns the *original* + // window as the window context. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let windowOpened = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.synthesizeMouseAtCenter("#new_window", {}, browser); + let win = await windowOpened; + // Now allow request to complete: + fetch(SJS_URL + "?finish"); + + let windowContext = await dialogAppeared; + is(windowContext, browser.ownerGlobal, "got the right windowContext"); + + // The window should close on its own. If not, this test will time out. + await BrowserTestUtils.domWindowClosed(win); + ok(win.closed, "window was opened and closed"); + + is( + await fetch(SJS_URL + "?reset").then(r => r.text()), + "OK", + "Test reseted" + ); + }); +}); + +add_task(async function new_window_no_opener() { + // Tests that a link that forces us to open a new window (by specifying a + // width and a height in window.open) opens a new window for the load, + // realizes that we need to close that window and returns the *original* + // window as the window context. + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + browser + ) { + let dialogAppeared = promiseHelperAppDialog(); + let windowOpened = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#new_window_no_opener", + {}, + browser + ); + let win = await windowOpened; + // Now allow request to complete: + fetch(SJS_URL + "?finish"); + + await dialogAppeared; + + // The window should close on its own. If not, this test will time out. + await BrowserTestUtils.domWindowClosed(win); + ok(win.closed, "window was opened and closed"); + + is( + await fetch(SJS_URL + "?reset").then(r => r.text()), + "OK", + "Test reseted" + ); + }); +}); + +add_task(async function nested_window_opens() { + // Tests that the window auto-closing feature works if the download is + // initiated by a window that, itself, has an opener (see bug 1373109). + await BrowserTestUtils.withNewTab({ gBrowser, url: PAGE_URL }, async function( + outerBrowser + ) { + let secondTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + `${PAGE_URL}?newwin`, + true + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "#open_in_new_tab", + {}, + outerBrowser + ); + let secondTab = await secondTabPromise; + let nestedBrowser = secondTab.linkedBrowser; + + await SpecialPowers.spawn(nestedBrowser, [], function() { + ok(content.opener, "this window has an opener"); + }); + + await testNewTab(nestedBrowser); + + isnot( + secondTab.linkedBrowser, + null, + "the page that triggered the download is still open" + ); + BrowserTestUtils.removeTab(secondTab); + }); +}); + +add_task(async function cleanup() { + // Unregister our factory from XPCOM and restore the original CID. + registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID, mockHelperAppService); + registrar.registerFactory( + HELPERAPP_DIALOG_CID, + "", + HELPERAPP_DIALOG_CONTRACT_ID, + null + ); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js new file mode 100644 index 0000000000..bd421d51f3 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_always_ask_preferred_app.js @@ -0,0 +1,25 @@ +add_task(async function() { + // Create mocked objects for test + let launcher = createMockedObjects(false); + // Open helper app dialog with mocked launcher + let dlg = await openHelperAppDialog(launcher); + let doc = dlg.document; + let location = doc.getElementById("source"); + let expectedValue = launcher.source.prePath; + if (location.value != expectedValue) { + info("Waiting for dialog to be populated."); + await BrowserTestUtils.waitForAttribute("value", location, expectedValue); + } + is( + doc.getElementById("mode").selectedItem.id, + "open", + "Should be opening the file." + ); + ok( + !dlg.document.getElementById("openHandler").selectedItem.hidden, + "Should not have selected a hidden item." + ); + let helperAppDialogHiddenPromise = BrowserTestUtils.windowClosed(dlg); + doc.getElementById("unknownContentType").cancelDialog(); + await helperAppDialogHiddenPromise; +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js new file mode 100644 index 0000000000..ef7174f30f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_open_with_internal_handler.js @@ -0,0 +1,612 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/Downloads.jsm", this); +const { DownloadIntegration } = ChromeUtils.import( + "resource://gre/modules/DownloadIntegration.jsm" +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +function waitForAcceptButtonToGetEnabled(doc) { + let dialog = doc.querySelector("#unknownContentType"); + let button = dialog.getButton("accept"); + return TestUtils.waitForCondition( + () => !button.disabled, + "Wait for Accept button to get enabled" + ); +} + +async function waitForPdfJS(browser, url) { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.eventBusDispatchToDOM", true]], + }); + // Runs tests after all "load" event handlers have fired off + let loadPromise = BrowserTestUtils.waitForContentEvent( + browser, + "documentloaded", + false, + null, + true + ); + await SpecialPowers.spawn(browser, [url], contentUrl => { + content.location = contentUrl; + }); + return loadPromise; +} + +add_task(async function setup() { + // Remove the security delay for the dialog during the test. + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.dialog_enable_delay", 0], + ["browser.helperApps.showOpenOptionForPdfJS", true], + ["browser.helperApps.showOpenOptionForViewableInternally", true], + ], + }); + + // Restore handlers after the whole test has run + const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + const registerRestoreHandler = function(type, ext) { + const mimeInfo = mimeSvc.getFromTypeAndExtension(type, ext); + const existed = handlerSvc.exists(mimeInfo); + registerCleanupFunction(() => { + if (existed) { + handlerSvc.store(mimeInfo); + } else { + handlerSvc.remove(mimeInfo); + } + }); + }; + registerRestoreHandler("application/pdf", "pdf"); + registerRestoreHandler("binary/octet-stream", "pdf"); + registerRestoreHandler("application/unknown", "pdf"); +}); + +/** + * Check that loading a PDF file with content-disposition: attachment + * shows an option to open with the internal handler, and that the + * internal option handler is not present when the download button + * is clicked from pdf.js. + */ +add_task(async function test_check_open_with_internal_handler() { + for (let file of [ + "file_pdf_application_pdf.pdf", + "file_pdf_binary_octet_stream.pdf", + ]) { + info("Testing with " + file); + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + // Add an extra tab after the loading tab so we can test that + // pdf.js is opened in the adjacent tab and not at the end of + // the tab strip. + let extraTab = await BrowserTestUtils.addTab(gBrowser, "about:blank"); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + let internalHandlerRadio = doc.querySelector("#handleInternally"); + + await waitForAcceptButtonToGetEnabled(doc); + + ok(!internalHandlerRadio.hidden, "The option should be visible for PDF"); + ok(internalHandlerRadio.selected, "The option should be selected"); + + let downloadFinishedPromise = promiseDownloadFinished(publicList); + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + let dialog = doc.querySelector("#unknownContentType"); + let button = dialog.getButton("accept"); + button.disabled = false; + dialog.acceptDialog(); + info("waiting for new tab to open"); + let newTab = await newTabPromise; + + is( + newTab._tPos - 1, + loadingTab._tPos, + "pdf.js should be opened in an adjacent tab" + ); + + await ContentTask.spawn(newTab.linkedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.readyState == "complete" + ); + }); + + let publicDownloads = await publicList.getAll(); + is( + publicDownloads.length, + 1, + "download should appear in publicDownloads list" + ); + + let download = await downloadFinishedPromise; + + let subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + // Current tab has file: URI and TEST_PATH is http uri, so uri will be different + BrowserTestUtils.loadURI(newTab.linkedBrowser, TEST_PATH + file); + let subDialogWindow = await subdialogPromise; + let subDoc = subDialogWindow.document; + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(subDoc); + let subInternalHandlerRadio = subDoc.querySelector("#handleInternally"); + ok( + !subInternalHandlerRadio.hidden, + "This option should be shown when the dialog is shown for another PDF" + ); + // Cancel dialog + subDoc.querySelector("#unknownContentType").cancelDialog(); + + subdialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + await SpecialPowers.spawn(newTab.linkedBrowser, [], async () => { + let downloadButton; + await ContentTaskUtils.waitForCondition(() => { + downloadButton = content.document.querySelector("#download"); + return !!downloadButton; + }); + ok(downloadButton, "Download button should be present in pdf.js"); + downloadButton.click(); + }); + info( + "Waiting for unknown content type dialog to appear from pdf.js download button click" + ); + subDialogWindow = await subdialogPromise; + subDoc = subDialogWindow.document; + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(subDoc); + subInternalHandlerRadio = subDoc.querySelector("#handleInternally"); + ok( + subInternalHandlerRadio.hidden, + "The option should be hidden when the dialog is opened from pdf.js" + ); + subDoc.querySelector("#open").click(); + + let tabOpenListener = () => { + ok( + false, + "A new tab should not be opened when accepting the dialog with 'open-with-external-app' chosen" + ); + }; + gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener); + + let oldLaunchFile = DownloadIntegration.launchFile; + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = async () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + downloadFinishedPromise = promiseDownloadFinished(publicList); + + info("Accepting the dialog"); + subDoc.querySelector("#unknownContentType").acceptDialog(); + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + DownloadIntegration.launchFile = oldLaunchFile; + + // Remove the first file (can't do this sooner or the second load fails): + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + + gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener); + BrowserTestUtils.removeTab(loadingTab); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(extraTab); + + // Remove the remaining file once complete. + download = await downloadFinishedPromise; + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + await publicList.removeFinished(); + } +}); + +/** + * Test that choosing to open in an external application doesn't + * open the PDF into pdf.js + */ +add_task(async function test_check_open_with_external_application() { + for (let file of [ + "file_pdf_application_pdf.pdf", + "file_pdf_binary_octet_stream.pdf", + ]) { + info("Testing with " + file); + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + + let oldLaunchFile = DownloadIntegration.launchFile; + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + let doc = dialogWindow.document; + await waitForAcceptButtonToGetEnabled(doc); + let dialog = doc.querySelector("#unknownContentType"); + doc.querySelector("#open").click(); + let button = dialog.getButton("accept"); + button.disabled = false; + info("Accepting the dialog"); + dialog.acceptDialog(); + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + DownloadIntegration.launchFile = oldLaunchFile; + + let publicDownloads = await publicList.getAll(); + is( + publicDownloads.length, + 1, + "download should appear in publicDownloads list" + ); + let download = publicDownloads[0]; + ok( + !download.launchWhenSucceeded, + "launchWhenSucceeded should be false after launchFile is called" + ); + + BrowserTestUtils.removeTab(loadingTab); + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + await publicList.removeFinished(); + } +}); + +/** + * Test that choosing to open a PDF with an external application works and + * then downloading the same file again and choosing Open with Firefox opens + * the download in Firefox. + */ +add_task(async function test_check_open_with_external_then_internal() { + // This test only runs on Windows because appPicker.xhtml is only used on Windows. + if (AppConstants.platform != "win") { + return; + } + + // This test covers a bug that only occurs when the mimeInfo is set to Always Ask + const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + const mimeInfo = mimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + mimeInfo.preferredAction = mimeInfo.alwaysAsk; + mimeInfo.alwaysAskBeforeHandling = true; + handlerSvc.store(mimeInfo); + + for (let [file, mimeType] of [ + ["file_pdf_application_pdf.pdf", "application/pdf"], + ["file_pdf_binary_octet_stream.pdf", "binary/octet-stream"], + ["file_pdf_application_unknown.pdf", "application/unknown"], + ]) { + info("Testing with " + file); + let originalMimeInfo = mimeSvc.getFromTypeAndExtension(mimeType, "pdf"); + + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + // Open a new tab to the PDF file which will trigger the Unknown Content Type dialog + // and choose to open the PDF with an external application. + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + + let oldLaunchFile = DownloadIntegration.launchFile; + let waitForLaunchFileCalled = new Promise(resolve => { + DownloadIntegration.launchFile = () => { + ok(true, "The file should be launched with an external application"); + resolve(); + }; + }); + + let doc = dialogWindow.document; + await waitForAcceptButtonToGetEnabled(doc); + let dialog = doc.querySelector("#unknownContentType"); + let openHandlerMenulist = doc.querySelector("#openHandler"); + let originalDefaultHandler = openHandlerMenulist.label; + doc.querySelector("#open").click(); + doc.querySelector("#openHandlerPopup").click(); + let oldOpenDialog = dialogWindow.openDialog; + dialogWindow.openDialog = (location, unused2, unused3, params) => { + is(location, "chrome://global/content/appPicker.xhtml", "app picker"); + let handlerApp = params.mimeInfo.possibleLocalHandlers.queryElementAt( + 0, + Ci.nsILocalHandlerApp + ); + ok(handlerApp.executable, "handlerApp should be executable"); + ok(handlerApp.executable.isFile(), "handlerApp should be a file"); + params.handlerApp = handlerApp; + }; + doc.querySelector("#choose").click(); + dialogWindow.openDialog = oldOpenDialog; + await TestUtils.waitForCondition( + () => originalDefaultHandler != openHandlerMenulist.label, + "waiting for openHandler to get updated" + ); + let newDefaultHandler = openHandlerMenulist.label; + info(`was ${originalDefaultHandler}, now ${newDefaultHandler}`); + let button = dialog.getButton("accept"); + button.disabled = false; + info("Accepting the dialog"); + dialog.acceptDialog(); + info("Waiting until DownloadIntegration.launchFile is called"); + await waitForLaunchFileCalled; + BrowserTestUtils.removeTab(loadingTab); + + // Now, open a new tab to the PDF file which will trigger the Unknown Content Type dialog + // and choose to open the PDF internally. The previously used external application should be shown as + // the external option. + dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + + DownloadIntegration.launchFile = () => { + ok(false, "The file should not be launched with an external application"); + }; + + doc = dialogWindow.document; + await waitForAcceptButtonToGetEnabled(doc); + openHandlerMenulist = doc.querySelector("#openHandler"); + is(openHandlerMenulist.label, newDefaultHandler, "'new' handler"); + dialog = doc.querySelector("#unknownContentType"); + doc.querySelector("#handleInternally").click(); + info("Accepting the dialog"); + button = dialog.getButton("accept"); + button.disabled = false; + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser); + dialog.acceptDialog(); + + info("waiting for new tab to open"); + let newTab = await newTabPromise; + + await ContentTask.spawn(newTab.linkedBrowser, null, async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.readyState == "complete" + ); + }); + + is( + newTab.linkedBrowser.contentPrincipal.origin, + "resource://pdf.js", + "PDF should be opened with pdf.js" + ); + + BrowserTestUtils.removeTab(loadingTab); + BrowserTestUtils.removeTab(newTab); + + // Now trigger the dialog again and select the system + // default option to reset the state for the next iteration of the test. + // Reset the state for the next iteration of the test. + handlerSvc.store(originalMimeInfo); + DownloadIntegration.launchFile = oldLaunchFile; + let [download] = await publicList.getAll(); + if (download?.target.exists) { + try { + info("removing " + download.target.path); + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + await publicList.removeFinished(); + } +}); + +/** + * Check that the "Open with internal handler" option is presented + * for other viewable internally types. + */ +add_task( + async function test_internal_handler_hidden_with_viewable_internally_type() { + for (let [file, checkDefault] of [ + // The default for binary/octet-stream is changed by the PDF tests above, + // this may change given bug 1659008, so I'm just ignoring the default for now. + ["file_xml_attachment_binary_octet_stream.xml", false], + ["file_xml_attachment_test.xml", true], + ]) { + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + let internalHandlerRadio = doc.querySelector("#handleInternally"); + + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(doc); + + ok(!internalHandlerRadio.hidden, "The option should be visible for XML"); + if (checkDefault) { + ok(internalHandlerRadio.selected, "The option should be selected"); + } + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + } + } +); + +/** + * Check that the "Open with internal handler" option is not presented + * for non-PDF, non-viewable-internally types. + */ +add_task(async function test_internal_handler_hidden_with_other_type() { + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_txt_attachment_test.txt" + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + + // Prevent racing with initialization of the dialog and make sure that + // the final state of the dialog has the correct visibility of the internal-handler option. + await waitForAcceptButtonToGetEnabled(doc); + + let internalHandlerRadio = doc.querySelector("#handleInternally"); + ok( + internalHandlerRadio.hidden, + "The option should be hidden for unknown file type" + ); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); +}); + +/** + * Check that the "Open with internal handler" option is not presented + * when the feature is disabled for PDFs. + */ +add_task(async function test_internal_handler_hidden_with_pdf_pref_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.helperApps.showOpenOptionForPdfJS", false]], + }); + for (let file of [ + "file_pdf_application_pdf.pdf", + "file_pdf_binary_octet_stream.pdf", + ]) { + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + + await waitForAcceptButtonToGetEnabled(doc); + + let internalHandlerRadio = doc.querySelector("#handleInternally"); + ok( + internalHandlerRadio.hidden, + "The option should be hidden for PDF when the pref is false" + ); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + } +}); + +/** + * Check that the "Open with internal handler" option is not presented + * for other viewable internally types when disabled. + */ +add_task( + async function test_internal_handler_hidden_with_viewable_internally_pref_disabled() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.helperApps.showOpenOptionForViewableInternally", false]], + }); + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_xml_attachment_test.xml" + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + + await waitForAcceptButtonToGetEnabled(doc); + + let internalHandlerRadio = doc.querySelector("#handleInternally"); + ok( + internalHandlerRadio.hidden, + "The option should be hidden for XML when the pref is false" + ); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.cancelDialog(); + BrowserTestUtils.removeTab(loadingTab); + } +); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js new file mode 100644 index 0000000000..02cf6c3941 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_privatebrowsing.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that downloads started from a private window by clicking on a link end + * up in the global list of private downloads (see bug 1367581). + */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/Downloads.jsm", this); +ChromeUtils.import("resource://gre/modules/DownloadPaths.jsm", this); +ChromeUtils.import("resource://testing-common/FileTestUtils.jsm", this); +ChromeUtils.import("resource://testing-common/MockRegistrar.jsm", this); + +add_task(async function test_setup() { + // Save downloads to disk without showing the dialog. + let cid = MockRegistrar.register("@mozilla.org/helperapplauncherdialog;1", { + QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]), + show(launcher) { + launcher.promptForSaveDestination(); + }, + promptForSaveToFileAsync(launcher) { + // The dialog should create the empty placeholder file. + let file = FileTestUtils.getTempFile(); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + launcher.saveDestinationAvailable(file); + }, + }); + registerCleanupFunction(() => { + MockRegistrar.unregister(cid); + }); +}); + +add_task(async function test_download_privatebrowsing() { + let privateList = await Downloads.getList(Downloads.PRIVATE); + let publicList = await Downloads.getList(Downloads.PUBLIC); + + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + try { + let tab = await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + `data:text/html,<a download href="data:text/plain,">download</a>` + ); + + let promiseNextPrivateDownload = new Promise(resolve => { + privateList.addView({ + onDownloadAdded(download) { + privateList.removeView(this); + resolve(download); + }, + }); + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + content.document.querySelector("a").click(); + }); + + // Wait for the download to finish so the file can be safely deleted later. + let download = await promiseNextPrivateDownload; + await download.whenSucceeded(); + + // Clean up after checking that there are no new public downloads either. + let publicDownloads = await publicList.getAll(); + Assert.equal(publicDownloads.length, 0); + await privateList.removeFinished(); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js new file mode 100644 index 0000000000..ffab8146b6 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_download_urlescape.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +registerCleanupFunction(() => MockFilePicker.cleanup()); + +/** + * Check downloading files URL-escapes content-disposition + * information when necessary. + */ +add_task(async function test_check_filename_urlescape() { + let pendingPromise; + let pendingTest = ""; + let expectedFileName = ""; + MockFilePicker.showCallback = function(fp) { + info(`${pendingTest} - Filepicker shown, checking filename`); + is( + fp.defaultString, + expectedFileName, + `${pendingTest} - Should have escaped filename` + ); + ok( + pendingPromise, + `${pendingTest} - Should have expected this picker open.` + ); + if (pendingPromise) { + pendingPromise.resolve(); + } + return Ci.nsIFilePicker.returnCancel; + }; + function runTestFor(fileName, selector) { + return BrowserTestUtils.withNewTab(TEST_PATH + fileName, async browser => { + expectedFileName = fileName; + let tabLabel = gBrowser.getTabForBrowser(browser).getAttribute("label"); + ok( + tabLabel.startsWith(fileName), + `"${tabLabel}" should have been escaped.` + ); + + pendingTest = "save browser"; + pendingPromise = PromiseUtils.defer(); + // First try to save the browser + saveBrowser(browser); + await pendingPromise.promise; + + // Next, try the context menu: + pendingTest = "save from context menu"; + pendingPromise = PromiseUtils.defer(); + let menu = document.getElementById("contentAreaContextMenu"); + let menuShown = BrowserTestUtils.waitForEvent(menu, "popupshown"); + BrowserTestUtils.synthesizeMouse( + selector, + 5, + 5, + { type: "contextmenu", button: 2 }, + browser + ); + await menuShown; + gContextMenu.saveMedia(); + menu.hidePopup(); + await pendingPromise.promise; + pendingPromise = null; + }); + } + await runTestFor("file_with@@funny_name.png", "img"); + await runTestFor("file_with[funny_name.webm", "video"); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_extension_correction.js b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js new file mode 100644 index 0000000000..b806ee9ace --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_extension_correction.js @@ -0,0 +1,145 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +let gPathsToRemove = []; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.download.useDownloadDir", true]], + }); + registerCleanupFunction(async () => { + for (let path of gPathsToRemove) { + // IOUtils.remove ignores non-existing files out of the box. + await IOUtils.remove(path); + } + let publicList = await Downloads.getList(Downloads.PUBLIC); + await publicList.removeFinished(); + }); +}); + +async function testLinkWithoutExtension(type, shouldHaveExtension) { + info("Checking " + type); + + let task = function() { + return SpecialPowers.spawn(gBrowser.selectedBrowser, [type], mimetype => { + let link = content.document.createElement("a"); + link.textContent = "Click me"; + link.href = "data:" + mimetype + ",hello"; + link.download = "somefile"; + content.document.body.appendChild(link); + link.click(); + }); + }; + await checkDownloadWithExtensionState(task, { type, shouldHaveExtension }); +} + +async function checkDownloadWithExtensionState( + task, + { type, shouldHaveExtension, expectedName = null } +) { + let winPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + + await task(); + + info("Waiting for dialog."); + let win = await winPromise; + + let actualName = win.document.getElementById("location").value; + if (shouldHaveExtension) { + expectedName ??= "somefile." + getMIMEInfoForType(type).primaryExtension; + is(actualName, expectedName, `${type} should get an extension`); + } else { + expectedName ??= "somefile"; + is(actualName, expectedName, `${type} should not get an extension`); + } + + let closedPromise = BrowserTestUtils.windowClosed(win); + + if (shouldHaveExtension) { + // Wait for the download. + let publicList = await Downloads.getList(Downloads.PUBLIC); + let downloadFinishedPromise = promiseDownloadFinished(publicList); + + // Then pick "save" in the dialog. + let dialog = win.document.getElementById("unknownContentType"); + win.document.getElementById("save").click(); + let button = dialog.getButton("accept"); + button.disabled = false; + dialog.acceptDialog(); + + // Wait for the download to finish and check the extension is correct. + let download = await downloadFinishedPromise; + is( + PathUtils.filename(download.target.path), + expectedName, + `Downloaded file should also match ${expectedName}` + ); + gPathsToRemove.push(download.target.path); + let pathToRemove = download.target.path; + // Avoid one file interfering with subsequent files. + await publicList.removeFinished(); + await IOUtils.remove(pathToRemove); + } else { + // We just cancel out for files that would end up without a path, as we'd + // prompt for a filename. + win.close(); + } + return closedPromise; +} + +/** + * Check that for document types, images, videos and audio files, + * we enforce a useful extension. + */ +add_task(async function test_enforce_useful_extension() { + await BrowserTestUtils.withNewTab("data:text/html,", async browser => { + await testLinkWithoutExtension("image/png", true); + await testLinkWithoutExtension("audio/ogg", true); + await testLinkWithoutExtension("video/webm", true); + await testLinkWithoutExtension("application/msword", true); + await testLinkWithoutExtension("application/pdf", true); + + await testLinkWithoutExtension("application/x-gobbledygook", false); + await testLinkWithoutExtension("application/octet-stream", false); + await testLinkWithoutExtension("binary/octet-stream", false); + await testLinkWithoutExtension("application/x-msdownload", false); + }); +}); + +/** + * Check that we still use URL extension info when we don't have anything else, + * despite bogus local info. + */ +add_task(async function test_broken_saved_handlerinfo_and_useless_mimetypes() { + let bogusType = getMIMEInfoForType("binary/octet-stream"); + bogusType.setFileExtensions(["jpg"]); + let handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + handlerSvc.store(bogusType); + let tabToClean = null; + let task = function() { + return BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_as.exe?foo=bar" + ).then(tab => { + return (tabToClean = tab); + }); + }; + await checkDownloadWithExtensionState(task, { + type: "binary/octet-stream", + shouldHaveExtension: true, + expectedName: "file_as.exe", + }); + // Downloads should really close their tabs... + if (tabToClean?.isConnected) { + BrowserTestUtils.removeTab(tabToClean); + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js new file mode 100644 index 0000000000..b6f401e5e1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_first_prompt_not_blocked_without_user_interaction.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +add_task(setupMailHandler); + +add_task(async function test_open_without_user_interaction() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.disable_open_during_load", true], + ["dom.block_external_protocol_in_iframes", true], + ["dom.delay.block_external_protocol_in_iframes.enabled", false], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.loadURI( + tab.linkedBrowser, + TEST_PATH + "file_external_protocol_iframe.html" + ); + + let dialog = await dialogWindowPromise; + ok(dialog, "Should show the dialog even without user interaction"); + + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + + // Adding another iframe without user interaction should be blocked. + let blockedWarning = new Promise(resolve => { + Services.console.registerListener(function onMessage(msg) { + let { message, logLevel } = msg; + if (logLevel != Ci.nsIConsoleMessage.warn) { + return; + } + if (!message.includes("Iframe with external protocol was blocked")) { + return; + } + Services.console.unregisterListener(onMessage); + resolve(); + }); + }); + + info("Adding another frame without user interaction"); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let frame = content.document.createElement("iframe"); + frame.src = "mailto:foo@baz.com"; + content.document.body.appendChild(frame); + }); + + await blockedWarning; + + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js new file mode 100644 index 0000000000..a36443edd1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_open_internal_choice_persistence.js @@ -0,0 +1,255 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import("resource://gre/modules/Downloads.jsm", this); +const { DownloadIntegration } = ChromeUtils.import( + "resource://gre/modules/DownloadIntegration.jsm" +); + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +function waitForAcceptButtonToGetEnabled(doc) { + let dialog = doc.querySelector("#unknownContentType"); + let button = dialog.getButton("accept"); + return TestUtils.waitForCondition( + () => !button.disabled, + "Wait for Accept button to get enabled" + ); +} + +async function waitForPdfJS(browser, url) { + await SpecialPowers.pushPrefEnv({ + set: [["pdfjs.eventBusDispatchToDOM", true]], + }); + // Runs tests after all "load" event handlers have fired off + let loadPromise = BrowserTestUtils.waitForContentEvent( + browser, + "documentloaded", + false, + null, + true + ); + await SpecialPowers.spawn(browser, [url], contentUrl => { + content.location = contentUrl; + }); + return loadPromise; +} + +add_task(async function setup() { + // Remove the security delay for the dialog during the test. + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.dialog_enable_delay", 0], + ["browser.helperApps.showOpenOptionForPdfJS", true], + ["browser.helperApps.showOpenOptionForViewableInternally", true], + ], + }); + + // Restore handlers after the whole test has run + const registerRestoreHandler = function(type, ext) { + const mimeInfo = gMimeSvc.getFromTypeAndExtension(type, ext); + const existed = gHandlerSvc.exists(mimeInfo); + registerCleanupFunction(() => { + if (existed) { + gHandlerSvc.store(mimeInfo); + } else { + gHandlerSvc.remove(mimeInfo); + } + }); + }; + registerRestoreHandler("application/pdf", "pdf"); +}); + +const { handleInternally, saveToDisk, useSystemDefault } = Ci.nsIHandlerInfo; + +const kTestCases = [ + { + description: + "Saving to disk when internal handling is the default shouldn't change prefs.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: false }, + dialogActions(doc) { + let saveItem = doc.querySelector("#save"); + saveItem.click(); + ok(saveItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: false, + expectedPreferredAction: handleInternally, + expectedAsk: false, + }, + { + description: + "Opening externally when internal handling is the default shouldn't change prefs.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: false }, + dialogActions(doc) { + let openItem = doc.querySelector("#open"); + openItem.click(); + ok(openItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: true, + expectedPreferredAction: handleInternally, + expectedAsk: false, + }, + { + description: + "Saving to disk when internal handling is the default *should* change prefs if checkbox is ticked.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: false }, + dialogActions(doc) { + let saveItem = doc.querySelector("#save"); + saveItem.click(); + ok(saveItem.selected, "The 'save' option should now be selected"); + let checkbox = doc.querySelector("#rememberChoice"); + checkbox.checked = true; + checkbox.doCommand(); + }, + expectTab: false, + expectLaunch: false, + expectedPreferredAction: saveToDisk, + expectedAsk: false, + }, + { + description: + "Saving to disk when asking is the default should change persisted default.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: true }, + dialogActions(doc) { + let saveItem = doc.querySelector("#save"); + saveItem.click(); + ok(saveItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: false, + expectedPreferredAction: saveToDisk, + expectedAsk: true, + }, + { + description: + "Opening externally when asking is the default should change persisted default.", + preDialogState: { preferredAction: handleInternally, alwaysAsk: true }, + dialogActions(doc) { + let openItem = doc.querySelector("#open"); + openItem.click(); + ok(openItem.selected, "The 'save' option should now be selected"); + }, + expectTab: false, + expectLaunch: true, + expectedPreferredAction: useSystemDefault, + expectedAsk: true, + }, +]; + +function ensureMIMEState({ preferredAction, alwaysAsk }) { + const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + mimeInfo.preferredAction = preferredAction; + mimeInfo.alwaysAskBeforeHandling = alwaysAsk; + gHandlerSvc.store(mimeInfo); +} + +/** + * Test that if we have PDFs set to handle internally, and the user chooses to + * do something else with it, we do not alter the saved state. + */ +add_task(async function test_check_saving_handler_choices() { + let publicList = await Downloads.getList(Downloads.PUBLIC); + registerCleanupFunction(async () => { + await publicList.removeFinished(); + }); + for (let testCase of kTestCases) { + let file = "file_pdf_application_pdf.pdf"; + info("Testing with " + file + "; " + testCase.description); + ensureMIMEState(testCase.preDialogState); + + let dialogWindowPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + let loadingTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + file + ); + let dialogWindow = await dialogWindowPromise; + is( + dialogWindow.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Should have seen the unknown content dialogWindow." + ); + let doc = dialogWindow.document; + let internalHandlerRadio = doc.querySelector("#handleInternally"); + + await waitForAcceptButtonToGetEnabled(doc); + + ok(!internalHandlerRadio.hidden, "The option should be visible for PDF"); + ok( + internalHandlerRadio.selected, + "The Firefox option should be selected by default" + ); + + const { expectTab, expectLaunch, description } = testCase; + // Prep to intercept things so we only see the results we want. + let tabOpenListener = ev => { + ok( + expectTab, + `A new tab should ${expectTab ? "" : "not "}be opened - ${description}` + ); + BrowserTestUtils.removeTab(ev.target); + }; + gBrowser.tabContainer.addEventListener("TabOpen", tabOpenListener); + + let oldLaunchFile = DownloadIntegration.launchFile; + let fileLaunched = PromiseUtils.defer(); + DownloadIntegration.launchFile = () => { + ok( + expectLaunch, + `The file should ${ + expectLaunch ? "" : "not " + }be launched with an external application - ${description}` + ); + fileLaunched.resolve(); + }; + let downloadFinishedPromise = promiseDownloadFinished(publicList); + + await testCase.dialogActions(doc); + + let dialog = doc.querySelector("#unknownContentType"); + dialog.acceptDialog(); + + let download = await downloadFinishedPromise; + if (expectLaunch) { + await fileLaunched.promise; + } + DownloadIntegration.launchFile = oldLaunchFile; + gBrowser.tabContainer.removeEventListener("TabOpen", tabOpenListener); + + is( + (await publicList.getAll()).length, + 1, + "download should appear in public list" + ); + + // Check mime info: + const mimeInfo = gMimeSvc.getFromTypeAndExtension("application/pdf", "pdf"); + gHandlerSvc.fillHandlerInfo(mimeInfo, ""); + is( + mimeInfo.preferredAction, + testCase.expectedPreferredAction, + "preferredAction - " + description + ); + is( + mimeInfo.alwaysAskBeforeHandling, + testCase.expectedAsk, + "alwaysAsk - " + description + ); + + BrowserTestUtils.removeTab(loadingTab); + await publicList.removeFinished(); + if (download?.target.exists) { + try { + await IOUtils.remove(download.target.path); + } catch (ex) { + /* ignore */ + } + } + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js new file mode 100644 index 0000000000..5f60e39bb1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog.js @@ -0,0 +1,398 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const CONTENT_HANDLING_URL = + "chrome://mozapps/content/handling/appChooser.xhtml"; + +add_task(setupMailHandler); + +/** + * Check that if we open the protocol handler dialog from a subframe, we close + * it when closing the tab. + */ +add_task(async function test_closed_by_tab_closure() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_nested_protocol_request.html" + ); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed."); +}); + +/** + * Check that if we open the protocol handler dialog from a subframe, we close + * it when navigating the tab to a non-same-origin URL. + */ +add_task(async function test_closed_by_tab_navigation() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_nested_protocol_request.html" + ); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + info( + "Set up unload handler to ensure we don't break when the window global gets cleared" + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + content.addEventListener("unload", function() {}); + }); + + info("Navigating tab to a different but same origin page."); + BrowserTestUtils.loadURI(tab.linkedBrowser, TEST_PATH); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, TEST_PATH); + ok(dialog._frame.contentWindow, "Dialog should stay open."); + + // The use of weak references in various parts of the code means that we're + // susceptible to dropping crucial bits of our implementation on the floor, + // if they get GC'd, and then the test hangs. + // Do a bunch of GC/CC runs so that if we ever break, it's deterministic. + let numCycles = 3; + for (let i = 0; i < numCycles; i++) { + Cu.forceGC(); + Cu.forceCC(); + } + + info("Now navigate to a cross-origin page."); + const CROSS_ORIGIN_TEST_PATH = TEST_PATH.replace(".com", ".org"); + BrowserTestUtils.loadURI(tab.linkedBrowser, CROSS_ORIGIN_TEST_PATH); + let loadPromise = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + CROSS_ORIGIN_TEST_PATH + ); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed."); + + // Avoid errors from aborted loads by waiting for it to finish. + await loadPromise; + gBrowser.removeTab(tab); +}); + +/** + * Check that we cannot open more than one of these dialogs. + */ +add_task(async function test_multiple_dialogs() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_PATH + "file_nested_protocol_request.html" + ); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + + // Navigate the parent frame: + await ContentTask.spawn(tab.linkedBrowser, [], () => + content.eval("location.href = 'mailto:help@example.com'") + ); + + // Wait for a few ticks: + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 100)); + // Check we only have one dialog + + let tabDialogBox = gBrowser.getTabDialogBox(tab.linkedBrowser); + let dialogs = tabDialogBox + .getTabDialogManager() + ._dialogs.filter(d => d._openedURL == CONTENT_HANDLING_URL); + + is(dialogs.length, 1, "Should only have 1 dialog open"); + + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + dialog = await dialogClosedPromise; + + ok(!dialog._openedURL, "The dialog should have been closed."); + + // Then reopen the dialog again, to make sure we don't keep blocking: + dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Second dialog URL is as expected" + ); + + dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed again."); +}); + +/** + * Check that navigating invisible frames to external-proto URLs + * is handled correctly. + */ +add_task(async function invisible_iframes() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + // Ensure we notice the dialog opening: + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + let frame = content.document.createElement("iframe"); + frame.style.display = "none"; + frame.src = "mailto:help@example.com"; + content.document.body.append(frame); + }); + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog opens as expected for invisible iframe" + ); + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + await dialogClosedPromise; + gBrowser.removeTab(tab); +}); + +/** + * Check that nested iframes are handled correctly. + */ +add_task(async function nested_iframes() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + // Ensure we notice the dialog opening: + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + let innerLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + "https://example.org/" + ); + info("Constructing top frame"); + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + let frame = content.document.createElement("iframe"); + frame.src = "https://example.org/"; // cross-origin frame. + content.document.body.prepend(frame); + + content.eval( + `window.addEventListener("message", e => e.source.location = "mailto:help@example.com");` + ); + }); + + await innerLoaded; + let parentBC = tab.linkedBrowser.browsingContext; + + info("Creating innermost frame"); + await SpecialPowers.spawn(parentBC.children[0], [], async function() { + let innerFrame = content.document.createElement("iframe"); + let frameLoaded = ContentTaskUtils.waitForEvent(innerFrame, "load", true); + content.document.body.prepend(innerFrame); + await frameLoaded; + }); + + info("Posting event from innermost frame"); + await SpecialPowers.spawn( + parentBC.children[0].children[0], + [], + async function() { + // Top browsing context needs reference to the innermost, which is cross origin. + content.eval("top.postMessage('hello', '*')"); + } + ); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog opens as expected for deeply nested cross-origin iframe" + ); + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + await dialogClosedPromise; + gBrowser.removeTab(tab); +}); + +add_task(async function test_oop_iframe() { + const URI = `data:text/html,<div id="root"><iframe src="http://example.com/document-builder.sjs?html=<a href='mailto:help@example.com'>Mail it</a>"></iframe></div>`; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URI); + + // Wait for the window and then click the link. + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + + BrowserTestUtils.synthesizeMouseAtCenter( + "a:link", + {}, + tab.linkedBrowser.browsingContext.children[0] + ); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog URL is as expected" + ); + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + + info("Removing tab to close the dialog."); + gBrowser.removeTab(tab); + await dialogClosedPromise; + ok(!dialog._frame.contentWindow, "The dialog should have been closed."); +}); + +/** + * Check that a cross-origin iframe can navigate the top frame + * to an external protocol. + */ +add_task(async function xorigin_iframe_can_navigate_top() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com/" + ); + + // Ensure we notice the dialog opening: + let dialogWindowPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + true + ); + let innerLoaded = BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + true, + "https://example.org/" + ); + info("Constructing frame"); + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + let frame = content.document.createElement("iframe"); + frame.src = "https://example.org/"; // cross-origin frame. + content.document.body.prepend(frame); + }); + await innerLoaded; + + info("Navigating top bc from frame"); + let parentBC = tab.linkedBrowser.browsingContext; + await SpecialPowers.spawn(parentBC.children[0], [], async function() { + content.eval("window.top.location.href = 'mailto:example@example.com';"); + }); + + let dialog = await dialogWindowPromise; + + is( + dialog._frame.contentDocument.location.href, + CONTENT_HANDLING_URL, + "Dialog opens as expected for navigating the top frame from an x-origin frame." + ); + // Close the dialog: + let dialogClosedPromise = waitForProtocolAppChooserDialog( + tab.linkedBrowser, + false + ); + dialog.close(); + await dialogClosedPromise; + gBrowser.removeTab(tab); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js new file mode 100644 index 0000000000..7050616dc3 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocol_ask_dialog_permission.js @@ -0,0 +1,754 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm", + this +); + +let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +// Testing multiple protocol / origin combinations takes long on debug. +requestLongerTimeout(3); + +const DIALOG_URL_APP_CHOOSER = + "chrome://mozapps/content/handling/appChooser.xhtml"; +const DIALOG_URL_PERMISSION = + "chrome://mozapps/content/handling/permissionDialog.xhtml"; + +const PROTOCOL_HANDLER_OPEN_PERM_KEY = "open-protocol-handler"; +const PERMISSION_KEY_DELIMITER = "^"; + +const TEST_PROTOS = ["foo", "bar"]; + +let testDir = getChromeDir(getResolvedURI(gTestPath)); + +const ORIGIN1 = "https://example.com"; +const ORIGIN2 = "https://example.org"; +const ORIGIN3 = Services.io.newFileURI(testDir).spec; +const PRINCIPAL1 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + ORIGIN1 +); +const PRINCIPAL2 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + ORIGIN2 +); +const PRINCIPAL3 = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + ORIGIN3 +); + +let testExtension; + +/** + * Get the open protocol handler permission key for a given protocol scheme. + * @param {string} aProtocolScheme - Scheme of protocol to construct permission + * key with. + */ +function getSkipProtoDialogPermissionKey(aProtocolScheme) { + return ( + PROTOCOL_HANDLER_OPEN_PERM_KEY + PERMISSION_KEY_DELIMITER + aProtocolScheme + ); +} + +/** + * Creates dummy web protocol handlers used for testing. + */ +function initTestHandlers() { + TEST_PROTOS.forEach(scheme => { + let webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + webHandler.name = scheme + "Handler"; + webHandler.uriTemplate = ORIGIN1 + "/?url=%s"; + + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(scheme); + handlerInfo.possibleApplicationHandlers.appendElement(webHandler); + handlerInfo.preferredApplicationHandler = webHandler; + + gHandlerService.store(handlerInfo); + }); +} + +/** + * Update whether the protocol handler dialog is shown for our test protocol + + * handler. + * @param {string} scheme - Scheme of the protocol to change the ask state for. + * @param {boolean} ask - true => show dialog, false => skip dialog. + */ +function updateAlwaysAsk(scheme, ask) { + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme); + handlerInfo.alwaysAskBeforeHandling = ask; + gHandlerService.store(handlerInfo); +} + +/** + * Test whether the protocol handler dialog is set to show for our + * test protocol + handler. + * @param {string} scheme - Scheme of the protocol to test the ask state for. + * @param {boolean} ask - true => show dialog, false => skip dialog. + */ +function testAlwaysAsk(scheme, ask) { + is( + HandlerServiceTestUtils.getHandlerInfo(scheme).alwaysAskBeforeHandling, + ask, + "Should have correct alwaysAsk state." + ); +} + +/** + * Open a test URL with the desired scheme. + * By default the load is triggered by the content principal of the browser. + * @param {MozBrowser} browser - Browser to load the test URL in. + * @param {string} scheme - Scheme of the test URL. + * @param {Object} [opts] - Options for the triggering principal. + * @param {nsIPrincipal} [opts.triggeringPrincipal] - Principal to trigger the + * load with. Defaults to the browsers content principal. + * @param {boolean} [opts.useNullPrincipal] - If true, we will trigger the load + * with a null principal. + * @param {boolean} [opts.useExtensionPrincipal] - If true, we will trigger the + * load with an extension. + * @param {boolean} [opts.omitTriggeringPrincipal] - If true, we will directly + * call the protocol handler dialogs without a principal. + */ +async function triggerOpenProto( + browser, + scheme, + { + triggeringPrincipal = browser.contentPrincipal, + useNullPrincipal = false, + useExtensionPrincipal = false, + omitTriggeringPrincipal = false, + } = {} +) { + let uri = `${scheme}://test`; + + if (useNullPrincipal) { + // Create and load iframe with data URI. + // This will be a null principal. + ContentTask.spawn(browser, { uri }, args => { + let frame = content.document.createElement("iframe"); + frame.src = `data:text/html,<script>location.href="${args.uri}"</script>`; + content.document.body.appendChild(frame); + }); + return; + } + + if (useExtensionPrincipal) { + const EXTENSION_DATA = { + manifest: { + content_scripts: [ + { + matches: [browser.currentURI.spec], + js: ["navigate.js"], + }, + ], + }, + files: { + "navigate.js": `window.location.href = "${uri}";`, + }, + }; + + testExtension = ExtensionTestUtils.loadExtension(EXTENSION_DATA); + await testExtension.startup(); + return; + } + + if (omitTriggeringPrincipal) { + // Directly call ContentDispatchChooser without a triggering principal + let contentDispatchChooser = Cc[ + "@mozilla.org/content-dispatch-chooser;1" + ].createInstance(Ci.nsIContentDispatchChooser); + + let handler = HandlerServiceTestUtils.getHandlerInfo(scheme); + + contentDispatchChooser.handleURI( + handler, + Services.io.newURI(uri), + null, + browser.browsingContext + ); + return; + } + + info("Loading uri: " + uri); + browser.loadURI(uri, { triggeringPrincipal }); +} + +/** + * Navigates to a test URL with the given protocol scheme and waits for the + * result. + * @param {MozBrowser} browser - Browser to navigate. + * @param {string} scheme - Scheme of the test url. e.g. irc + * @param {Object} [options] - Test options. + * @param {Object} [options.permDialogOptions] - Test options for the permission + * dialog. If defined, we expect this dialog to be shown. + * @param {Object} [options.chooserDialogOptions] - Test options for the chooser + * dialog. If defined, we expect this dialog to be shown. + * @param {Object} [options.loadOptions] - Options for triggering the protocol + * load which causes the dialog to show. + * @param {nsIPrincipal} [options.triggeringPrincipal] - Principal to trigger + * the load with. Defaults to the browsers content principal. + * @returns {Promise} - A promise which resolves once the test is complete. + */ +async function testOpenProto( + browser, + scheme, + { permDialogOptions, chooserDialogOptions, loadOptions } = {} +) { + let permDialogOpenPromise; + let chooserDialogOpenPromise; + + if (permDialogOptions) { + info("Should see permission dialog"); + permDialogOpenPromise = waitForProtocolPermissionDialog(browser, true); + } + + if (chooserDialogOptions) { + info("Should see chooser dialog"); + chooserDialogOpenPromise = waitForProtocolAppChooserDialog(browser, true); + } + await triggerOpenProto(browser, scheme, loadOptions); + let webHandlerLoadedPromise; + + let webHandlerShouldOpen = + (!permDialogOptions && !chooserDialogOptions) || + ((permDialogOptions?.actionConfirm || permDialogOptions?.actionChangeApp) && + chooserDialogOptions?.actionConfirm); + + // Register web handler load listener if we expect to trigger it. + if (webHandlerShouldOpen) { + webHandlerLoadedPromise = waitForHandlerURL(browser, scheme); + } + + if (permDialogOpenPromise) { + let dialog = await permDialogOpenPromise; + let dialogEl = getDialogElementFromSubDialog(dialog); + let dialogType = getDialogType(dialog); + + let { + hasCheckbox, + hasChangeApp, + chooserIsNext, + actionCheckbox, + actionConfirm, + actionChangeApp, + } = permDialogOptions; + + if (actionChangeApp) { + actionConfirm = false; + } + + await testCheckbox(dialogEl, dialogType, { + hasCheckbox, + actionCheckbox, + }); + + // Check the button label depending on whether we would show the chooser + // dialog next or directly open the handler. + let acceptBtnLabel = dialogEl.getButton("accept")?.label; + if (chooserIsNext) { + is( + acceptBtnLabel, + "Choose Application", + "Accept button has choose app label" + ); + } else { + is(acceptBtnLabel, "Open Link", "Accept button has open link label"); + } + + let changeAppLink = dialogEl.ownerDocument.getElementById("change-app"); + if (typeof hasChangeApp == "boolean") { + ok(changeAppLink, "Permission dialog should have changeApp link label"); + is( + !changeAppLink.hidden, + hasChangeApp, + "Permission dialog change app link label" + ); + } + + if (actionChangeApp) { + let dialogClosedPromise = waitForProtocolPermissionDialog(browser, false); + changeAppLink.click(); + await dialogClosedPromise; + } else { + await closeDialog(browser, dialog, actionConfirm, scheme); + } + } + + if (chooserDialogOpenPromise) { + let dialog = await chooserDialogOpenPromise; + let dialogEl = getDialogElementFromSubDialog(dialog); + let dialogType = getDialogType(dialog); + + let { hasCheckbox, actionCheckbox, actionConfirm } = chooserDialogOptions; + + await testCheckbox(dialogEl, dialogType, { + hasCheckbox, + actionCheckbox, + }); + + await closeDialog(browser, dialog, actionConfirm, scheme); + } + + if (webHandlerShouldOpen) { + info("Waiting for web handler to open"); + await webHandlerLoadedPromise; + } else { + info("Web handler open canceled"); + } + + // Clean up test extension if needed. + await testExtension?.unload(); +} + +/** + * Inspects the checkbox state and interacts with it. + * @param {dialog} dialogEl + * @param {string} dialogType - String identifier of dialog type. + * Either "permission" or "chooser". + * @param {Object} options - Test Options. + * @param {boolean} [options.hasCheckbox] - Whether the dialog is expected to + * have a visible checkbox. + * @param {boolean} [options.hasCheckboxState] - The check state of the checkbox + * to test for. true = checked, false = unchecked. + * @param {boolean} [options.actionCheckbox] - The state to set on the checkbox. + * true = checked, false = unchecked. + */ +async function testCheckbox( + dialogEl, + dialogType, + { hasCheckbox, hasCheckboxState = false, actionCheckbox } +) { + let checkbox = dialogEl.ownerDocument.getElementById("remember"); + if (typeof hasCheckbox == "boolean") { + let hiddenEl; + if (dialogType == "permission") { + hiddenEl = checkbox.parentElement; + } else { + hiddenEl = checkbox; + } + is( + checkbox && !hiddenEl.hidden, + hasCheckbox, + "Dialog checkbox has correct visibility." + ); + } + + if (typeof hasCheckboxState == "boolean") { + is(checkbox.checked, hasCheckboxState, "Dialog checkbox has correct state"); + } + + if (typeof actionCheckbox == "boolean") { + checkbox.focus(); + await EventUtils.synthesizeKey("VK_SPACE", undefined, dialogEl.ownerWindow); + } +} + +/** + * Get the dialog element which is a child of the SubDialogs browser frame. + * @param {SubDialog} subDialog - Dialog to get the dialog element for. + */ +function getDialogElementFromSubDialog(subDialog) { + let dialogEl = subDialog._frame.contentDocument.querySelector("dialog"); + ok(dialogEl, "SubDialog should have dialog element"); + return dialogEl; +} + +/** + * Wait for the test handler to be opened. + * @param {MozBrowser} browser - The browser the load should occur in. + * @param {string} scheme - Scheme which triggered the handler to open. + */ +function waitForHandlerURL(browser, scheme) { + return BrowserTestUtils.browserLoaded( + browser, + false, + url => url == `${ORIGIN1}/?url=${scheme}%3A%2F%2Ftest` + ); +} + +/** + * Test for open-protocol-handler permission. + * @param {nsIPrincipal} principal - The principal to test the permission on. + * @param {string} scheme - Scheme to generate permission key. + * @param {boolean} hasPerm - Whether we expect the princial to set the + * permission (true), or not (false). + */ +function testPermission(principal, scheme, hasPerm) { + let permKey = getSkipProtoDialogPermissionKey(scheme); + let result = Services.perms.testPermissionFromPrincipal(principal, permKey); + let message = `${permKey} ${hasPerm ? "is" : "is not"} set for ${ + principal.origin + }.`; + is(result == Services.perms.ALLOW_ACTION, hasPerm, message); +} + +/** + * Get the checkbox element of the dialog used to remember the handler choice or + * store the permission. + * @param {SubDialog} dialog - Protocol handler dialog embedded in a SubDialog. + * @param {string} dialogType - Type of the dialog which holds the checkbox. + * @returns {HTMLInputElement} - Checkbox of the dialog. + */ +function getDialogCheckbox(dialog, dialogType) { + let id; + if (dialogType == "permission") { + id = "remember-permission"; + } else { + id = "remember"; + } + return dialog._frame.contentDocument.getElementById(id); +} + +function getDialogType(dialog) { + let url = dialog._frame.currentURI.spec; + + if (url === DIALOG_URL_PERMISSION) { + return "permission"; + } + if (url === DIALOG_URL_APP_CHOOSER) { + return "chooser"; + } + throw new Error("Dialog with unexpected url"); +} + +/** + * Exit a protocol handler SubDialog and wait for it to be fully closed. + * @param {MozBrowser} browser - Browser element of the tab where the dialog is + * shown. + * @param {SubDialog} dialog - SubDialog object which holds the protocol handler + * @param {boolean} confirm - Whether to confirm (true) or cancel (false) the + * dialog. + * @param {string} scheme - The scheme of the protocol the dialog is opened for. + * dialog. + */ +async function closeDialog(browser, dialog, confirm, scheme) { + let dialogClosedPromise = waitForSubDialog(browser, dialog._openedURL, false); + let dialogEl = getDialogElementFromSubDialog(dialog); + + if (confirm) { + if (getDialogType(dialog) == "chooser") { + // Select our test protocol handler + let listItem = dialogEl.ownerDocument.querySelector( + `richlistitem[name="${scheme}Handler"]` + ); + listItem.click(); + } + + dialogEl.setAttribute("buttondisabledaccept", false); + dialogEl.acceptDialog(); + } else { + dialogEl.cancelDialog(); + } + + return dialogClosedPromise; +} + +registerCleanupFunction(function() { + // Clean up test handlers + TEST_PROTOS.forEach(scheme => { + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo(scheme); + gHandlerService.remove(handlerInfo); + }); + + // Clear permissions + Services.perms.removeAll(); +}); + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", true]], + }); + initTestHandlers(); +}); + +/** + * Tests that when "remember" is unchecked, we only allow the protocol to be + * opened once and don't store any permission. + */ +add_task(async function test_permission_allow_once() { + for (let scheme of TEST_PROTOS) { + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // No permission should be set + testPermission(PRINCIPAL1, scheme, false); + testPermission(PRINCIPAL2, scheme, false); + + // No preferred app should be set + testAlwaysAsk(scheme, true); + + // If we open again we should see the permission dialog + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: false, + }, + }); + }); + } +}); + +/** + * Tests that when checking the "remember" checkbox, the protocol permission + * is set correctly and allows the caller to skip the permission dialog in + * subsequent calls. + */ +add_task(async function test_permission_allow_persist() { + for (let [origin, principal] of [ + [ORIGIN1, PRINCIPAL1], + [ORIGIN3, PRINCIPAL3], + ]) { + for (let scheme of TEST_PROTOS) { + info("Testing with origin " + origin); + info("testing with principal of origin " + principal.origin); + info("testing with protocol " + scheme); + + // Set a permission for an unrelated protocol. + // We should still see the permission dialog. + Services.perms.addFromPrincipal( + principal, + getSkipProtoDialogPermissionKey("foobar"), + Services.perms.ALLOW_ACTION + ); + + await BrowserTestUtils.withNewTab(origin, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionCheckbox: true, + actionConfirm: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // Permission should be set + testPermission(principal, scheme, true); + testPermission(PRINCIPAL2, scheme, false); + + // No preferred app should be set + testAlwaysAsk(scheme, true); + + // If we open again with the origin where we granted permission, we should + // directly get the chooser dialog. + await BrowserTestUtils.withNewTab(origin, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, + }, + }); + }); + + // If we open with the other origin, we should see the permission dialog + await BrowserTestUtils.withNewTab(ORIGIN2, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: false, + chooserIsNext: true, + actionConfirm: false, + }, + }); + }); + + // Cleanup permissions + Services.perms.removeAll(); + } + } +}); + +/** + * Tests that if a preferred protocol handler is set, the permission dialog + * shows the application name and a link which leads to the app chooser. + */ +add_task(async function test_permission_application_set() { + let scheme = TEST_PROTOS[0]; + updateAlwaysAsk(scheme, false); + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + hasChangeApp: true, + chooserIsNext: false, + actionChangeApp: true, + }, + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + // Cleanup + updateAlwaysAsk(scheme, true); +}); + +/** + * Tests that we correctly handle system principals. They should always + * skip the permission dialog. + */ +add_task(async function test_permission_system_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { hasCheckbox: true, actionConfirm: false }, + loadOptions: { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }, + }); + }); +}); + +/** + * Tests that we don't show the permission dialog if the permission is disabled + * by pref. + */ +add_task(async function test_permission_disabled() { + let scheme = TEST_PROTOS[0]; + + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", false]], + }); + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + chooserDialogOptions: { hasCheckbox: true, actionConfirm: true }, + }); + }); + + await SpecialPowers.popPrefEnv(); +}); + +/** + * Tests that we directly open the handler if permission and handler are set. + */ +add_task(async function test_app_and_permission_set() { + let scheme = TEST_PROTOS[1]; + + updateAlwaysAsk(scheme, false); + Services.perms.addFromPrincipal( + PRINCIPAL2, + getSkipProtoDialogPermissionKey(scheme), + Services.perms.ALLOW_ACTION + ); + + await BrowserTestUtils.withNewTab(ORIGIN2, async browser => { + await testOpenProto(browser, scheme); + }); + + // Cleanup + Services.perms.removeAll(); + updateAlwaysAsk(scheme, true); +}); + +/** + * Tests that the alwaysAsk state is not updated if the user cancels the dialog + */ +add_task(async function test_change_app_checkbox_cancel() { + let scheme = TEST_PROTOS[0]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + permDialogOptions: { + hasCheckbox: true, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionCheckbox: true, // Activate checkbox + actionConfirm: false, // Cancel dialog + }, + }); + }); + + // Should not have applied value from checkbox + testAlwaysAsk(scheme, true); +}); + +/** + * Tests that the external protocol dialogs behave correctly when a null + * principal is passed. + */ +add_task(async function test_null_principal() { + let scheme = TEST_PROTOS[0]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + loadOptions: { + useNullPrincipal: true, + }, + permDialogOptions: { + hasCheckbox: false, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Tests that the external protocol dialogs behave correctly when no principal + * is passed. + */ +add_task(async function test_no_principal() { + let scheme = TEST_PROTOS[1]; + + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + loadOptions: { + omitTriggeringPrincipal: true, + }, + permDialogOptions: { + hasCheckbox: false, + chooserIsNext: true, + hasChangeApp: false, + actionConfirm: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); + +/** + * Tests that we skip the permission dialog for extension callers. + */ +add_task(async function test_extension_principal() { + let scheme = TEST_PROTOS[0]; + await BrowserTestUtils.withNewTab(ORIGIN1, async browser => { + await testOpenProto(browser, scheme, { + loadOptions: { + useExtensionPrincipal: true, + }, + chooserDialogOptions: { + hasCheckbox: true, + actionConfirm: false, // Cancel dialog + }, + }); + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js new file mode 100644 index 0000000000..b9ba4a7955 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_protocolhandler_loop.js @@ -0,0 +1,76 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_helperapp() { + // Set up the test infrastructure: + const kProt = "foopydoopydoo"; + const extProtocolSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + let handlerInfo = extProtocolSvc.getProtocolHandlerInfo(kProt); + if (handlerSvc.exists(handlerInfo)) { + handlerSvc.fillHandlerInfo(handlerInfo, ""); + } + // Say we want to use a specific app: + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.alwaysAskBeforeHandling = false; + + // Say it's us: + let selfFile = Services.dirsvc.get("XREExeF", Ci.nsIFile); + // Make sure it's the .app + if (AppConstants.platform == "macosx") { + while ( + !selfFile.leafName.endsWith(".app") && + !selfFile.leafName.endsWith(".app/") + ) { + selfFile = selfFile.parent; + } + } + let selfHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + selfHandlerApp.executable = selfFile; + handlerInfo.possibleApplicationHandlers.appendElement(selfHandlerApp); + handlerInfo.preferredApplicationHandler = selfHandlerApp; + handlerSvc.store(handlerInfo); + + await BrowserTestUtils.withNewTab("about:blank", async browser => { + // Now, do some safety stubbing. If we do end up recursing we spawn + // infinite tabs. We definitely don't want that. Avoid it by stubbing + // our external URL handling bits: + let oldAddTab = gBrowser.addTab; + registerCleanupFunction( + () => (gBrowser.addTab = gBrowser.loadOneTab = oldAddTab) + ); + let wrongThingHappenedPromise = new Promise(resolve => { + gBrowser.addTab = gBrowser.loadOneTab = function(aURI) { + ok(false, "Tried to open unexpected URL in a tab: " + aURI); + resolve(null); + // Pass a dummy object to avoid upsetting BrowserContentHandler - + // if it thinks opening the tab failed, it tries to open a window instead, + // which we can't prevent as easily, and at which point we still end up + // with runaway tabs. + return {}; + }; + }); + + let askedUserPromise = waitForProtocolAppChooserDialog(browser, true); + + BrowserTestUtils.loadURI(browser, kProt + ":test"); + let dialog = await Promise.race([ + wrongThingHappenedPromise, + askedUserPromise, + ]); + ok(dialog, "Should have gotten a dialog"); + + let closePromise = waitForProtocolAppChooserDialog(browser, false); + dialog.close(); + await closePromise; + askedUserPromise = null; + }); +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js new file mode 100644 index 0000000000..28bd50a120 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_remember_download_option.js @@ -0,0 +1,61 @@ +add_task(async function() { + // create mocked objects + let launcher = createMockedObjects(true); + + // open helper app dialog with mocked launcher + let dlg = await openHelperAppDialog(launcher); + + let doc = dlg.document; + let dialogElement = doc.getElementById("unknownContentType"); + + // Set remember choice + ok( + !doc.getElementById("rememberChoice").checked, + "Remember choice checkbox should be not checked." + ); + doc.getElementById("rememberChoice").checked = true; + + // Make sure the mock handler information is not in nsIHandlerService + ok( + !gHandlerSvc.exists(launcher.MIMEInfo), + "Should not be in nsIHandlerService." + ); + + // close the dialog by pushing the ok button. + let dialogClosedPromise = BrowserTestUtils.windowClosed(dlg); + // Make sure the ok button is enabled, since the ok button might be disabled by + // EnableDelayHelper mechanism. Please refer the detailed + // https://searchfox.org/mozilla-central/source/toolkit/components/prompts/src/SharedPromptUtils.jsm#53 + dialogElement.getButton("accept").disabled = false; + dialogElement.acceptDialog(); + await dialogClosedPromise; + + // check the mocked handler information is saved in nsIHandlerService + ok(gHandlerSvc.exists(launcher.MIMEInfo), "Should be in nsIHandlerService."); + // check the extension. + var mimeType = gHandlerSvc.getTypeFromExtension("abc"); + is(mimeType, launcher.MIMEInfo.type, "Got correct mime type."); + for (let handlerInfo of gHandlerSvc.enumerate()) { + if (handlerInfo.type == launcher.MIMEInfo.type) { + // check the alwaysAskBeforeHandling + ok( + !handlerInfo.alwaysAskBeforeHandling, + "Should turn off the always ask." + ); + // check the preferredApplicationHandler + ok( + handlerInfo.preferredApplicationHandler.equals( + launcher.MIMEInfo.preferredApplicationHandler + ), + "Should be equal to the mockedHandlerApp." + ); + // check the perferredAction + is( + handlerInfo.preferredAction, + launcher.MIMEInfo.preferredAction, + "Should be equal to Ci.nsIHandlerInfo.useHelperApp." + ); + break; + } + } +}); diff --git a/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js new file mode 100644 index 0000000000..66f461834c --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/browser_web_protocol_handlers.js @@ -0,0 +1,124 @@ +let testURL = + "https://example.com/browser/" + + "uriloader/exthandler/tests/mochitest/protocolHandler.html"; + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["security.external_protocol_requires_permission", false]], + }); + + // Load a page registering a protocol handler. + let browser = gBrowser.selectedBrowser; + BrowserTestUtils.loadURI(browser, testURL); + await BrowserTestUtils.browserLoaded(browser, false, testURL); + + // Register the protocol handler by clicking the notificationbar button. + let notificationValue = "Protocol Registration: web+testprotocol"; + let getNotification = () => + gBrowser.getNotificationBox().getNotificationWithValue(notificationValue); + await BrowserTestUtils.waitForCondition(getNotification); + let notification = getNotification(); + let button = notification.querySelector("button"); + ok(button, "got registration button"); + button.click(); + + // Set the new handler as default. + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + let protoInfo = protoSvc.getProtocolHandlerInfo("web+testprotocol"); + is( + protoInfo.preferredAction, + protoInfo.useHelperApp, + "using a helper application is the preferred action" + ); + ok(!protoInfo.preferredApplicationHandler, "no preferred handler is set"); + let handlers = protoInfo.possibleApplicationHandlers; + is(1, handlers.length, "only one handler registered for web+testprotocol"); + let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp); + ok(handler instanceof Ci.nsIWebHandlerApp, "the handler is a web handler"); + is( + handler.uriTemplate, + "https://example.com/foobar?uri=%s", + "correct url template" + ); + protoInfo.preferredApplicationHandler = handler; + protoInfo.alwaysAskBeforeHandling = false; + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + handlerSvc.store(protoInfo); + + const expectedURL = + "https://example.com/foobar?uri=web%2Btestprotocol%3Atest"; + + // Create a framed link: + await SpecialPowers.spawn(browser, [], async function() { + let iframe = content.document.createElement("iframe"); + iframe.src = `data:text/html,<a href="web+testprotocol:test">Click me</a>`; + content.document.body.append(iframe); + // Can't return this promise because it resolves to the event object. + await ContentTaskUtils.waitForEvent(iframe, "load"); + iframe.contentDocument.querySelector("a").click(); + }); + let kidContext = browser.browsingContext.children[0]; + await TestUtils.waitForCondition(() => { + let spec = kidContext.currentWindowGlobal?.documentURI?.spec || ""; + return spec == expectedURL; + }); + is( + kidContext.currentWindowGlobal.documentURI.spec, + expectedURL, + "Should load in frame." + ); + + // Middle-click a testprotocol link and check the new tab is correct + let link = "#link"; + + let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, expectedURL); + await BrowserTestUtils.synthesizeMouseAtCenter(link, { button: 1 }, browser); + let tab = await promiseTabOpened; + gBrowser.selectedTab = tab; + is( + gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + BrowserTestUtils.removeTab(tab); + + // Shift-click the testprotocol link and check the new window. + let newWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: expectedURL, + }); + await BrowserTestUtils.synthesizeMouseAtCenter( + link, + { shiftKey: true }, + browser + ); + let win = await newWindowPromise; + await BrowserTestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == expectedURL + ); + is( + win.gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + await BrowserTestUtils.closeWindow(win); + + // Click the testprotocol link and check the url in the current tab. + let loadPromise = BrowserTestUtils.browserLoaded(browser); + await BrowserTestUtils.synthesizeMouseAtCenter(link, {}, browser); + await loadPromise; + await BrowserTestUtils.waitForCondition(() => gURLBar.value != testURL); + is( + gURLBar.value, + expectedURL, + "the expected URL is displayed in the location bar" + ); + + // Cleanup. + protoInfo.preferredApplicationHandler = null; + handlers.removeElementAt(0); + handlerSvc.store(protoInfo); +}); diff --git a/uriloader/exthandler/tests/mochitest/download.bin b/uriloader/exthandler/tests/mochitest/download.bin new file mode 100644 index 0000000000..0e4b0c7bae --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download.bin @@ -0,0 +1 @@ +abc123 diff --git a/uriloader/exthandler/tests/mochitest/download.sjs b/uriloader/exthandler/tests/mochitest/download.sjs new file mode 100644 index 0000000000..bee7bd7015 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download.sjs @@ -0,0 +1,38 @@ +"use strict"; + +Cu.import("resource://gre/modules/Timer.jsm"); + +function actuallyHandleRequest(req, res) { + res.setHeader("Content-Type", "application/octet-stream", false); + res.write("abc123"); + res.finish(); +} + +function handleRequest(req, res) { + if (req.queryString.includes('finish')) { + res.write("OK"); + let downloadReq = null; + getObjectState("downloadReq", o => { downloadReq = o }); + // Two possibilities: either the download request has already reached us, or not. + if (downloadReq) { + downloadReq.wrappedJSObject.callback(); + } else { + // Set a variable to allow the request to complete immediately: + setState("finishReq", "true"); + } + } else if (req.queryString.includes('reset')) { + res.write("OK"); + setObjectState("downloadReq", null); + setState("finishReq", "false"); + } else { + res.processAsync(); + if (getState("finishReq") === "true") { + actuallyHandleRequest(req, res); + } else { + let o = {callback() { actuallyHandleRequest(req, res) }}; + o.wrappedJSObject = o; + o.QueryInterface = () => o; + setObjectState("downloadReq", o); + } + } +} diff --git a/uriloader/exthandler/tests/mochitest/download_page.html b/uriloader/exthandler/tests/mochitest/download_page.html new file mode 100644 index 0000000000..5a264888fa --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/download_page.html @@ -0,0 +1,22 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE html> +<html> +<head> + <meta charset=UTF-8> + <title>Test page for link clicking</title> + <script type="text/javascript"> + function launch_download(extra) { + window.open("download.sjs", "_blank", "height=100,width=100" + extra); + } + </script> +</head> +<body> + <a href="download.bin" id="regular_load">regular load</a> + <a href="download.bin" id="target_blank" target="_blank" rel="opener">target blank</a> + <a href="#" onclick="launch_download(''); return false" id="new_window">new window</a> + <a href="#" onclick="window.open('download_page.html?newwin'); return false" id="open_in_new_tab">click to reopen</a> + <a href="download.bin" id="target_blank_no_opener" rel="noopener" target="_blank">target blank (noopener)</a> + <a href="#" onclick="window.open('download.bin', '_blank', 'noopener'); return false" id="open_in_new_tab_no_opener">click to reopen (noopener)</a> + <a href="#" onclick="launch_download(',noopener'); return false" id="new_window_no_opener">new window (noopener)</a> +</body> diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe b/uriloader/exthandler/tests/mochitest/file_as.exe new file mode 100644 index 0000000000..f2f5ab47f3 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_as.exe @@ -0,0 +1 @@ +Not actually an executable... but let's pretend! diff --git a/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ new file mode 100644 index 0000000000..89f22e30be --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_as.exe^headers^ @@ -0,0 +1,2 @@ +Content-Type: binary/octet-stream +Content-Disposition: attachment diff --git a/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html new file mode 100644 index 0000000000..eb2fb74441 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_external_protocol_iframe.html @@ -0,0 +1 @@ +<iframe src="mailto:foo@bar.com"></iframe> diff --git a/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html new file mode 100644 index 0000000000..b1bb863f89 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_nested_protocol_request.html @@ -0,0 +1 @@ +<iframe srcdoc="<a href='mailto:help@example.com'>Mail someone</a>"></iframe> diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ new file mode 100644 index 0000000000..d1d59b9754 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_pdf.pdf^headers^ @@ -0,0 +1,2 @@ +content-disposition: attachment; filename=file_pdf_application_pdf.pdf; filename*=UTF-8''file_pdf_application_pdf.pdf +content-type: application/pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ new file mode 100644 index 0000000000..157c0e0943 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_application_unknown.pdf^headers^ @@ -0,0 +1,2 @@ +content-disposition: attachment; filename=file_pdf_application_unknown.pdf; filename*=UTF-8''file_pdf_application_unknown.pdf +content-type: application/unknown diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf diff --git a/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ new file mode 100644 index 0000000000..6358f54f48 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_pdf_binary_octet_stream.pdf^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename="file_pdf_binary_octet_stream.pdf"; filename*=UTF-8''file_pdf_binary_octet_stream.pdf +Content-Type: binary/octet-stream diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt diff --git a/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ new file mode 100644 index 0000000000..37823166a4 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_txt_attachment_test.txt^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename=file_text_attachment_test.txt +Content-Type: text/plain diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png Binary files differnew file mode 100644 index 0000000000..743292dc6f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png diff --git a/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ new file mode 100644 index 0000000000..06e0cd957f --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with@@funny_name.png^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: inline; filename=file_with%40%40funny_name.png +Content-Type: image/png diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm Binary files differnew file mode 100644 index 0000000000..7bc738b8b4 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm diff --git a/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ new file mode 100644 index 0000000000..b77e9d3687 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_with[funny_name.webm^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: inline; filename=file_with%5Bfunny_name.webm +Content-Type: video/webm diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml new file mode 100644 index 0000000000..3a5792586a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml @@ -0,0 +1,4 @@ +<?xml version = "1.0" encoding = "utf-8"?> + +<something> +</something> diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ new file mode 100644 index 0000000000..5bdc4448e8 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_binary_octet_stream.xml^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment +Content-Type: binary/octet-stream diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml new file mode 100644 index 0000000000..3a5792586a --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml @@ -0,0 +1,4 @@ +<?xml version = "1.0" encoding = "utf-8"?> + +<something> +</something> diff --git a/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ new file mode 100644 index 0000000000..ac0355d976 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/file_xml_attachment_test.xml^headers^ @@ -0,0 +1,2 @@ +Content-Disposition: attachment; filename=file_xml_attachment_test.xml +Content-Type: text/xml diff --git a/uriloader/exthandler/tests/mochitest/handlerApp.xhtml b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml new file mode 100644 index 0000000000..e519e80029 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/handlerApp.xhtml @@ -0,0 +1,28 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Pseudo Web Handler App</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="onLoad()"> +Pseudo Web Handler App + +<script class="testbody" type="text/javascript"> +<![CDATA[ +function onLoad() { + // if we have a window.opener, this must be the windowContext + // instance of this test. check that we got the URI right and clean up. + if (window.opener) { + window.opener.is(location.search, + "?uri=" + encodeURIComponent(window.opener.testURI), + "uri passed to web-handler app"); + window.opener.SimpleTest.finish(); + } + + window.close(); +} +]]> +</script> + +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/handlerApps.js b/uriloader/exthandler/tests/mochitest/handlerApps.js new file mode 100644 index 0000000000..aa841f13be --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/handlerApps.js @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// handlerApp.xhtml grabs this for verification purposes via window.opener +var testURI = "webcal://127.0.0.1/rheeeeet.html"; + +const Cc = SpecialPowers.Cc; + +function test() { + // set up the web handler object + var webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(SpecialPowers.Ci.nsIWebHandlerApp); + webHandler.name = "Test Web Handler App"; + webHandler.uriTemplate = + "https://example.com/tests/uriloader/exthandler/tests/mochitest/" + + "handlerApp.xhtml?uri=%s"; + + // set up the uri to test with + /* eslint-disable mozilla/use-services */ + + var ioService = Cc["@mozilla.org/network/io-service;1"].getService( + SpecialPowers.Ci.nsIIOService + ); + + var uri = ioService.newURI(testURI); + + // create a window, and launch the handler in it + var newWindow = window.open("", "handlerWindow", "height=300,width=300"); + var windowContext = SpecialPowers.wrap(newWindow).docShell; + + webHandler.launchWithURI(uri, windowContext); + + // if we get this far without an exception, we've at least partly passed + // (remaining check in handlerApp.xhtml) + ok(true, "webHandler launchWithURI (existing window/tab) started"); + + // make the web browser launch in its own window/tab + webHandler.launchWithURI(uri); + + // if we get this far without an exception, we've passed + ok(true, "webHandler launchWithURI (new window/tab) test started"); + + // set up the local handler object + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(SpecialPowers.Ci.nsILocalHandlerApp); + localHandler.name = "Test Local Handler App"; + + // get a local app that we know will be there and do something sane + /* eslint-disable mozilla/use-services */ + + var osString = Cc["@mozilla.org/xre/app-info;1"].getService( + SpecialPowers.Ci.nsIXULRuntime + ).OS; + + var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService( + SpecialPowers.Ci.nsIDirectoryServiceProvider + ); + if (osString == "WINNT") { + var windowsDir = dirSvc.getFile("WinD", {}); + var exe = windowsDir.clone().QueryInterface(SpecialPowers.Ci.nsIFile); + exe.appendRelativePath("SYSTEM32\\HOSTNAME.EXE"); + } else if (osString == "Darwin") { + var localAppsDir = dirSvc.getFile("LocApp", {}); + exe = localAppsDir.clone(); + exe.append("iCal.app"); // lingers after the tests finish, but this seems + // seems better than explicitly killing it, since + // developers who run the tests locally may well + // information in their running copy of iCal + + if (navigator.userAgent.match(/ SeaMonkey\//)) { + // SeaMonkey tinderboxes don't like to have iCal lingering (and focused) + // on next test suite run(s). + todo(false, "On SeaMonkey, testing OS X as generic Unix. (Bug 749872)"); + + // assume a generic UNIX variant + exe = Cc["@mozilla.org/file/local;1"].createInstance( + SpecialPowers.Ci.nsIFile + ); + exe.initWithPath("/bin/echo"); + } + } else { + // assume a generic UNIX variant + exe = Cc["@mozilla.org/file/local;1"].createInstance( + SpecialPowers.Ci.nsIFile + ); + exe.initWithPath("/bin/echo"); + } + + localHandler.executable = exe; + localHandler.launchWithURI(ioService.newURI(testURI)); + + // if we get this far without an exception, we've passed + ok(true, "localHandler launchWithURI test"); + + // if we ever decide that killing iCal is the right thing to do, change + // the if statement below from "NOTDarwin" to "Darwin" + if (osString == "NOTDarwin") { + var killall = Cc["@mozilla.org/file/local;1"].createInstance( + SpecialPowers.Ci.nsIFile + ); + killall.initWithPath("/usr/bin/killall"); + + var process = Cc["@mozilla.org/process/util;1"].createInstance( + SpecialPowers.Ci.nsIProcess + ); + process.init(killall); + + var args = ["iCal"]; + process.run(false, args, args.length); + } + + SimpleTest.waitForExplicitFinish(); +} + +test(); diff --git a/uriloader/exthandler/tests/mochitest/head.js b/uriloader/exthandler/tests/mochitest/head.js new file mode 100644 index 0000000000..3b0d8a4072 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/head.js @@ -0,0 +1,277 @@ +var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); +var { HandlerServiceTestUtils } = ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm" +); + +var gMimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); +var gHandlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService +); + +function createMockedHandlerApp() { + // Mock the executable + let mockedExecutable = FileUtils.getFile("TmpD", ["mockedExecutable"]); + if (!mockedExecutable.exists()) { + mockedExecutable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + } + + // Mock the handler app + let mockedHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + mockedHandlerApp.executable = mockedExecutable; + mockedHandlerApp.detailedDescription = "Mocked handler app"; + + registerCleanupFunction(function() { + // remove the mocked executable from disk. + if (mockedExecutable.exists()) { + mockedExecutable.remove(true); + } + }); + + return mockedHandlerApp; +} + +function createMockedObjects(createHandlerApp) { + // Mock the mime info + let internalMockedMIME = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + internalMockedMIME.alwaysAskBeforeHandling = true; + internalMockedMIME.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + internalMockedMIME.appendExtension("abc"); + if (createHandlerApp) { + let mockedHandlerApp = createMockedHandlerApp(); + internalMockedMIME.description = mockedHandlerApp.detailedDescription; + internalMockedMIME.possibleApplicationHandlers.appendElement( + mockedHandlerApp + ); + internalMockedMIME.preferredApplicationHandler = mockedHandlerApp; + } + + // Proxy for the mocked MIME info for faking the read-only attributes + let mockedMIME = new Proxy(internalMockedMIME, { + get(target, property) { + switch (property) { + case "hasDefaultHandler": + return true; + case "defaultDescription": + return "Default description"; + default: + return target[property]; + } + }, + }); + + // Mock the launcher: + let mockedLauncher = { + MIMEInfo: mockedMIME, + source: Services.io.newURI("http://www.mozilla.org/"), + suggestedFileName: "test_download_dialog.abc", + targetFileIsExecutable: false, + saveToDisk() {}, + cancel() {}, + launchWithApplication() {}, + setWebProgressListener() {}, + saveDestinationAvailable() {}, + contentLength: 42, + targetFile: null, // never read + // PRTime is microseconds since epoch, Date.now() returns milliseconds: + timeDownloadStarted: Date.now() * 1000, + QueryInterface: ChromeUtils.generateQI([ + "nsICancelable", + "nsIHelperAppLauncher", + ]), + }; + + registerCleanupFunction(function() { + // remove the mocked mime info from database. + let mockHandlerInfo = gMimeSvc.getFromTypeAndExtension( + "text/x-test-handler", + null + ); + if (gHandlerSvc.exists(mockHandlerInfo)) { + gHandlerSvc.remove(mockHandlerInfo); + } + }); + + return mockedLauncher; +} + +async function openHelperAppDialog(launcher) { + let helperAppDialog = Cc[ + "@mozilla.org/helperapplauncherdialog;1" + ].createInstance(Ci.nsIHelperAppLauncherDialog); + + let helperAppDialogShownPromise = BrowserTestUtils.domWindowOpenedAndLoaded(); + try { + helperAppDialog.show(launcher, window, "foopy"); + } catch (ex) { + ok( + false, + "Trying to show unknownContentType.xhtml failed with exception: " + ex + ); + Cu.reportError(ex); + } + let dlg = await helperAppDialogShownPromise; + + is( + dlg.location.href, + "chrome://mozapps/content/downloads/unknownContentType.xhtml", + "Got correct dialog" + ); + + return dlg; +} + +async function waitForSubDialog(browser, url, state) { + let eventStr = state ? "dialogopen" : "dialogclose"; + + let tabDialogBox = gBrowser.getTabDialogBox(browser); + let dialogStack = tabDialogBox.getTabDialogManager()._dialogStack; + + let checkFn; + + if (state) { + checkFn = dialogEvent => dialogEvent.detail.dialog?._openedURL == url; + } + + let event = await BrowserTestUtils.waitForEvent( + dialogStack, + eventStr, + true, + checkFn + ); + + let { dialog } = event.detail; + + // If the dialog is closing wait for it to be fully closed before resolving + if (!state) { + await dialog._closingPromise; + } + + return event.detail.dialog; +} + +/** + * Wait for protocol permission dialog open/close. + * @param {MozBrowser} browser - Browser element the dialog belongs to. + * @param {boolean} state - true: dialog open, false: dialog close + * @returns {Promise<SubDialog>} - Returns a promise which resolves with the + * SubDialog object of the dialog which closed or opened. + */ +async function waitForProtocolPermissionDialog(browser, state) { + return waitForSubDialog( + browser, + "chrome://mozapps/content/handling/permissionDialog.xhtml", + state + ); +} + +/** + * Wait for protocol app chooser dialog open/close. + * @param {MozBrowser} browser - Browser element the dialog belongs to. + * @param {boolean} state - true: dialog open, false: dialog close + * @returns {Promise<SubDialog>} - Returns a promise which resolves with the + * SubDialog object of the dialog which closed or opened. + */ +async function waitForProtocolAppChooserDialog(browser, state) { + return waitForSubDialog( + browser, + "chrome://mozapps/content/handling/appChooser.xhtml", + state + ); +} + +async function promiseDownloadFinished(list) { + return new Promise(resolve => { + list.addView({ + onDownloadChanged(download) { + info("Download changed!"); + if (download.succeeded || download.error) { + info("Download succeeded or errored"); + list.removeView(this); + resolve(download); + } + }, + }); + }); +} + +function setupMailHandler() { + let mailHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("mailto"); + let gOldMailHandlers = []; + + // Remove extant web handlers because they have icons that + // we fetch from the web, which isn't allowed in tests. + let handlers = mailHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp); + gOldMailHandlers.push(handler); + // If we get here, this is a web handler app. Remove it: + handlers.removeElementAt(i); + } catch (ex) {} + } + + let previousHandling = mailHandlerInfo.alwaysAskBeforeHandling; + mailHandlerInfo.alwaysAskBeforeHandling = true; + + // Create a dummy web mail handler so we always know the mailto: protocol. + // Without this, the test fails on VMs without a default mailto: handler, + // because no dialog is ever shown, as we ignore subframe navigations to + // protocols that cannot be handled. + let dummy = Cc["@mozilla.org/uriloader/web-handler-app;1"].createInstance( + Ci.nsIWebHandlerApp + ); + dummy.name = "Handler 1"; + dummy.uriTemplate = "https://example.com/first/%s"; + mailHandlerInfo.possibleApplicationHandlers.appendElement(dummy); + + gHandlerSvc.store(mailHandlerInfo); + registerCleanupFunction(() => { + // Re-add the original protocol handlers: + let mailHandlers = mailHandlerInfo.possibleApplicationHandlers; + for (let i = handlers.Count() - 1; i >= 0; i--) { + try { + // See if this is a web handler. If it is, it'll throw, otherwise, + // we will remove it. + mailHandlers.queryElementAt(i, Ci.nsIWebHandlerApp); + mailHandlers.removeElementAt(i); + } catch (ex) {} + } + for (let h of gOldMailHandlers) { + mailHandlers.appendElement(h); + } + mailHandlerInfo.alwaysAskBeforeHandling = previousHandling; + gHandlerSvc.store(mailHandlerInfo); + }); +} + +let gDownloadDir; + +async function setDownloadDir() { + let tmpDir = await PathUtils.getTempDir(); + tmpDir = PathUtils.join( + tmpDir, + "testsavedir" + Math.floor(Math.random() * 2 ** 32) + ); + // Create this dir if it doesn't exist (ignores existing dirs) + await IOUtils.makeDirectory(tmpDir); + registerCleanupFunction(async function() { + try { + await IOUtils.remove(tmpDir, { recursive: true }); + } catch (e) { + Cu.reportError(e); + } + }); + Services.prefs.setIntPref("browser.download.folderList", 2); + Services.prefs.setCharPref("browser.download.dir", tmpDir); + return tmpDir; +} + +add_task(async function test_common_initialize() { + gDownloadDir = await setDownloadDir(); + Services.prefs.setCharPref("browser.download.loglevel", "Debug"); +}); diff --git a/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs new file mode 100644 index 0000000000..d12e2904d9 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/invalidCharFileExtension.sjs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) + return; + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader("Content-Type", "image/png; name=\"" + name + "\""); + response.setHeader("Content-Disposition", "attachment; filename=\"" + name + "\""); +} diff --git a/uriloader/exthandler/tests/mochitest/mochitest.ini b/uriloader/exthandler/tests/mochitest/mochitest.ini new file mode 100644 index 0000000000..be3b6bb37e --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/mochitest.ini @@ -0,0 +1,23 @@ +[DEFAULT] +support-files = + handlerApp.xhtml + handlerApps.js + +[test_handlerApps.xhtml] +skip-if = (toolkit == 'android' || os == 'mac') || e10s # OS X: bug 786938 +scheme = https +[test_invalidCharFileExtension.xhtml] +skip-if = toolkit == 'android' && !is_fennec # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js + invalidCharFileExtension.sjs +[test_nullCharFile.xhtml] +skip-if = toolkit == 'android' && !is_fennec # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js +[test_unknown_ext_protocol_handlers.html] +[test_unsafeBidiChars.xhtml] +skip-if = toolkit == 'android' && !is_fennec # Bug 1525959 +support-files = + HelperAppLauncherDialog_chromeScript.js + unsafeBidiFileName.sjs diff --git a/uriloader/exthandler/tests/mochitest/protocolHandler.html b/uriloader/exthandler/tests/mochitest/protocolHandler.html new file mode 100644 index 0000000000..eff8a53aab --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/protocolHandler.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <title>Protocol handler</title> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + </head> + <body> + <script type="text/javascript"> + navigator.registerProtocolHandler("web+testprotocol", + "https://example.com/foobar?uri=%s", + "Test Protocol"); + </script> + <a id="link" href="web+testprotocol:test">testprotocol link</a> + </body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml new file mode 100644 index 0000000000..d6166fd270 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_handlerApps.xhtml @@ -0,0 +1,11 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handler Apps </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="handlerApps.js"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml new file mode 100644 index 0000000000..4ee1a6a1c1 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_invalidCharFileExtension.xhtml @@ -0,0 +1,47 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handling of unsafe bidi chars</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe id="test"></iframe> +<script type="text/javascript"> +var tests = [ + ["test.png:large", "test.png"], + ["test.png/large", "test.png"], + [":test.png::large:", "test.png"], +]; + +add_task(async function() { + let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js"); + let chromeScript = SpecialPowers.loadChromeScript(url); + + for (let [name, expected] of tests) { + let promiseName = new Promise(function(resolve) { + chromeScript.addMessageListener("suggestedFileName", + function listener(data) { + chromeScript.removeMessageListener("suggestedFileName", listener); + resolve(data); + }); + }); + document.getElementById("test").src = + "invalidCharFileExtension.sjs?name=" + encodeURIComponent(name); + is((await promiseName), expected, "got the expected sanitized name"); + } + + let promise = new Promise(function(resolve) { + chromeScript.addMessageListener("unregistered", function listener() { + chromeScript.removeMessageListener("unregistered", listener); + resolve(); + }); + }); + chromeScript.sendAsyncMessage("unregister"); + await promise; + + chromeScript.destroy(); +}); +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml new file mode 100644 index 0000000000..9bb1140718 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_nullCharFile.xhtml @@ -0,0 +1,49 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handling of null char</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe id="test"></iframe> +<script type="text/javascript"> +var tests = [ + ["test.html\u0000.png", "test.html_.png"], + ["test.html.\u0000png", "test.html._png"], +]; + +add_task(async function() { + let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js"); + let chromeScript = SpecialPowers.loadChromeScript(url); + + for (let [name, expected] of tests) { + let promiseName = new Promise(function(resolve) { + chromeScript.addMessageListener("suggestedFileName", + function listener(data) { + chromeScript.removeMessageListener("suggestedFileName", listener); + resolve(data); + }); + }); + const a = document.createElement('a'); + // Pass an unknown mimetype so we don't "correct" the extension: + a.href = "data:application/baconizer;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="; + a.download = name; + a.dispatchEvent(new MouseEvent('click')); + is((await promiseName), expected, "got the expected sanitized name"); + } + + let promise = new Promise(function(resolve) { + chromeScript.addMessageListener("unregistered", function listener() { + chromeScript.removeMessageListener("unregistered", listener); + resolve(); + }); + }); + chromeScript.sendAsyncMessage("unregister"); + await promise; + + chromeScript.destroy(); +}); +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html new file mode 100644 index 0000000000..f8727db605 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_unknown_ext_protocol_handlers.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for no error reporting for unknown external protocols</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe id="testFrame"></iframe> +<script type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +window.onload = () => { + let testFrame = document.getElementById("testFrame"); + + try { + testFrame.contentWindow.location.href = "unknownextproto:"; + ok(true, "There is no error reporting for unknown external protocol navigation."); + } catch (e) { + ok(false, "There should be no error reporting for unknown external protocol navigation."); + } + + SimpleTest.finish(); +}; +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml new file mode 100644 index 0000000000..4f62b32d99 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/test_unsafeBidiChars.xhtml @@ -0,0 +1,72 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Handling of unsafe bidi chars</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<iframe id="test"></iframe> +<script type="text/javascript"> +var unsafeBidiChars = [ + "\xe2\x80\xaa", // LRE + "\xe2\x80\xab", // RLE + "\xe2\x80\xac", // PDF + "\xe2\x80\xad", // LRO + "\xe2\x80\xae", // RLO +]; + +var tests = [ + "{1}.test", + "{1}File.test", + "Fi{1}le.test", + "File{1}.test", + "File.{1}test", + "File.te{1}st", + "File.test{1}", + "File.{1}", +]; + +function replace(name, x) { + return name.replace(/\{1\}/, x); +} + +function sanitize(name) { + return replace(name, "_"); +} + +add_task(async function() { + let url = SimpleTest.getTestFileURL("HelperAppLauncherDialog_chromeScript.js"); + let chromeScript = SpecialPowers.loadChromeScript(url); + + for (let test of tests) { + for (let char of unsafeBidiChars) { + let promiseName = new Promise(function(resolve) { + chromeScript.addMessageListener("suggestedFileName", + function listener(data) { + chromeScript.removeMessageListener("suggestedFileName", listener); + resolve(data); + }); + }); + let name = replace(test, char); + let expected = sanitize(test); + document.getElementById("test").src = + "unsafeBidiFileName.sjs?name=" + encodeURIComponent(name); + is((await promiseName), expected, "got the expected sanitized name"); + } + } + + let promise = new Promise(function(resolve) { + chromeScript.addMessageListener("unregistered", function listener() { + chromeScript.removeMessageListener("unregistered", listener); + resolve(); + }); + }); + chromeScript.sendAsyncMessage("unregister"); + await promise; + + chromeScript.destroy(); +}); +</script> +</body> +</html> diff --git a/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs new file mode 100644 index 0000000000..48301be5b4 --- /dev/null +++ b/uriloader/exthandler/tests/mochitest/unsafeBidiFileName.sjs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + + if (!request.queryString.match(/^name=/)) + return; + var name = decodeURIComponent(request.queryString.substring(5)); + + response.setHeader("Content-Type", "application/octet-stream; name=\"" + name + "\""); + response.setHeader("Content-Disposition", "inline; filename=\"" + name + "\""); +} diff --git a/uriloader/exthandler/tests/moz.build b/uriloader/exthandler/tests/moz.build new file mode 100644 index 0000000000..3d96aaa448 --- /dev/null +++ b/uriloader/exthandler/tests/moz.build @@ -0,0 +1,31 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ["mochitest/mochitest.ini"] + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] + +BROWSER_CHROME_MANIFESTS += ["mochitest/browser.ini"] + +TESTING_JS_MODULES += [ + "HandlerServiceTestUtils.jsm", +] + +GeckoSimplePrograms( + [ + "WriteArgument", + ], + linkage=None, +) + +if CONFIG["COMPILE_ENVIRONMENT"]: + TEST_HARNESS_FILES.xpcshell.uriloader.exthandler.tests.unit += [ + "!WriteArgument%s" % CONFIG["BIN_SUFFIX"] + ] + +USE_LIBS += [ + "nspr", +] diff --git a/uriloader/exthandler/tests/unit/handlers.json b/uriloader/exthandler/tests/unit/handlers.json new file mode 100644 index 0000000000..40e88f930a --- /dev/null +++ b/uriloader/exthandler/tests/unit/handlers.json @@ -0,0 +1,90 @@ +{ + "defaultHandlersVersion": { + "en-US": 999 + }, + "mimeTypes": { + "example/type.handleinternally": { + "unknownProperty": "preserved", + "action": 3, + "extensions": [ + "example_one" + ] + }, + "example/type.savetodisk": { + "action": 0, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + } + ], + "extensions": [ + "example_two", + "example_three" + ] + }, + "example/type.usehelperapp": { + "action": 2, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + }, + { + "name": "Example Possible Handler One", + "uriTemplate": "http://www.example.com/?id=1&url=%s" + }, + { + "name": "Example Possible Handler Two", + "uriTemplate": "http://www.example.com/?id=2&url=%s" + } + ], + "extensions": [ + "example_two", + "example_three" + ] + }, + "example/type.usesystemdefault": { + "action": 4, + "handlers": [ + null, + { + "name": "Example Possible Handler", + "uriTemplate": "http://www.example.com/?url=%s" + } + ] + } + }, + "schemes": { + "examplescheme.usehelperapp": { + "action": 2, + "ask": true, + "handlers": [ + { + "name": "Example Default Handler", + "uriTemplate": "https://www.example.com/?url=%s" + }, + { + "name": "Example Possible Handler One", + "uriTemplate": "http://www.example.com/?id=1&url=%s" + }, + { + "name": "Example Possible Handler Two", + "uriTemplate": "http://www.example.com/?id=2&url=%s" + } + ] + }, + "examplescheme.usesystemdefault": { + "action": 4, + "handlers": [ + null, + { + "name": "Example Possible Handler", + "uriTemplate": "http://www.example.com/?url=%s" + } + ] + } + } +} diff --git a/uriloader/exthandler/tests/unit/head.js b/uriloader/exthandler/tests/unit/head.js new file mode 100644 index 0000000000..3330a309be --- /dev/null +++ b/uriloader/exthandler/tests/unit/head.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Initialization for tests related to invoking external handler applications. + */ + +"use strict"; + +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { OS, require } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +ChromeUtils.import( + "resource://testing-common/HandlerServiceTestUtils.jsm", + this +); +var { TestUtils } = ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "gHandlerService", + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService" +); + +do_get_profile(); + +let jsonPath = OS.Path.join(OS.Constants.Path.profileDir, "handlers.json"); + +/** + * Unloads the nsIHandlerService data store, so the back-end file can be + * accessed or modified, and the new data will be loaded at the next access. + */ +let unloadHandlerStore = async function() { + // If this function is called before the nsIHandlerService instance has been + // initialized for the first time, the observer below will not be registered. + // We have to force initialization to prevent the function from stalling. + gHandlerService; + + let promise = TestUtils.topicObserved("handlersvc-json-replace-complete"); + Services.obs.notifyObservers(null, "handlersvc-json-replace"); + await promise; +}; + +/** + * Unloads the data store and deletes it. + */ +let deleteHandlerStore = async function() { + await unloadHandlerStore(); + + await OS.File.remove(jsonPath, { ignoreAbsent: true }); +}; + +/** + * Unloads the data store and replaces it with the test data file. + */ +let copyTestDataToHandlerStore = async function() { + await unloadHandlerStore(); + + await OS.File.copy(do_get_file("handlers.json").path, jsonPath); +}; + +/** + * Ensures the files are removed and the services unloaded when the tests end. + */ +registerCleanupFunction(async function test_terminate() { + await deleteHandlerStore(); +}); diff --git a/uriloader/exthandler/tests/unit/mailcap b/uriloader/exthandler/tests/unit/mailcap new file mode 100644 index 0000000000..dc93ef8042 --- /dev/null +++ b/uriloader/exthandler/tests/unit/mailcap @@ -0,0 +1,2 @@ +text/plain; cat '%s'; needsterminal +text/plain; sed '%s' diff --git a/uriloader/exthandler/tests/unit/test_badMIMEType.js b/uriloader/exthandler/tests/unit/test_badMIMEType.js new file mode 100644 index 0000000000..49c5e8d848 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_badMIMEType.js @@ -0,0 +1,29 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + // "text/plain" has an 0xFF character appended to it. This means it's an + // invalid string, which is tricky to enter using a text editor (I used + // emacs' hexl-mode). It also means an ordinary text editor might drop it + // or convert it to something that *is* valid (in UTF8). So we measure + // its length to make sure this hasn't happened. + var badMimeType = "text/plainÿ"; + Assert.equal(badMimeType.length, 11); + try { + Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getFromTypeAndExtension(badMimeType, "txt"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException) || + e.result != Cr.NS_ERROR_NOT_AVAILABLE + ) { + throw e; + } + // This is an expected exception, thrown if the type can't be determined + } + // Not crashing is good enough + Assert.equal(true, true); +} diff --git a/uriloader/exthandler/tests/unit/test_defaults_handlerService.js b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js new file mode 100644 index 0000000000..f9f9feda23 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_defaults_handlerService.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "gExternalProtocolService", + "@mozilla.org/uriloader/external-protocol-service;1", + "nsIExternalProtocolService" +); + +const kDefaultHandlerList = Services.prefs + .getChildList("gecko.handlerService.schemes") + .filter(p => { + try { + let val = Services.prefs.getComplexValue(p, Ci.nsIPrefLocalizedString) + .data; + return !!val; + } catch (ex) { + return false; + } + }); + +add_task(async function test_check_defaults_get_added() { + let protocols = new Set( + kDefaultHandlerList.map(p => p.match(/schemes\.(\w+)/)[1]) + ); + for (let protocol of protocols) { + const kPrefStr = `schemes.${protocol}.`; + let matchingPrefs = kDefaultHandlerList.filter(p => p.includes(kPrefStr)); + let protocolHandlerCount = matchingPrefs.length / 2; + Assert.ok( + protocolHandlerCount, + `Prefs for ${protocol} have at least 1 protocol handler` + ); + Assert.ok( + gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry, + `Expect stub for ${protocol}` + ); + let info = gExternalProtocolService.getProtocolHandlerInfo(protocol, {}); + Assert.ok( + info, + `Should be able to get protocol handler info for ${protocol}` + ); + let handlers = Array.from( + info.possibleApplicationHandlers.enumerate(Ci.nsIHandlerApp) + ); + handlers = handlers.filter(h => h instanceof Ci.nsIWebHandlerApp); + Assert.equal( + handlers.length, + protocolHandlerCount, + `Default web handlers for ${protocol} should match` + ); + let { alwaysAskBeforeHandling, preferredAction } = info; + // Actually store something, pretending there was a change: + let infoToWrite = gExternalProtocolService.getProtocolHandlerInfo( + protocol, + {} + ); + gHandlerService.store(infoToWrite); + ok( + !gHandlerService.wrappedJSObject._store.data.schemes[protocol].stubEntry, + "Expect stub entry info to go away" + ); + + let newInfo = gExternalProtocolService.getProtocolHandlerInfo(protocol, {}); + Assert.equal( + alwaysAskBeforeHandling, + newInfo.alwaysAskBeforeHandling, + protocol + " - always ask shouldn't change" + ); + Assert.equal( + preferredAction, + newInfo.preferredAction, + protocol + " - preferred action shouldn't change" + ); + await deleteHandlerStore(); + } +}); + +add_task(async function test_check_default_modification() { + let mailtoHandlerCount = + kDefaultHandlerList.filter(p => p.includes("mailto")).length / 2; + Assert.ok(mailtoHandlerCount, "Prefs have at least 1 mailto handler"); + Assert.ok( + true, + JSON.stringify(gHandlerService.wrappedJSObject._store.data.schemes.mailto) + ); + Assert.ok( + gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry, + "Expect stub for mailto" + ); + let mailInfo = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + mailInfo.alwaysAskBeforeHandling = false; + mailInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault; + gHandlerService.store(mailInfo); + Assert.ok( + !gHandlerService.wrappedJSObject._store.data.schemes.mailto.stubEntry, + "Stub entry should be removed immediately." + ); + let newMail = gExternalProtocolService.getProtocolHandlerInfo("mailto", {}); + Assert.equal(newMail.preferredAction, Ci.nsIHandlerInfo.useSystemDefault); + Assert.equal(newMail.alwaysAskBeforeHandling, false); + await deleteHandlerStore(); +}); + +/** + * Check that we don't add bogus handlers. + */ +add_task(async function test_check_restrictions() { + const kTestData = { + testdeleteme: [ + ["Delete me", ""], + ["Delete me insecure", "http://example.com/%s"], + ["Delete me no substitution", "https://example.com/"], + ["Keep me", "https://example.com/%s"], + ], + testreallydeleteme: [ + // used to check we remove the entire entry. + ["Delete me", "http://example.com/%s"], + ], + }; + for (let [scheme, handlers] of Object.entries(kTestData)) { + let count = 1; + for (let [name, uriTemplate] of handlers) { + let pref = `gecko.handlerService.schemes.${scheme}.${count}.`; + let obj = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + obj.data = name; + Services.prefs.setComplexValue( + pref + "name", + Ci.nsIPrefLocalizedString, + obj + ); + obj.data = uriTemplate; + Services.prefs.setComplexValue( + pref + "uriTemplate", + Ci.nsIPrefLocalizedString, + obj + ); + count++; + } + } + + gHandlerService.wrappedJSObject._injectDefaultProtocolHandlers(); + let schemeData = gHandlerService.wrappedJSObject._store.data.schemes; + + Assert.ok(schemeData.testdeleteme, "Expect an entry for testdeleteme"); + Assert.ok( + schemeData.testdeleteme.stubEntry, + "Expect a stub entry for testdeleteme" + ); + + Assert.deepEqual( + schemeData.testdeleteme.handlers, + [null, { name: "Keep me", uriTemplate: "https://example.com/%s" }], + "Expect only one handler is kept." + ); + + Assert.ok(!schemeData.testreallydeleteme, "No entry for reallydeleteme"); +}); diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js new file mode 100644 index 0000000000..62031da1e6 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_pdf.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyServiceGetter( + this, + "gMIMEService", + "@mozilla.org/mime;1", + "nsIMIMEService" +); +XPCOMUtils.defineLazyServiceGetter( + this, + "gBundleService", + "@mozilla.org/intl/stringbundle;1", + "nsIStringBundleService" +); + +// PDF files should always have a generic description instead +// of relying on what is registered with the Operating System. +add_task(async function test_check_unknown_mime_type() { + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let pdfType = mimeService.getTypeFromExtension("pdf"); + Assert.equal(pdfType, "application/pdf"); + let extension = mimeService.getPrimaryExtension("application/pdf", ""); + Assert.equal(extension, "pdf", "Expect pdf extension when given mime"); + let mimeInfo = gMIMEService.getFromTypeAndExtension("", "pdf"); + let stringBundle = gBundleService.createBundle( + "chrome://mozapps/locale/downloads/unknownContentType.properties" + ); + Assert.equal( + mimeInfo.description, + stringBundle.GetStringFromName("pdfExtHandlerDescription"), + "PDF has generic description" + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js new file mode 100644 index 0000000000..9beef9d9c5 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getMIMEInfo_unknown_mime_type.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Zip files can be opened by Windows explorer, so we should always be able to +// determine a description and default handler for them. However, things can +// get messy if they are sent to us with a mime type other than what Windows +// considers the "right" mimetype (application/x-zip-compressed), like +// application/zip, which is what most places (IANA, macOS, probably all linux +// distros, Apache, etc.) think is the "right" mimetype. +add_task(async function test_check_unknown_mime_type() { + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let zipType = mimeService.getTypeFromExtension("zip"); + Assert.equal(zipType, "application/x-zip-compressed"); + try { + let extension = mimeService.getPrimaryExtension("application/zip", ""); + Assert.equal( + extension, + "zip", + "Expect our own info to provide an extension for zip files." + ); + } catch (ex) { + Assert.ok(false, "We shouldn't throw when getting zip info."); + } + let found = {}; + let mimeInfo = mimeService.getMIMEInfoFromOS("application/zip", "zip", found); + Assert.ok( + mimeInfo.hasDefaultHandler, + "Should have a default app for zip files" + ); +}); diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js new file mode 100644 index 0000000000..7202db58de --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_ext_to_type_mapping.js @@ -0,0 +1,65 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 508030 <https://bugzilla.mozilla.org/show_bug.cgi?id=508030>: + * nsIMIMEService.getTypeFromExtension fails to find a match in the + * "ext-to-type-mapping" category if the provided extension is not lowercase. + */ +function run_test() { + // --- Common services --- + + const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + + const categoryManager = Services.catMan; + + // --- Test procedure --- + + const kTestExtension = "testextension"; + const kTestExtensionMixedCase = "testExtensIon"; + const kTestMimeType = "application/x-testextension"; + + // Ensure that the test extension is not initially recognized by the operating + // system or the "ext-to-type-mapping" category. + try { + // Try and get the MIME type associated with the extension. + mimeService.getTypeFromExtension(kTestExtension); + // The line above should have thrown an exception. + do_throw("nsIMIMEService.getTypeFromExtension succeeded unexpectedly"); + } catch (e) { + if ( + !(e instanceof Ci.nsIException) || + e.result != Cr.NS_ERROR_NOT_AVAILABLE + ) { + throw e; + } + // This is an expected exception, thrown if the type can't be determined. + // Any other exception would cause the test to fail. + } + + // Add a temporary category entry mapping the extension to the MIME type. + categoryManager.addCategoryEntry( + "ext-to-type-mapping", + kTestExtension, + kTestMimeType, + false, + true + ); + + // Check that the mapping is recognized in the simple case. + var type = mimeService.getTypeFromExtension(kTestExtension); + Assert.equal(type, kTestMimeType); + + // Check that the mapping is recognized even if the extension has mixed case. + type = mimeService.getTypeFromExtension(kTestExtensionMixedCase); + Assert.equal(type, kTestMimeType); + + // Clean up after ourselves. + categoryManager.deleteCategoryEntry( + "ext-to-type-mapping", + kTestExtension, + false + ); +} diff --git a/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js new file mode 100644 index 0000000000..dad5530856 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_getTypeFromExtension_with_empty_Content_Type.js @@ -0,0 +1,190 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 484579 <https://bugzilla.mozilla.org/show_bug.cgi?id=484579>: + * nsIMIMEService.getTypeFromExtension may fail unexpectedly on Windows when + * "Content Type" is empty in the registry. + */ + +// We must use a file extension that isn't listed in nsExternalHelperAppService's +// defaultMimeEntries, otherwise the code takes a shortcut skipping the registry. +const FILE_EXTENSION = ".nfo"; +// This is used to ensure the test properly used the mock, so that if we change +// the underlying code, it won't be skipped. +let gTestUsedOurMock = false; + +function run_test() { + // Activate the override of the file association data in the registry. + registerMockWindowsRegKeyFactory(); + + // Check the mock has been properly activated. + let regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + regKey.open( + Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT, + FILE_EXTENSION, + Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE + ); + Assert.equal( + regKey.readStringValue("content type"), + "", + "Check the mock replied as expected." + ); + Assert.ok(gTestUsedOurMock, "The test properly used the mock registry"); + // Reset gTestUsedOurMock, because we just used it. + gTestUsedOurMock = false; + // Try and get the MIME type associated with the extension. If this + // operation does not throw an unexpected exception, the test succeeds. + Assert.throws( + () => { + Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromExtension(FILE_EXTENSION); + }, + /NS_ERROR_NOT_AVAILABLE/, + "Should throw a NOT_AVAILABLE exception" + ); + + Assert.ok(gTestUsedOurMock, "The test properly used the mock registry"); +} + +/** + * Constructs a new mock registry key by wrapping the provided object. + * + * This mock implementation is tailored for this test, and forces consumers + * of the readStringValue method to believe that the "Content Type" value of + * the FILE_EXTENSION key under HKEY_CLASSES_ROOT is an empty string. + * + * The same value read from "HKEY_LOCAL_MACHINE\SOFTWARE\Classes" is not + * affected. + * + * @param aWrappedObject An actual nsIWindowsRegKey implementation. + */ +function MockWindowsRegKey(aWrappedObject) { + this._wrappedObject = aWrappedObject; + + // This function creates a forwarding function for wrappedObject + function makeForwardingFunction(functionName) { + return function() { + return aWrappedObject[functionName].apply(aWrappedObject, arguments); + }; + } + + // Forward all the functions that are not explicitly overridden + for (var propertyName in aWrappedObject) { + if (!(propertyName in this)) { + if (typeof aWrappedObject[propertyName] == "function") { + this[propertyName] = makeForwardingFunction(propertyName); + } else { + this[propertyName] = aWrappedObject[propertyName]; + } + } + } +} + +MockWindowsRegKey.prototype = { + // --- Overridden nsISupports interface functions --- + + QueryInterface: ChromeUtils.generateQI(["nsIWindowsRegKey"]), + + // --- Overridden nsIWindowsRegKey interface functions --- + + open(aRootKey, aRelPath, aMode) { + // Remember the provided root key and path + this._rootKey = aRootKey; + this._relPath = aRelPath; + + // Create the actual registry key + return this._wrappedObject.open(aRootKey, aRelPath, aMode); + }, + + openChild(aRelPath, aMode) { + // Open the child key and wrap it + var innerKey = this._wrappedObject.openChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + createChild(aRelPath, aMode) { + // Create the child key and wrap it + var innerKey = this._wrappedObject.createChild(aRelPath, aMode); + var key = new MockWindowsRegKey(innerKey); + + // Set the properties of the child key and return it + key._rootKey = this._rootKey; + key._relPath = this._relPath + aRelPath; + return key; + }, + + get childCount() { + return this._wrappedObject.childCount; + }, + + get valueCount() { + return this._wrappedObject.valueCount; + }, + + readStringValue(aName) { + // If this is the key under test, return a fake value + if ( + this._rootKey == Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT && + this._relPath.toLowerCase() == FILE_EXTENSION && + aName.toLowerCase() == "content type" + ) { + gTestUsedOurMock = true; + return ""; + } + // Return the real value from the registry + return this._wrappedObject.readStringValue(aName); + }, +}; + +function registerMockWindowsRegKeyFactory() { + const kMockCID = Components.ID("{9b23dfe9-296b-4740-ba1c-d39c9a16e55e}"); + const kWindowsRegKeyContractID = "@mozilla.org/windows-registry-key;1"; + // Preserve the original CID. + let originalWindowsRegKeyCID = Cc[kWindowsRegKeyContractID].number; + + info("Create a mock RegKey factory"); + let originalRegKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + let mockWindowsRegKeyFactory = { + createInstance(outer, iid) { + if (outer != null) { + throw Components.Exception("", Cr.NS_ERROR_NO_AGGREGATION); + } + info("Create a mock wrapper around RegKey"); + var key = new MockWindowsRegKey(originalRegKey); + return key.QueryInterface(iid); + }, + }; + info("Register the mock RegKey factory"); + let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory( + kMockCID, + "Mock Windows Registry Key Implementation", + kWindowsRegKeyContractID, + mockWindowsRegKeyFactory + ); + + registerCleanupFunction(() => { + // Free references to the mock factory + registrar.unregisterFactory(kMockCID, mockWindowsRegKeyFactory); + // Restore the original factory + registrar.registerFactory( + Components.ID(originalWindowsRegKeyCID), + "", + kWindowsRegKeyContractID, + null + ); + }); +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService.js b/uriloader/exthandler/tests/unit/test_handlerService.js new file mode 100644 index 0000000000..610eb5b749 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService.js @@ -0,0 +1,474 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + //* *************************************************************************// + // Constants + + const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].getService( + Ci.nsIHandlerService + ); + + const mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + + const protoSvc = Cc[ + "@mozilla.org/uriloader/external-protocol-service;1" + ].getService(Ci.nsIExternalProtocolService); + + const prefSvc = Services.prefs; + + const env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + const rootPrefBranch = prefSvc.getBranch(""); + + let noMailto = false; + if (mozinfo.os == "win") { + // Check mailto handler from registry. + // If registry entry is nothing, no mailto handler + let regSvc = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + regSvc.open(regSvc.ROOT_KEY_CLASSES_ROOT, "mailto", regSvc.ACCESS_READ); + noMailto = false; + } catch (ex) { + noMailto = true; + } + regSvc.close(); + } + + if (mozinfo.os == "linux") { + // Check mailto handler from GIO + // If there isn't one, then we have no mailto handler + let gIOSvc = Cc["@mozilla.org/gio-service;1"].createInstance( + Ci.nsIGIOService + ); + try { + gIOSvc.getAppForURIScheme("mailto"); + noMailto = false; + } catch (ex) { + noMailto = true; + } + } + + //* *************************************************************************// + // Sample Data + + // It doesn't matter whether or not this nsIFile is actually executable, + // only that it has a path and exists. Since we don't know any executable + // that exists on all platforms (except possibly the application being + // tested, but there doesn't seem to be a way to get a reference to that + // from the directory service), we use the temporary directory itself. + var executable = Services.dirsvc.get("TmpD", Ci.nsIFile); + // XXX We could, of course, create an actual executable in the directory: + // executable.append("localhandler"); + // if (!executable.exists()) + // executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o755); + + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandler.name = "Local Handler"; + localHandler.executable = executable; + + var webHandler = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" + ].createInstance(Ci.nsIWebHandlerApp); + webHandler.name = "Web Handler"; + webHandler.uriTemplate = "http://www.example.com/?%s"; + + // FIXME: these tests create and manipulate enough variables that it would + // make sense to move each test into its own scope so we don't run the risk + // of one test stomping on another's data. + + //* *************************************************************************// + // Test Default Properties + + // Get a handler info for a MIME type that neither the application nor + // the OS knows about and make sure its properties are set to the proper + // default values. + + var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + // Make sure it's also an nsIHandlerInfo. + Assert.ok(handlerInfo instanceof Ci.nsIHandlerInfo); + + Assert.equal(handlerInfo.type, "nonexistent/type"); + + // Deprecated property, but we should still make sure it's set correctly. + Assert.equal(handlerInfo.MIMEType, "nonexistent/type"); + + // These properties are the ones the handler service knows how to store. + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.saveToDisk); + Assert.equal(handlerInfo.preferredApplicationHandler, null); + Assert.equal(handlerInfo.possibleApplicationHandlers.length, 0); + Assert.ok(handlerInfo.alwaysAskBeforeHandling); + + // These properties are initialized to default values by the service, + // so we might as well make sure they're initialized to the right defaults. + Assert.equal(handlerInfo.description, ""); + Assert.equal(handlerInfo.hasDefaultHandler, false); + Assert.equal(handlerInfo.defaultDescription, ""); + + // test some default protocol info properties + var haveDefaultHandlersVersion = false; + try { + // If we have a defaultHandlersVersion pref, then assume that we're in the + // firefox tree and that we'll also have default handlers. + // Bug 395131 has been filed to make this test work more generically + // by providing our own prefs for this test rather than this icky + // special casing. + rootPrefBranch.getCharPref("gecko.handlerService.defaultHandlersVersion"); + haveDefaultHandlersVersion = true; + } catch (ex) {} + + const kExternalWarningDefault = + "network.protocol-handler.warn-external-default"; + prefSvc.setBoolPref(kExternalWarningDefault, true); + + // XXX add more thorough protocol info property checking + + // no OS default handler exists + var protoInfo = protoSvc.getProtocolHandlerInfo("x-moz-rheet"); + Assert.equal(protoInfo.preferredAction, protoInfo.alwaysAsk); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: false + const kExternalWarningPrefPrefix = "network.protocol-handler.warn-external."; + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", false); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + Assert.ok(!protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default does not exist, + // explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "http", true); + protoInfo = protoSvc.getProtocolHandlerInfo("http"); + // OS handler isn't included in possibleApplicationHandlers, so length is 0 + // Once they become instances of nsILocalHandlerApp, this number will need + // to change. + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + + // OS default exists, injected default exists, explicit warning pref: false + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) { + Assert.equal(2, protoInfo.possibleApplicationHandlers.length); + } else { + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + } + + // Win7+ or Linux's GIO might not have a default mailto: handler + if (noMailto) { + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } else { + Assert.ok(!protoInfo.alwaysAskBeforeHandling); + } + + // OS default exists, injected default exists, explicit warning pref: true + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", true); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + if (haveDefaultHandlersVersion) { + Assert.equal(2, protoInfo.possibleApplicationHandlers.length); + // Win7+ or Linux's GIO may have no default mailto: handler, so we'd ask + // anyway. Otherwise, the default handlers will not have stored preferred + // actions etc., so re-requesting them after the warning pref has changed + // will use the updated pref value. So both when we have and do not have + // a default mailto: handler, we'll ask: + Assert.ok(protoInfo.alwaysAskBeforeHandling); + // As soon as anyone actually stores updated defaults into the profile + // database, that default will stop tracking the warning pref. + } else { + Assert.equal(0, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } + + if (haveDefaultHandlersVersion) { + // Now set the value stored in RDF to true, and the pref to false, to make + // sure we still get the right value. (Basically, same thing as above but + // with the values reversed.) + prefSvc.setBoolPref(kExternalWarningPrefPrefix + "mailto", false); + protoInfo.alwaysAskBeforeHandling = true; + handlerSvc.store(protoInfo); + protoInfo = protoSvc.getProtocolHandlerInfo("mailto"); + Assert.equal(2, protoInfo.possibleApplicationHandlers.length); + Assert.ok(protoInfo.alwaysAskBeforeHandling); + } + + //* *************************************************************************// + // Test Round-Trip Data Integrity + + // Test round-trip data integrity by setting the properties of the handler + // info object to different values, telling the handler service to store the + // object, and then retrieving a new info object for the same type and making + // sure its properties are identical. + + handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + handlerInfo.preferredApplicationHandler = localHandler; + handlerInfo.alwaysAskBeforeHandling = false; + + handlerSvc.store(handlerInfo); + + handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null); + + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + + Assert.notEqual(handlerInfo.preferredApplicationHandler, null); + var preferredHandler = handlerInfo.preferredApplicationHandler; + Assert.equal(typeof preferredHandler, "object"); + Assert.equal(preferredHandler.name, "Local Handler"); + Assert.ok(preferredHandler instanceof Ci.nsILocalHandlerApp); + preferredHandler.QueryInterface(Ci.nsILocalHandlerApp); + Assert.equal(preferredHandler.executable.path, localHandler.executable.path); + + Assert.ok(!handlerInfo.alwaysAskBeforeHandling); + + // Make sure the handler service's enumerate method lists all known handlers. + var handlerInfo2 = mimeSvc.getFromTypeAndExtension("nonexistent/type2", null); + handlerSvc.store(handlerInfo2); + var handlerTypes = ["nonexistent/type", "nonexistent/type2"]; + if (haveDefaultHandlersVersion) { + handlerTypes.push("mailto"); + handlerTypes.push("irc"); + handlerTypes.push("ircs"); + } + for (let handler of handlerSvc.enumerate()) { + Assert.notEqual(handlerTypes.indexOf(handler.type), -1); + handlerTypes.splice(handlerTypes.indexOf(handler.type), 1); + } + Assert.equal(handlerTypes.length, 0); + // Make sure the handler service's remove method removes a handler record. + handlerSvc.remove(handlerInfo2); + let handlers = handlerSvc.enumerate(); + while (handlers.hasMoreElements()) { + Assert.notEqual( + handlers.getNext().QueryInterface(Ci.nsIHandlerInfo).type, + handlerInfo2.type + ); + } + + // Make sure we can store and retrieve a handler info object with no preferred + // handler. + var noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/no-preferred-handler", + null + ); + handlerSvc.store(noPreferredHandlerInfo); + noPreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/no-preferred-handler", + null + ); + Assert.equal(noPreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure that the handler service removes an existing handler record + // if we store a handler info object with no preferred handler. + var removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + removePreferredHandlerInfo.preferredApplicationHandler = localHandler; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + removePreferredHandlerInfo.preferredApplicationHandler = null; + handlerSvc.store(removePreferredHandlerInfo); + removePreferredHandlerInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/rem-preferred-handler", + null + ); + Assert.equal(removePreferredHandlerInfo.preferredApplicationHandler, null); + + // Make sure we can store and retrieve a handler info object with possible + // handlers. We test both adding and removing handlers. + + // Get a handler info and make sure it has no possible handlers. + var possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Store and re-retrieve the handler and make sure it still has no possible + // handlers. + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 0); + + // Add two handlers, store the object, re-retrieve it, and make sure it has + // two handlers. + possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler); + possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 2); + + // Figure out which is the local and which is the web handler and the index + // in the array of the local handler, which is the one we're going to remove + // to test removal of a handler. + var handler1 = possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 0, + Ci.nsIHandlerApp + ); + var handler2 = possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 1, + Ci.nsIHandlerApp + ); + var localPossibleHandler, webPossibleHandler, localIndex; + if (handler1 instanceof Ci.nsILocalHandlerApp) { + [localPossibleHandler, webPossibleHandler, localIndex] = [ + handler1, + handler2, + 0, + ]; + } else { + [localPossibleHandler, webPossibleHandler, localIndex] = [ + handler2, + handler1, + 1, + ]; + } + localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp); + webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp); + + // Make sure the two handlers are the ones we stored. + Assert.equal(localPossibleHandler.name, localHandler.name); + Assert.ok(localPossibleHandler.equals(localHandler)); + Assert.equal(webPossibleHandler.name, webHandler.name); + Assert.ok(webPossibleHandler.equals(webHandler)); + + // Remove a handler, store the object, re-retrieve it, and make sure + // it only has one handler. + possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex); + handlerSvc.store(possibleHandlersInfo); + possibleHandlersInfo = mimeSvc.getFromTypeAndExtension( + "nonexistent/possible-handlers", + null + ); + Assert.equal(possibleHandlersInfo.possibleApplicationHandlers.length, 1); + + // Make sure the handler is the one we didn't remove. + webPossibleHandler = possibleHandlersInfo.possibleApplicationHandlers.queryElementAt( + 0, + Ci.nsIWebHandlerApp + ); + Assert.equal(webPossibleHandler.name, webHandler.name); + Assert.ok(webPossibleHandler.equals(webHandler)); + + // //////////////////////////////////////////////////// + // handler info command line parameters and equality + var localApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + var handlerApp = localApp.QueryInterface(Ci.nsIHandlerApp); + + Assert.ok(handlerApp.equals(localApp)); + + localApp.executable = executable; + + Assert.equal(0, localApp.parameterCount); + localApp.appendParameter("-test1"); + Assert.equal(1, localApp.parameterCount); + localApp.appendParameter("-test2"); + Assert.equal(2, localApp.parameterCount); + Assert.ok(localApp.parameterExists("-test1")); + Assert.ok(localApp.parameterExists("-test2")); + Assert.ok(!localApp.parameterExists("-false")); + localApp.clearParameters(); + Assert.equal(0, localApp.parameterCount); + + var localApp2 = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + + localApp2.executable = executable; + + localApp.clearParameters(); + Assert.ok(localApp.equals(localApp2)); + + // equal: + // cut -d 1 -f 2 + // cut -d 1 -f 2 + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test3"); + Assert.ok(localApp.equals(localApp2)); + + // not equal: + // cut -d 1 -f 2 + // cut -f 1 -d 2 + + localApp.clearParameters(); + localApp2.clearParameters(); + + localApp.appendParameter("-test1"); + localApp.appendParameter("-test2"); + localApp.appendParameter("-test3"); + localApp2.appendParameter("-test2"); + localApp2.appendParameter("-test1"); + localApp2.appendParameter("-test3"); + Assert.ok(!localApp2.equals(localApp)); + + var str; + str = localApp.getParameter(0); + Assert.equal(str, "-test1"); + str = localApp.getParameter(1); + Assert.equal(str, "-test2"); + str = localApp.getParameter(2); + Assert.equal(str, "-test3"); + + // FIXME: test round trip integrity for a protocol. + // FIXME: test round trip integrity for a handler info with a web handler. + + //* *************************************************************************// + // getTypeFromExtension tests + + // test nonexistent extension + var lolType = handlerSvc.getTypeFromExtension("lolcat"); + Assert.equal(lolType, ""); + + // add a handler for the extension + var lolHandler = mimeSvc.getFromTypeAndExtension("application/lolcat", null); + + Assert.ok(!lolHandler.extensionExists("lolcat")); + lolHandler.preferredAction = Ci.nsIHandlerInfo.useHelperApp; + lolHandler.preferredApplicationHandler = localHandler; + lolHandler.alwaysAskBeforeHandling = false; + lolHandler.appendExtension("lolcat"); + + // store the handler + Assert.ok(!handlerSvc.exists(lolHandler)); + handlerSvc.store(lolHandler); + Assert.ok(handlerSvc.exists(lolHandler)); + + // test now-existent extension + lolType = handlerSvc.getTypeFromExtension("lolcat"); + Assert.equal(lolType, "application/lolcat"); + + // test mailcap entries with needsterminal are ignored on non-Windows non-Mac. + if (mozinfo.os != "win" && mozinfo.os != "mac") { + env.set("PERSONAL_MAILCAP", do_get_file("mailcap").path); + handlerInfo = mimeSvc.getFromTypeAndExtension("text/plain", null); + Assert.equal( + handlerInfo.preferredAction, + Ci.nsIHandlerInfo.useSystemDefault + ); + Assert.equal(handlerInfo.defaultDescription, "sed"); + } +} diff --git a/uriloader/exthandler/tests/unit/test_handlerService_store.js b/uriloader/exthandler/tests/unit/test_handlerService_store.js new file mode 100644 index 0000000000..aa2efde822 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_handlerService_store.js @@ -0,0 +1,771 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests the nsIHandlerService interface. + */ + +// Set up an nsIWebHandlerApp instance that can be used in multiple tests. +let webHandlerApp = Cc[ + "@mozilla.org/uriloader/web-handler-app;1" +].createInstance(Ci.nsIWebHandlerApp); +webHandlerApp.name = "Web Handler"; +webHandlerApp.uriTemplate = "https://www.example.com/?url=%s"; +let expectedWebHandlerApp = { + name: webHandlerApp.name, + uriTemplate: webHandlerApp.uriTemplate, +}; + +// Set up an nsILocalHandlerApp instance that can be used in multiple tests. The +// executable should exist, but it doesn't need to point to an actual file, so +// we simply initialize it to the path of an existing directory. +let localHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" +].createInstance(Ci.nsILocalHandlerApp); +localHandlerApp.name = "Local Handler"; +localHandlerApp.executable = FileUtils.getFile("TmpD", []); +let expectedLocalHandlerApp = { + name: localHandlerApp.name, + executable: localHandlerApp.executable, +}; + +/** + * Returns a new nsIHandlerInfo instance initialized to known values that don't + * depend on the platform and are easier to verify later. + * + * @param type + * Because the "preferredAction" is initialized to saveToDisk, this + * should represent a MIME type rather than a protocol. + */ +function getKnownHandlerInfo(type) { + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(type); + handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfo.alwaysAskBeforeHandling = false; + return handlerInfo; +} + +/** + * Checks that the information stored in the handler service instance under + * testing matches the test data files. + */ +function assertAllHandlerInfosMatchTestData() { + let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + + // It's important that the MIME types we check here do not exist at the + // operating system level, otherwise the list of handlers and file extensions + // will be merged. The current implementation avoids duplicate entries. + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.handleinternally", + preferredAction: Ci.nsIHandlerInfo.handleInternally, + alwaysAskBeforeHandling: false, + fileExtensions: ["example_one"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.savetodisk", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + ], + fileExtensions: ["example_two", "example_three"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.usehelperapp", + preferredAction: Ci.nsIHandlerInfo.useHelperApp, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + { + name: "Example Possible Handler One", + uriTemplate: "http://www.example.com/?id=1&url=%s", + }, + { + name: "Example Possible Handler Two", + uriTemplate: "http://www.example.com/?id=2&url=%s", + }, + ], + fileExtensions: ["example_two", "example_three"], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "example/type.usesystemdefault", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "examplescheme.usehelperapp", + preferredAction: Ci.nsIHandlerInfo.useHelperApp, + alwaysAskBeforeHandling: true, + preferredApplicationHandler: { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + possibleApplicationHandlers: [ + { + name: "Example Default Handler", + uriTemplate: "https://www.example.com/?url=%s", + }, + { + name: "Example Possible Handler One", + uriTemplate: "http://www.example.com/?id=1&url=%s", + }, + { + name: "Example Possible Handler Two", + uriTemplate: "http://www.example.com/?id=2&url=%s", + }, + ], + }); + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "examplescheme.usesystemdefault", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + + Assert.equal(handlerInfos.length, 0); +} + +/** + * Loads data from a file in a predefined format, verifying that the format is + * recognized and all the known properties are loaded and saved. + */ +add_task(async function test_store_fillHandlerInfo_predefined() { + // Test that the file format used in previous versions can be loaded. + await copyTestDataToHandlerStore(); + await assertAllHandlerInfosMatchTestData(); + + // Keep a copy of the nsIHandlerInfo instances, then delete the handler store + // and populate it with the known data. Since the handler store is empty, the + // default handlers for the current locale are also injected, so we have to + // delete them manually before adding the other nsIHandlerInfo instances. + let testHandlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + await deleteHandlerStore(); + for (let handlerInfo of HandlerServiceTestUtils.getAllHandlerInfos()) { + gHandlerService.remove(handlerInfo); + } + for (let handlerInfo of testHandlerInfos) { + gHandlerService.store(handlerInfo); + } + + // Test that the known data still matches after saving it and reloading. + await unloadHandlerStore(); + await assertAllHandlerInfosMatchTestData(); +}); + +/** + * Check that "store" is able to add new instances, that "remove" and "exists" + * work, and that "fillHandlerInfo" throws when the instance does not exist. + */ +add_task(async function test_store_remove_exists() { + // Test both MIME types and protocols. + for (let type of [ + "example/type.usehelperapp", + "examplescheme.usehelperapp", + ]) { + // Create new nsIHandlerInfo instances before loading the test data. + await deleteHandlerStore(); + let handlerInfoPresent = HandlerServiceTestUtils.getHandlerInfo(type); + let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + + // Set up known properties that we can verify later. + handlerInfoAbsent.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfoAbsent.alwaysAskBeforeHandling = false; + + await copyTestDataToHandlerStore(); + + Assert.ok(gHandlerService.exists(handlerInfoPresent)); + Assert.ok(!gHandlerService.exists(handlerInfoAbsent)); + + gHandlerService.store(handlerInfoAbsent); + gHandlerService.remove(handlerInfoPresent); + + await unloadHandlerStore(); + + Assert.ok(!gHandlerService.exists(handlerInfoPresent)); + Assert.ok(gHandlerService.exists(handlerInfoAbsent)); + + Assert.throws( + () => gHandlerService.fillHandlerInfo(handlerInfoPresent, ""), + ex => ex.result == Cr.NS_ERROR_NOT_AVAILABLE + ); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: type + "2", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + }); + } +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredAction" that is alwaysAsk or has an unknown value, but the + * action always becomes useHelperApp when reloading. + */ +add_task(async function test_store_preferredAction() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + + for (let preferredAction of [Ci.nsIHandlerInfo.alwaysAsk, 999]) { + handlerInfo.preferredAction = preferredAction; + gHandlerService.store(handlerInfo); + gHandlerService.fillHandlerInfo(handlerInfo, ""); + Assert.equal(handlerInfo.preferredAction, Ci.nsIHandlerInfo.useHelperApp); + } +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance containing an + * nsILocalHandlerApp instance pointing to an executable that doesn't exist, but + * this entry is ignored when reloading. + */ +add_task(async function test_store_localHandlerApp_missing() { + if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + let missingHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + missingHandlerApp.name = "Non-existing Handler"; + missingHandlerApp.executable = FileUtils.getFile("TmpD", ["nonexisting"]); + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = missingHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(missingHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [expectedWebHandlerApp], + }); +}); + +/** + * Test saving and reloading an instance of nsIDBusHandlerApp. + */ +add_task(async function test_store_dBusHandlerApp() { + if (!("@mozilla.org/uriloader/dbus-handler-app;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + // Set up an nsIDBusHandlerApp instance for testing. + let dBusHandlerApp = Cc[ + "@mozilla.org/uriloader/dbus-handler-app;1" + ].createInstance(Ci.nsIDBusHandlerApp); + dBusHandlerApp.name = "DBus Handler"; + dBusHandlerApp.service = "test.method.server"; + dBusHandlerApp.method = "Method"; + dBusHandlerApp.dBusInterface = "test.method.Type"; + dBusHandlerApp.objectPath = "/test/method/Object"; + let expectedDBusHandlerApp = { + name: dBusHandlerApp.name, + service: dBusHandlerApp.service, + method: dBusHandlerApp.method, + dBusInterface: dBusHandlerApp.dBusInterface, + objectPath: dBusHandlerApp.objectPath, + }; + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = dBusHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(dBusHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedDBusHandlerApp, + possibleApplicationHandlers: [expectedDBusHandlerApp], + }); +}); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredApplicationHandler" and no "possibleApplicationHandlers", but the + * former is always included in the latter list when reloading. + */ +add_task( + async function test_store_possibleApplicationHandlers_includes_preferred() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = localHandlerApp; + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/new" + ); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedLocalHandlerApp, + possibleApplicationHandlers: [expectedLocalHandlerApp], + }); + } +); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with a + * "preferredApplicationHandler" that is not the first element in + * "possibleApplicationHandlers", but the former is always included as the first + * element of the latter list when reloading. + */ +add_task( + async function test_store_possibleApplicationHandlers_preferred_first() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = webHandlerApp; + // The preferred handler is appended after the other one. + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/new" + ); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedWebHandlerApp, + possibleApplicationHandlers: [ + expectedWebHandlerApp, + expectedLocalHandlerApp, + ], + }); + } +); + +/** + * Tests that it is possible to save an nsIHandlerInfo instance with an + * uppercase file extension, but it is converted to lowercase when reloading. + */ +add_task(async function test_store_fileExtensions_lowercase() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("EXTENSION_test2"); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + fileExtensions: ["extension_test1", "extension_test2"], + }); +}); + +/** + * Tests that appendExtension doesn't add duplicates, and that anyway duplicates + * from possibleApplicationHandlers are removed when saving and reloading. + */ +add_task(async function test_store_no_duplicates() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = webHandlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(localHandlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("extension_test2"); + handlerInfo.appendExtension("extension_test1"); + handlerInfo.appendExtension("EXTENSION_test1"); + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), [ + "extension_test1", + "extension_test2", + ]); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedWebHandlerApp, + possibleApplicationHandlers: [ + expectedWebHandlerApp, + expectedLocalHandlerApp, + ], + fileExtensions: ["extension_test1", "extension_test2"], + }); +}); + +/** + * Tests that setFileExtensions doesn't add duplicates. + */ +add_task(async function test_setFileExtensions_no_duplicates() { + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.setFileExtensions("a,b,A,b,c,a"); + let expected = ["a", "b", "c"]; + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected); + // Test empty extensions, also at begin and end. + handlerInfo.setFileExtensions(",a,,b,A,c,"); + Assert.deepEqual(Array.from(handlerInfo.getFileExtensions()), expected); +}); + +/** + * Tests that "store" deletes properties that have their default values from + * the data store. + * + * File extensions are never deleted once they have been associated. + */ +add_task(async function test_store_deletes_properties_except_extensions() { + await deleteHandlerStore(); + + // Prepare an nsIHandlerInfo instance with all the properties set to values + // that will result in deletions. The preferredAction is also set to a defined + // value so we can more easily verify it later. + let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo( + "example/type.savetodisk" + ); + handlerInfo.preferredAction = Ci.nsIHandlerInfo.saveToDisk; + handlerInfo.alwaysAskBeforeHandling = false; + + // All the properties for "example/type.savetodisk" are present in the test + // data, so we load the data before overwriting their values. + await copyTestDataToHandlerStore(); + gHandlerService.store(handlerInfo); + + // Now we can reload the data and verify that no extra values have been kept. + await unloadHandlerStore(); + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/type.savetodisk" + ); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/type.savetodisk", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + fileExtensions: ["example_two", "example_three"], + }); +}); + +/** + * Tests the "overrideType" argument of "fillHandlerInfo". + */ +add_task(async function test_fillHandlerInfo_overrideType() { + // Test both MIME types and protocols. + for (let type of [ + "example/type.usesystemdefault", + "examplescheme.usesystemdefault", + ]) { + await deleteHandlerStore(); + + // Create new nsIHandlerInfo instances before loading the test data. + let handlerInfoAbsent = HandlerServiceTestUtils.getHandlerInfo(type + "2"); + + // Fill the nsIHandlerInfo instance using the type that actually exists. + await copyTestDataToHandlerStore(); + gHandlerService.fillHandlerInfo(handlerInfoAbsent, type); + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfoAbsent, { + // While the data is populated from another type, the type is unchanged. + type: type + "2", + preferredAction: Ci.nsIHandlerInfo.useSystemDefault, + alwaysAskBeforeHandling: false, + possibleApplicationHandlers: [ + { + name: "Example Possible Handler", + uriTemplate: "http://www.example.com/?url=%s", + }, + ], + }); + } +}); + +/** + * Tests "getTypeFromExtension" including unknown extensions. + */ +add_task(async function test_getTypeFromExtension() { + await copyTestDataToHandlerStore(); + + Assert.equal(gHandlerService.getTypeFromExtension(""), ""); + Assert.equal(gHandlerService.getTypeFromExtension("example_unknown"), ""); + Assert.equal( + gHandlerService.getTypeFromExtension("example_one"), + "example/type.handleinternally" + ); + Assert.equal( + gHandlerService.getTypeFromExtension("EXAMPLE_one"), + "example/type.handleinternally" + ); +}); + +/** + * Checks that the information stored in the handler service instance under + * testing matches the default handlers for the English locale. + */ +function assertAllHandlerInfosMatchDefaultHandlers() { + let handlerInfos = HandlerServiceTestUtils.getAllHandlerInfos(); + + for (let type of ["irc", "ircs"]) { + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type, + preferredActionOSDependent: true, + possibleApplicationHandlers: [ + { + name: "Mibbit", + uriTemplate: "https://www.mibbit.com/?url=%s", + }, + ], + }); + } + + HandlerServiceTestUtils.assertHandlerInfoMatches(handlerInfos.shift(), { + type: "mailto", + preferredActionOSDependent: true, + possibleApplicationHandlers: [ + { + name: "Yahoo! Mail", + uriTemplate: "https://compose.mail.yahoo.com/?To=%s", + }, + { + name: "Gmail", + uriTemplate: "https://mail.google.com/mail/?extsrc=mailto&url=%s", + }, + ], + }); + + Assert.equal(handlerInfos.length, 0); +} + +/** + * Tests the default protocol handlers imported from the locale-specific data. + */ +add_task(async function test_default_protocol_handlers() { + if ( + !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion") + ) { + info("This platform or locale does not have default handlers."); + return; + } + + // This will inject the default protocol handlers for the current locale. + await deleteHandlerStore(); + + await assertAllHandlerInfosMatchDefaultHandlers(); +}); + +/** + * Tests that the default protocol handlers are not imported again from the + * locale-specific data if they already exist. + */ +add_task(async function test_default_protocol_handlers_no_duplicates() { + if ( + !Services.prefs.getPrefType("gecko.handlerService.defaultHandlersVersion") + ) { + info("This platform or locale does not have default handlers."); + return; + } + + // This will inject the default protocol handlers for the current locale. + await deleteHandlerStore(); + + // Remove the "irc" handler so we can verify that the injection is repeated. + let ircHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("irc"); + gHandlerService.remove(ircHandlerInfo); + + let originalDefaultHandlersVersion = Services.prefs.getComplexValue( + "gecko.handlerService.defaultHandlersVersion", + Ci.nsIPrefLocalizedString + ); + + // Set the preference to an arbitrarily high number to force injecting again. + Services.prefs.setStringPref( + "gecko.handlerService.defaultHandlersVersion", + "999" + ); + + await unloadHandlerStore(); + + // Check that "irc" exists to make sure that the injection was repeated. + Assert.ok(gHandlerService.exists(ircHandlerInfo)); + + // There should be no duplicate handlers in the protocols. + await assertAllHandlerInfosMatchDefaultHandlers(); + + Services.prefs.setStringPref( + "gecko.handlerService.defaultHandlersVersion", + originalDefaultHandlersVersion + ); +}); + +/** + * Ensures forward compatibility by checking that the "store" method preserves + * unknown properties in the test data. + */ +add_task(async function test_store_keeps_unknown_properties() { + // Create a new nsIHandlerInfo instance before loading the test data. + await deleteHandlerStore(); + let handlerInfo = HandlerServiceTestUtils.getHandlerInfo( + "example/type.handleinternally" + ); + + await copyTestDataToHandlerStore(); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + let data = JSON.parse(new TextDecoder().decode(await OS.File.read(jsonPath))); + Assert.equal( + data.mimeTypes["example/type.handleinternally"].unknownProperty, + "preserved" + ); +}); + +/** + * Runs the asyncInit method, ensuring that it successfully inits the store + * and calls the handlersvc-store-initialized topic. + */ +add_task(async function test_async_init() { + await deleteHandlerStore(); + await copyTestDataToHandlerStore(); + gHandlerService.asyncInit(); + await TestUtils.topicObserved("handlersvc-store-initialized"); + await assertAllHandlerInfosMatchTestData(); + + await unloadHandlerStore(); +}); + +/** + * Races the asyncInit method against the sync init (implicit in enumerate), + * to ensure that the store will be synchronously initialized without any + * ill effects. + */ +add_task(async function test_race_async_init() { + await deleteHandlerStore(); + await copyTestDataToHandlerStore(); + let storeInitialized = false; + // Pass a callback to synchronously observe the topic, as a promise would + // resolve asynchronously + TestUtils.topicObserved("handlersvc-store-initialized", () => { + storeInitialized = true; + return true; + }); + gHandlerService.asyncInit(); + Assert.ok(!storeInitialized); + gHandlerService.enumerate(); + Assert.ok(storeInitialized); + await assertAllHandlerInfosMatchTestData(); + + await unloadHandlerStore(); +}); + +/** + * Test saving and reloading an instance of nsIGIOMimeApp. + */ +add_task(async function test_store_gioHandlerApp() { + if (!("@mozilla.org/gio-service;1" in Cc)) { + info("Skipping test because it does not apply to this platform."); + return; + } + + // Create dummy exec file that following won't fail because file not found error + let dummyHandlerFile = FileUtils.getFile("TmpD", ["dummyHandler"]); + dummyHandlerFile.createUnique( + Ci.nsIFile.NORMAL_FILE_TYPE, + parseInt("777", 8) + ); + + // Set up an nsIGIOMimeApp instance for testing. + let handlerApp = Cc["@mozilla.org/gio-service;1"] + .getService(Ci.nsIGIOService) + .createAppFromCommand(dummyHandlerFile.path, "Dummy GIO handler"); + let expectedGIOMimeHandlerApp = { + name: handlerApp.name, + command: handlerApp.command, + }; + + await deleteHandlerStore(); + + let handlerInfo = getKnownHandlerInfo("example/new"); + handlerInfo.preferredApplicationHandler = handlerApp; + handlerInfo.possibleApplicationHandlers.appendElement(handlerApp); + handlerInfo.possibleApplicationHandlers.appendElement(webHandlerApp); + gHandlerService.store(handlerInfo); + + await unloadHandlerStore(); + + let actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: expectedGIOMimeHandlerApp, + possibleApplicationHandlers: [expectedGIOMimeHandlerApp, webHandlerApp], + }); + + await OS.File.remove(dummyHandlerFile.path); + + // After removing dummyHandlerFile, the handler should disappear from the + // list of possibleApplicationHandlers and preferredAppHandler should be null. + actualHandlerInfo = HandlerServiceTestUtils.getHandlerInfo("example/new"); + HandlerServiceTestUtils.assertHandlerInfoMatches(actualHandlerInfo, { + type: "example/new", + preferredAction: Ci.nsIHandlerInfo.saveToDisk, + alwaysAskBeforeHandling: false, + preferredApplicationHandler: null, + possibleApplicationHandlers: [webHandlerApp], + }); +}); diff --git a/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js b/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js new file mode 100644 index 0000000000..161cce8d33 --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_protocol_ask_dialog_telemetry.js @@ -0,0 +1,117 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { ContentDispatchChooserTelemetry } = ChromeUtils.import( + "resource://gre/modules/ContentDispatchChooser.jsm" +); + +let telemetryLabels = Services.telemetry.getCategoricalLabels() + .EXTERNAL_PROTOCOL_HANDLER_DIALOG_CONTEXT_SCHEME; + +let schemeToLabel = ContentDispatchChooserTelemetry.SCHEME_TO_LABEL; +let schemePrefixToLabel = + ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL; + +/** + * Test for scheme-label mappings of protocol ask dialog telemetry. + */ +add_task(async function test_telemetry_label_maps() { + let mapValues = Object.values(schemeToLabel).concat( + Object.values(schemePrefixToLabel) + ); + + // Scheme - label maps must have valid label values. + mapValues.forEach(label => { + // Mapped labels must be valid. + Assert.ok(telemetryLabels.includes(label), `Exists label: ${label}`); + }); + + // Uppercase labels must have a mapping. + telemetryLabels.forEach(label => { + Assert.equal( + label == "OTHER" || mapValues.includes(label), + label == label.toUpperCase(), + `Exists label: ${label}` + ); + }); + + Object.keys(schemeToLabel).forEach(key => { + // Schemes which have a mapping must not exist as as label. + Assert.ok(!telemetryLabels.includes(key), `Not exists label: ${key}`); + + // There must be no key duplicates across the two maps. + Assert.ok(!schemePrefixToLabel[key], `No duplicate key: ${key}`); + }); +}); + +/** + * Tests the getTelemetryLabel method. + */ +add_task(async function test_telemetry_getTelemetryLabel() { + // Method should return the correct mapping. + Object.keys(schemeToLabel).forEach(scheme => { + Assert.equal( + schemeToLabel[scheme], + ContentDispatchChooserTelemetry._getTelemetryLabel(scheme) + ); + }); + + // Passing null to _getTelemetryLabel should throw. + Assert.throws(() => { + ContentDispatchChooserTelemetry._getTelemetryLabel(null); + }, /Invalid scheme/); + + // Replace maps with test data + ContentDispatchChooserTelemetry.SCHEME_TO_LABEL = { + foo: "FOOLABEL", + bar: "BARLABEL", + }; + + ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL = { + fooPrefix: "FOOPREFIXLABEL", + barPrefix: "BARPREFIXLABEL", + fo: "PREFIXLABEL", + }; + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("foo"), + "FOOLABEL", + "Non prefix mapping should have priority" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("bar"), + "BARLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix"), + "FOOPREFIXLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix1"), + "FOOPREFIXLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("fooPrefix2"), + "FOOPREFIXLABEL", + "Should return the correct label" + ); + + Assert.equal( + ContentDispatchChooserTelemetry._getTelemetryLabel("doesnotexist"), + "OTHER", + "Should return the correct label for unknown scheme" + ); + + // Restore maps + ContentDispatchChooserTelemetry.SCHEME_TO_LABEL = schemeToLabel; + ContentDispatchChooserTelemetry.SCHEME_PREFIX_TO_LABEL = schemePrefixToLabel; +}); diff --git a/uriloader/exthandler/tests/unit/test_punycodeURIs.js b/uriloader/exthandler/tests/unit/test_punycodeURIs.js new file mode 100644 index 0000000000..638128d11b --- /dev/null +++ b/uriloader/exthandler/tests/unit/test_punycodeURIs.js @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Encoded test URI to work on all platforms/independent of file encoding +const kTestURI = "http://\u65e5\u672c\u8a93.jp/"; +const kExpectedURI = "http://xn--wgv71a309e.jp/"; +const kOutputFile = "result.txt"; + +// Try several times in case the box we're running on is slow. +const kMaxCheckExistAttempts = 30; // seconds +var gCheckExistsAttempts = 0; + +const tempDir = do_get_tempdir(); + +function checkFile() { + // This is where we expect the output + var tempFile = tempDir.clone(); + tempFile.append(kOutputFile); + + if (!tempFile.exists()) { + if (gCheckExistsAttempts >= kMaxCheckExistAttempts) { + do_throw( + "Expected File " + + tempFile.path + + " does not exist after " + + kMaxCheckExistAttempts + + " seconds" + ); + } else { + ++gCheckExistsAttempts; + // Wait a bit longer then try again + do_timeout(1000, checkFile); + return; + } + } + + // Now read it + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + fstream.init(tempFile, -1, 0, 0); + sstream.init(fstream); + + // Read the first line only as that's the one we expect WriteArguments + // to be writing the argument to. + var data = sstream.read(4096); + + sstream.close(); + fstream.close(); + + // Now remove the old file + tempFile.remove(false); + + // This currently fails on Mac with an argument like -psn_0_nnnnnn + // This seems to be to do with how the executable is called, but I couldn't + // find a way around it. + // Additionally the lack of OS detection in xpcshell tests sucks, so we'll + // have to check for the argument mac gives us. + if (data.substring(0, 7) != "-psn_0_") { + Assert.equal(data, kExpectedURI); + } + + do_test_finished(); +} + +function run_test() { + if (mozinfo.os == "mac") { + dump("INFO | test_punycodeURIs.js | Skipping test on mac, bug 599475"); + return; + } + + // set up the uri to test with + var ioService = Services.io; + + // set up the local handler object + var localHandler = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandler.name = "Test Local Handler App"; + + // WriteArgument will just dump its arguments to a file for us. + var processDir = do_get_cwd(); + var exe = processDir.clone(); + exe.append("WriteArgument"); + + if (!exe.exists()) { + // Maybe we are on windows + exe.leafName = "WriteArgument.exe"; + if (!exe.exists()) { + do_throw("Could not locate the WriteArgument tests executable\n"); + } + } + + var outFile = tempDir.clone(); + outFile.append(kOutputFile); + + // Set an environment variable for WriteArgument to pick up + var envSvc = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + + // The Write Argument file needs to know where its libraries are, so + // just force the path variable + // For mac + var greDir = Services.dirsvc.get("GreD", Ci.nsIFile); + + envSvc.set("DYLD_LIBRARY_PATH", greDir.path); + // For Linux + envSvc.set("LD_LIBRARY_PATH", greDir.path); + // XXX: handle windows + + // Now tell it where we want the file. + envSvc.set("WRITE_ARGUMENT_FILE", outFile.path); + + var uri = ioService.newURI(kTestURI); + + // Just check we've got these matching, if we haven't there's a problem + // with ascii spec or our test case. + Assert.equal(uri.asciiSpec, kExpectedURI); + + localHandler.executable = exe; + localHandler.launchWithURI(uri); + + do_test_pending(); + do_timeout(1000, checkFile); +} diff --git a/uriloader/exthandler/tests/unit/xpcshell.ini b/uriloader/exthandler/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..d7bf9e54d6 --- /dev/null +++ b/uriloader/exthandler/tests/unit/xpcshell.ini @@ -0,0 +1,28 @@ +[DEFAULT] +head = head.js +run-sequentially = Bug 912235 - Intermittent failures +firefox-appdir = browser + +[test_defaults_handlerService.js] +# No default stored handlers on android given lack of support. +# No default stored handlers on Thunderbird. +skip-if = os == "android" || appname == "thunderbird" +[test_getMIMEInfo_pdf.js] +[test_getMIMEInfo_unknown_mime_type.js] +run-if = os == "win" # Windows only test +[test_getTypeFromExtension_ext_to_type_mapping.js] +[test_getTypeFromExtension_with_empty_Content_Type.js] +run-if = os == "win" # Windows only test +[test_badMIMEType.js] +[test_handlerService.js] +skip-if = (verify && (os == 'win')) +support-files = mailcap +# Bug 676997: test consistently fails on Android +fail-if = os == "android" +[test_handlerService_store.js] +# Disabled for 1563343 -- the app should determine possible handlers in GV. +fail-if = os == "android" +support-files = handlers.json +[test_punycodeURIs.js] +[test_protocol_ask_dialog_telemetry.js] +skip-if = os == "android" # Desktop telemetry diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h new file mode 100644 index 0000000000..35672ed20c --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nslocalhandlerappuikit_h_ +#define nslocalhandlerappuikit_h_ + +#include "nsLocalHandlerApp.h" + +class nsLocalHandlerAppUIKit final : public nsLocalHandlerApp { + public: + nsLocalHandlerAppUIKit() {} + ~nsLocalHandlerAppUIKit() {} + + nsLocalHandlerAppUIKit(const char16_t* aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + nsLocalHandlerAppUIKit(const nsAString& aName, nsIFile* aExecutable) + : nsLocalHandlerApp(aName, aExecutable) {} + + NS_IMETHOD LaunchWithURI(nsIURI* aURI, + BrowsingContext* aBrowsingContext) override; +}; + +#endif /* nslocalhandlerappuikit_h_ */ diff --git a/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm new file mode 100644 index 0000000000..b9fe2766ba --- /dev/null +++ b/uriloader/exthandler/uikit/nsLocalHandlerAppUIKit.mm @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import <CoreFoundation/CoreFoundation.h> + +#include "nsLocalHandlerAppUIKit.h" +#include "nsIURI.h" + +NS_IMETHODIMP +nsLocalHandlerAppUIKit::LaunchWithURI(nsIURI* aURI, nsIInterfaceRequestor* aWindowContext) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h new file mode 100644 index 0000000000..8638b94f75 --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.h @@ -0,0 +1,31 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUIKit_h_ +#define nsMIMEInfoUIKit_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUIKit final : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoUIKit(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUIKit(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + + protected: + virtual nsresult LoadUriInternal(nsIURI* aURI); +#ifdef DEBUG + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) { + MOZ_ASSERT_UNREACHABLE("do not call this method, use LaunchWithFile"); + return NS_ERROR_UNEXPECTED; + } +#endif +}; + +#endif diff --git a/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm new file mode 100644 index 0000000000..2ed0c1eb2e --- /dev/null +++ b/uriloader/exthandler/uikit/nsMIMEInfoUIKit.mm @@ -0,0 +1,12 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUIKit.h" + +NS_IMETHODIMP +nsMIMEInfoUIKit::LaunchWithFile(nsIFile* aFile) { return NS_ERROR_NOT_IMPLEMENTED; } + +nsresult nsMIMEInfoUIKit::LoadUriInternal(nsIURI* aURI) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.h b/uriloader/exthandler/uikit/nsOSHelperAppService.h new file mode 100644 index 0000000000..8c1b1fb6b3 --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and +// is implemented on each platform. It contains platform specific code for +// finding helper applications for a given mime type in addition to launching +// those applications. This is the UIKit version. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsOSHelperAppService final : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval); + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval); + + // method overrides --> used to hook the mime service into internet config.... + NS_IMETHOD GetFromTypeAndExtension(const nsACString& aType, + const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo); + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval); + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile); + + nsresult OSProtocolHandlerExists(const char* aScheme, + bool* aHandlerExists) override; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/uikit/nsOSHelperAppService.mm b/uriloader/exthandler/uikit/nsOSHelperAppService.mm new file mode 100644 index 0000000000..c0eb14d28d --- /dev/null +++ b/uriloader/exthandler/uikit/nsOSHelperAppService.mm @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:expandtab:shiftwidth=2:tabstop=2:cin: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOSHelperAppService.h" + +nsOSHelperAppService::nsOSHelperAppService() : nsExternalHelperAppService() {} + +nsOSHelperAppService::~nsOSHelperAppService() {} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) { + *aHandlerExists = false; + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetApplicationDescription(const nsACString& aScheme, nsAString& _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, bool* _retval) { + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath(const char16_t* aPlatformAppPath, + nsIFile** aFile) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetFromTypeAndExtension(const nsACString& aType, const nsACString& aFileExt, + nsIMIMEInfo** aMIMEInfo) { + return nsExternalHelperAppService::GetFromTypeAndExtension(aType, aFileExt, aMIMEInfo); +} + +NS_IMETHODIMP nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aMIMEInfo = nullptr; + *aFound = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const char* aScheme, bool* found, + nsIHandlerInfo** _retval) { + *found = false; + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.cpp b/uriloader/exthandler/unix/nsGNOMERegistry.cpp new file mode 100644 index 0000000000..6136709d91 --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsGNOMERegistry.h" +#include "nsString.h" +#include "nsMIMEInfoUnix.h" +#include "nsIGIOService.h" + +/* static */ +bool nsGNOMERegistry::HandlerExists(const char* aProtocolScheme) { + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return false; + } + + nsCOMPtr<nsIHandlerApp> app; + return NS_SUCCEEDED(giovfs->GetAppForURIScheme( + nsDependentCString(aProtocolScheme), getter_AddRefs(app))); +} + +// XXX Check HandlerExists() before calling LoadURL. + +/* static */ +nsresult nsGNOMERegistry::LoadURL(nsIURI* aURL) { + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return NS_ERROR_FAILURE; + } + + return giovfs->ShowURI(aURL); +} + +/* static */ +void nsGNOMERegistry::GetAppDescForScheme(const nsACString& aScheme, + nsAString& aDesc) { + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) return; + + nsCOMPtr<nsIHandlerApp> app; + if (NS_FAILED(giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(app)))) + return; + + app->GetName(aDesc); +} + +/* static */ +already_AddRefed<nsMIMEInfoBase> nsGNOMERegistry::GetFromExtension( + const nsACString& aFileExt) { + nsAutoCString mimeType; + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + // Get the MIME type from the extension, then call GetFromType to + // fill in the MIMEInfo. + if (NS_FAILED(giovfs->GetMimeTypeFromExtension(aFileExt, mimeType)) || + mimeType.EqualsLiteral("application/octet-stream")) { + return nullptr; + } + + RefPtr<nsMIMEInfoBase> mi = GetFromType(mimeType); + if (mi) { + mi->AppendExtension(aFileExt); + } + + return mi.forget(); +} + +/* static */ +already_AddRefed<nsMIMEInfoBase> nsGNOMERegistry::GetFromType( + const nsACString& aMIMEType) { + RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(aMIMEType); + NS_ENSURE_TRUE(mimeInfo, nullptr); + + nsAutoString name; + nsAutoCString description; + + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return nullptr; + } + + nsCOMPtr<nsIHandlerApp> handlerApp; + if (NS_FAILED( + giovfs->GetAppForMimeType(aMIMEType, getter_AddRefs(handlerApp))) || + !handlerApp) { + return nullptr; + } + handlerApp->GetName(name); + giovfs->GetDescriptionForMimeType(aMIMEType, description); + + mimeInfo->SetDefaultDescription(name); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + mimeInfo->SetDescription(NS_ConvertUTF8toUTF16(description)); + + return mimeInfo.forget(); +} diff --git a/uriloader/exthandler/unix/nsGNOMERegistry.h b/uriloader/exthandler/unix/nsGNOMERegistry.h new file mode 100644 index 0000000000..ea626c5b50 --- /dev/null +++ b/uriloader/exthandler/unix/nsGNOMERegistry.h @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsGNOMERegistry_h +#define nsGNOMERegistry_h + +#include "nsIURI.h" +#include "nsCOMPtr.h" + +class nsMIMEInfoBase; + +class nsGNOMERegistry { + public: + static bool HandlerExists(const char* aProtocolScheme); + + static nsresult LoadURL(nsIURI* aURL); + + static void GetAppDescForScheme(const nsACString& aScheme, nsAString& aDesc); + + static already_AddRefed<nsMIMEInfoBase> GetFromExtension( + const nsACString& aFileExt); + + static already_AddRefed<nsMIMEInfoBase> GetFromType( + const nsACString& aMIMEType); +}; + +#endif // nsGNOMERegistry_h diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp new file mode 100644 index 0000000000..7cbefcce3e --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.cpp @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMIMEInfoUnix.h" +#include "nsGNOMERegistry.h" +#include "nsIGIOService.h" +#include "nsNetCID.h" +#include "nsIIOService.h" +#ifdef MOZ_ENABLE_DBUS +# include "nsDBusHandlerApp.h" +#endif + +nsresult nsMIMEInfoUnix::LoadUriInternal(nsIURI* aURI) { + return nsGNOMERegistry::LoadURL(aURI); +} + +NS_IMETHODIMP +nsMIMEInfoUnix::GetHasDefaultHandler(bool* _retval) { + // if mDefaultApplication is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (mDefaultApplication) return nsMIMEInfoImpl::GetHasDefaultHandler(_retval); + + *_retval = false; + + if (mClass == eProtocolInfo) { + *_retval = nsGNOMERegistry::HandlerExists(mSchemeOrType.get()); + } else { + RefPtr<nsMIMEInfoBase> mimeInfo = + nsGNOMERegistry::GetFromType(mSchemeOrType); + if (!mimeInfo) { + nsAutoCString ext; + nsresult rv = GetPrimaryExtension(ext); + if (NS_SUCCEEDED(rv)) { + mimeInfo = nsGNOMERegistry::GetFromExtension(ext); + } + } + if (mimeInfo) *_retval = true; + } + + if (*_retval) return NS_OK; + + return NS_OK; +} + +nsresult nsMIMEInfoUnix::LaunchDefaultWithFile(nsIFile* aFile) { + // if mDefaultApplication is set, it means the application has been set from + // either /etc/mailcap or ${HOME}/.mailcap, in which case we don't want to + // give the GNOME answer. + if (mDefaultApplication) return nsMIMEInfoImpl::LaunchDefaultWithFile(aFile); + + nsAutoCString nativePath; + aFile->GetNativePath(nativePath); + + nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID); + if (!giovfs) { + return NS_ERROR_FAILURE; + } + + // nsGIOMimeApp->Launch wants a URI string instead of local file + nsresult rv; + nsCOMPtr<nsIIOService> ioservice = + do_GetService(NS_IOSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> uri; + rv = ioservice->NewFileURI(aFile, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHandlerApp> app; + if (NS_FAILED( + giovfs->GetAppForMimeType(mSchemeOrType, getter_AddRefs(app))) || + !app) { + return NS_ERROR_FILE_NOT_FOUND; + } + + return app->LaunchWithURI(uri, nullptr); +} diff --git a/uriloader/exthandler/unix/nsMIMEInfoUnix.h b/uriloader/exthandler/unix/nsMIMEInfoUnix.h new file mode 100644 index 0000000000..2e32be4915 --- /dev/null +++ b/uriloader/exthandler/unix/nsMIMEInfoUnix.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoUnix_h_ +#define nsMIMEInfoUnix_h_ + +#include "nsMIMEInfoImpl.h" + +class nsMIMEInfoUnix : public nsMIMEInfoImpl { + public: + explicit nsMIMEInfoUnix(const char* aMIMEType = "") + : nsMIMEInfoImpl(aMIMEType) {} + explicit nsMIMEInfoUnix(const nsACString& aMIMEType) + : nsMIMEInfoImpl(aMIMEType) {} + nsMIMEInfoUnix(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoImpl(aType, aClass) {} + static bool HandlerExists(const char* aProtocolScheme); + + protected: + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + + virtual nsresult LoadUriInternal(nsIURI* aURI) override; + + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile) override; +}; + +#endif // nsMIMEInfoUnix_h_ diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.cpp b/uriloader/exthandler/unix/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..8ffa98c40b --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.cpp @@ -0,0 +1,1409 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <sys/types.h> +#include <sys/stat.h> + +#include "nsOSHelperAppService.h" +#include "nsMIMEInfoUnix.h" +#ifdef MOZ_WIDGET_GTK +# include "nsGNOMERegistry.h" +#endif +#include "nsISupports.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsIFileStreams.h" +#include "nsILineInputStream.h" +#include "nsIFile.h" +#include "nsIProcess.h" +#include "nsNetCID.h" +#include "nsXPCOM.h" +#include "nsComponentManagerUtils.h" +#include "nsCRT.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsXULAppAPI.h" +#include "ContentHandlerService.h" +#include "prenv.h" // for PR_GetEnv() +#include "mozilla/Preferences.h" +#include "nsMimeTypes.h" + +using namespace mozilla; + +#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args) +#define LOG_ENABLED() MOZ_LOG_TEST(mLog, mozilla::LogLevel::Debug) + +static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter); +static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter); + +inline bool IsNetscapeFormat(const nsACString& aBuffer); + +nsOSHelperAppService::~nsOSHelperAppService() {} + +/* + * Take a command with all the mailcap escapes in it and unescape it + * Ideally this needs the mime type, mime type options, and location of the + * temporary file, but this last can't be got from here + */ +// static +nsresult nsOSHelperAppService::UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand) { + LOG(("-- UnescapeCommand")); + LOG(("Command to escape: '%s'\n", + NS_LossyConvertUTF16toASCII(aEscapedCommand).get())); + // XXX This function will need to get the mime type and various stuff like + // that being passed in to work properly + + LOG( + ("UnescapeCommand really needs some work -- it should actually do some " + "unescaping\n")); + + CopyUTF16toUTF8(aEscapedCommand, aUnEscapedCommand); + LOG(("Escaped command: '%s'\n", PromiseFlatCString(aUnEscapedCommand).get())); + return NS_OK; +} + +/* Put aSemicolon_iter at the first non-escaped semicolon after + * aStart_iter but before aEnd_iter + */ + +static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter, + const nsAString::const_iterator& aEnd_iter) { + bool semicolonFound = false; + while (aSemicolon_iter != aEnd_iter && !semicolonFound) { + switch (*aSemicolon_iter) { + case '\\': + aSemicolon_iter.advance(2); + break; + case ';': + semicolonFound = true; + break; + default: + ++aSemicolon_iter; + break; + } + } + return NS_OK; +} + +static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter, + nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, + const nsAString::const_iterator& aEnd_iter) { + nsAString::const_iterator iter(aStart_iter); + + // skip leading whitespace + while (iter != aEnd_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeStart = iter; + + // find major/minor separator ('/') + while (iter != aEnd_iter && *iter != '/') { + ++iter; + } + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMajorTypeEnd = iter; + + // skip '/' + ++iter; + + if (iter == aEnd_iter) { + return NS_ERROR_INVALID_ARG; + } + + aMinorTypeStart = iter; + + // find end of minor type, delimited by whitespace or ';' + while (iter != aEnd_iter && !nsCRT::IsAsciiSpace(*iter) && *iter != ';') { + ++iter; + } + + aMinorTypeEnd = iter; + + return NS_OK; +} + +// static +nsresult nsOSHelperAppService::GetFileLocation(const char* aPrefName, + const char* aEnvVarName, + nsAString& aFileLocation) { + LOG(("-- GetFileLocation. Pref: '%s' EnvVar: '%s'\n", aPrefName, + aEnvVarName)); + MOZ_ASSERT(aPrefName, "Null pref name passed; don't do that!"); + + aFileLocation.Truncate(); + /* The lookup order is: + 1) user pref + 2) env var + 3) pref + */ + NS_ENSURE_TRUE(Preferences::GetRootBranch(), NS_ERROR_FAILURE); + + /* + If we have an env var we should check whether the pref is a user + pref. If we do not, we don't care. + */ + if (Preferences::HasUserValue(aPrefName) && + NS_SUCCEEDED(Preferences::GetString(aPrefName, aFileLocation))) { + return NS_OK; + } + + if (aEnvVarName && *aEnvVarName) { + char* prefValue = PR_GetEnv(aEnvVarName); + if (prefValue && *prefValue) { + // the pref is in the system charset and it's a filepath... The + // natural way to do the charset conversion is by just initing + // an nsIFile with the native path and asking it for the Unicode + // version. + nsresult rv; + nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->InitWithNativePath(nsDependentCString(prefValue)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->GetPath(aFileLocation); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + } + + return Preferences::GetString(aPrefName, aFileLocation); +} + +/* Get the mime.types file names from prefs and look up info in them + based on extension */ +// static +nsresult nsOSHelperAppService::LookUpTypeAndDescription( + const nsAString& aFileExtension, nsAString& aMajorType, + nsAString& aMinorType, nsAString& aDescription, bool aUserData) { + LOG(("-- LookUpTypeAndDescription for extension '%s'\n", + NS_LossyConvertUTF16toASCII(aFileExtension).get())); + nsAutoString mimeFileName; + + const char* filenamePref = aUserData ? "helpers.private_mime_types_file" + : "helpers.global_mime_types_file"; + + nsresult rv = GetFileLocation(filenamePref, nullptr, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetTypeAndDescriptionFromMimetypesFile( + mimeFileName, aFileExtension, aMajorType, aMinorType, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +inline bool IsNetscapeFormat(const nsACString& aBuffer) { + return StringBeginsWith( + aBuffer, + nsLiteralCString( + "#--Netscape Communications Corporation MIME Information")) || + StringBeginsWith(aBuffer, "#--MCOM MIME Information"_ns); +} + +/* + * Create a file stream and line input stream for the filename. + * Leaves the first line of the file in aBuffer and sets the format to + * true for netscape files and false for normail ones + */ +// static +nsresult nsOSHelperAppService::CreateInputStream( + const nsAString& aFilename, nsIFileInputStream** aFileInputStream, + nsILineInputStream** aLineInputStream, nsACString& aBuffer, + bool* aNetscapeFormat, bool* aMore) { + LOG(("-- CreateInputStream")); + nsresult rv = NS_OK; + + nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = file->InitWithPath(aFilename); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFileInputStream> fileStream( + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = fileStream->Init(file, -1, -1, false); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsILineInputStream> lineStream(do_QueryInterface(fileStream, &rv)); + + if (NS_FAILED(rv)) { + LOG(("Interface trouble in stream land!")); + return rv; + } + + rv = lineStream->ReadLine(aBuffer, aMore); + if (NS_FAILED(rv)) { + fileStream->Close(); + return rv; + } + + *aNetscapeFormat = IsNetscapeFormat(aBuffer); + + *aFileInputStream = fileStream; + NS_ADDREF(*aFileInputStream); + *aLineInputStream = lineStream; + NS_ADDREF(*aLineInputStream); + + return NS_OK; +} + +/* Open the file, read the first line, decide what type of file it is, + then get info based on extension */ +// static +nsresult nsOSHelperAppService::GetTypeAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aFileExtension, + nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription) { + LOG(("-- GetTypeAndDescriptionFromMimetypesFile\n")); + LOG(("Getting type and description from types file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using extension '%s'\n", + NS_LossyConvertUTF16toASCII(aFileExtension).get())); + nsCOMPtr<nsIFileInputStream> mimeFile; + nsCOMPtr<nsILineInputStream> mimeTypes; + bool netscapeFormat; + nsAutoString buf; + nsAutoCString cBuf; + bool more = false; + nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), + getter_AddRefs(mimeTypes), cBuf, + &netscapeFormat, &more); + + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsAutoStringN<101> entry; + nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG(("Bogus entry; trying 'normal' mode\n")); + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG(("Bogus entry; trying 'Netscape' mode\n")); + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv)) { // entry parses + nsAString::const_iterator start, end; + extensions.BeginReading(start); + extensions.EndReading(end); + nsAString::const_iterator iter(start); + + while (start != end) { + FindCharInReadable(',', iter, end); + if (Substring(start, iter) + .Equals(aFileExtension, + nsCaseInsensitiveStringComparator)) { + // it's a match. Assign the type and description and run + aMajorType.Assign(Substring(majorTypeStart, majorTypeEnd)); + aMinorType.Assign(Substring(minorTypeStart, minorTypeEnd)); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } + if (iter != end) { + ++iter; + } + start = iter; + } + } else { + LOG(("Failed to parse entry: %s\n", + NS_LossyConvertUTF16toASCII(entry).get())); + } + // truncate the entry for the next iteration + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* Get the mime.types file names from prefs and look up info in them + based on mimetype */ +// static +nsresult nsOSHelperAppService::LookUpExtensionsAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aFileExtensions, nsAString& aDescription) { + LOG(("-- LookUpExtensionsAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsAutoString mimeFileName; + + nsresult rv = + GetFileLocation("helpers.private_mime_types_file", nullptr, mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile( + mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + if (NS_FAILED(rv) || aFileExtensions.IsEmpty()) { + rv = GetFileLocation("helpers.global_mime_types_file", nullptr, + mimeFileName); + if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) { + rv = GetExtensionsAndDescriptionFromMimetypesFile( + mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + } + return rv; +} + +/* Open the file, read the first line, decide what type of file it is, + then get info based on extension */ +// static +nsresult nsOSHelperAppService::GetExtensionsAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aFileExtensions, + nsAString& aDescription) { + LOG(("-- GetExtensionsAndDescriptionFromMimetypesFile\n")); + LOG(("Getting extensions and description from types file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsCOMPtr<nsIFileInputStream> mimeFile; + nsCOMPtr<nsILineInputStream> mimeTypes; + bool netscapeFormat; + nsAutoCString cBuf; + nsAutoString buf; + bool more = false; + nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile), + getter_AddRefs(mimeTypes), cBuf, + &netscapeFormat, &more); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoString extensions; + nsAutoStringN<101> entry; + nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, descriptionStart, descriptionEnd; + + do { + CopyASCIItoUTF16(cBuf, buf); + // read through, building up an entry. If we finish an entry, check for + // a match and return out of the loop if we match + + // skip comments and empty lines + if (!buf.IsEmpty() && buf.First() != '#') { + entry.Append(buf); + if (entry.Last() == '\\') { + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + if (netscapeFormat) { + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like RealPlayer appending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 106381. + LOG(("Bogus entry; trying 'normal' mode\n")); + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } else { + rv = ParseNormalMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd, + extensions, descriptionStart, descriptionEnd); + + if (NS_FAILED(rv)) { + // We sometimes get things like StarOffice prepending + // "normal" entries to "Netscape" .mime.types files. Try + // to handle that. Bug 136670. + LOG(("Bogus entry; trying 'Netscape' mode\n")); + rv = ParseNetscapeMIMETypesEntry( + entry, majorTypeStart, majorTypeEnd, minorTypeStart, + minorTypeEnd, extensions, descriptionStart, descriptionEnd); + } + } + + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, majorTypeEnd) + .Equals(aMajorType, nsCaseInsensitiveStringComparator) && + Substring(minorTypeStart, minorTypeEnd) + .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { + // it's a match + aFileExtensions.Assign(extensions); + aDescription.Assign(Substring(descriptionStart, descriptionEnd)); + mimeFile->Close(); + return NS_OK; + } + if (NS_FAILED(rv)) { + LOG(("Failed to parse entry: %s\n", + NS_LossyConvertUTF16toASCII(entry).get())); + } + + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + // read the next line + rv = mimeTypes->ReadLine(cBuf, &more); + } while (NS_SUCCEEDED(rv)); + + mimeFile->Close(); + return rv; +} + +/* + * This parses a Netscape format mime.types entry. There are two + * possible formats: + * + * type=foo/bar; options exts="baz" description="Some type" + * + * and + * + * type=foo/bar; options description="Some type" exts="baz" + */ +// static +nsresult nsOSHelperAppService::ParseNetscapeMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG(("-- ParseNetscapeMIMETypesEntry\n")); + NS_ASSERTION(!aEntry.IsEmpty(), + "Empty Netscape MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, match_start, match_end; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); + // if we're pointing to a quote, don't advance -- we don't want to + // include the quote.... + if (*end_iter != '"') ++end_iter; + match_start = start_iter; + match_end = end_iter; + + // Get the major and minor types + // First the major type + if (!FindInReadable(u"type="_ns, match_start, match_end)) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && *match_end != '/') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMajorTypeStart = match_start; + aMajorTypeEnd = match_end; + + // now the minor type + if (++match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + match_start = match_end; + + while (match_end != end_iter && !nsCRT::IsAsciiSpace(*match_end) && + *match_end != ';') { + ++match_end; + } + if (match_end == end_iter) { + return NS_ERROR_FAILURE; + } + + aMinorTypeStart = match_start; + aMinorTypeEnd = match_end; + + // ignore everything up to the end of the mime type from here on + start_iter = match_end; + + // get the extensions + match_start = match_end; + match_end = end_iter; + if (FindInReadable(u"exts="_ns, match_start, match_end)) { + nsAString::const_iterator extStart, extEnd; + + if (match_end == end_iter || + (*match_end == '"' && ++match_end == end_iter)) { + return NS_ERROR_FAILURE; + } + + extStart = match_end; + match_start = extStart; + match_end = end_iter; + if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { + // exts= before desc=, so we have to find the actual end of the extensions + extEnd = match_start; + if (extEnd == extStart) { + return NS_ERROR_FAILURE; + } + + do { + --extEnd; + } while (extEnd != extStart && nsCRT::IsAsciiSpace(*extEnd)); + + if (extEnd != extStart && *extEnd == '"') { + --extEnd; + } + } else { + // desc= before exts=, so we can use end_iter as the end of the extensions + extEnd = end_iter; + } + aExtensions = Substring(extStart, extEnd); + } else { + // no extensions + aExtensions.Truncate(); + } + + // get the description + match_start = start_iter; + match_end = end_iter; + if (FindInReadable(u"desc=\""_ns, match_start, match_end)) { + aDescriptionStart = match_end; + match_start = aDescriptionStart; + match_end = end_iter; + if (FindInReadable(u"exts="_ns, match_start, match_end)) { + // exts= after desc=, so have to find actual end of description + aDescriptionEnd = match_start; + if (aDescriptionEnd == aDescriptionStart) { + return NS_ERROR_FAILURE; + } + + do { + --aDescriptionEnd; + } while (aDescriptionEnd != aDescriptionStart && + nsCRT::IsAsciiSpace(*aDescriptionEnd)); + } else { + // desc= after exts=, so use end_iter for the description end + aDescriptionEnd = end_iter; + } + } else { + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + } + + return NS_OK; +} + +/* + * This parses a normal format mime.types entry. The format is: + * + * major/minor ext1 ext2 ext3 + */ +// static +nsresult nsOSHelperAppService::ParseNormalMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd) { + LOG(("-- ParseNormalMIMETypesEntry\n")); + NS_ASSERTION(!aEntry.IsEmpty(), + "Empty Normal MIME types entry being parsed."); + + nsAString::const_iterator start_iter, end_iter, iter; + + aEntry.BeginReading(start_iter); + aEntry.EndReading(end_iter); + + // no description + aDescriptionStart = start_iter; + aDescriptionEnd = start_iter; + + // skip leading whitespace + while (start_iter != end_iter && nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + if (start_iter == end_iter) { + return NS_ERROR_FAILURE; + } + // skip trailing whitespace + do { + --end_iter; + } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter)); + + ++end_iter; // point to first whitespace char (or to end of string) + iter = start_iter; + + // get the major type + if (!FindCharInReadable('/', iter, end_iter)) return NS_ERROR_FAILURE; + + nsAString::const_iterator equals_sign_iter(start_iter); + if (FindCharInReadable('=', equals_sign_iter, iter)) + return NS_ERROR_FAILURE; // see bug 136670 + + aMajorTypeStart = start_iter; + aMajorTypeEnd = iter; + + // get the minor type + if (++iter == end_iter) { + return NS_ERROR_FAILURE; + } + start_iter = iter; + + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aMinorTypeStart = start_iter; + aMinorTypeEnd = iter; + + // get the extensions + aExtensions.Truncate(); + while (iter != end_iter) { + while (iter != end_iter && nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + + start_iter = iter; + while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) { + ++iter; + } + aExtensions.Append(Substring(start_iter, iter)); + if (iter != end_iter) { // not the last extension + aExtensions.Append(char16_t(',')); + } + } + + return NS_OK; +} + +// static +nsresult nsOSHelperAppService::LookUpHandlerAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags) { + // The mailcap lookup is two-pass to handle the case of mailcap files + // that have something like: + // + // text/*; emacs %s + // text/rtf; soffice %s + // + // in that order. We want to pick up "soffice" for text/rtf in such cases + nsresult rv = DoLookUpHandlerAndDescription( + aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags, true); + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, aMinorType, aHandler, + aDescription, aMozillaFlags, false); + } + + // maybe we have an entry for "aMajorType/*"? + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, + aDescription, aMozillaFlags, true); + } + + if (NS_FAILED(rv)) { + rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler, + aDescription, aMozillaFlags, false); + } + + return rv; +} + +// static +nsresult nsOSHelperAppService::DoLookUpHandlerAndDescription( + const nsAString& aMajorType, const nsAString& aMinorType, + nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags, + bool aUserData) { + LOG(("-- LookUpHandlerAndDescription for type '%s/%s'\n", + NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + nsAutoString mailcapFileName; + + const char* filenamePref = aUserData ? "helpers.private_mailcap_file" + : "helpers.global_mailcap_file"; + const char* filenameEnvVar = aUserData ? "PERSONAL_MAILCAP" : "MAILCAP"; + + nsresult rv = GetFileLocation(filenamePref, filenameEnvVar, mailcapFileName); + if (NS_SUCCEEDED(rv) && !mailcapFileName.IsEmpty()) { + rv = GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, aMajorType, + aMinorType, aHandler, + aDescription, aMozillaFlags); + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + return rv; +} + +// static +nsresult nsOSHelperAppService::GetHandlerAndDescriptionFromMailcapFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, + nsAString& aMozillaFlags) { + LOG(("-- GetHandlerAndDescriptionFromMailcapFile\n")); + LOG(("Getting handler and description from mailcap file '%s'\n", + NS_LossyConvertUTF16toASCII(aFilename).get())); + LOG(("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(), + NS_LossyConvertUTF16toASCII(aMinorType).get())); + + nsresult rv = NS_OK; + bool more = false; + + nsCOMPtr<nsIFile> file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = file->InitWithPath(aFilename); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFileInputStream> mailcapFile( + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + rv = mailcapFile->Init(file, -1, -1, false); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsILineInputStream> mailcap(do_QueryInterface(mailcapFile, &rv)); + + if (NS_FAILED(rv)) { + LOG(("Interface trouble in stream land!")); + return rv; + } + + nsAutoStringN<129> entry; + nsAutoStringN<81> buffer; + nsAutoCStringN<81> cBuffer; + rv = mailcap->ReadLine(cBuffer, &more); + if (NS_FAILED(rv)) { + mailcapFile->Close(); + return rv; + } + + do { // return on end-of-file in the loop + + CopyASCIItoUTF16(cBuffer, buffer); + if (!buffer.IsEmpty() && buffer.First() != '#') { + entry.Append(buffer); + if (entry.Last() == '\\') { // entry continues on next line + entry.Truncate(entry.Length() - 1); + entry.Append(char16_t( + ' ')); // in case there is no trailing whitespace on this line + } else { // we have a full entry in entry. Check it for the type + LOG(("Current entry: '%s'\n", + NS_LossyConvertUTF16toASCII(entry).get())); + + nsAString::const_iterator semicolon_iter, start_iter, end_iter, + majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd; + entry.BeginReading(start_iter); + entry.EndReading(end_iter); + semicolon_iter = start_iter; + FindSemicolon(semicolon_iter, end_iter); + if (semicolon_iter != + end_iter) { // we have something resembling a valid entry + rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, semicolon_iter); + if (NS_SUCCEEDED(rv) && + Substring(majorTypeStart, majorTypeEnd) + .Equals(aMajorType, nsCaseInsensitiveStringComparator) && + Substring(minorTypeStart, minorTypeEnd) + .Equals(aMinorType, nsCaseInsensitiveStringComparator)) { + // we have a match + bool match = true; + ++semicolon_iter; // point at the first char past the semicolon + start_iter = semicolon_iter; // handler string starts here + FindSemicolon(semicolon_iter, end_iter); + while (start_iter != semicolon_iter && + nsCRT::IsAsciiSpace(*start_iter)) { + ++start_iter; + } + + LOG(("The real handler is: '%s'\n", + NS_LossyConvertUTF16toASCII( + Substring(start_iter, semicolon_iter)) + .get())); + + // XXX ugly hack. Just grab the executable name + nsAString::const_iterator end_handler_iter = semicolon_iter; + nsAString::const_iterator end_executable_iter = start_iter; + while (end_executable_iter != end_handler_iter && + !nsCRT::IsAsciiSpace(*end_executable_iter)) { + ++end_executable_iter; + } + // XXX End ugly hack + + aHandler = Substring(start_iter, end_executable_iter); + + nsAString::const_iterator start_option_iter, end_optionname_iter, + equal_sign_iter; + bool equalSignFound; + while (match && semicolon_iter != end_iter && + ++semicolon_iter != + end_iter) { // there are options left and we still match + start_option_iter = semicolon_iter; + // skip over leading whitespace + while (start_option_iter != end_iter && + nsCRT::IsAsciiSpace(*start_option_iter)) { + ++start_option_iter; + } + if (start_option_iter == end_iter) { // nothing actually here + break; + } + semicolon_iter = start_option_iter; + FindSemicolon(semicolon_iter, end_iter); + equal_sign_iter = start_option_iter; + equalSignFound = false; + while (equal_sign_iter != semicolon_iter && !equalSignFound) { + switch (*equal_sign_iter) { + case '\\': + equal_sign_iter.advance(2); + break; + case '=': + equalSignFound = true; + break; + default: + ++equal_sign_iter; + break; + } + } + end_optionname_iter = start_option_iter; + // find end of option name + while (end_optionname_iter != equal_sign_iter && + !nsCRT::IsAsciiSpace(*end_optionname_iter)) { + ++end_optionname_iter; + } + nsDependentSubstring optionName(start_option_iter, + end_optionname_iter); + if (equalSignFound) { + // This is an option that has a name and value + if (optionName.EqualsLiteral("description")) { + aDescription = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("x-mozilla-flags")) { + aMozillaFlags = Substring(++equal_sign_iter, semicolon_iter); + } else if (optionName.EqualsLiteral("test")) { + nsAutoCString testCommand; + rv = UnescapeCommand( + Substring(++equal_sign_iter, semicolon_iter), aMajorType, + aMinorType, testCommand); + if (NS_FAILED(rv)) continue; + nsCOMPtr<nsIProcess> process = + do_CreateInstance(NS_PROCESS_CONTRACTID, &rv); + if (NS_FAILED(rv)) continue; + nsCOMPtr<nsIFile> file( + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) continue; + rv = file->InitWithNativePath("/bin/sh"_ns); + if (NS_FAILED(rv)) continue; + rv = process->Init(file); + if (NS_FAILED(rv)) continue; + const char* args[] = {"-c", testCommand.get()}; + LOG(("Running Test: %s\n", testCommand.get())); + rv = process->Run(true, args, 2); + if (NS_FAILED(rv)) continue; + int32_t exitValue; + rv = process->GetExitValue(&exitValue); + if (NS_FAILED(rv)) continue; + LOG(("Exit code: %d\n", exitValue)); + if (exitValue) { + match = false; + } + } + } else { + // This is an option that just has a name but no value (eg + // "copiousoutput") + if (optionName.EqualsLiteral("needsterminal")) { + match = false; + } + } + } + + if (match) { // we did not fail any test clauses; all is good + // get out of here + mailcapFile->Close(); + return NS_OK; + } + // pretend that this match never happened + aDescription.Truncate(); + aMozillaFlags.Truncate(); + aHandler.Truncate(); + } + } + // zero out the entry for the next cycle + entry.Truncate(); + } + } + if (!more) { + rv = NS_ERROR_NOT_AVAILABLE; + break; + } + rv = mailcap->ReadLine(cBuffer, &more); + } while (NS_SUCCEEDED(rv)); + mailcapFile->Close(); + return rv; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + nsresult rv = NS_OK; + + if (!XRE_IsContentProcess()) { +#ifdef MOZ_WIDGET_GTK + // Check the GNOME registry for a protocol handler + *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme); +#else + *aHandlerExists = false; +#endif + } else { + *aHandlerExists = false; + nsCOMPtr<nsIHandlerService> handlerSvc = + do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && handlerSvc) { + rv = handlerSvc->ExistsForProtocolOS(nsCString(aProtocolScheme), + aHandlerExists); + } + } + + return rv; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription( + const nsACString& aScheme, nsAString& _retval) { +#ifdef MOZ_WIDGET_GTK + nsGNOMERegistry::GetAppDescForScheme(aScheme, _retval); + return _retval.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK; +#else + return NS_ERROR_NOT_AVAILABLE; +#endif +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + *_retval = false; + return NS_OK; +} + +nsresult nsOSHelperAppService::GetFileTokenForPath( + const char16_t* platformAppPath, nsIFile** aFile) { + LOG(("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n", + NS_LossyConvertUTF16toASCII(platformAppPath).get())); + if (!*platformAppPath) { // empty filename--return error + NS_WARNING("Empty filename passed in."); + return NS_ERROR_INVALID_ARG; + } + + // first check if the base class implementation finds anything + nsresult rv = + nsExternalHelperAppService::GetFileTokenForPath(platformAppPath, aFile); + if (NS_SUCCEEDED(rv)) return rv; + // If the reason for failure was that the file doesn't exist, return too + // (because it means the path was absolute, and so that we shouldn't search in + // the path) + if (rv == NS_ERROR_FILE_NOT_FOUND) return rv; + + // If we get here, we really should have a relative path. + NS_ASSERTION(*platformAppPath != char16_t('/'), "Unexpected absolute path"); + + nsCOMPtr<nsIFile> localFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID)); + + if (!localFile) return NS_ERROR_NOT_INITIALIZED; + + bool exists = false; + // ugly hack. Walk the PATH variable... + char* unixpath = PR_GetEnv("PATH"); + nsAutoCString path(unixpath); + + const char* start_iter = path.BeginReading(start_iter); + const char* colon_iter = start_iter; + const char* end_iter = path.EndReading(end_iter); + + while (start_iter != end_iter && !exists) { + while (colon_iter != end_iter && *colon_iter != ':') { + ++colon_iter; + } + localFile->InitWithNativePath(Substring(start_iter, colon_iter)); + rv = localFile->AppendRelativePath(nsDependentString(platformAppPath)); + // Failing AppendRelativePath is a bad thing - it should basically always + // succeed given a relative path. Show a warning if it does fail. + // To prevent infinite loops when it does fail, return at this point. + NS_ENSURE_SUCCESS(rv, rv); + localFile->Exists(&exists); + if (!exists) { + if (colon_iter == end_iter) { + break; + } + ++colon_iter; + start_iter = colon_iter; + } + } + + if (exists) { + rv = NS_OK; + } else { + rv = NS_ERROR_NOT_AVAILABLE; + } + + *aFile = localFile; + NS_IF_ADDREF(*aFile); + + return rv; +} + +already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromExtension( + const nsCString& aFileExt) { + // if the extension is empty, return immediately + if (aFileExt.IsEmpty()) return nullptr; + + LOG(("Here we do an extension lookup for '%s'\n", aFileExt.get())); + + nsAutoString majorType, minorType, mime_types_description, + mailcap_description, handler, mozillaFlags; + + nsresult rv = + LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, + minorType, mime_types_description, true); + + if (NS_FAILED(rv) || majorType.IsEmpty()) { +#ifdef MOZ_WIDGET_GTK + LOG(("Looking in GNOME registry\n")); + RefPtr<nsMIMEInfoBase> gnomeInfo = + nsGNOMERegistry::GetFromExtension(aFileExt); + if (gnomeInfo) { + LOG(("Got MIMEInfo from GNOME registry\n")); + return gnomeInfo.forget(); + } +#endif + + rv = LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType, + minorType, mime_types_description, false); + } + + if (NS_FAILED(rv)) return nullptr; + + NS_LossyConvertUTF16toASCII asciiMajorType(majorType); + NS_LossyConvertUTF16toASCII asciiMinorType(minorType); + + LOG( + ("Type/Description results: majorType='%s', minorType='%s', " + "description='%s'\n", + asciiMajorType.get(), asciiMinorType.get(), + NS_LossyConvertUTF16toASCII(mime_types_description).get())); + + if (majorType.IsEmpty() && minorType.IsEmpty()) { + // we didn't get a type mapping, so we can't do anything useful + return nullptr; + } + + nsAutoCString mimeType(asciiMajorType + "/"_ns + asciiMinorType); + RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(mimeType); + + mimeInfo->AppendExtension(aFileExt); + rv = LookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags); + LOG( + ("Handler/Description results: handler='%s', description='%s', " + "mozillaFlags='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get(), + NS_LossyConvertUTF16toASCII(mozillaFlags).get())); + mailcap_description.Trim(" \t\""); + mozillaFlags.Trim(" \t"); + if (!mime_types_description.IsEmpty()) { + mimeInfo->SetDescription(mime_types_description); + } else { + mimeInfo->SetDescription(mailcap_description); + } + + if (NS_SUCCEEDED(rv) && handler.IsEmpty()) { + rv = NS_ERROR_NOT_AVAILABLE; + } + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIFile> handlerFile; + rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); + + if (NS_SUCCEEDED(rv)) { + mimeInfo->SetDefaultApplication(handlerFile); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + mimeInfo->SetDefaultDescription(handler); + } + } + + if (NS_FAILED(rv)) { + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + return mimeInfo.forget(); +} + +already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromType( + const nsCString& aMIMEType) { + // if the type is empty, return immediately + if (aMIMEType.IsEmpty()) return nullptr; + + LOG(("Here we do a mimetype lookup for '%s'\n", aMIMEType.get())); + + // extract the major and minor types + NS_ConvertASCIItoUTF16 mimeType(aMIMEType); + nsAString::const_iterator start_iter, end_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd; + + mimeType.BeginReading(start_iter); + mimeType.EndReading(end_iter); + + // XXX FIXME: add typeOptions parsing in here + nsresult rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd, + minorTypeStart, minorTypeEnd, end_iter); + + if (NS_FAILED(rv)) { + return nullptr; + } + + nsDependentSubstring majorType(majorTypeStart, majorTypeEnd); + nsDependentSubstring minorType(minorTypeStart, minorTypeEnd); + + // First check the user's private mailcap file + nsAutoString mailcap_description, handler, mozillaFlags; + DoLookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags, true); + + LOG(("Private Handler/Description results: handler='%s', description='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get())); + + // Now look up our extensions + nsAutoString extensions, mime_types_description; + LookUpExtensionsAndDescription(majorType, minorType, extensions, + mime_types_description); + +#ifdef MOZ_WIDGET_GTK + if (handler.IsEmpty()) { + RefPtr<nsMIMEInfoBase> gnomeInfo = nsGNOMERegistry::GetFromType(aMIMEType); + if (gnomeInfo) { + LOG( + ("Got MIMEInfo from GNOME registry without extensions; setting them " + "to %s\n", + NS_LossyConvertUTF16toASCII(extensions).get())); + + NS_ASSERTION(!gnomeInfo->HasExtensions(), "How'd that happen?"); + gnomeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); + return gnomeInfo.forget(); + } + } +#endif + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, minorType, handler, + mailcap_description, mozillaFlags, false); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, + mailcap_description, mozillaFlags, true); + } + + if (handler.IsEmpty()) { + DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler, + mailcap_description, mozillaFlags, false); + } + + LOG( + ("Handler/Description results: handler='%s', description='%s', " + "mozillaFlags='%s'\n", + NS_LossyConvertUTF16toASCII(handler).get(), + NS_LossyConvertUTF16toASCII(mailcap_description).get(), + NS_LossyConvertUTF16toASCII(mozillaFlags).get())); + + mailcap_description.Trim(" \t\""); + mozillaFlags.Trim(" \t"); + + if (handler.IsEmpty() && extensions.IsEmpty() && + mailcap_description.IsEmpty() && mime_types_description.IsEmpty()) { + // No real useful info + return nullptr; + } + + RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(aMIMEType); + + mimeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions)); + if (!mime_types_description.IsEmpty()) { + mimeInfo->SetDescription(mime_types_description); + } else { + mimeInfo->SetDescription(mailcap_description); + } + + rv = NS_ERROR_NOT_AVAILABLE; + nsCOMPtr<nsIFile> handlerFile; + if (!handler.IsEmpty()) { + rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile)); + } + + if (NS_SUCCEEDED(rv)) { + mimeInfo->SetDefaultApplication(handlerFile); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + mimeInfo->SetDefaultDescription(handler); + } else { + mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk); + } + + return mimeInfo.forget(); +} + +nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType, + const nsACString& aFileExt, + bool* aFound, + nsIMIMEInfo** aMIMEInfo) { + *aFound = true; + RefPtr<nsMIMEInfoBase> retval; + // Fallback to lookup by extension when generic 'application/octet-stream' + // content type is received. + if (!aType.EqualsLiteral(APPLICATION_OCTET_STREAM)) { + retval = GetFromType(PromiseFlatCString(aType)); + } + bool hasDefault = false; + if (retval) retval->GetHasDefaultHandler(&hasDefault); + if (!retval || !hasDefault) { + RefPtr<nsMIMEInfoBase> miByExt = + GetFromExtension(PromiseFlatCString(aFileExt)); + // If we had no extension match, but a type match, use that + if (!miByExt && retval) { + retval.forget(aMIMEInfo); + return NS_OK; + } + // If we had an extension match but no type match, set the mimetype and use + // it + if (!retval && miByExt) { + if (!aType.IsEmpty()) miByExt->SetMIMEType(aType); + miByExt.swap(retval); + + retval.forget(aMIMEInfo); + return NS_OK; + } + // If we got nothing, make a new mimeinfo + if (!retval) { + *aFound = false; + retval = new nsMIMEInfoUnix(aType); + if (retval) { + if (!aFileExt.IsEmpty()) retval->AppendExtension(aFileExt); + } + + retval.forget(aMIMEInfo); + return NS_OK; + } + + // Copy the attributes of retval (mimeinfo from type) onto miByExt, to + // return it + // but reset to just collected mDefaultAppDescription (from ext) + nsAutoString byExtDefault; + miByExt->GetDefaultDescription(byExtDefault); + retval->SetDefaultDescription(byExtDefault); + retval->CopyBasicDataTo(miByExt); + + miByExt.swap(retval); + } + retval.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = + OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoUnix* handlerInfo = + new nsMIMEInfoUnix(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} diff --git a/uriloader/exthandler/unix/nsOSHelperAppService.h b/uriloader/exthandler/unix/nsOSHelperAppService.h new file mode 100644 index 0000000000..ffa83f6a23 --- /dev/null +++ b/uriloader/exthandler/unix/nsOSHelperAppService.h @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" + +class nsIFileInputStream; +class nsILineInputStream; +class nsMIMEInfoBase; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + virtual ~nsOSHelperAppService(); + + // method overrides for mime.types and mime.info look up steps + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMimeType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) override; + + // override nsIExternalProtocolService methods + nsresult OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + // GetFileTokenForPath must be implemented by each platform. + // platformAppPath --> a platform specific path to an application that we got + // out of the rdf data source. This can be a mac file + // spec, a unix path or a windows path depending on the + // platform + // aFile --> an nsIFile representation of that platform application path. + virtual nsresult GetFileTokenForPath(const char16_t* platformAppPath, + nsIFile** aFile) override; + + protected: + already_AddRefed<nsMIMEInfoBase> GetFromType(const nsCString& aMimeType); + already_AddRefed<nsMIMEInfoBase> GetFromExtension(const nsCString& aFileExt); + + private: + // Helper methods which have to access static members + static nsresult UnescapeCommand(const nsAString& aEscapedCommand, + const nsAString& aMajorType, + const nsAString& aMinorType, + nsACString& aUnEscapedCommand); + static nsresult GetFileLocation(const char* aPrefName, + const char* aEnvVarName, + nsAString& aFileLocation); + static nsresult LookUpTypeAndDescription(const nsAString& aFileExtension, + nsAString& aMajorType, + nsAString& aMinorType, + nsAString& aDescription, + bool aUserData); + static nsresult CreateInputStream(const nsAString& aFilename, + nsIFileInputStream** aFileInputStream, + nsILineInputStream** aLineInputStream, + nsACString& aBuffer, bool* aNetscapeFormat, + bool* aMore); + + static nsresult GetTypeAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aFileExtension, + nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription); + + static nsresult LookUpExtensionsAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult GetExtensionsAndDescriptionFromMimetypesFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aFileExtensions, + nsAString& aDescription); + + static nsresult ParseNetscapeMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult ParseNormalMIMETypesEntry( + const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart, + nsAString::const_iterator& aMajorTypeEnd, + nsAString::const_iterator& aMinorTypeStart, + nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions, + nsAString::const_iterator& aDescriptionStart, + nsAString::const_iterator& aDescriptionEnd); + + static nsresult LookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags); + + static nsresult DoLookUpHandlerAndDescription(const nsAString& aMajorType, + const nsAString& aMinorType, + nsAString& aHandler, + nsAString& aDescription, + nsAString& aMozillaFlags, + bool aUserData); + + static nsresult GetHandlerAndDescriptionFromMailcapFile( + const nsAString& aFilename, const nsAString& aMajorType, + const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription, + nsAString& aMozillaFlags); +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.cpp b/uriloader/exthandler/win/nsMIMEInfoWin.cpp new file mode 100644 index 0000000000..9f36a97ff9 --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.cpp @@ -0,0 +1,900 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsArrayEnumerator.h" +#include "nsCOMArray.h" +#include "nsLocalFile.h" +#include "nsMIMEInfoWin.h" +#include "nsNetUtil.h" +#include <windows.h> +#include <shellapi.h> +#include "nsIMutableArray.h" +#include "nsTArray.h" +#include "shlobj.h" +#include "windows.h" +#include "nsIWindowsRegKey.h" +#include "nsUnicharUtils.h" +#include "nsITextToSubURI.h" +#include "nsVariant.h" +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/ShellHeaderOnlyUtils.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/UrlmonHeaderOnlyUtils.h" +#include "mozilla/UniquePtrExtensions.h" + +#define RUNDLL32_EXE L"\\rundll32.exe" + +NS_IMPL_ISUPPORTS_INHERITED(nsMIMEInfoWin, nsMIMEInfoBase, nsIPropertyBag) + +nsMIMEInfoWin::~nsMIMEInfoWin() {} + +nsresult nsMIMEInfoWin::LaunchDefaultWithFile(nsIFile* aFile) { + // Launch the file, unless it is an executable. + bool executable = true; + aFile->IsExecutable(&executable); + if (executable) return NS_ERROR_FAILURE; + + return aFile->Launch(); +} + +nsresult nsMIMEInfoWin::ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc, + const wchar_t** aArgv) { + nsresult rv; + + NS_ASSERTION(aArgc >= 1, "aArgc must be at least 1"); + + nsAutoString execPath; + rv = aExecutable->GetTarget(execPath); + if (NS_FAILED(rv) || execPath.IsEmpty()) { + rv = aExecutable->GetPath(execPath); + } + if (NS_FAILED(rv)) { + return rv; + } + + auto assembledArgs = mozilla::MakeCommandLine(aArgc, aArgv); + if (!assembledArgs) { + return NS_ERROR_FILE_EXECUTION_FAILED; + } + + _bstr_t execPathBStr(execPath.get()); + // Pass VT_ERROR/DISP_E_PARAMNOTFOUND to omit an optional RPC parameter + // to execute a file with the default verb. + _variant_t verbDefault(DISP_E_PARAMNOTFOUND, VT_ERROR); + _variant_t workingDir; + _variant_t showCmd(SW_SHOWNORMAL); + + // Ask Explorer to ShellExecute on our behalf, as some applications such as + // Skype for Business do not start correctly when inheriting our process's + // migitation policies. + // It does not work in a special environment such as Citrix. In such a case + // we fall back to launching an application as a child process. We need to + // find a way to handle the combination of these interop issues. + mozilla::LauncherVoidResult shellExecuteOk = mozilla::ShellExecuteByExplorer( + execPathBStr, assembledArgs.get(), verbDefault, workingDir, showCmd); + if (shellExecuteOk.isErr()) { + // No need to pass assembledArgs to LaunchWithIProcess. aArgv will be + // processed in nsProcess::RunProcess. + return LaunchWithIProcess(aExecutable, aArgc, + reinterpret_cast<const char16_t**>(aArgv)); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::LaunchWithFile(nsIFile* aFile) { + nsresult rv; + + // it doesn't make any sense to call this on protocol handlers + NS_ASSERTION(mClass == eMIMEInfo, + "nsMIMEInfoBase should have mClass == eMIMEInfo"); + + if (mPreferredAction == useSystemDefault) { + if (mDefaultApplication && + StaticPrefs::browser_pdf_launchDefaultEdgeAsApp()) { + // Since Edgium is the default handler for PDF and other kinds of files, + // if we're using the OS default and it's Edgium prefer its app mode so it + // operates as a viewer (without browser toolbars). Bug 1632277. + nsAutoCString defaultAppExecutable; + rv = mDefaultApplication->GetNativeLeafName(defaultAppExecutable); + if (NS_SUCCEEDED(rv) && + defaultAppExecutable.LowerCaseEqualsLiteral("msedge.exe")) { + nsAutoString path; + rv = aFile->GetPath(path); + if (NS_SUCCEEDED(rv)) { + // If the --app flag doesn't work we'll want to fallback to a + // regular path. Send two args so we call `msedge.exe --app={path} + // {path}`. + nsAutoString appArg; + appArg.AppendLiteral("--app="); + appArg.Append(path); + const wchar_t* argv[] = {appArg.get(), path.get()}; + + return ShellExecuteWithIFile(mDefaultApplication, + mozilla::ArrayLength(argv), argv); + } + } + } + return LaunchDefaultWithFile(aFile); + } + + if (mPreferredAction == useHelperApp) { + if (!mPreferredApplication) return NS_ERROR_FILE_NOT_FOUND; + + // at the moment, we only know how to hand files off to local handlers + nsCOMPtr<nsILocalHandlerApp> localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + // Deal with local dll based handlers + nsCString filename; + executable->GetNativeLeafName(filename); + if (filename.Length() > 4) { + nsCString extension(Substring(filename, filename.Length() - 4, 4)); + + if (extension.LowerCaseEqualsLiteral(".dll")) { + nsAutoString args; + + // executable is rundll32, everything else is a list of parameters, + // including the dll handler. + if (!GetDllLaunchInfo(executable, aFile, args, false)) + return NS_ERROR_INVALID_ARG; + + WCHAR rundll32Path[MAX_PATH + sizeof(RUNDLL32_EXE) / sizeof(WCHAR) + + 1] = {L'\0'}; + if (!GetSystemDirectoryW(rundll32Path, MAX_PATH)) { + return NS_ERROR_FILE_NOT_FOUND; + } + lstrcatW(rundll32Path, RUNDLL32_EXE); + + SHELLEXECUTEINFOW seinfo; + memset(&seinfo, 0, sizeof(seinfo)); + seinfo.cbSize = sizeof(SHELLEXECUTEINFOW); + seinfo.fMask = 0; + seinfo.hwnd = nullptr; + seinfo.lpVerb = nullptr; + seinfo.lpFile = rundll32Path; + seinfo.lpParameters = args.get(); + seinfo.lpDirectory = nullptr; + seinfo.nShow = SW_SHOWNORMAL; + if (ShellExecuteExW(&seinfo)) return NS_OK; + + switch ((LONG_PTR)seinfo.hInstApp) { + case 0: + case SE_ERR_OOM: + return NS_ERROR_OUT_OF_MEMORY; + case SE_ERR_ACCESSDENIED: + return NS_ERROR_FILE_ACCESS_DENIED; + case SE_ERR_ASSOCINCOMPLETE: + case SE_ERR_NOASSOC: + return NS_ERROR_UNEXPECTED; + case SE_ERR_DDEBUSY: + case SE_ERR_DDEFAIL: + case SE_ERR_DDETIMEOUT: + return NS_ERROR_NOT_AVAILABLE; + case SE_ERR_DLLNOTFOUND: + return NS_ERROR_FAILURE; + case SE_ERR_SHARE: + return NS_ERROR_FILE_IS_LOCKED; + default: + switch (GetLastError()) { + case ERROR_FILE_NOT_FOUND: + return NS_ERROR_FILE_NOT_FOUND; + case ERROR_PATH_NOT_FOUND: + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + case ERROR_BAD_FORMAT: + return NS_ERROR_FILE_CORRUPTED; + } + } + return NS_ERROR_FILE_EXECUTION_FAILED; + } + } + nsAutoString path; + aFile->GetPath(path); + const wchar_t* argv[] = {path.get()}; + return ShellExecuteWithIFile(executable, mozilla::ArrayLength(argv), argv); + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetHasDefaultHandler(bool* _retval) { + // We have a default application if we have a description + // We can ShellExecute anything; however, callers are probably interested if + // there is really an application associated with this type of file + *_retval = !mDefaultAppDescription.IsEmpty(); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetEnumerator(nsISimpleEnumerator** _retval) { + nsCOMArray<nsIVariant> properties; + + nsCOMPtr<nsIVariant> variant; + GetProperty(u"defaultApplicationIconURL"_ns, getter_AddRefs(variant)); + if (variant) properties.AppendObject(variant); + + GetProperty(u"customApplicationIconURL"_ns, getter_AddRefs(variant)); + if (variant) properties.AppendObject(variant); + + return NS_NewArrayEnumerator(_retval, properties, NS_GET_IID(nsIVariant)); +} + +static nsresult GetIconURLVariant(nsIFile* aApplication, nsIVariant** _retval) { + nsAutoCString fileURLSpec; + NS_GetURLSpecFromFile(aApplication, fileURLSpec); + nsAutoCString iconURLSpec; + iconURLSpec.AssignLiteral("moz-icon://"); + iconURLSpec += fileURLSpec; + RefPtr<nsVariant> writable(new nsVariant()); + writable->SetAsAUTF8String(iconURLSpec); + writable.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::GetProperty(const nsAString& aName, nsIVariant** _retval) { + nsresult rv; + if (mDefaultApplication && + aName.EqualsLiteral(PROPERTY_DEFAULT_APP_ICON_URL)) { + rv = GetIconURLVariant(mDefaultApplication, _retval); + NS_ENSURE_SUCCESS(rv, rv); + } else if (mPreferredApplication && + aName.EqualsLiteral(PROPERTY_CUSTOM_APP_ICON_URL)) { + nsCOMPtr<nsILocalHandlerApp> localHandler = + do_QueryInterface(mPreferredApplication, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> executable; + rv = localHandler->GetExecutable(getter_AddRefs(executable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetIconURLVariant(executable, _retval); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// this implementation was pretty much copied verbatime from +// Tony Robinson's code in nsExternalProtocolWin.cpp +nsresult nsMIMEInfoWin::LoadUriInternal(nsIURI* aURL) { + nsresult rv = NS_OK; + + // 1. Find the default app for this protocol + // 2. Set up the command line + // 3. Launch the app. + + // For now, we'll just cheat essentially, check for the command line + // then just call ShellExecute()! + + if (aURL) { + // extract the url spec from the url + nsAutoCString urlSpec; + aURL->GetAsciiSpec(urlSpec); + + // Unescape non-ASCII characters in the URL + nsAutoString utf16Spec; + + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_FAILED(textToSubURI->UnEscapeNonAsciiURI("UTF-8"_ns, urlSpec, + utf16Spec))) { + CopyASCIItoUTF16(urlSpec, utf16Spec); + } + + // Ask the shell/urlmon to parse |utf16Spec| to avoid malformed URLs. + // Failure is indicative of a potential security issue so we should + // bail out if so. + LauncherResult<_bstr_t> validatedUri = UrlmonValidateUri(utf16Spec.get()); + if (validatedUri.isErr()) { + return NS_ERROR_FAILURE; + } + + _variant_t args; + _variant_t verb(L"open"); + _variant_t workingDir; + _variant_t showCmd(SW_SHOWNORMAL); + + // To open a uri, we first try ShellExecuteByExplorer, which starts a new + // process as a child process of explorer.exe, because applications may not + // support the mitigation policies inherited from our process. If it fails, + // we fall back to ShellExecuteExW. + // + // For Thunderbird, however, there is a known issue that + // ShellExecuteByExplorer succeeds but explorer.exe shows an error popup + // if a uri to open includes credentials. This does not happen in Firefox + // because Firefox does not have to launch a process to open a uri. + // + // Since Thunderbird does not use mitigation policies which could cause + // compatibility issues, we get no benefit from using + // ShellExecuteByExplorer. Thus we skip it and go straight to + // ShellExecuteExW for Thunderbird. +#ifndef MOZ_THUNDERBIRD + mozilla::LauncherVoidResult shellExecuteOk = + mozilla::ShellExecuteByExplorer(validatedUri.inspect(), args, verb, + workingDir, showCmd); + if (shellExecuteOk.isOk()) { + return NS_OK; + } +#endif // MOZ_THUNDERBIRD + + SHELLEXECUTEINFOW sinfo = {sizeof(sinfo)}; + sinfo.fMask = SEE_MASK_NOASYNC; + sinfo.lpVerb = V_BSTR(&verb); + sinfo.nShow = showCmd; + sinfo.lpFile = validatedUri.inspect(); + + BOOL result = ShellExecuteExW(&sinfo); + if (!result || reinterpret_cast<LONG_PTR>(sinfo.hInstApp) < 32) { + rv = NS_ERROR_FAILURE; + } + } + + return rv; +} + +// Given a path to a local file, return its nsILocalHandlerApp instance. +bool nsMIMEInfoWin::GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr<nsILocalHandlerApp>& aApp) { + nsCOMPtr<nsIFile> locfile; + nsresult rv = NS_NewLocalFile(aCommandHandler, true, getter_AddRefs(locfile)); + if (NS_FAILED(rv)) return false; + + aApp = do_CreateInstance("@mozilla.org/uriloader/local-handler-app;1"); + if (!aApp) return false; + + aApp->SetExecutable(locfile); + return true; +} + +// Return the cleaned up file path associated with a command verb +// located in root/Applications. +bool nsMIMEInfoWin::GetAppsVerbCommandHandler(const nsAString& appExeName, + nsAString& applicationPath, + bool edit) { + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe + nsAutoString applicationsPath; + applicationsPath.AppendLiteral("Applications\\"); + applicationsPath.Append(appExeName); + + nsresult rv = + appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + // Check for the NoOpenWith flag, if it exists + uint32_t value; + if (NS_SUCCEEDED(appKey->ReadIntValue(u"NoOpenWith"_ns, &value)) && + value == 1) + return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Return a fully populated command string based on +// passing information. Used in launchWithFile to trace +// back to the full handler path based on the dll. +// (dll, targetfile, return args, open/edit) +bool nsMIMEInfoWin::GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile, + nsAString& args, bool edit) { + if (!aDll || !aFile) return false; + + nsString appExeName; + aDll->GetLeafName(appExeName); + + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe + nsAutoString applicationsPath; + applicationsPath.AppendLiteral("Applications\\"); + applicationsPath.Append(appExeName); + + nsresult rv = + appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + // Check for the NoOpenWith flag, if it exists + uint32_t value; + rv = appKey->ReadIntValue(u"NoOpenWith"_ns, &value); + if (NS_SUCCEEDED(rv) && value == 1) return false; + + nsAutoString dummy; + if (NS_SUCCEEDED(appKey->ReadStringValue(u"NoOpenWith"_ns, dummy))) + return false; + + appKey->Close(); + + // HKEY_CLASSES_ROOT\Applications\iexplore.exe\shell\open\command + applicationsPath.AssignLiteral("Applications\\"); + applicationsPath.Append(appExeName); + if (!edit) + applicationsPath.AppendLiteral("\\shell\\open\\command"); + else + applicationsPath.AppendLiteral("\\shell\\edit\\command"); + + rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, applicationsPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Replace embedded environment variables. + uint32_t bufLength = + ::ExpandEnvironmentStringsW(appFilesystemCommand.get(), nullptr, 0); + if (bufLength == 0) // Error + return false; + + auto destination = mozilla::MakeUniqueFallible<wchar_t[]>(bufLength); + if (!destination) return false; + if (!::ExpandEnvironmentStringsW(appFilesystemCommand.get(), + destination.get(), bufLength)) + return false; + + appFilesystemCommand.Assign(destination.get()); + + // C:\Windows\System32\rundll32.exe "C:\Program Files\Windows + // Photo Gallery\PhotoViewer.dll", ImageView_Fullscreen %1 + nsAutoString params; + constexpr auto rundllSegment = u"rundll32.exe "_ns; + int32_t index = appFilesystemCommand.Find(rundllSegment); + if (index > kNotFound) { + params.Append( + Substring(appFilesystemCommand, index + rundllSegment.Length())); + } else { + params.Append(appFilesystemCommand); + } + + // check to make sure we have a %1 and fill it + constexpr auto percentOneParam = u"%1"_ns; + index = params.Find(percentOneParam); + if (index == kNotFound) // no parameter + return false; + + nsString target; + aFile->GetTarget(target); + params.Replace(index, 2, target); + + args = params; + + return true; + } + return false; +} + +// Return the cleaned up file path associated with a progid command +// verb located in root. +bool nsMIMEInfoWin::GetProgIDVerbCommandHandler(const nsAString& appProgIDName, + nsAString& applicationPath, + bool edit) { + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return false; + + nsAutoString appProgId(appProgIDName); + + // HKEY_CLASSES_ROOT\Windows.XPSReachViewer\shell\open\command + if (!edit) + appProgId.AppendLiteral("\\shell\\open\\command"); + else + appProgId.AppendLiteral("\\shell\\edit\\command"); + + nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, appProgId, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString appFilesystemCommand; + if (NS_SUCCEEDED(appKey->ReadStringValue(u""_ns, appFilesystemCommand))) { + // Expand environment vars, clean up any misc. + if (!nsLocalFile::CleanupCmdHandlerPath(appFilesystemCommand)) return false; + + applicationPath = appFilesystemCommand; + return true; + } + return false; +} + +// Helper routine used in tracking app lists. Converts path +// entries to lower case and stores them in the trackList array. +void nsMIMEInfoWin::ProcessPath(nsCOMPtr<nsIMutableArray>& appList, + nsTArray<nsString>& trackList, + const nsAString& appFilesystemCommand) { + nsAutoString lower(appFilesystemCommand); + ToLowerCase(lower); + + // Don't include firefox.exe in the list + WCHAR exe[MAX_PATH + 1]; + uint32_t len = GetModuleFileNameW(nullptr, exe, MAX_PATH); + if (len < MAX_PATH && len != 0) { + int32_t index = lower.Find(exe); + if (index != -1) return; + } + + nsCOMPtr<nsILocalHandlerApp> aApp; + if (!GetLocalHandlerApp(appFilesystemCommand, aApp)) return; + + // Save in our main tracking arrays + appList->AppendElement(aApp); + trackList.AppendElement(lower); +} + +// Helper routine that handles a compare between a path +// and an array of paths. +static bool IsPathInList(nsAString& appPath, nsTArray<nsString>& trackList) { + // trackList data is always lowercase, see ProcessPath + // above. + nsAutoString tmp(appPath); + ToLowerCase(tmp); + + for (uint32_t i = 0; i < trackList.Length(); i++) { + if (tmp.Equals(trackList[i])) return true; + } + return false; +} + +/** + * Returns a list of nsILocalHandlerApp objects containing local + * handlers associated with this mimeinfo. Implemented per + * platform using information in this object to generate the + * best list. Typically used for an "open with" style user + * option. + * + * @return nsIArray of nsILocalHandlerApp + */ +NS_IMETHODIMP +nsMIMEInfoWin::GetPossibleLocalHandlers(nsIArray** _retval) { + nsresult rv; + + *_retval = nullptr; + + nsCOMPtr<nsIMutableArray> appList = do_CreateInstance("@mozilla.org/array;1"); + + if (!appList) return NS_ERROR_FAILURE; + + nsTArray<nsString> trackList; + + nsAutoCString fileExt; + GetPrimaryExtension(fileExt); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_FAILURE; + nsCOMPtr<nsIWindowsRegKey> appKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!appKey) return NS_ERROR_FAILURE; + + nsAutoString workingRegistryPath; + + bool extKnown = false; + if (fileExt.IsEmpty()) { + extKnown = true; + // Mime type discovery is possible in some cases, through + // HKEY_CLASSES_ROOT\MIME\Database\Content Type, however, a number + // of file extensions related to mime type are simply not defined, + // (application/rss+xml & application/atom+xml are good examples) + // in which case we can only provide a generic list. + nsAutoCString mimeType; + GetMIMEType(mimeType); + if (!mimeType.IsEmpty()) { + workingRegistryPath.AppendLiteral("MIME\\Database\\Content Type\\"); + workingRegistryPath.Append(NS_ConvertASCIItoUTF16(mimeType)); + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString mimeFileExt; + if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, mimeFileExt))) { + CopyUTF16toUTF8(mimeFileExt, fileExt); + extKnown = false; + } + } + } + } + + nsAutoString fileExtToUse; + if (!fileExt.IsEmpty() && fileExt.First() != '.') { + fileExtToUse = char16_t('.'); + } + fileExtToUse.Append(NS_ConvertUTF8toUTF16(fileExt)); + + // Note, the order in which these occur has an effect on the + // validity of the resulting display list. + + if (!extKnown) { + // 1) Get the default handler if it exists + workingRegistryPath = fileExtToUse; + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString appProgId; + if (NS_SUCCEEDED(regKey->ReadStringValue(u""_ns, appProgId))) { + // Bug 358297 - ignore the embedded internet explorer handler + if (appProgId != u"XPSViewer.Document"_ns) { + nsAutoString appFilesystemCommand; + if (GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) && + !IsPathInList(appFilesystemCommand, trackList)) { + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + regKey->Close(); + } + + // 2) list HKEY_CLASSES_ROOT\.ext\OpenWithList + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 3) List HKEY_CLASSES_ROOT\.ext\OpenWithProgids, with the + // different step of resolving the progids for the command handler. + + workingRegistryPath = fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + // HKEY_CLASSES_ROOT\.ext\OpenWithProgids\Windows.XPSReachViewer + nsAutoString appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 4) Add any non configured applications located in the MRU list + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithList + workingRegistryPath = nsLiteralString( + u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithList"); + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName, appValue; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + if (appName.EqualsLiteral("MRUList")) continue; + if (NS_FAILED(regKey->ReadStringValue(appName, appValue))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appValue, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + // 5) Add any non configured progids in the MRU list, with the + // different step of resolving the progids for the command handler. + + // HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion + // \Explorer\FileExts\.ext\OpenWithProgids + workingRegistryPath = nsLiteralString( + u"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\"); + workingRegistryPath += fileExtToUse; + workingRegistryPath.AppendLiteral("\\OpenWithProgids"); + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appIndex, appProgId; + if (NS_FAILED(regKey->GetValueName(index, appProgId))) continue; + + nsAutoString appFilesystemCommand; + if (!GetProgIDVerbCommandHandler(appProgId, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 6) Check the perceived type value, and use this to lookup the + // perceivedtype open with list. + // http://msdn2.microsoft.com/en-us/library/aa969373.aspx + + workingRegistryPath = fileExtToUse; + + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + nsAutoString perceivedType; + rv = regKey->ReadStringValue(u"PerceivedType"_ns, perceivedType); + if (NS_SUCCEEDED(rv)) { + nsAutoString openWithListPath(u"SystemFileAssociations\\"_ns); + openWithListPath.Append(perceivedType); // no period + openWithListPath.AppendLiteral("\\OpenWithList"); + + nsresult rv = appKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + openWithListPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, + false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + } + } + } // extKnown == false + + // 7) list global HKEY_CLASSES_ROOT\*\OpenWithList + // Listing general purpose handlers, not specific to a mime type or file + // extension + + workingRegistryPath = u"*\\OpenWithList"_ns; + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + workingRegistryPath, nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetValueCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetValueName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + regKey->Close(); + } + + // 8) General application's list - not file extension specific on windows + workingRegistryPath = u"Applications"_ns; + + rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, workingRegistryPath, + nsIWindowsRegKey::ACCESS_ENUMERATE_SUB_KEYS | + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + uint32_t count = 0; + if (NS_SUCCEEDED(regKey->GetChildCount(&count)) && count > 0) { + for (uint32_t index = 0; index < count; index++) { + nsAutoString appName; + if (NS_FAILED(regKey->GetChildName(index, appName))) continue; + + // HKEY_CLASSES_ROOT\Applications\firefox.exe = "path params" + nsAutoString appFilesystemCommand; + if (!GetAppsVerbCommandHandler(appName, appFilesystemCommand, false) || + IsPathInList(appFilesystemCommand, trackList)) + continue; + ProcessPath(appList, trackList, appFilesystemCommand); + } + } + } + + // Return to the caller + *_retval = appList; + NS_ADDREF(*_retval); + + return NS_OK; +} + +NS_IMETHODIMP +nsMIMEInfoWin::IsCurrentAppOSDefault(bool* _retval) { + *_retval = false; + if (mDefaultApplication) { + // Determine if the default executable is our executable. + nsCOMPtr<nsIFile> ourBinary; + XRE_GetBinaryPath(getter_AddRefs(ourBinary)); + bool isSame = false; + nsresult rv = mDefaultApplication->Equals(ourBinary, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + } + return NS_OK; +} diff --git a/uriloader/exthandler/win/nsMIMEInfoWin.h b/uriloader/exthandler/win/nsMIMEInfoWin.h new file mode 100644 index 0000000000..1de1f152ad --- /dev/null +++ b/uriloader/exthandler/win/nsMIMEInfoWin.h @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMIMEInfoWin_h_ +#define nsMIMEInfoWin_h_ + +#include "nsMIMEInfoImpl.h" +#include "nsIPropertyBag.h" +#include "nsIMutableArray.h" +#include "nsTArray.h" + +class nsMIMEInfoWin : public nsMIMEInfoBase, public nsIPropertyBag { + virtual ~nsMIMEInfoWin(); + + public: + explicit nsMIMEInfoWin(const char* aType = "") : nsMIMEInfoBase(aType) {} + explicit nsMIMEInfoWin(const nsACString& aMIMEType) + : nsMIMEInfoBase(aMIMEType) {} + nsMIMEInfoWin(const nsACString& aType, HandlerClass aClass) + : nsMIMEInfoBase(aType, aClass) {} + + NS_IMETHOD LaunchWithFile(nsIFile* aFile) override; + NS_IMETHOD GetHasDefaultHandler(bool* _retval) override; + NS_IMETHOD GetPossibleLocalHandlers(nsIArray** _retval) override; + NS_IMETHOD IsCurrentAppOSDefault(bool* _retval) override; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROPERTYBAG + + void SetDefaultApplicationHandler(nsIFile* aDefaultApplication) { + mDefaultApplication = aDefaultApplication; + } + + protected: + virtual nsresult LoadUriInternal(nsIURI* aURI); + virtual nsresult LaunchDefaultWithFile(nsIFile* aFile); + + private: + nsCOMPtr<nsIFile> mDefaultApplication; + + // Given a path to a local handler, return its + // nsILocalHandlerApp instance. + bool GetLocalHandlerApp(const nsAString& aCommandHandler, + nsCOMPtr<nsILocalHandlerApp>& aApp); + + // Return the cleaned up file path associated + // with a command verb located in root/Applications. + bool GetAppsVerbCommandHandler(const nsAString& appExeName, + nsAString& applicationPath, bool bEdit); + + // Return the cleaned up file path associated + // with a progid command verb located in root. + bool GetProgIDVerbCommandHandler(const nsAString& appProgIDName, + nsAString& applicationPath, bool bEdit); + + // Lookup a rundll command handler and return + // a populated command template for use with rundll32.exe. + bool GetDllLaunchInfo(nsIFile* aDll, nsIFile* aFile, nsAString& args, + bool bEdit); + + // Helper routine used in tracking app lists + void ProcessPath(nsCOMPtr<nsIMutableArray>& appList, + nsTArray<nsString>& trackList, + const nsAString& appFilesystemCommand); + + // Helper routine to call mozilla::ShellExecuteByExplorer + nsresult ShellExecuteWithIFile(nsIFile* aExecutable, int aArgc, + const wchar_t** aArgv); +}; + +#endif diff --git a/uriloader/exthandler/win/nsOSHelperAppService.cpp b/uriloader/exthandler/win/nsOSHelperAppService.cpp new file mode 100644 index 0000000000..48c5ec64d8 --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.cpp @@ -0,0 +1,589 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sts=2 sw=2 et cin: + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsComponentManagerUtils.h" +#include "nsOSHelperAppService.h" +#include "nsISupports.h" +#include "nsString.h" +#include "nsIMIMEInfo.h" +#include "nsMIMEInfoWin.h" +#include "nsMimeTypes.h" +#include "plstr.h" +#include "nsNativeCharsetUtils.h" +#include "nsLocalFile.h" +#include "nsIWindowsRegKey.h" +#include "nsXULAppAPI.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/WindowsVersion.h" + +// shellapi.h is needed to build with WIN32_LEAN_AND_MEAN +#include <shellapi.h> +#include <shlwapi.h> + +#define LOG(args) MOZ_LOG(mLog, mozilla::LogLevel::Debug, args) + +// helper methods: forward declarations... +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension); + +nsOSHelperAppService::nsOSHelperAppService() + : nsExternalHelperAppService(), mAppAssoc(nullptr) { + CoInitialize(nullptr); + CoCreateInstance(CLSID_ApplicationAssociationRegistration, nullptr, + CLSCTX_INPROC, IID_IApplicationAssociationRegistration, + (void**)&mAppAssoc); +} + +nsOSHelperAppService::~nsOSHelperAppService() { + if (mAppAssoc) mAppAssoc->Release(); + mAppAssoc = nullptr; + CoUninitialize(); +} + +// The windows registry provides a mime database key which lists a set of mime +// types and corresponding "Extension" values. we can use this to look up our +// mime type to see if there is a preferred extension for the mime type. +static nsresult GetExtensionFromWindowsMimeDatabase(const nsACString& aMimeType, + nsString& aFileExtension) { + nsAutoString mimeDatabaseKey; + mimeDatabaseKey.AssignLiteral("MIME\\Database\\Content Type\\"); + + AppendASCIItoUTF16(aMimeType, mimeDatabaseKey); + + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, mimeDatabaseKey, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + + if (NS_SUCCEEDED(rv)) + regKey->ReadStringValue(u"Extension"_ns, aFileExtension); + + return NS_OK; +} + +nsresult nsOSHelperAppService::OSProtocolHandlerExists( + const char* aProtocolScheme, bool* aHandlerExists) { + // look up the protocol scheme in the windows registry....if we find a match + // then we have a handler for it... + *aHandlerExists = false; + if (aProtocolScheme && *aProtocolScheme) { + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + wchar_t* pResult = nullptr; + NS_ConvertASCIItoUTF16 scheme(aProtocolScheme); + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(scheme.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + CoTaskMemFree(pResult); + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + nsDependentString(scheme.get()), + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + // Open will fail if the registry key path doesn't exist. + return NS_OK; + } + + bool hasValue; + rv = regKey->HasValue(u"URL Protocol"_ns, &hasValue); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + if (!hasValue) { + return NS_OK; + } + + *aHandlerExists = true; + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription( + const nsACString& aScheme, nsAString& _retval) { + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + NS_ConvertASCIItoUTF16 buf(aScheme); + + if (mozilla::IsWin8OrLater()) { + wchar_t result[1024]; + DWORD resultSize = 1024; + HRESULT hr = AssocQueryString(0x1000 /* ASSOCF_IS_PROTOCOL */, + ASSOCSTR_FRIENDLYAPPNAME, buf.get(), NULL, + result, &resultSize); + if (SUCCEEDED(hr)) { + _retval = result; + return NS_OK; + } + } + + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + wchar_t* pResult = nullptr; + // We are responsible for freeing returned strings. + HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + nsCOMPtr<nsIFile> app; + nsAutoString appInfo(pResult); + CoTaskMemFree(pResult); + if (NS_SUCCEEDED(GetDefaultAppInfo(appInfo, _retval, getter_AddRefs(app)))) + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol( + const nsACString& aScheme, bool* _retval) { + *_retval = false; + + NS_ENSURE_TRUE(mAppAssoc, NS_ERROR_NOT_AVAILABLE); + + NS_ConvertASCIItoUTF16 buf(aScheme); + + // Find the progID + wchar_t* pResult = nullptr; + HRESULT hr = mAppAssoc->QueryCurrentDefault(buf.get(), AT_URLPROTOCOL, + AL_EFFECTIVE, &pResult); + if (FAILED(hr)) { + return NS_ERROR_FAILURE; + } + nsAutoString progID(pResult); + // We are responsible for freeing returned strings. + CoTaskMemFree(pResult); + + // Find the default executable. + nsAutoString description; + nsCOMPtr<nsIFile> appFile; + nsresult rv = GetDefaultAppInfo(progID, description, getter_AddRefs(appFile)); + if (NS_FAILED(rv)) { + return rv; + } + // Determine if the default executable is our executable. + nsCOMPtr<nsIFile> ourBinary; + XRE_GetBinaryPath(getter_AddRefs(ourBinary)); + bool isSame = false; + rv = appFile->Equals(ourBinary, &isSame); + if (NS_FAILED(rv)) { + return rv; + } + *_retval = isSame; + return NS_OK; +} + +// GetMIMEInfoFromRegistry: This function obtains the values of some of the +// nsIMIMEInfo attributes for the mimeType/extension associated with the input +// registry key. The default entry for that key is the name of a registry key +// under HKEY_CLASSES_ROOT. The default value for *that* key is the descriptive +// name of the type. The EditFlags value is a binary value; the low order bit +// of the third byte of which indicates that the user does not need to be +// prompted. +// +// This function sets only the Description attribute of the input nsIMIMEInfo. +/* static */ +nsresult nsOSHelperAppService::GetMIMEInfoFromRegistry(const nsString& fileType, + nsIMIMEInfo* pInfo) { + nsresult rv = NS_OK; + + NS_ENSURE_ARG(pInfo); + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_NOT_AVAILABLE; + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileType, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + // OK, the default value here is the description of the type. + nsAutoString description; + rv = regKey->ReadStringValue(u""_ns, description); + if (NS_SUCCEEDED(rv)) pInfo->SetDescription(description); + + return NS_OK; +} + +///////////////////////////////////////////////////////////////////////////////////////////////// +// method overrides used to gather information from the windows registry for +// various mime types. +//////////////////////////////////////////////////////////////////////////////////////////////// + +/// Looks up the type for the extension aExt and compares it to aType +/* static */ +bool nsOSHelperAppService::typeFromExtEquals(const char16_t* aExt, + const char* aType) { + if (!aType) return false; + nsAutoString fileExtToUse; + if (aExt[0] != char16_t('.')) fileExtToUse = char16_t('.'); + + fileExtToUse.Append(aExt); + + bool eq = false; + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return eq; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return eq; + + nsAutoString type; + rv = regKey->ReadStringValue(u"Content Type"_ns, type); + if (NS_SUCCEEDED(rv)) eq = type.LowerCaseEqualsASCII(aType); + + return eq; +} + +// The "real" name of a given helper app (as specified by the path to the +// executable file held in various registry keys) is stored n the VERSIONINFO +// block in the file's resources. We need to find the path to the executable +// and then retrieve the "FileDescription" field value from the file. +nsresult nsOSHelperAppService::GetDefaultAppInfo( + const nsAString& aAppInfo, nsAString& aDefaultDescription, + nsIFile** aDefaultApplication) { + nsAutoString handlerCommand; + + // If all else fails, use the file type key name, which will be + // something like "pngfile" for .pngs, "WMVFile" for .wmvs, etc. + aDefaultDescription = aAppInfo; + *aDefaultApplication = nullptr; + + if (aAppInfo.IsEmpty()) return NS_ERROR_FAILURE; + + // aAppInfo may be a file, file path, program id, or + // Applications reference - + // c:\dir\app.exe + // Applications\appfile.exe/dll (shell\open...) + // ProgID.progid (shell\open...) + + nsAutoString handlerKeyName(aAppInfo); + + nsCOMPtr<nsIWindowsRegKey> chkKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!chkKey) return NS_ERROR_FAILURE; + + nsresult rv = + chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) { + // It's a file system path to a handler + handlerCommand.Assign(aAppInfo); + } else { + handlerKeyName.AppendLiteral("\\shell\\open\\command"); + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return NS_ERROR_FAILURE; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, handlerKeyName, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return NS_ERROR_FAILURE; + + // OK, the default value here is the description of the type. + rv = regKey->ReadStringValue(u""_ns, handlerCommand); + if (NS_FAILED(rv)) { + // Check if there is a DelegateExecute string + nsAutoString delegateExecute; + rv = regKey->ReadStringValue(u"DelegateExecute"_ns, delegateExecute); + NS_ENSURE_SUCCESS(rv, rv); + + // Look for InProcServer32 + nsAutoString delegateExecuteRegPath; + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\InProcServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_SUCCEEDED(rv)) { + rv = chkKey->ReadStringValue(u""_ns, handlerCommand); + } + + if (NS_FAILED(rv)) { + // Look for LocalServer32 + delegateExecuteRegPath.AssignLiteral("CLSID\\"); + delegateExecuteRegPath.Append(delegateExecute); + delegateExecuteRegPath.AppendLiteral("\\LocalServer32"); + rv = chkKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, + delegateExecuteRegPath, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + rv = chkKey->ReadStringValue(u""_ns, handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + // XXX FIXME: If this fails, the UI will display the full command + // string. + // There are some rare cases this can happen - ["url.dll" -foo] + // for example won't resolve correctly to the system dir. The + // subsequent launch of the helper app will work though. + nsCOMPtr<nsILocalFileWin> lf = new nsLocalFile(); + rv = lf->InitWithCommandLine(handlerCommand); + NS_ENSURE_SUCCESS(rv, rv); + lf.forget(aDefaultApplication); + + wchar_t friendlyName[1024]; + DWORD friendlyNameSize = 1024; + HRESULT hr = AssocQueryString(ASSOCF_NONE, ASSOCSTR_FRIENDLYAPPNAME, + PromiseFlatString(aAppInfo).get(), NULL, + friendlyName, &friendlyNameSize); + if (SUCCEEDED(hr) && friendlyNameSize > 1) { + aDefaultDescription.Assign(friendlyName, friendlyNameSize - 1); + } + + return NS_OK; +} + +already_AddRefed<nsMIMEInfoWin> nsOSHelperAppService::GetByExtension( + const nsString& aFileExt, const char* aTypeHint) { + if (aFileExt.IsEmpty()) return nullptr; + + // Determine the mime type. + nsAutoCString typeToUse; + if (aTypeHint && *aTypeHint) { + typeToUse.Assign(aTypeHint); + } else if (!GetMIMETypeFromOSForExtension(NS_ConvertUTF16toUTF8(aFileExt), + typeToUse)) { + return nullptr; + } + + RefPtr<nsMIMEInfoWin> mimeInfo = new nsMIMEInfoWin(typeToUse); + + // windows registry assumes your file extension is going to include the '.', + // but our APIs expect it to not be there, so make sure we normalize that bit. + nsAutoString fileExtToUse; + if (aFileExt.First() != char16_t('.')) fileExtToUse = char16_t('.'); + + fileExtToUse.Append(aFileExt); + + // don't append the '.' for our APIs. + nsAutoCString lowerFileExt = + NS_ConvertUTF16toUTF8(Substring(fileExtToUse, 1)); + ToLowerCase(lowerFileExt); + mimeInfo->AppendExtension(lowerFileExt); + mimeInfo->SetPreferredAction(nsIMIMEInfo::useSystemDefault); + + nsAutoString appInfo; + bool found; + + // Retrieve the default application for this extension + NS_ENSURE_TRUE(mAppAssoc, nullptr); + nsString assocType(fileExtToUse); + wchar_t* pResult = nullptr; + HRESULT hr = mAppAssoc->QueryCurrentDefault(assocType.get(), AT_FILEEXTENSION, + AL_EFFECTIVE, &pResult); + if (SUCCEEDED(hr)) { + found = true; + appInfo.Assign(pResult); + CoTaskMemFree(pResult); + } else { + found = false; + } + + // Bug 358297 - ignore the default handler, force the user to choose app + if (appInfo.EqualsLiteral("XPSViewer.Document")) found = false; + + if (!found) { + return nullptr; + } + + // Get other nsIMIMEInfo fields from registry, if possible. + nsAutoString defaultDescription; + nsCOMPtr<nsIFile> defaultApplication; + + if (NS_FAILED(GetDefaultAppInfo(appInfo, defaultDescription, + getter_AddRefs(defaultApplication)))) { + return nullptr; + } + + mimeInfo->SetDefaultDescription(defaultDescription); + mimeInfo->SetDefaultApplicationHandler(defaultApplication); + + // Grab the general description + GetMIMEInfoFromRegistry(appInfo, mimeInfo); + + return mimeInfo.forget(); +} + +NS_IMETHODIMP +nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, + bool* aFound, nsIMIMEInfo** aMIMEInfo) { + *aFound = false; + + const nsCString& flatType = PromiseFlatCString(aMIMEType); + nsAutoString fileExtension; + CopyUTF8toUTF16(aFileExt, fileExtension); + + /* XXX The octet-stream check is a gross hack to wallpaper over the most + * common Win32 extension issues caused by the fix for bug 116938. See bug + * 120327, comment 271 for why this is needed. Not even sure we + * want to remove this once we have fixed all this stuff to work + * right; any info we get from the OS on this type is pretty much + * useless.... + */ + bool haveMeaningfulMimeType = + !aMIMEType.IsEmpty() && + !aMIMEType.LowerCaseEqualsLiteral(APPLICATION_OCTET_STREAM); + LOG(("Extension lookup on '%s' with mimetype '%s'%s\n", fileExtension.get(), + flatType.get(), + haveMeaningfulMimeType ? " (treated as meaningful)" : "")); + + RefPtr<nsMIMEInfoWin> mi; + + // We should have *something* to go on here. + nsAutoString extensionFromMimeType; + if (haveMeaningfulMimeType) { + GetExtensionFromWindowsMimeDatabase(aMIMEType, extensionFromMimeType); + } + if (fileExtension.IsEmpty() && extensionFromMimeType.IsEmpty()) { + // Without an extension from the mimetype or the file, we can't + // do anything here. + mi = new nsMIMEInfoWin(flatType.get()); + mi.forget(aMIMEInfo); + return NS_OK; + } + + // Either fileExtension or extensionFromMimeType must now be non-empty. + + *aFound = true; + + // On Windows, we prefer the file extension for lookups over the mimetype, + // because that's how windows does things. + // If we have no file extension or it doesn't match the mimetype, use the + // mime type's default file extension instead. + bool usedMimeTypeExtensionForLookup = false; + if (fileExtension.IsEmpty() || + (!extensionFromMimeType.IsEmpty() && + !typeFromExtEquals(fileExtension.get(), flatType.get()))) { + usedMimeTypeExtensionForLookup = true; + fileExtension = extensionFromMimeType; + LOG(("Now using '%s' mimetype's default file extension '%s' for lookup\n", + flatType.get(), fileExtension.get())); + } + + // If we have an extension, use it for lookup: + mi = GetByExtension(fileExtension, flatType.get()); + LOG(("Extension lookup on '%s' found: 0x%p\n", fileExtension.get(), + mi.get())); + + if (mi) { + bool hasDefault = false; + mi->GetHasDefaultHandler(&hasDefault); + // If we don't find a default handler description, see if we can find one + // using the mimetype. + if (!hasDefault && !usedMimeTypeExtensionForLookup) { + RefPtr<nsMIMEInfoWin> miFromMimeType = + GetByExtension(extensionFromMimeType, flatType.get()); + LOG(("Mime-based ext. lookup for '%s' found 0x%p\n", + extensionFromMimeType.get(), miFromMimeType.get())); + if (miFromMimeType) { + nsAutoString desc; + miFromMimeType->GetDefaultDescription(desc); + mi->SetDefaultDescription(desc); + } + } + mi.forget(aMIMEInfo); + return NS_OK; + } + + // The extension didn't work. Try the extension from the mimetype if + // different: + if (!extensionFromMimeType.IsEmpty() && !usedMimeTypeExtensionForLookup) { + mi = GetByExtension(extensionFromMimeType, flatType.get()); + LOG(("Mime-based ext. lookup for '%s' found 0x%p\n", + extensionFromMimeType.get(), mi.get())); + } + if (mi) { + mi.forget(aMIMEInfo); + return NS_OK; + } + // This didn't work either, so just return an empty dummy mimeinfo. + *aFound = false; + mi = new nsMIMEInfoWin(flatType.get()); + // If we didn't resort to the mime type's extension, we must have had a + // valid extension, so stick its lowercase version on the mime info. + if (!usedMimeTypeExtensionForLookup) { + nsAutoCString lowerFileExt; + ToLowerCase(aFileExt, lowerFileExt); + mi->AppendExtension(lowerFileExt); + } + mi.forget(aMIMEInfo); + return NS_OK; +} + +NS_IMETHODIMP +nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval) { + NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!"); + + nsresult rv = + OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found); + if (NS_FAILED(rv)) return rv; + + nsMIMEInfoWin* handlerInfo = + new nsMIMEInfoWin(aScheme, nsMIMEInfoBase::eProtocolInfo); + NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY); + NS_ADDREF(*_retval = handlerInfo); + + if (!*found) { + // Code that calls this requires an object regardless if the OS has + // something for us, so we return the empty object. + return NS_OK; + } + + nsAutoString desc; + GetApplicationDescription(aScheme, desc); + handlerInfo->SetDefaultDescription(desc); + + return NS_OK; +} + +bool nsOSHelperAppService::GetMIMETypeFromOSForExtension( + const nsACString& aExtension, nsACString& aMIMEType) { + if (aExtension.IsEmpty()) return false; + + // windows registry assumes your file extension is going to include the '.'. + // so make sure it's there... + nsAutoString fileExtToUse; + if (aExtension.First() != '.') fileExtToUse = char16_t('.'); + + AppendUTF8toUTF16(aExtension, fileExtToUse); + + // Try to get an entry from the windows registry. + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1"); + if (!regKey) return false; + + nsresult rv = + regKey->Open(nsIWindowsRegKey::ROOT_KEY_CLASSES_ROOT, fileExtToUse, + nsIWindowsRegKey::ACCESS_QUERY_VALUE); + if (NS_FAILED(rv)) return false; + + nsAutoString mimeType; + if (NS_FAILED(regKey->ReadStringValue(u"Content Type"_ns, mimeType)) || + mimeType.IsEmpty()) { + return false; + } + // Content-Type is always in ASCII + aMIMEType.Truncate(); + LossyAppendUTF16toASCII(mimeType, aMIMEType); + return true; +} diff --git a/uriloader/exthandler/win/nsOSHelperAppService.h b/uriloader/exthandler/win/nsOSHelperAppService.h new file mode 100644 index 0000000000..0b5cbc21e5 --- /dev/null +++ b/uriloader/exthandler/win/nsOSHelperAppService.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOSHelperAppService_h__ +#define nsOSHelperAppService_h__ + +// The OS helper app service is a subclass of nsExternalHelperAppService and is +// implemented on each platform. It contains platform specific code for finding +// helper applications for a given mime type in addition to launching those +// applications. + +#include "nsExternalHelperAppService.h" +#include "nsCExternalHandlerService.h" +#include "nsCOMPtr.h" +#include <windows.h> + +#ifdef _WIN32_WINNT +# undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 +#include <shlobj.h> + +class nsMIMEInfoWin; +class nsIMIMEInfo; + +class nsOSHelperAppService : public nsExternalHelperAppService { + public: + nsOSHelperAppService(); + virtual ~nsOSHelperAppService(); + + // override nsIExternalProtocolService methods + NS_IMETHOD OSProtocolHandlerExists(const char* aProtocolScheme, + bool* aHandlerExists) override; + nsresult LoadUriInternal(nsIURI* aURL); + NS_IMETHOD GetApplicationDescription(const nsACString& aScheme, + nsAString& _retval) override; + + NS_IMETHOD IsCurrentAppOSDefaultForProtocol(const nsACString& aScheme, + bool* _retval) override; + + // method overrides for windows registry look up steps.... + NS_IMETHOD GetMIMEInfoFromOS(const nsACString& aMIMEType, + const nsACString& aFileExt, bool* aFound, + nsIMIMEInfo** aMIMEInfo) override; + NS_IMETHOD GetProtocolHandlerInfoFromOS(const nsACString& aScheme, + bool* found, + nsIHandlerInfo** _retval); + virtual bool GetMIMETypeFromOSForExtension(const nsACString& aExtension, + nsACString& aMIMEType) override; + + /** Get the string value of a registry value and store it in result. + * @return true on success, false on failure + */ + static bool GetValueString(HKEY hKey, const char16_t* pValueName, + nsAString& result); + + protected: + nsresult GetDefaultAppInfo(const nsAString& aTypeName, + nsAString& aDefaultDescription, + nsIFile** aDefaultApplication); + // Lookup a mime info by extension, using an optional type hint + already_AddRefed<nsMIMEInfoWin> GetByExtension( + const nsString& aFileExt, const char* aTypeHint = nullptr); + nsresult FindOSMimeInfoForType(const char* aMimeContentType, nsIURI* aURI, + char** aFileExtension, + nsIMIMEInfo** aMIMEInfo); + + static nsresult GetMIMEInfoFromRegistry(const nsString& fileType, + nsIMIMEInfo* pInfo); + /// Looks up the type for the extension aExt and compares it to aType + static bool typeFromExtEquals(const char16_t* aExt, const char* aType); + + private: + IApplicationAssociationRegistration* mAppAssoc; +}; + +#endif // nsOSHelperAppService_h__ diff --git a/uriloader/moz.build b/uriloader/moz.build new file mode 100644 index 0000000000..07659656c6 --- /dev/null +++ b/uriloader/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +SPHINX_TREES["/uriloader"] = "docs" + +with Files("**"): + BUG_COMPONENT = ("Firefox", "File Handling") + +DIRS += [ + "base", + "exthandler", + "prefetch", + "preload", +] diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.cpp b/uriloader/prefetch/OfflineCacheUpdateChild.cpp new file mode 100644 index 0000000000..10907acc34 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateChild.cpp @@ -0,0 +1,472 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BackgroundUtils.h" +#include "OfflineCacheUpdateChild.h" +#include "nsOfflineCacheUpdate.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/OfflineResourceListBinding.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoCommon.h" + +#include "nsIApplicationCacheChannel.h" +#include "nsIDocShell.h" +#include "nsPIDOMWindow.h" +#include "mozilla/dom/Document.h" +#include "mozilla/net/CookieJarSettings.h" +#include "nsIObserverService.h" +#include "nsIBrowserChild.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Logging.h" +#include "nsApplicationCache.h" + +using namespace mozilla::ipc; +using namespace mozilla::net; +using mozilla::dom::BrowserChild; +using mozilla::dom::ContentChild; +using mozilla::dom::Document; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +extern mozilla::LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) \ + MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() \ + MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +namespace mozilla { +namespace docshell { + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateChild::nsISupports +//----------------------------------------------------------------------------- + +NS_INTERFACE_MAP_BEGIN(OfflineCacheUpdateChild) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIOfflineCacheUpdate) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(OfflineCacheUpdateChild) +NS_IMPL_RELEASE(OfflineCacheUpdateChild) + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateChild <public> +//----------------------------------------------------------------------------- + +OfflineCacheUpdateChild::OfflineCacheUpdateChild(nsPIDOMWindowInner* aWindow) + : mState(STATE_UNINITIALIZED), + mIsUpgrade(false), + mSucceeded(false), + mWindow(aWindow), + mByteProgress(0) {} + +OfflineCacheUpdateChild::~OfflineCacheUpdateChild() { + LOG(("OfflineCacheUpdateChild::~OfflineCacheUpdateChild [%p]", this)); +} + +void OfflineCacheUpdateChild::GatherObservers( + nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers) { + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer) + aObservers.AppendObject(observer); + else + mWeakObservers.RemoveObjectAt(i--); + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + aObservers.AppendObject(mObservers[i]); + } +} + +void OfflineCacheUpdateChild::SetDocument(Document* aDocument) { + // The design is one document for one cache update on the content process. + NS_ASSERTION( + !mDocument, + "Setting more then a single document on a child offline cache update"); + + LOG(("Document %p added to update child %p", aDocument, this)); + + // Add document only if it was not loaded from an offline cache. + // If it were loaded from an offline cache then it has already + // been associated with it and must not be again cached as + // implicit (which are the reasons we collect documents here). + if (!aDocument) return; + + mCookieJarSettings = aDocument->CookieJarSettings(); + + nsIChannel* channel = aDocument->GetChannel(); + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(channel); + if (!appCacheChannel) return; + + bool loadedFromAppCache; + appCacheChannel->GetLoadedFromApplicationCache(&loadedFromAppCache); + if (loadedFromAppCache) return; + + mDocument = aDocument; +} + +nsresult OfflineCacheUpdateChild::AssociateDocument( + Document* aDocument, nsIApplicationCache* aApplicationCache) { + // Check that the document that requested this update was + // previously associated with an application cache. If not, it + // should be associated with the new one. + nsCOMPtr<nsIApplicationCache> existingCache; + nsresult rv = aDocument->GetApplicationCache(getter_AddRefs(existingCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!existingCache) { + if (LOG_ENABLED()) { + nsAutoCString clientID; + if (aApplicationCache) { + aApplicationCache->GetClientID(clientID); + } + LOG(("Update %p: associating app cache %s to document %p", this, + clientID.get(), aDocument)); + } + + rv = aDocument->SetApplicationCache(aApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateChild::nsIOfflineCacheUpdate +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +OfflineCacheUpdateChild::Init(nsIURI* aManifestURI, nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + Document* aDocument, nsIFile* aCustomProfileDir) { + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) return NS_ERROR_FAILURE; + + if (aCustomProfileDir) { + NS_ERROR("Custom Offline Cache Update not supported on child process"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + LOG(("OfflineCacheUpdateChild::Init [%p]", this)); + + // Only http and https applications are supported. + if (!aManifestURI->SchemeIs("http") && !aManifestURI->SchemeIs("https")) { + return NS_ERROR_ABORT; + } + + mManifestURI = aManifestURI; + + rv = mManifestURI->GetAsciiHost(mUpdateDomain); + NS_ENSURE_SUCCESS(rv, rv); + + mDocumentURI = aDocumentURI; + mLoadingPrincipal = aLoadingPrincipal; + + mState = STATE_INITIALIZED; + + if (aDocument) SetDocument(aDocument); + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::InitPartial(nsIURI* aManifestURI, + const nsACString& clientID, + nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + nsICookieJarSettings* aCookieJarSettings) { + MOZ_ASSERT_UNREACHABLE( + "Not expected to do partial offline cache updates" + " on the child process"); + // For now leaving this method, we may discover we need it. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::InitForUpdateCheck(nsIURI* aManifestURI, + nsIPrincipal* aLoadingPrincipal, + nsIObserver* aObserver) { + MOZ_ASSERT_UNREACHABLE( + "Not expected to do only update checks" + " from the child process"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetUpdateDomain(nsACString& aUpdateDomain) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + aUpdateDomain = mUpdateDomain; + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetStatus(uint16_t* aStatus) { + switch (mState) { + case STATE_CHECKING: + *aStatus = mozilla::dom::OfflineResourceList_Binding::CHECKING; + return NS_OK; + case STATE_DOWNLOADING: + *aStatus = mozilla::dom::OfflineResourceList_Binding::DOWNLOADING; + return NS_OK; + default: + *aStatus = mozilla::dom::OfflineResourceList_Binding::IDLE; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetPartial(bool* aPartial) { + *aPartial = false; + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetLoadingPrincipal(nsIPrincipal** aLoadingPrincipal) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + NS_IF_ADDREF(*aLoadingPrincipal = mLoadingPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetManifestURI(nsIURI** aManifestURI) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + NS_IF_ADDREF(*aManifestURI = mManifestURI); + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetSucceeded(bool* aSucceeded) { + NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE); + + *aSucceeded = mSucceeded; + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetIsUpgrade(bool* aIsUpgrade) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + *aIsUpgrade = mIsUpgrade; + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::AddDynamicURI(nsIURI* aURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::Cancel() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +OfflineCacheUpdateChild::AddObserver(nsIOfflineCacheUpdateObserver* aObserver, + bool aHoldWeak) { + LOG(("OfflineCacheUpdateChild::AddObserver [%p]", this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + if (aHoldWeak) { + nsWeakPtr weakRef = do_GetWeakReference(aObserver); + mWeakObservers.AppendObject(weakRef); + } else { + mObservers.AppendObject(aObserver); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::RemoveObserver( + nsIOfflineCacheUpdateObserver* aObserver) { + LOG(("OfflineCacheUpdateChild::RemoveObserver [%p]", this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer == aObserver) { + mWeakObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + if (mObservers[i] == aObserver) { + mObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::GetByteProgress(uint64_t* _result) { + NS_ENSURE_ARG(_result); + + *_result = mByteProgress; + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateChild::Schedule() { + LOG(("OfflineCacheUpdateChild::Schedule [%p]", this)); + + NS_ASSERTION(mWindow, + "Window must be provided to the offline cache update child"); + + nsCOMPtr<nsPIDOMWindowInner> window = std::move(mWindow); + nsCOMPtr<nsIDocShell> docshell = window->GetDocShell(); + if (!docshell) { + NS_WARNING("doc shell tree item is null"); + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_OK; + PrincipalInfo loadingPrincipalInfo; + rv = PrincipalToPrincipalInfo(mLoadingPrincipal, &loadingPrincipalInfo); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-added")); + observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-added", nullptr); + LOG(("Done offline-cache-update-added")); + } + + // mDocument is non-null if both: + // 1. this update was initiated by a document that referred a manifest + // 2. the document has not already been loaded from the application cache + // This tells the update to cache this document even in case the manifest + // has not been changed since the last fetch. + // See also nsOfflineCacheUpdate::ScheduleImplicit. + bool stickDocument = mDocument != nullptr; + + CookieJarSettingsArgs csArgs; + if (mCookieJarSettings) { + CookieJarSettings::Cast(mCookieJarSettings)->Serialize(csArgs); + } + + ContentChild::GetSingleton()->SendPOfflineCacheUpdateConstructor( + this, mManifestURI, mDocumentURI, loadingPrincipalInfo, stickDocument, + csArgs); + + return NS_OK; +} + +mozilla::ipc::IPCResult OfflineCacheUpdateChild::RecvAssociateDocuments( + const nsCString& cacheGroupId, const nsCString& cacheClientId) { + LOG(("OfflineCacheUpdateChild::RecvAssociateDocuments [%p, cache=%s]", this, + cacheClientId.get())); + + nsCOMPtr<nsIApplicationCache> cache = new nsApplicationCache(); + + cache->InitAsHandle(cacheGroupId, cacheClientId); + + if (mDocument) { + AssociateDocument(mDocument, cache); + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) + observers[i]->ApplicationCacheAvailable(cache); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult OfflineCacheUpdateChild::RecvNotifyStateEvent( + const uint32_t& event, const uint64_t& byteProgress) { + LOG(("OfflineCacheUpdateChild::RecvNotifyStateEvent [%p]", this)); + + mByteProgress = byteProgress; + + // Convert the public observer state to our internal state + switch (event) { + case nsIOfflineCacheUpdateObserver::STATE_CHECKING: + mState = STATE_CHECKING; + break; + + case nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING: + mState = STATE_DOWNLOADING; + break; + + default: + break; + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) + observers[i]->UpdateStateChanged(this, event); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult OfflineCacheUpdateChild::RecvFinish( + const bool& succeeded, const bool& isUpgrade) { + LOG(("OfflineCacheUpdateChild::RecvFinish [%p]", this)); + + RefPtr<OfflineCacheUpdateChild> kungFuDeathGrip(this); + + mState = STATE_FINISHED; + mSucceeded = succeeded; + mIsUpgrade = isUpgrade; + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-completed")); + observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-completed", nullptr); + LOG(("Done offline-cache-update-completed")); + } + + // This is by contract the last notification from the parent, release + // us now. This is corresponding to AddRef in Schedule(). + // BrowserChild::DeallocPOfflineCacheUpdate will call Release. + OfflineCacheUpdateChild::Send__delete__(this); + + return IPC_OK(); +} + +} // namespace docshell +} // namespace mozilla diff --git a/uriloader/prefetch/OfflineCacheUpdateChild.h b/uriloader/prefetch/OfflineCacheUpdateChild.h new file mode 100644 index 0000000000..4099c9dfdb --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateChild.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdateChild_h +#define nsOfflineCacheUpdateChild_h + +#include "mozilla/docshell/POfflineCacheUpdateChild.h" +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIURI.h" +#include "nsIWeakReference.h" +#include "nsString.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { +class Document; +} + +namespace docshell { + +class OfflineCacheUpdateChild : public nsIOfflineCacheUpdate, + public POfflineCacheUpdateChild { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATE + + mozilla::ipc::IPCResult RecvNotifyStateEvent(const uint32_t& stateEvent, + const uint64_t& byteProgress); + + mozilla::ipc::IPCResult RecvAssociateDocuments( + const nsCString& cacheGroupId, const nsCString& cacheClientId); + + mozilla::ipc::IPCResult RecvFinish(const bool& succeeded, + const bool& isUpgrade); + + explicit OfflineCacheUpdateChild(nsPIDOMWindowInner* aWindow); + + void SetDocument(dom::Document* aDocument); + + private: + ~OfflineCacheUpdateChild(); + + nsresult AssociateDocument(dom::Document* aDocument, + nsIApplicationCache* aApplicationCache); + void GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers); + nsresult Finish(); + + enum { + STATE_UNINITIALIZED, + STATE_INITIALIZED, + STATE_CHECKING, + STATE_DOWNLOADING, + STATE_CANCELLED, + STATE_FINISHED + } mState; + + bool mIsUpgrade; + bool mSucceeded; + + nsCString mUpdateDomain; + nsCOMPtr<nsIURI> mManifestURI; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + + nsCOMPtr<nsIObserverService> mObserverService; + + /* Clients watching this update for changes */ + nsCOMArray<nsIWeakReference> mWeakObservers; + nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers; + + /* Document that requested this update */ + nsCOMPtr<dom::Document> mDocument; + + /* Keep reference to the window that owns this update to call the + parent offline cache update construcor */ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + + uint64_t mByteProgress; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/OfflineCacheUpdateGlue.cpp b/uriloader/prefetch/OfflineCacheUpdateGlue.cpp new file mode 100644 index 0000000000..85515b5547 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateGlue.cpp @@ -0,0 +1,220 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OfflineCacheUpdateGlue.h" +#include "nsOfflineCacheUpdate.h" +#include "mozilla/Services.h" + +#include "nsIApplicationCache.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIChannel.h" +#include "mozilla/dom/Document.h" +#include "mozilla/Logging.h" + +using mozilla::dom::Document; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Info level information and places all output in +// the file offlineupdate.log +// +extern mozilla::LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) \ + MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() \ + MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +namespace mozilla { +namespace docshell { + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateGlue::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(OfflineCacheUpdateGlue, nsIOfflineCacheUpdate, + nsIOfflineCacheUpdateObserver, nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateGlue <public> +//----------------------------------------------------------------------------- + +OfflineCacheUpdateGlue::OfflineCacheUpdateGlue() : mCoalesced(false) { + LOG(("OfflineCacheUpdateGlue::OfflineCacheUpdateGlue [%p]", this)); +} + +OfflineCacheUpdateGlue::~OfflineCacheUpdateGlue() { + LOG(("OfflineCacheUpdateGlue::~OfflineCacheUpdateGlue [%p]", this)); +} + +nsIOfflineCacheUpdate* OfflineCacheUpdateGlue::EnsureUpdate() { + if (!mUpdate) { + mUpdate = new nsOfflineCacheUpdate(); + LOG(("OfflineCacheUpdateGlue [%p] is using update [%p]", this, + mUpdate.get())); + + mUpdate->SetCookieJarSettings(mCookieJarSettings); + } + + return mUpdate; +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::Schedule() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-added")); + observerService->NotifyObservers(static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-added", nullptr); + LOG(("Done offline-cache-update-added")); + } + + if (!EnsureUpdate()) return NS_ERROR_NULL_POINTER; + + // Do not use weak reference, we must survive! + mUpdate->AddObserver(this, false); + + if (mCoalesced) // already scheduled + return NS_OK; + + return mUpdate->Schedule(); +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::Init(nsIURI* aManifestURI, nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + Document* aDocument, nsIFile* aCustomProfileDir) { + nsresult rv; + + nsAutoCString originSuffix; + rv = aLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (service) { + service->FindUpdate(aManifestURI, originSuffix, aCustomProfileDir, + getter_AddRefs(mUpdate)); + mCoalesced = !!mUpdate; + } + + if (!EnsureUpdate()) return NS_ERROR_NULL_POINTER; + + mDocumentURI = aDocumentURI; + mLoadingPrincipal = aLoadingPrincipal; + + if (aDocument) SetDocument(aDocument); + + if (mCoalesced) { // already initialized + LOG(("OfflineCacheUpdateGlue %p coalesced with update %p", this, + mUpdate.get())); + return NS_OK; + } + + rv = mUpdate->Init(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr, + aCustomProfileDir); + + mUpdate->SetCookieJarSettings(mCookieJarSettings); + + return rv; +} + +void OfflineCacheUpdateGlue::SetDocument(Document* aDocument) { + // The design is one document for one cache update on the content process. + NS_ASSERTION(!mDocument, + "Setting more then a single document on an instance of " + "OfflineCacheUpdateGlue"); + + LOG(("Document %p added to update glue %p", aDocument, this)); + + // Add document only if it was not loaded from an offline cache. + // If it were loaded from an offline cache then it has already + // been associated with it and must not be again cached as + // implicit (which are the reasons we collect documents here). + if (!aDocument) return; + + mCookieJarSettings = aDocument->CookieJarSettings(); + + nsIChannel* channel = aDocument->GetChannel(); + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(channel); + if (!appCacheChannel) return; + + bool loadedFromAppCache; + appCacheChannel->GetLoadedFromApplicationCache(&loadedFromAppCache); + if (loadedFromAppCache) return; + + if (EnsureUpdate()) { + mUpdate->StickDocument(mDocumentURI); + } + + mDocument = aDocument; +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::UpdateStateChanged(nsIOfflineCacheUpdate* aUpdate, + uint32_t state) { + if (state == nsIOfflineCacheUpdateObserver::STATE_FINISHED) { + LOG(("OfflineCacheUpdateGlue got STATE_FINISHED [%p]", this)); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + LOG(("Calling offline-cache-update-completed")); + observerService->NotifyObservers( + static_cast<nsIOfflineCacheUpdate*>(this), + "offline-cache-update-completed", nullptr); + LOG(("Done offline-cache-update-completed")); + } + + aUpdate->RemoveObserver(this); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateGlue::ApplicationCacheAvailable( + nsIApplicationCache* aApplicationCache) { + NS_ENSURE_ARG(aApplicationCache); + + // Check that the document that requested this update was + // previously associated with an application cache. If not, it + // should be associated with the new one. + if (!mDocument) { + return NS_OK; + } + + nsCOMPtr<nsIApplicationCache> existingCache; + nsresult rv = mDocument->GetApplicationCache(getter_AddRefs(existingCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!existingCache) { + if (LOG_ENABLED()) { + nsAutoCString clientID; + if (aApplicationCache) { + aApplicationCache->GetClientID(clientID); + } + LOG(("Update %p: associating app cache %s to document %p", this, + clientID.get(), mDocument.get())); + } + + rv = mDocument->SetApplicationCache(aApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +} // namespace docshell +} // namespace mozilla diff --git a/uriloader/prefetch/OfflineCacheUpdateGlue.h b/uriloader/prefetch/OfflineCacheUpdateGlue.h new file mode 100644 index 0000000000..7c1d08cc73 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateGlue.h @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdateGlue_h +#define nsOfflineCacheUpdateGlue_h + +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" + +class nsOfflineCacheUpdate; + +namespace mozilla { +namespace dom { +class Document; +} + +namespace docshell { + +// Like FORWARD_SAFE except methods: +// Schedule +// Init +#define NS_ADJUSTED_FORWARD_NSIOFFLINECACHEUPDATE(_to) \ + NS_IMETHOD GetStatus(uint16_t* aStatus) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetStatus(aStatus); \ + } \ + NS_IMETHOD GetPartial(bool* aPartial) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetPartial(aPartial); \ + } \ + NS_IMETHOD GetIsUpgrade(bool* aIsUpgrade) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetIsUpgrade(aIsUpgrade); \ + } \ + NS_IMETHOD GetUpdateDomain(nsACString& aUpdateDomain) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetUpdateDomain(aUpdateDomain); \ + } \ + NS_IMETHOD GetLoadingPrincipal(nsIPrincipal** aLoadingPrincipal) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->GetLoadingPrincipal(aLoadingPrincipal); \ + } \ + NS_IMETHOD GetManifestURI(nsIURI** aManifestURI) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetManifestURI(aManifestURI); \ + } \ + NS_IMETHOD GetSucceeded(bool* aSucceeded) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetSucceeded(aSucceeded); \ + } \ + NS_IMETHOD InitPartial(nsIURI* aManifestURI, const nsACString& aClientID, \ + nsIURI* aDocumentURI, \ + nsIPrincipal* aLoadingPrincipal, \ + nsICookieJarSettings* aCookieJarSettings) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->InitPartial(aManifestURI, aClientID, aDocumentURI, \ + aLoadingPrincipal, aCookieJarSettings); \ + } \ + NS_IMETHOD InitForUpdateCheck(nsIURI* aManifestURI, \ + nsIPrincipal* aLoadingPrincipal, \ + nsIObserver* aObserver) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->InitForUpdateCheck(aManifestURI, aLoadingPrincipal, \ + aObserver); \ + } \ + NS_IMETHOD AddDynamicURI(nsIURI* aURI) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->AddDynamicURI(aURI); \ + } \ + NS_IMETHOD AddObserver(nsIOfflineCacheUpdateObserver* aObserver, \ + bool aHoldWeak) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->AddObserver(aObserver, aHoldWeak); \ + } \ + NS_IMETHOD RemoveObserver(nsIOfflineCacheUpdateObserver* aObserver) \ + override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->RemoveObserver(aObserver); \ + } \ + NS_IMETHOD GetByteProgress(uint64_t* _result) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetByteProgress(_result); \ + } \ + NS_IMETHOD Cancel() override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->Cancel(); \ + } + +class OfflineCacheUpdateGlue final : public nsSupportsWeakReference, + public nsIOfflineCacheUpdate, + public nsIOfflineCacheUpdateObserver { + public: + NS_DECL_ISUPPORTS + + private: + nsIOfflineCacheUpdate* EnsureUpdate(); + + public: + NS_ADJUSTED_FORWARD_NSIOFFLINECACHEUPDATE(EnsureUpdate()) + NS_IMETHOD Schedule(void) override; + NS_IMETHOD Init(nsIURI* aManifestURI, nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + mozilla::dom::Document* aDocument, + nsIFile* aCustomProfileDir) override; + + NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER + + OfflineCacheUpdateGlue(); + + void SetDocument(mozilla::dom::Document* aDocument); + + private: + ~OfflineCacheUpdateGlue(); + + RefPtr<nsOfflineCacheUpdate> mUpdate; + bool mCoalesced; + + /* Document that requested this update */ + RefPtr<mozilla::dom::Document> mDocument; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/OfflineCacheUpdateParent.cpp b/uriloader/prefetch/OfflineCacheUpdateParent.cpp new file mode 100644 index 0000000000..3414617385 --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateParent.cpp @@ -0,0 +1,287 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OfflineCacheUpdateParent.h" + +#include "BackgroundUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsOfflineCacheUpdate.h" +#include "nsIApplicationCache.h" +#include "nsNetUtil.h" + +using namespace mozilla::ipc; +using mozilla::BasePrincipal; +using mozilla::OriginAttributes; +using mozilla::dom::BrowserParent; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +extern mozilla::LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) \ + MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() \ + MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +namespace mozilla { +namespace docshell { + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateParent::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(OfflineCacheUpdateParent, nsIOfflineCacheUpdateObserver, + nsILoadContext) + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateParent <public> +//----------------------------------------------------------------------------- + +OfflineCacheUpdateParent::OfflineCacheUpdateParent() : mIPCClosed(false) { + // Make sure the service has been initialized + nsOfflineCacheUpdateService::EnsureService(); + + LOG(("OfflineCacheUpdateParent::OfflineCacheUpdateParent [%p]", this)); +} + +OfflineCacheUpdateParent::~OfflineCacheUpdateParent() { + LOG(("OfflineCacheUpdateParent::~OfflineCacheUpdateParent [%p]", this)); +} + +void OfflineCacheUpdateParent::ActorDestroy(ActorDestroyReason why) { + mIPCClosed = true; +} + +nsresult OfflineCacheUpdateParent::Schedule( + nsIURI* aManifestURI, nsIURI* aDocumentURI, + const PrincipalInfo& aLoadingPrincipalInfo, const bool& stickDocument, + const CookieJarSettingsArgs& aCookieJarSettingsArgs) { + LOG(("OfflineCacheUpdateParent::RecvSchedule [%p]", this)); + + RefPtr<nsOfflineCacheUpdate> update; + if (!aManifestURI) { + return NS_ERROR_FAILURE; + } + + auto loadingPrincipalOrErr = PrincipalInfoToPrincipal(aLoadingPrincipalInfo); + + if (NS_WARN_IF(loadingPrincipalOrErr.isErr())) { + return loadingPrincipalOrErr.unwrapErr(); + } + + mLoadingPrincipal = loadingPrincipalOrErr.unwrap(); + + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) { + return NS_ERROR_FAILURE; + } + + bool offlinePermissionAllowed = false; + + nsresult rv = + service->OfflineAppAllowed(mLoadingPrincipal, &offlinePermissionAllowed); + NS_ENSURE_SUCCESS(rv, rv); + + if (!offlinePermissionAllowed) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + if (!aDocumentURI) { + return NS_ERROR_FAILURE; + } + + if (!NS_SecurityCompareURIs(aManifestURI, aDocumentURI, false)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsAutoCString originSuffix; + rv = mLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + service->FindUpdate(aManifestURI, originSuffix, nullptr, + getter_AddRefs(update)); + if (!update) { + update = new nsOfflineCacheUpdate(); + + // Leave aDocument argument null. Only glues and children keep + // document instances. + rv = update->Init(aManifestURI, aDocumentURI, mLoadingPrincipal, nullptr, + nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + update->SetCookieJarSettingsArgs(aCookieJarSettingsArgs); + + // Must add before Schedule() call otherwise we would miss + // oncheck event notification. + update->AddObserver(this, false); + + rv = update->Schedule(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + update->AddObserver(this, false); + } + + if (stickDocument) { + update->StickDocument(aDocumentURI); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::UpdateStateChanged(nsIOfflineCacheUpdate* aUpdate, + uint32_t state) { + if (mIPCClosed) { + return NS_ERROR_UNEXPECTED; + } + + LOG(("OfflineCacheUpdateParent::StateEvent [%p]", this)); + + uint64_t byteProgress; + aUpdate->GetByteProgress(&byteProgress); + Unused << SendNotifyStateEvent(state, byteProgress); + + if (state == nsIOfflineCacheUpdateObserver::STATE_FINISHED) { + // Tell the child the particulars after the update has finished. + // Sending the Finish event will release the child side of the protocol + // and notify "offline-cache-update-completed" on the child process. + bool isUpgrade; + aUpdate->GetIsUpgrade(&isUpgrade); + bool succeeded; + aUpdate->GetSucceeded(&succeeded); + + Unused << SendFinish(succeeded, isUpgrade); + } + + return NS_OK; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::ApplicationCacheAvailable( + nsIApplicationCache* aApplicationCache) { + if (mIPCClosed) { + return NS_ERROR_UNEXPECTED; + } + + NS_ENSURE_ARG(aApplicationCache); + + nsCString cacheClientId; + aApplicationCache->GetClientID(cacheClientId); + nsCString cacheGroupId; + aApplicationCache->GetGroupID(cacheGroupId); + + Unused << SendAssociateDocuments(cacheGroupId, cacheClientId); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// OfflineCacheUpdateParent::nsILoadContext +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetAssociatedWindow( + mozIDOMWindowProxy** aAssociatedWindow) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetTopWindow(mozIDOMWindowProxy** aTopWindow) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetTopFrameElement(dom::Element** aElement) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetIsContent(bool* aIsContent) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetUsePrivateBrowsing(bool* aUsePrivateBrowsing) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP +OfflineCacheUpdateParent::SetUsePrivateBrowsing(bool aUsePrivateBrowsing) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::SetPrivateBrowsing(bool aUsePrivateBrowsing) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetUseRemoteTabs(bool* aUseRemoteTabs) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::SetRemoteTabs(bool aUseRemoteTabs) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetUseRemoteSubframes(bool* aUseRemoteSubframes) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::SetRemoteSubframes(bool aUseRemoteSubframes) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetScriptableOriginAttributes( + JSContext* aCx, JS::MutableHandleValue aAttrs) { + NS_ENSURE_TRUE(mLoadingPrincipal, NS_ERROR_UNEXPECTED); + + nsresult rv = mLoadingPrincipal->GetOriginAttributes(aCx, aAttrs); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP_(void) +OfflineCacheUpdateParent::GetOriginAttributes( + mozilla::OriginAttributes& aAttrs) { + if (mLoadingPrincipal) { + aAttrs = mLoadingPrincipal->OriginAttributesRef(); + } +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::GetUseTrackingProtection( + bool* aUseTrackingProtection) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OfflineCacheUpdateParent::SetUseTrackingProtection( + bool aUseTrackingProtection) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace docshell +} // namespace mozilla diff --git a/uriloader/prefetch/OfflineCacheUpdateParent.h b/uriloader/prefetch/OfflineCacheUpdateParent.h new file mode 100644 index 0000000000..b1bc3d90cc --- /dev/null +++ b/uriloader/prefetch/OfflineCacheUpdateParent.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdateParent_h +#define nsOfflineCacheUpdateParent_h + +#include "mozilla/docshell/POfflineCacheUpdateParent.h" +#include "mozilla/BasePrincipal.h" +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsILoadContext.h" + +class nsIPrincipal; + +namespace mozilla { + +namespace ipc { +class URIParams; +} // namespace ipc + +namespace net { +class CookieJarSettingsArgs; +} + +namespace docshell { + +class OfflineCacheUpdateParent : public POfflineCacheUpdateParent, + public nsIOfflineCacheUpdateObserver, + public nsILoadContext { + typedef mozilla::ipc::URIParams URIParams; + typedef mozilla::ipc::PrincipalInfo PrincipalInfo; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER + NS_DECL_NSILOADCONTEXT + + nsresult Schedule(nsIURI* manifestURI, nsIURI* documentURI, + const PrincipalInfo& loadingPrincipalInfo, + const bool& stickDocument, + const net::CookieJarSettingsArgs& aCookieJarSettingsArgs); + + void StopSendingMessagesToChild() { mIPCClosed = true; } + + explicit OfflineCacheUpdateParent(); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~OfflineCacheUpdateParent(); + + bool mIPCClosed; + + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; +}; + +} // namespace docshell +} // namespace mozilla + +#endif diff --git a/uriloader/prefetch/POfflineCacheUpdate.ipdl b/uriloader/prefetch/POfflineCacheUpdate.ipdl new file mode 100644 index 0000000000..5b4a0961a0 --- /dev/null +++ b/uriloader/prefetch/POfflineCacheUpdate.ipdl @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PContent; + +namespace mozilla { +namespace docshell { + +//------------------------------------------------------------------- +refcounted protocol POfflineCacheUpdate +{ + manager PContent; + +parent: + async __delete__(); + +child: + async NotifyStateEvent(uint32_t stateEvent, uint64_t byteProgress); + async AssociateDocuments(nsCString cacheGroupId, nsCString cacheClientId); + async Finish(bool succeeded, bool isUpgrade); +}; + +} +} diff --git a/uriloader/prefetch/moz.build b/uriloader/prefetch/moz.build new file mode 100644 index 0000000000..9c3a00bac4 --- /dev/null +++ b/uriloader/prefetch/moz.build @@ -0,0 +1,44 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: Cache") + +XPIDL_SOURCES += [ + "nsIOfflineCacheUpdate.idl", + "nsIPrefetchService.idl", +] + +XPIDL_MODULE = "prefetch" + +EXPORTS.mozilla.docshell += [ + "OfflineCacheUpdateChild.h", + "OfflineCacheUpdateParent.h", +] + +UNIFIED_SOURCES += [ + "nsOfflineCacheUpdate.cpp", + "nsOfflineCacheUpdateService.cpp", + "nsPrefetchService.cpp", + "OfflineCacheUpdateChild.cpp", + "OfflineCacheUpdateGlue.cpp", + "OfflineCacheUpdateParent.cpp", +] + +IPDL_SOURCES += [ + "POfflineCacheUpdate.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", +] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] diff --git a/uriloader/prefetch/nsIOfflineCacheUpdate.idl b/uriloader/prefetch/nsIOfflineCacheUpdate.idl new file mode 100644 index 0000000000..00dc8c6e19 --- /dev/null +++ b/uriloader/prefetch/nsIOfflineCacheUpdate.idl @@ -0,0 +1,291 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindow; +interface nsIURI; +interface nsIOfflineCacheUpdate; +interface nsIPrincipal; +interface nsIPrefBranch; +interface nsIApplicationCache; +interface nsIFile; +interface nsIObserver; +interface nsICookieJarSettings; +webidl Document; + +[scriptable, uuid(47360d57-8ef4-4a5d-8865-1a27a739ad1a)] +interface nsIOfflineCacheUpdateObserver : nsISupports { + const unsigned long STATE_ERROR = 1; + const unsigned long STATE_CHECKING = 2; + const unsigned long STATE_NOUPDATE = 3; + const unsigned long STATE_OBSOLETE = 4; + const unsigned long STATE_DOWNLOADING = 5; + const unsigned long STATE_ITEMSTARTED = 6; + const unsigned long STATE_ITEMCOMPLETED = 7; + const unsigned long STATE_ITEMPROGRESS = 8; + const unsigned long STATE_FINISHED = 10; + + /** + * aUpdate has changed its state. + * + * @param aUpdate + * The nsIOfflineCacheUpdate being processed. + * @param event + * See enumeration above + */ + void updateStateChanged(in nsIOfflineCacheUpdate aUpdate, in uint32_t state); + + /** + * Informs the observer about an application being available to associate. + * + * @param applicationCache + * The application cache instance that has been created or found by the + * update to associate with + */ + void applicationCacheAvailable(in nsIApplicationCache applicationCache); +}; + +/** + * An nsIOfflineCacheUpdate is used to update an application's offline + * resources. + * + * It can be used to perform partial or complete updates. + * + * One update object will be updating at a time. The active object will + * load its items one by one, sending itemCompleted() to any registered + * observers. + */ +[scriptable, uuid(6e3e26ea-45b2-4db7-9e4a-93b965679298)] +interface nsIOfflineCacheUpdate : nsISupports { + /** + * Fetch the status of the running update. This will return a value + * defined in OfflineResourceList.webidl. + */ + readonly attribute unsigned short status; + + /** + * TRUE if the update is being used to add specific resources. + * FALSE if the complete cache update process is happening. + */ + readonly attribute boolean partial; + + /** + * TRUE if this is an upgrade attempt, FALSE if it is a new cache + * attempt. + */ + readonly attribute boolean isUpgrade; + + /** + * The domain being updated, and the domain that will own any URIs added + * with this update. + */ + readonly attribute ACString updateDomain; + + /** + * The manifest for the offline application being updated. + */ + readonly attribute nsIURI manifestURI; + + /** + * The principal of the page that is requesting the update. + */ + readonly attribute nsIPrincipal loadingPrincipal; + + /** + * TRUE if the cache update completed successfully. + */ + readonly attribute boolean succeeded; + + /** + * Initialize the update. + * + * @param aManifestURI + * The manifest URI to be checked. + * @param aDocumentURI + * The page that is requesting the update. + * @param aLoadingPrincipal + * The principal of the page that is requesting the update. + */ + void init(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in Document aDocument, + [optional] in nsIFile aCustomProfileDir); + + /** + * Initialize the update for partial processing. + * + * @param aManifestURI + * The manifest URI of the related cache. + * @param aClientID + * Client ID of the cache to store resource to. This ClientID + * must be ID of cache in the cache group identified by + * the manifest URI passed in the first parameter. + * @param aDocumentURI + * The page that is requesting the update. May be null + * when this information is unknown. + * @param aCookieJarSettings + * The cookie jar settings belonging to the page that is requesting + * the update. + */ + void initPartial(in nsIURI aManifestURI, in ACString aClientID, + in nsIURI aDocumentURI, in nsIPrincipal aPrincipal, + in nsICookieJarSettings aCookieJarSettings); + + /** + * Initialize the update to only check whether there is an update + * to the manifest available (if it has actually changed on the server). + * + * @param aManifestURI + * The manifest URI of the related cache. + * @param aObserver + * nsIObserver implementation that receives the result. + * When aTopic == "offline-cache-update-available" there is an update to + * to download. Update of the app cache will lead to a new version + * download. + * When aTopic == "offline-cache-update-unavailable" then there is no + * update available (the manifest has not changed on the server). + */ + void initForUpdateCheck(in nsIURI aManifestURI, + in nsIPrincipal aLoadingPrincipal, + in nsIObserver aObserver); + + /** + * Add a dynamic URI to the offline cache as part of the update. + * + * @param aURI + * The URI to add. + */ + void addDynamicURI(in nsIURI aURI); + + /** + * Add the update to the offline update queue. An offline-cache-update-added + * event will be sent to the observer service. + */ + void schedule(); + + /** + * Observe loads that are added to the update. + * + * @param aObserver + * object that notifications will be sent to. + * @param aHoldWeak + * TRUE if you want the update to hold a weak reference to the + * observer, FALSE for a strong reference. + */ + void addObserver(in nsIOfflineCacheUpdateObserver aObserver, + [optional] in boolean aHoldWeak); + + /** + * Remove an observer from the update. + * + * @param aObserver + * the observer to remove. + */ + void removeObserver(in nsIOfflineCacheUpdateObserver aObserver); + + /** + * Cancel the update when still in progress. This stops all running resource + * downloads and discards the downloaded cache version. Throws when update + * has already finished and made the new cache version active. + */ + void cancel(); + + /** + * Return the number of bytes downloaded so far + */ + readonly attribute uint64_t byteProgress; +}; + +[scriptable, uuid(44971e74-37e4-4140-8677-a4cf213a3f4b)] +interface nsIOfflineCacheUpdateService : nsISupports { + /** + * Constants for the offline-app permission. + * + * XXX: This isn't a great place for this, but it's really the only + * private offline-app-related interface + */ + + /** + * Allow the domain to use offline APIs, and don't warn about excessive + * usage. + */ + const unsigned long ALLOW_NO_WARN = 3; + + /** + * Access to the list of cache updates that have been scheduled. + */ + readonly attribute unsigned long numUpdates; + nsIOfflineCacheUpdate getUpdate(in unsigned long index); + + /** + * Schedule a cache update for a given offline manifest. If an + * existing update is scheduled or running, that update will be returned. + * Otherwise a new update will be scheduled. + */ + nsIOfflineCacheUpdate scheduleUpdate(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in mozIDOMWindow aWindow); + + /** + * Schedule a cache update for a given offline manifest using app cache + * bound to the given appID flag. If an existing update is scheduled or + * running, that update will be returned. Otherwise a new update will be + * scheduled. + */ + nsIOfflineCacheUpdate scheduleAppUpdate(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in nsIFile aProfileDir); + + /** + * Schedule a cache update for a manifest when the document finishes + * loading. + */ + void scheduleOnDocumentStop(in nsIURI aManifestURI, + in nsIURI aDocumentURI, + in nsIPrincipal aLoadingPrincipal, + in Document aDocument); + + /** + * Schedule a check to see if an update is available. + * + * This will not update or make any changes to the appcache. + * It only notifies the observer to indicate whether the manifest has + * changed on the server (or not): a changed manifest means that an + * update is available. + * + * For arguments see nsIOfflineCacheUpdate.initForUpdateCheck() method + * description. + */ + void checkForUpdate(in nsIURI aManifestURI, + in nsIPrincipal aLoadingPrincipal, + in nsIObserver aObserver); + + /** + * Checks whether a principal should have access to the offline + * cache. + * @param aPrincipal + * The principal to check. + */ + boolean offlineAppAllowed(in nsIPrincipal aPrincipal); + + /** + * Checks whether a document at the given URI should have access + * to the offline cache. + * @param aURI + * The URI to check + */ + boolean offlineAppAllowedForURI(in nsIURI aURI); + + /** + * Sets the "offline-app" permission for the principal. + * In the single process model calls directly on permission manager. + * In the multi process model dispatches to the parent process. + */ + void allowOfflineApp(in nsIPrincipal aPrincipal); +}; diff --git a/uriloader/prefetch/nsIPrefetchService.idl b/uriloader/prefetch/nsIPrefetchService.idl new file mode 100644 index 0000000000..160d051116 --- /dev/null +++ b/uriloader/prefetch/nsIPrefetchService.idl @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIContentPolicy.idl" + +interface nsIURI; +interface nsISimpleEnumerator; +interface nsIReferrerInfo; + +webidl Node; + +[scriptable, uuid(422a1807-4e7f-463d-b8d7-ca2ceb9b5d53)] +interface nsIPrefetchService : nsISupports +{ + /** + * Enqueue a request to prefetch the specified URI. + * + * @param aURI the URI of the document to prefetch + * @param aReferrerInfo the referrerInfo of the request + * @param aSource the DOM node (such as a <link> tag) that requested this + * fetch, or null if the prefetch was not requested by a DOM node. + * @param aExplicit the link element has an explicit prefetch link type + */ + void prefetchURI(in nsIURI aURI, + in nsIReferrerInfo aReferrerInfo, + in Node aSource, + in boolean aExplicit); + + /** + * Start a preload of the specified URI. + * + * @param aURI the URI of the document to preload + * @param aReferrerInfo the referrerInfo of the request + * @param aSource the DOM node (such as a <link> tag) that requested this + * fetch, or null if the prefetch was not requested by a DOM node. + * @param aPolicyType content policy to be used for this load. + */ + void preloadURI(in nsIURI aURI, + in nsIReferrerInfo aReferrerInfo, + in Node aSource, + in nsContentPolicyType aPolicyType); + + /** + * Find out if there are any prefetches running or queued + */ + boolean hasMoreElements(); + + /** + * Cancel prefetch or preload for a Node. + */ + void cancelPrefetchPreloadURI(in nsIURI aURI, in Node aSource); +}; diff --git a/uriloader/prefetch/nsOfflineCacheUpdate.cpp b/uriloader/prefetch/nsOfflineCacheUpdate.cpp new file mode 100644 index 0000000000..62561cdaa3 --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdate.cpp @@ -0,0 +1,2332 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsOfflineCacheUpdate.h" + +#include "nsCURILoader.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIApplicationCacheService.h" +#include "nsICachingChannel.h" +#include "nsIContent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/OfflineResourceListBinding.h" +#include "mozilla/dom/Document.h" +#include "nsIURL.h" +#include "nsICryptoHash.h" +#include "nsICacheEntry.h" +#include "nsIHttpChannel.h" +#include "nsIPrincipal.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "nsIConsoleService.h" +#include "mozilla/Logging.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "nsContentUtils.h" +#include "nsIPrincipal.h" +#include "nsDiskCacheDeviceSQL.h" +#include "ReferrerInfo.h" + +#include "nsXULAppAPI.h" + +using namespace mozilla; + +static const uint32_t kRescheduleLimit = 3; +// Max number of retries for every entry of pinned app. +static const uint32_t kPinnedEntryRetriesLimit = 3; +// Maximum number of parallel items loads +static const uint32_t kParallelLoadLimit = 15; + +// Quota for offline apps when preloading +static const int32_t kCustomProfileQuota = 512000; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +extern LazyLogModule gOfflineCacheUpdateLog; + +#undef LOG +#define LOG(args) \ + MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() \ + MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +namespace { + +nsresult DropReferenceFromURL(nsCOMPtr<nsIURI>& aURI) { + // XXXdholbert If this SetRef fails, callers of this method probably + // want to call aURI->CloneIgnoringRef() and use the result of that. + nsCOMPtr<nsIURI> uri(aURI); + return NS_GetURIWithoutRef(uri, getter_AddRefs(aURI)); +} + +void LogToConsole(const char* message, + nsOfflineCacheUpdateItem* item = nullptr) { + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) { + nsAutoString messageUTF16 = NS_ConvertUTF8toUTF16(message); + if (item && item->mURI) { + messageUTF16.AppendLiteral(", URL="); + messageUTF16.Append( + NS_ConvertUTF8toUTF16(item->mURI->GetSpecOrDefault())); + } + consoleService->LogStringMessage(messageUTF16.get()); + } +} + +} // namespace + +//----------------------------------------------------------------------------- +// nsManifestCheck +//----------------------------------------------------------------------------- + +class nsManifestCheck final : public nsIStreamListener, + public nsIChannelEventSink, + public nsIInterfaceRequestor { + public: + nsManifestCheck(nsOfflineCacheUpdate* aUpdate, nsIURI* aURI, + nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal) + : mUpdate(aUpdate), + mURI(aURI), + mReferrerURI(aReferrerURI), + mLoadingPrincipal(aLoadingPrincipal) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + + nsresult Begin(); + + private: + ~nsManifestCheck() {} + + static nsresult ReadManifest(nsIInputStream* aInputStream, void* aClosure, + const char* aFromSegment, uint32_t aOffset, + uint32_t aCount, uint32_t* aBytesConsumed); + + RefPtr<nsOfflineCacheUpdate> mUpdate; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mReferrerURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsICryptoHash> mManifestHash; + nsCOMPtr<nsIChannel> mChannel; +}; + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsISupports +//----------------------------------------------------------------------------- +NS_IMPL_ISUPPORTS(nsManifestCheck, nsIRequestObserver, nsIStreamListener, + nsIChannelEventSink, nsIInterfaceRequestor) + +//----------------------------------------------------------------------------- +// nsManifestCheck <public> +//----------------------------------------------------------------------------- + +nsresult nsManifestCheck::Begin() { + nsresult rv; + mManifestHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mManifestHash->Init(nsICryptoHash::MD5); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewChannel(getter_AddRefs(mChannel), mURI, mLoadingPrincipal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_OTHER, mUpdate->CookieJarSettings(), + nullptr, // PerformanceStorage + nullptr, // loadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_BYPASS_CACHE); + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + nsCOMPtr<nsIReferrerInfo> referrerInfo = + new mozilla::dom::ReferrerInfo(mReferrerURI); + rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = + httpChannel->SetRequestHeader("X-Moz"_ns, "offline-resource"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + return mChannel->AsyncOpen(this); +} + +//----------------------------------------------------------------------------- +// nsManifestCheck <public> +//----------------------------------------------------------------------------- + +/* static */ +nsresult nsManifestCheck::ReadManifest(nsIInputStream* aInputStream, + void* aClosure, const char* aFromSegment, + uint32_t aOffset, uint32_t aCount, + uint32_t* aBytesConsumed) { + nsManifestCheck* manifestCheck = static_cast<nsManifestCheck*>(aClosure); + + nsresult rv; + *aBytesConsumed = aCount; + + rv = manifestCheck->mManifestHash->Update( + reinterpret_cast<const uint8_t*>(aFromSegment), aCount); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsManifestCheck::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } + +NS_IMETHODIMP +nsManifestCheck::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + uint32_t bytesRead; + aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead); + return NS_OK; +} + +NS_IMETHODIMP +nsManifestCheck::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + nsAutoCString manifestHash; + if (NS_SUCCEEDED(aStatus)) { + mManifestHash->Finish(true, manifestHash); + } + + mUpdate->ManifestCheckCompleted(aStatus, manifestHash); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsManifestCheck::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIChannelEventSink*>(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// nsManifestCheck::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsManifestCheck::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + // Redirects should cause the load (and therefore the update) to fail. + if (aFlags & nsIChannelEventSink::REDIRECT_INTERNAL) { + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + + LogToConsole("Manifest check failed because its response is a redirect"); + + aOldChannel->Cancel(NS_ERROR_ABORT); + return NS_ERROR_ABORT; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateItem, nsIRequestObserver, + nsIStreamListener, nsIRunnable, nsIInterfaceRequestor, + nsIChannelEventSink) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem <public> +//----------------------------------------------------------------------------- + +nsOfflineCacheUpdateItem::nsOfflineCacheUpdateItem( + nsIURI* aURI, nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache* aApplicationCache, + nsIApplicationCache* aPreviousApplicationCache, uint32_t type, + uint32_t loadFlags) + : mURI(aURI), + mReferrerURI(aReferrerURI), + mLoadingPrincipal(aLoadingPrincipal), + mApplicationCache(aApplicationCache), + mPreviousApplicationCache(aPreviousApplicationCache), + mItemType(type), + mLoadFlags(loadFlags), + mChannel(nullptr), + mState(LoadStatus::UNINITIALIZED), + mBytesRead(0) {} + +nsOfflineCacheUpdateItem::~nsOfflineCacheUpdateItem() {} + +nsresult nsOfflineCacheUpdateItem::OpenChannel(nsOfflineCacheUpdate* aUpdate) { + if (LOG_ENABLED()) { + LOG(("%p: Opening channel for %s", this, mURI->GetSpecOrDefault().get())); + } + + if (mUpdate) { + // Holding a reference to the update means this item is already + // in progress (has a channel, or is just in between OnStopRequest() + // and its Run() call. We must never open channel on this item again. + LOG((" %p is already running! ignoring", this)); + return NS_ERROR_ALREADY_OPENED; + } + + nsresult rv = nsOfflineCacheUpdate::GetCacheKey(mURI, mCacheKey); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t flags = + nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED; + + if (mApplicationCache == mPreviousApplicationCache) { + // Same app cache to read from and to write to is used during + // an only-update-check procedure. Here we protect the existing + // cache from being modified. + flags |= nsIRequest::INHIBIT_CACHING; + } + + flags |= mLoadFlags; + + rv = NS_NewChannel(getter_AddRefs(mChannel), mURI, mLoadingPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER, aUpdate->CookieJarSettings(), + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + this, // aCallbacks + flags); + + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(mChannel, &rv); + + // Support for nsIApplicationCacheChannel is required. + NS_ENSURE_SUCCESS(rv, rv); + + // Use the existing application cache as the cache to check. + rv = appCacheChannel->SetApplicationCache(mPreviousApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the new application cache as the target for write. + rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + nsCOMPtr<nsIReferrerInfo> referrerInfo = + new mozilla::dom::ReferrerInfo(mReferrerURI); + rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = + httpChannel->SetRequestHeader("X-Moz"_ns, "offline-resource"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = mChannel->AsyncOpen(this); + NS_ENSURE_SUCCESS(rv, rv); + + mUpdate = aUpdate; + + mState = LoadStatus::REQUESTED; + + return NS_OK; +} + +nsresult nsOfflineCacheUpdateItem::Cancel() { + if (mChannel) { + mChannel->Cancel(NS_ERROR_ABORT); + mChannel = nullptr; + } + + mState = LoadStatus::UNINITIALIZED; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnStartRequest(nsIRequest* aRequest) { + mState = LoadStatus::RECEIVING; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + uint32_t bytesRead = 0; + aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead); + mBytesRead += bytesRead; + LOG(("loaded %u bytes into offline cache [offset=%" PRIu64 "]\n", bytesRead, + aOffset)); + + mUpdate->OnByteProgress(bytesRead); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::OnStopRequest(nsIRequest* aRequest, + nsresult aStatus) { + if (LOG_ENABLED()) { + LOG(("%p: Done fetching offline item %s [status=%" PRIx32 "]\n", this, + mURI->GetSpecOrDefault().get(), static_cast<uint32_t>(aStatus))); + } + + if (mBytesRead == 0 && aStatus == NS_OK) { + // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was + // specified), but the object should report loadedSize as if it + // did. + mChannel->GetContentLength(&mBytesRead); + mUpdate->OnByteProgress(mBytesRead); + } + + if (NS_FAILED(aStatus)) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + bool isNoStore; + if (NS_SUCCEEDED(httpChannel->IsNoStoreResponse(&isNoStore)) && + isNoStore) { + LogToConsole( + "Offline cache manifest item has Cache-control: no-store header", + this); + } + } + } + + // We need to notify the update that the load is complete, but we + // want to give the channel a chance to close the cache entries. + NS_DispatchToCurrentThread(this); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIRunnable +//----------------------------------------------------------------------------- +NS_IMETHODIMP +nsOfflineCacheUpdateItem::Run() { + // Set mState to LOADED here rather than in OnStopRequest to prevent + // race condition when checking state of all mItems in ProcessNextURI(). + // If state would have been set in OnStopRequest we could mistakenly + // take this item as already finished and finish the update process too + // early when ProcessNextURI() would get called between OnStopRequest() + // and Run() of this item. Finish() would then have been called twice. + mState = LoadStatus::LOADED; + + RefPtr<nsOfflineCacheUpdate> update; + update.swap(mUpdate); + update->LoadCompleted(this); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIChannelEventSink*>(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateItem::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateItem::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* cb) { + if (!(aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)) { + // Don't allow redirect in case of non-internal redirect and cancel + // the channel to clean the cache entry. + LogToConsole("Offline cache manifest failed because an item redirects", + this); + + aOldChannel->Cancel(NS_ERROR_ABORT); + return NS_ERROR_ABORT; + } + + nsCOMPtr<nsIURI> newURI; + nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIApplicationCacheChannel> appCacheChannel = + do_QueryInterface(aNewChannel); + if (appCacheChannel) { + rv = appCacheChannel->SetApplicationCacheForWrite(mApplicationCache); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsAutoCString oldScheme; + mURI->GetScheme(oldScheme); + + if (!newURI->SchemeIs(oldScheme.get())) { + LOG(("rejected: redirected to a different scheme\n")); + return NS_ERROR_ABORT; + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + rv = httpChannel->SetRequestHeader("X-Moz"_ns, "offline-resource"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + mChannel = aNewChannel; + + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +nsresult nsOfflineCacheUpdateItem::GetRequestSucceeded(bool* succeeded) { + *succeeded = false; + + if (!mChannel) return NS_OK; + + nsresult rv; + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool reqSucceeded; + rv = httpChannel->GetRequestSucceeded(&reqSucceeded); + if (NS_ERROR_NOT_AVAILABLE == rv) return NS_OK; + NS_ENSURE_SUCCESS(rv, rv); + + if (!reqSucceeded) { + LOG(("Request failed")); + return NS_OK; + } + + nsresult channelStatus; + rv = httpChannel->GetStatus(&channelStatus); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_FAILED(channelStatus)) { + LOG(("Channel status=0x%08" PRIx32, static_cast<uint32_t>(channelStatus))); + return NS_OK; + } + + *succeeded = true; + return NS_OK; +} + +bool nsOfflineCacheUpdateItem::IsScheduled() { + return mState == LoadStatus::UNINITIALIZED; +} + +bool nsOfflineCacheUpdateItem::IsInProgress() { + return mState == LoadStatus::REQUESTED || mState == LoadStatus::RECEIVING; +} + +bool nsOfflineCacheUpdateItem::IsCompleted() { + return mState == LoadStatus::LOADED; +} + +nsresult nsOfflineCacheUpdateItem::GetStatus(uint16_t* aStatus) { + if (!mChannel) { + *aStatus = 0; + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t httpStatus; + rv = httpChannel->GetResponseStatus(&httpStatus); + if (rv == NS_ERROR_NOT_AVAILABLE) { + *aStatus = 0; + return NS_OK; + } + + NS_ENSURE_SUCCESS(rv, rv); + *aStatus = uint16_t(httpStatus); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineManifestItem +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// nsOfflineManifestItem <public> +//----------------------------------------------------------------------------- + +nsOfflineManifestItem::nsOfflineManifestItem( + nsIURI* aURI, nsIURI* aReferrerURI, nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache* aApplicationCache, + nsIApplicationCache* aPreviousApplicationCache) + : nsOfflineCacheUpdateItem(aURI, aReferrerURI, aLoadingPrincipal, + aApplicationCache, aPreviousApplicationCache, + nsIApplicationCache::ITEM_MANIFEST, 0), + mParserState(PARSE_INIT), + mNeedsUpdate(true), + mStrictFileOriginPolicy(false), + mManifestHashInitialized(false) { + ReadStrictFileOriginPolicyPref(); +} + +nsOfflineManifestItem::~nsOfflineManifestItem() {} + +//----------------------------------------------------------------------------- +// nsOfflineManifestItem <private> +//----------------------------------------------------------------------------- + +/* static */ +nsresult nsOfflineManifestItem::ReadManifest(nsIInputStream* aInputStream, + void* aClosure, + const char* aFromSegment, + uint32_t aOffset, uint32_t aCount, + uint32_t* aBytesConsumed) { + nsOfflineManifestItem* manifest = + static_cast<nsOfflineManifestItem*>(aClosure); + + nsresult rv; + + *aBytesConsumed = aCount; + + if (manifest->mParserState == PARSE_ERROR) { + // parse already failed, ignore this + return NS_OK; + } + + if (!manifest->mManifestHashInitialized) { + // Avoid re-creation of crypto hash when it fails from some reason the first + // time + manifest->mManifestHashInitialized = true; + + manifest->mManifestHash = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + if (NS_SUCCEEDED(rv)) { + rv = manifest->mManifestHash->Init(nsICryptoHash::MD5); + if (NS_FAILED(rv)) { + manifest->mManifestHash = nullptr; + LOG( + ("Could not initialize manifest hash for byte-to-byte check, " + "rv=%08" PRIx32, + static_cast<uint32_t>(rv))); + } + } + } + + if (manifest->mManifestHash) { + rv = manifest->mManifestHash->Update( + reinterpret_cast<const uint8_t*>(aFromSegment), aCount); + if (NS_FAILED(rv)) { + manifest->mManifestHash = nullptr; + LOG(("Could not update manifest hash, rv=%08" PRIx32, + static_cast<uint32_t>(rv))); + } + } + + manifest->mReadBuf.Append(aFromSegment, aCount); + + nsCString::const_iterator begin, iter, end; + manifest->mReadBuf.BeginReading(begin); + manifest->mReadBuf.EndReading(end); + + for (iter = begin; iter != end; iter++) { + if (*iter == '\r' || *iter == '\n') { + rv = manifest->HandleManifestLine(begin, iter); + + if (NS_FAILED(rv)) { + LOG(("HandleManifestLine failed with 0x%08" PRIx32, + static_cast<uint32_t>(rv))); + *aBytesConsumed = 0; // Avoid assertion failure in stream tee + return NS_ERROR_ABORT; + } + + begin = iter; + begin++; + } + } + + // any leftovers are saved for next time + manifest->mReadBuf = Substring(begin, end); + + return NS_OK; +} + +nsresult nsOfflineManifestItem::AddNamespace(uint32_t namespaceType, + const nsCString& namespaceSpec, + const nsCString& data) + +{ + nsresult rv; + if (!mNamespaces) { + mNamespaces = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIApplicationCacheNamespace> ns = new nsApplicationCacheNamespace(); + + rv = ns->Init(namespaceType, namespaceSpec, data); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mNamespaces->AppendElement(ns); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +static nsresult GetURIDirectory(nsIURI* uri, nsAutoCString& directory) { + nsresult rv; + + nsAutoCString path; + uri->GetFilePath(path); + if (path.Find("%2f") != kNotFound || path.Find("%2F") != kNotFound) { + return NS_ERROR_DOM_BAD_URI; + } + + nsCOMPtr<nsIURL> url(do_QueryInterface(uri, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = url->GetDirectory(directory); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +static nsresult CheckFileContainedInPath(nsIURI* file, + nsACString const& masterDirectory) { + nsresult rv; + + nsAutoCString directory; + rv = GetURIDirectory(file, directory); + if (NS_FAILED(rv)) { + return rv; + } + + bool contains = StringBeginsWith(directory, masterDirectory); + if (!contains) { + return NS_ERROR_DOM_BAD_URI; + } + + return NS_OK; +} + +nsresult nsOfflineManifestItem::HandleManifestLine( + const nsCString::const_iterator& aBegin, + const nsCString::const_iterator& aEnd) { + nsCString::const_iterator begin = aBegin; + nsCString::const_iterator end = aEnd; + + // all lines ignore trailing spaces and tabs + nsCString::const_iterator last = end; + --last; + while (end != begin && (*last == ' ' || *last == '\t')) { + --end; + --last; + } + + if (mParserState == PARSE_INIT) { + // Allow a UTF-8 BOM + if (begin != end && static_cast<unsigned char>(*begin) == 0xef) { + if (++begin == end || static_cast<unsigned char>(*begin) != 0xbb || + ++begin == end || static_cast<unsigned char>(*begin) != 0xbf) { + mParserState = PARSE_ERROR; + LogToConsole("Offline cache manifest BOM error", this); + return NS_OK; + } + ++begin; + } + + const nsACString& magic = Substring(begin, end); + + if (!magic.EqualsLiteral("CACHE MANIFEST")) { + mParserState = PARSE_ERROR; + LogToConsole("Offline cache manifest magic incorrect", this); + return NS_OK; + } + + mParserState = PARSE_CACHE_ENTRIES; + return NS_OK; + } + + // lines other than the first ignore leading spaces and tabs + while (begin != end && (*begin == ' ' || *begin == '\t')) begin++; + + // ignore blank lines and comments + if (begin == end || *begin == '#') return NS_OK; + + const nsACString& line = Substring(begin, end); + + if (line.EqualsLiteral("CACHE:")) { + mParserState = PARSE_CACHE_ENTRIES; + return NS_OK; + } + + if (line.EqualsLiteral("FALLBACK:")) { + mParserState = PARSE_FALLBACK_ENTRIES; + return NS_OK; + } + + if (line.EqualsLiteral("NETWORK:")) { + mParserState = PARSE_BYPASS_ENTRIES; + return NS_OK; + } + + // Every other section type we don't know must be silently ignored. + nsCString::const_iterator lastChar = end; + if (*(--lastChar) == ':') { + mParserState = PARSE_UNKNOWN_SECTION; + return NS_OK; + } + + nsresult rv; + + switch (mParserState) { + case PARSE_INIT: + case PARSE_ERROR: { + // this should have been dealt with earlier + return NS_ERROR_FAILURE; + } + + case PARSE_UNKNOWN_SECTION: { + // just jump over + return NS_OK; + } + + case PARSE_CACHE_ENTRIES: { + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), line, nullptr, mURI); + if (NS_FAILED(rv)) break; + if (NS_FAILED(DropReferenceFromURL(uri))) break; + + nsAutoCString scheme; + uri->GetScheme(scheme); + + // Manifest URIs must have the same scheme as the manifest. + if (!mURI->SchemeIs(scheme.get())) { + break; + } + + mExplicitURIs.AppendObject(uri); + + if (!NS_SecurityCompareURIs(mURI, uri, mStrictFileOriginPolicy)) { + mAnonymousURIs.AppendObject(uri); + } + + break; + } + + case PARSE_FALLBACK_ENTRIES: { + int32_t separator = line.FindChar(' '); + if (separator == kNotFound) { + separator = line.FindChar('\t'); + if (separator == kNotFound) break; + } + + nsCString namespaceSpec(Substring(line, 0, separator)); + nsCString fallbackSpec(Substring(line, separator + 1)); + namespaceSpec.CompressWhitespace(); + fallbackSpec.CompressWhitespace(); + + nsCOMPtr<nsIURI> namespaceURI; + rv = + NS_NewURI(getter_AddRefs(namespaceURI), namespaceSpec, nullptr, mURI); + if (NS_FAILED(rv)) break; + if (NS_FAILED(DropReferenceFromURL(namespaceURI))) break; + rv = namespaceURI->GetAsciiSpec(namespaceSpec); + if (NS_FAILED(rv)) break; + + nsCOMPtr<nsIURI> fallbackURI; + rv = NS_NewURI(getter_AddRefs(fallbackURI), fallbackSpec, nullptr, mURI); + if (NS_FAILED(rv)) break; + if (NS_FAILED(DropReferenceFromURL(fallbackURI))) break; + rv = fallbackURI->GetAsciiSpec(fallbackSpec); + if (NS_FAILED(rv)) break; + + // The following set of checks is preventing a website under + // a subdirectory to add fallback pages for the whole origin + // (or a parent directory) to prevent fallback attacks. + nsAutoCString manifestDirectory; + rv = GetURIDirectory(mURI, manifestDirectory); + if (NS_FAILED(rv)) { + break; + } + + rv = CheckFileContainedInPath(namespaceURI, manifestDirectory); + if (NS_FAILED(rv)) { + break; + } + + rv = CheckFileContainedInPath(fallbackURI, manifestDirectory); + if (NS_FAILED(rv)) { + break; + } + + // Manifest and namespace must be same origin + if (!NS_SecurityCompareURIs(mURI, namespaceURI, mStrictFileOriginPolicy)) + break; + + // Fallback and namespace must be same origin + if (!NS_SecurityCompareURIs(namespaceURI, fallbackURI, + mStrictFileOriginPolicy)) + break; + + mFallbackURIs.AppendObject(fallbackURI); + + AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_FALLBACK, + namespaceSpec, fallbackSpec); + break; + } + + case PARSE_BYPASS_ENTRIES: { + if (line[0] == '*' && + (line.Length() == 1 || line[1] == ' ' || line[1] == '\t')) { + // '*' indicates to make the online whitelist wildcard flag open, + // i.e. do allow load of resources not present in the offline cache + // or not conforming any namespace. + // We achive that simply by adding an 'empty' - i.e. universal + // namespace of BYPASS type into the cache. + AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, ""_ns, + ""_ns); + break; + } + + nsCOMPtr<nsIURI> bypassURI; + rv = NS_NewURI(getter_AddRefs(bypassURI), line, nullptr, mURI); + if (NS_FAILED(rv)) break; + + nsAutoCString scheme; + bypassURI->GetScheme(scheme); + if (!mURI->SchemeIs(scheme.get())) { + break; + } + if (NS_FAILED(DropReferenceFromURL(bypassURI))) break; + nsCString spec; + if (NS_FAILED(bypassURI->GetAsciiSpec(spec))) break; + + AddNamespace(nsIApplicationCacheNamespace::NAMESPACE_BYPASS, spec, ""_ns); + break; + } + } + + return NS_OK; +} + +nsresult nsOfflineManifestItem::GetOldManifestContentHash( + nsIRequest* aRequest) { + nsresult rv; + + nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // load the main cache token that is actually the old offline cache token and + // read previous manifest content hash value + nsCOMPtr<nsISupports> cacheToken; + cachingChannel->GetCacheToken(getter_AddRefs(cacheToken)); + if (cacheToken) { + nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheDescriptor->GetMetaDataElement( + "offline-manifest-hash", getter_Copies(mOldManifestHashValue)); + if (NS_FAILED(rv)) mOldManifestHashValue.Truncate(); + } + + return NS_OK; +} + +nsresult nsOfflineManifestItem::CheckNewManifestContentHash( + nsIRequest* aRequest) { + nsresult rv; + + if (!mManifestHash) { + // Nothing to compare against... + return NS_OK; + } + + nsCString newManifestHashValue; + rv = mManifestHash->Finish(true, mManifestHashValue); + mManifestHash = nullptr; + + if (NS_FAILED(rv)) { + LOG(("Could not finish manifest hash, rv=%08" PRIx32, + static_cast<uint32_t>(rv))); + // This is not critical error + return NS_OK; + } + + if (!ParseSucceeded()) { + // Parsing failed, the hash is not valid + return NS_OK; + } + + if (mOldManifestHashValue == mManifestHashValue) { + LOG( + ("Update not needed, downloaded manifest content is byte-for-byte " + "identical")); + mNeedsUpdate = false; + } + + // Store the manifest content hash value to the new + // offline cache token + nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> cacheToken; + cachingChannel->GetOfflineCacheToken(getter_AddRefs(cacheToken)); + if (cacheToken) { + nsCOMPtr<nsICacheEntry> cacheDescriptor(do_QueryInterface(cacheToken, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash", + mManifestHashValue.get()); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +void nsOfflineManifestItem::ReadStrictFileOriginPolicyPref() { + mStrictFileOriginPolicy = + Preferences::GetBool("security.fileuri.strict_origin_policy", true); +} + +NS_IMETHODIMP +nsOfflineManifestItem::OnStartRequest(nsIRequest* aRequest) { + nsresult rv; + + nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + bool succeeded; + rv = channel->GetRequestSucceeded(&succeeded); + NS_ENSURE_SUCCESS(rv, rv); + + if (!succeeded) { + LOG(("HTTP request failed")); + LogToConsole("Offline cache manifest HTTP request failed", this); + mParserState = PARSE_ERROR; + return NS_ERROR_ABORT; + } + + rv = GetOldManifestContentHash(aRequest); + NS_ENSURE_SUCCESS(rv, rv); + + return nsOfflineCacheUpdateItem::OnStartRequest(aRequest); +} + +NS_IMETHODIMP +nsOfflineManifestItem::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + uint32_t bytesRead = 0; + aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead); + mBytesRead += bytesRead; + + if (mParserState == PARSE_ERROR) { + LOG(("OnDataAvailable is canceling the request due a parse error\n")); + return NS_ERROR_ABORT; + } + + LOG(("loaded %u bytes into offline cache [offset=%" PRIu64 "]\n", bytesRead, + aOffset)); + + // All the parent method does is read and discard, don't bother + // chaining up. + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineManifestItem::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + if (mBytesRead == 0) { + // We didn't need to read (because LOAD_ONLY_IF_MODIFIED was + // specified). + mNeedsUpdate = false; + } else { + // Handle any leftover manifest data. + nsCString::const_iterator begin, end; + mReadBuf.BeginReading(begin); + mReadBuf.EndReading(end); + nsresult rv = HandleManifestLine(begin, end); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CheckNewManifestContentHash(aRequest); + NS_ENSURE_SUCCESS(rv, rv); + } + + return nsOfflineCacheUpdateItem::OnStopRequest(aRequest, aStatus); +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdate, nsIOfflineCacheUpdateObserver, + nsIOfflineCacheUpdate, nsIRunnable) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate <public> +//----------------------------------------------------------------------------- + +nsOfflineCacheUpdate::nsOfflineCacheUpdate() + : mState(STATE_UNINITIALIZED), + mAddedItems(false), + mPartialUpdate(false), + mOnlyCheckUpdate(false), + mSucceeded(true), + mObsolete(false), + mItemsInProgress(0), + mRescheduleCount(0), + mPinnedEntryRetriesCount(0), + mPinned(false), + mByteProgress(0) {} + +nsOfflineCacheUpdate::~nsOfflineCacheUpdate() { + LOG(("nsOfflineCacheUpdate::~nsOfflineCacheUpdate [%p]", this)); +} + +/* static */ +nsresult nsOfflineCacheUpdate::GetCacheKey(nsIURI* aURI, nsACString& aKey) { + aKey.Truncate(); + + nsCOMPtr<nsIURI> newURI; + nsresult rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(newURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = newURI->GetAsciiSpec(aKey); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::InitInternal(nsIURI* aManifestURI, + nsIPrincipal* aLoadingPrincipal) { + nsresult rv; + + // Only http and https applications are supported. + if (!aManifestURI->SchemeIs("http") && !aManifestURI->SchemeIs("https")) { + return NS_ERROR_ABORT; + } + + mManifestURI = aManifestURI; + mLoadingPrincipal = aLoadingPrincipal; + + rv = mManifestURI->GetAsciiHost(mUpdateDomain); + NS_ENSURE_SUCCESS(rv, rv); + + mPartialUpdate = false; + + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::Init(nsIURI* aManifestURI, nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + dom::Document* aDocument, + nsIFile* aCustomProfileDir) { + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) return NS_ERROR_FAILURE; + + LOG(("nsOfflineCacheUpdate::Init [%p]", this)); + + rv = InitInternal(aManifestURI, aLoadingPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString originSuffix; + rv = aLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + mDocumentURI = aDocumentURI; + + if (aDocument) { + mCookieJarSettings = aDocument->CookieJarSettings(); + } + + if (aCustomProfileDir) { + rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, + mGroupID); + NS_ENSURE_SUCCESS(rv, rv); + + // Create only a new offline application cache in the custom profile + // This is a preload of a new cache. + + // XXX Custom updates don't support "updating" of an existing cache + // in the custom profile at the moment. This support can be, though, + // simply added as well when needed. + mPreviousApplicationCache = nullptr; + + rv = cacheService->CreateCustomApplicationCache( + mGroupID, aCustomProfileDir, kCustomProfileQuota, + getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + mCustomProfileDir = aCustomProfileDir; + } else { + rv = cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, + mGroupID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->GetActiveCache( + mGroupID, getter_AddRefs(mPreviousApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->CreateApplicationCache( + mGroupID, getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI, + &mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + mState = STATE_INITIALIZED; + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::InitForUpdateCheck( + nsIURI* aManifestURI, nsIPrincipal* aLoadingPrincipal, + nsIObserver* aObserver) { + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) return NS_ERROR_FAILURE; + + LOG(("nsOfflineCacheUpdate::InitForUpdateCheck [%p]", this)); + + rv = InitInternal(aManifestURI, aLoadingPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString originSuffix; + rv = aLoadingPrincipal->GetOriginSuffix(originSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + rv = + cacheService->BuildGroupIDForSuffix(aManifestURI, originSuffix, mGroupID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->GetActiveCache(mGroupID, + getter_AddRefs(mPreviousApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + // To load the manifest properly using current app cache to satisfy and + // also to compare the cached content hash value we have to set 'some' + // app cache to write to on the channel. Otherwise the cached version will + // be used and no actual network request will be made. We use the same + // app cache here. OpenChannel prevents caching in this case using + // INHIBIT_CACHING load flag. + mApplicationCache = mPreviousApplicationCache; + + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aManifestURI, + &mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + mUpdateAvailableObserver = aObserver; + mOnlyCheckUpdate = true; + + mState = STATE_INITIALIZED; + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::InitPartial( + nsIURI* aManifestURI, const nsACString& clientID, nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, nsICookieJarSettings* aCookieJarSettings) { + nsresult rv; + + // Make sure the service has been initialized + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + if (!service) return NS_ERROR_FAILURE; + + LOG(("nsOfflineCacheUpdate::InitPartial [%p]", this)); + + mPartialUpdate = true; + mDocumentURI = aDocumentURI; + mLoadingPrincipal = aLoadingPrincipal; + + mManifestURI = aManifestURI; + rv = mManifestURI->GetAsciiHost(mUpdateDomain); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->GetApplicationCache(clientID, + getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mApplicationCache) { + nsAutoCString manifestSpec; + rv = GetCacheKey(mManifestURI, manifestSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheService->CreateApplicationCache( + manifestSpec, getter_AddRefs(mApplicationCache)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mApplicationCache->GetManifestURI(getter_AddRefs(mManifestURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString groupID; + rv = mApplicationCache->GetGroupID(groupID); + NS_ENSURE_SUCCESS(rv, rv); + + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(aDocumentURI, + &mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + mCookieJarSettings = aCookieJarSettings; + + mState = STATE_INITIALIZED; + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::HandleManifest(bool* aDoUpdate) { + // Be pessimistic + *aDoUpdate = false; + + bool succeeded; + nsresult rv = mManifestItem->GetRequestSucceeded(&succeeded); + NS_ENSURE_SUCCESS(rv, rv); + + if (!succeeded || !mManifestItem->ParseSucceeded()) { + return NS_ERROR_FAILURE; + } + + if (!mManifestItem->NeedsUpdate()) { + return NS_OK; + } + + // Add items requested by the manifest. + const nsCOMArray<nsIURI>& manifestURIs = mManifestItem->GetExplicitURIs(); + for (int32_t i = 0; i < manifestURIs.Count(); i++) { + rv = AddURI(manifestURIs[i], nsIApplicationCache::ITEM_EXPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + } + + const nsCOMArray<nsIURI>& anonURIs = mManifestItem->GetAnonymousURIs(); + for (int32_t i = 0; i < anonURIs.Count(); i++) { + rv = AddURI(anonURIs[i], nsIApplicationCache::ITEM_EXPLICIT, + nsIRequest::LOAD_ANONYMOUS); + NS_ENSURE_SUCCESS(rv, rv); + } + + const nsCOMArray<nsIURI>& fallbackURIs = mManifestItem->GetFallbackURIs(); + for (int32_t i = 0; i < fallbackURIs.Count(); i++) { + rv = AddURI(fallbackURIs[i], nsIApplicationCache::ITEM_FALLBACK); + NS_ENSURE_SUCCESS(rv, rv); + } + + // The document that requested the manifest is implicitly included + // as part of that manifest update. + rv = AddURI(mDocumentURI, nsIApplicationCache::ITEM_IMPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + + // Add items previously cached implicitly + rv = AddExistingItems(nsIApplicationCache::ITEM_IMPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + + // Add items requested by the script API + rv = AddExistingItems(nsIApplicationCache::ITEM_DYNAMIC); + NS_ENSURE_SUCCESS(rv, rv); + + // Add opportunistically cached items conforming current opportunistic + // namespace list + rv = AddExistingItems(nsIApplicationCache::ITEM_OPPORTUNISTIC, + &mManifestItem->GetOpportunisticNamespaces()); + NS_ENSURE_SUCCESS(rv, rv); + + *aDoUpdate = true; + + return NS_OK; +} + +bool nsOfflineCacheUpdate::CheckUpdateAvailability() { + nsresult rv; + + bool succeeded; + rv = mManifestItem->GetRequestSucceeded(&succeeded); + NS_ENSURE_SUCCESS(rv, false); + + if (!succeeded || !mManifestItem->ParseSucceeded()) { + return false; + } + + if (!mPinned) { + uint16_t status; + rv = mManifestItem->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, false); + + // Treat these as there would be an update available, + // since this is indication of demand to remove this + // offline cache. + if (status == 404 || status == 410) { + return true; + } + } + + return mManifestItem->NeedsUpdate(); +} + +void nsOfflineCacheUpdate::LoadCompleted(nsOfflineCacheUpdateItem* aItem) { + nsresult rv; + + LOG(("nsOfflineCacheUpdate::LoadCompleted [%p]", this)); + + if (mState == STATE_FINISHED) { + LOG((" after completion, ignoring")); + return; + } + + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + if (mState == STATE_CANCELLED) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + if (mState == STATE_CHECKING) { + // Manifest load finished. + + if (mOnlyCheckUpdate) { + Finish(); + NotifyUpdateAvailability(CheckUpdateAvailability()); + return; + } + + NS_ASSERTION(mManifestItem, "Must have a manifest item in STATE_CHECKING."); + NS_ASSERTION(mManifestItem == aItem, + "Unexpected aItem in nsOfflineCacheUpdate::LoadCompleted"); + + // A 404 or 410 is interpreted as an intentional removal of + // the manifest file, rather than a transient server error. + // Obsolete this cache group if one of these is returned. + uint16_t status; + rv = mManifestItem->GetStatus(&status); + if (NS_FAILED(rv)) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + if (status == 404 || status == 410) { + LogToConsole("Offline cache manifest removed, cache cleared", + mManifestItem); + mSucceeded = false; + if (mPreviousApplicationCache) { + if (mPinned) { + // Do not obsolete a pinned application. + NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE); + } else { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_OBSOLETE); + mObsolete = true; + } + } else { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + mObsolete = true; + } + Finish(); + return; + } + + bool doUpdate; + if (NS_FAILED(HandleManifest(&doUpdate))) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + if (!doUpdate) { + LogToConsole("Offline cache doesn't need to update", mManifestItem); + + mSucceeded = false; + + AssociateDocuments(mPreviousApplicationCache); + + ScheduleImplicit(); + + // If we didn't need an implicit update, we can + // send noupdate and end the update now. + if (!mImplicitUpdate) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE); + Finish(); + } + return; + } + + rv = mApplicationCache->MarkEntry(mManifestItem->mCacheKey, + mManifestItem->mItemType); + if (NS_FAILED(rv)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + mState = STATE_DOWNLOADING; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING); + + // Start fetching resources. + ProcessNextURI(); + + return; + } + + // Normal load finished. + if (mItemsInProgress) // Just to be safe here! + --mItemsInProgress; + + bool succeeded; + rv = aItem->GetRequestSucceeded(&succeeded); + + if (mPinned && NS_SUCCEEDED(rv) && succeeded) { + uint32_t dummy_cache_type; + rv = mApplicationCache->GetTypes(aItem->mCacheKey, &dummy_cache_type); + bool item_doomed = NS_FAILED(rv); // can not find it? -> doomed + + if (item_doomed && mPinnedEntryRetriesCount < kPinnedEntryRetriesLimit && + (aItem->mItemType & (nsIApplicationCache::ITEM_EXPLICIT | + nsIApplicationCache::ITEM_FALLBACK))) { + rv = EvictOneNonPinned(); + if (NS_FAILED(rv)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + // This reverts the item state to UNINITIALIZED that makes it to + // be scheduled for download again. + rv = aItem->Cancel(); + if (NS_FAILED(rv)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + mPinnedEntryRetriesCount++; + + LogToConsole("An unpinned offline cache deleted"); + + // Retry this item. + ProcessNextURI(); + return; + } + } + + // According to parallelism this may imply more pinned retries count, + // but that is not critical, since at one moment the algorithm will + // stop anyway. Also, this code may soon be completely removed + // after we have a separate storage for pinned apps. + mPinnedEntryRetriesCount = 0; + + // Check for failures. 3XX, 4XX and 5XX errors on items explicitly + // listed in the manifest will cause the update to fail. + if (NS_FAILED(rv) || !succeeded) { + if (aItem->mItemType & (nsIApplicationCache::ITEM_EXPLICIT | + nsIApplicationCache::ITEM_FALLBACK)) { + LogToConsole("Offline cache manifest item failed to load", aItem); + mSucceeded = false; + } + } else { + rv = mApplicationCache->MarkEntry(aItem->mCacheKey, aItem->mItemType); + if (NS_FAILED(rv)) { + mSucceeded = false; + } + } + + if (!mSucceeded) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + Finish(); + return; + } + + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMCOMPLETED); + + ProcessNextURI(); +} + +void nsOfflineCacheUpdate::ManifestCheckCompleted( + nsresult aStatus, const nsCString& aManifestHash) { + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + if (NS_SUCCEEDED(aStatus)) { + nsAutoCString firstManifestHash; + mManifestItem->GetManifestHash(firstManifestHash); + if (aManifestHash != firstManifestHash) { + LOG(("Manifest has changed during cache items download [%p]", this)); + LogToConsole("Offline cache manifest changed during update", + mManifestItem); + aStatus = NS_ERROR_FAILURE; + } + } + + if (NS_FAILED(aStatus)) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + } + + if (NS_FAILED(aStatus) && mRescheduleCount < kRescheduleLimit) { + // Do the final stuff but prevent notification of STATE_FINISHED. + // That would disconnect listeners that are responsible for document + // association after a successful update. Forwarding notifications + // from a new update through this dead update to them is absolutely + // correct. + FinishNoNotify(); + + RefPtr<nsOfflineCacheUpdate> newUpdate = new nsOfflineCacheUpdate(); + // Leave aDocument argument null. Only glues and children keep + // document instances. + newUpdate->Init(mManifestURI, mDocumentURI, mLoadingPrincipal, nullptr, + mCustomProfileDir); + + newUpdate->SetCookieJarSettings(mCookieJarSettings); + + // In a rare case the manifest will not be modified on the next refetch + // transfer all master document URIs to the new update to ensure that + // all documents refering it will be properly cached. + for (int32_t i = 0; i < mDocumentURIs.Count(); i++) { + newUpdate->StickDocument(mDocumentURIs[i]); + } + + newUpdate->mRescheduleCount = mRescheduleCount + 1; + newUpdate->AddObserver(this, false); + newUpdate->Schedule(); + } else { + LogToConsole("Offline cache update done", mManifestItem); + Finish(); + } +} + +nsresult nsOfflineCacheUpdate::Begin() { + LOG(("nsOfflineCacheUpdate::Begin [%p]", this)); + + // Keep the object alive through a ProcessNextURI()/Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + mItemsInProgress = 0; + + if (mState == STATE_CANCELLED) { + nsresult rv = NS_DispatchToMainThread( + NewRunnableMethod("nsOfflineCacheUpdate::AsyncFinishWithError", this, + &nsOfflineCacheUpdate::AsyncFinishWithError)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + if (mPartialUpdate) { + mState = STATE_DOWNLOADING; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_DOWNLOADING); + ProcessNextURI(); + return NS_OK; + } + + // Start checking the manifest. + mManifestItem = + new nsOfflineManifestItem(mManifestURI, mDocumentURI, mLoadingPrincipal, + mApplicationCache, mPreviousApplicationCache); + if (!mManifestItem) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mState = STATE_CHECKING; + mByteProgress = 0; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_CHECKING); + + nsresult rv = mManifestItem->OpenChannel(this); + if (NS_FAILED(rv)) { + LoadCompleted(mManifestItem); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate <private> +//----------------------------------------------------------------------------- + +nsresult nsOfflineCacheUpdate::AddExistingItems( + uint32_t aType, nsTArray<nsCString>* namespaceFilter) { + if (!mPreviousApplicationCache) { + return NS_OK; + } + + if (namespaceFilter && namespaceFilter->Length() == 0) { + // Don't bother to walk entries when there are no namespaces + // defined. + return NS_OK; + } + + nsTArray<nsCString> keys; + nsresult rv = mPreviousApplicationCache->GatherEntries(aType, keys); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto& key : keys) { + if (namespaceFilter) { + bool found = false; + for (uint32_t j = 0; j < namespaceFilter->Length() && !found; j++) { + found = StringBeginsWith(key, namespaceFilter->ElementAt(j)); + } + + if (!found) continue; + } + + nsCOMPtr<nsIURI> uri; + if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), key))) { + rv = AddURI(uri, aType); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::ProcessNextURI() { + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p, inprogress=%d, numItems=%zu]", + this, mItemsInProgress, mItems.Length())); + + if (mState != STATE_DOWNLOADING) { + LOG((" should only be called from the DOWNLOADING state, ignoring")); + return NS_ERROR_UNEXPECTED; + } + + nsOfflineCacheUpdateItem* runItem = nullptr; + uint32_t completedItems = 0; + for (uint32_t i = 0; i < mItems.Length(); ++i) { + nsOfflineCacheUpdateItem* item = mItems[i]; + + if (item->IsScheduled()) { + runItem = item; + break; + } + + if (item->IsCompleted()) ++completedItems; + } + + if (completedItems == mItems.Length()) { + LOG(("nsOfflineCacheUpdate::ProcessNextURI [%p]: all items loaded", this)); + + if (mPartialUpdate) { + return Finish(); + } else { + // Verify that the manifest wasn't changed during the + // update, to prevent capturing a cache while the server + // is being updated. The check will call + // ManifestCheckCompleted() when it's done. + RefPtr<nsManifestCheck> manifestCheck = new nsManifestCheck( + this, mManifestURI, mDocumentURI, mLoadingPrincipal); + if (NS_FAILED(manifestCheck->Begin())) { + mSucceeded = false; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + return Finish(); + } + + return NS_OK; + } + } + + if (!runItem) { + LOG( + ("nsOfflineCacheUpdate::ProcessNextURI [%p]:" + " No more items to include in parallel load", + this)); + return NS_OK; + } + + if (LOG_ENABLED()) { + LOG(("%p: Opening channel for %s", this, + runItem->mURI->GetSpecOrDefault().get())); + } + + ++mItemsInProgress; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMSTARTED); + + nsresult rv = runItem->OpenChannel(this); + if (NS_FAILED(rv)) { + LoadCompleted(runItem); + return rv; + } + + if (mItemsInProgress >= kParallelLoadLimit) { + LOG( + ("nsOfflineCacheUpdate::ProcessNextURI [%p]:" + " At parallel load limit", + this)); + return NS_OK; + } + + // This calls this method again via a post triggering + // a parallel item load + return NS_DispatchToCurrentThread(this); +} + +void nsOfflineCacheUpdate::GatherObservers( + nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers) { + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer) + aObservers.AppendObject(observer); + else + mWeakObservers.RemoveObjectAt(i--); + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + aObservers.AppendObject(mObservers[i]); + } +} + +void nsOfflineCacheUpdate::NotifyState(uint32_t state) { + LOG(("nsOfflineCacheUpdate::NotifyState [%p, %d]", this, state)); + + if (state == STATE_ERROR) { + LogToConsole("Offline cache update error", mManifestItem); + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) { + observers[i]->UpdateStateChanged(this, state); + } +} + +void nsOfflineCacheUpdate::NotifyUpdateAvailability(bool updateAvailable) { + if (!mUpdateAvailableObserver) return; + + LOG(("nsOfflineCacheUpdate::NotifyUpdateAvailability [this=%p, avail=%d]", + this, updateAvailable)); + + const char* topic = updateAvailable ? "offline-cache-update-available" + : "offline-cache-update-unavailable"; + + nsCOMPtr<nsIObserver> observer; + observer.swap(mUpdateAvailableObserver); + observer->Observe(mManifestURI, topic, nullptr); +} + +void nsOfflineCacheUpdate::AssociateDocuments(nsIApplicationCache* cache) { + if (!cache) { + LOG( + ("nsOfflineCacheUpdate::AssociateDocuments bypassed" + ", no cache provided [this=%p]", + this)); + return; + } + + nsCOMArray<nsIOfflineCacheUpdateObserver> observers; + GatherObservers(observers); + + for (int32_t i = 0; i < observers.Count(); i++) { + observers[i]->ApplicationCacheAvailable(cache); + } +} + +void nsOfflineCacheUpdate::StickDocument(nsIURI* aDocumentURI) { + if (!aDocumentURI) return; + + mDocumentURIs.AppendObject(aDocumentURI); +} + +void nsOfflineCacheUpdate::SetOwner(nsOfflineCacheUpdateOwner* aOwner) { + NS_ASSERTION(!mOwner, "Tried to set cache update owner twice."); + mOwner = aOwner; +} + +bool nsOfflineCacheUpdate::IsForGroupID(const nsACString& groupID) { + return mGroupID == groupID; +} + +bool nsOfflineCacheUpdate::IsForProfile(nsIFile* aCustomProfileDir) { + if (!mCustomProfileDir && !aCustomProfileDir) return true; + if (!mCustomProfileDir || !aCustomProfileDir) return false; + + bool equals; + nsresult rv = mCustomProfileDir->Equals(aCustomProfileDir, &equals); + + return NS_SUCCEEDED(rv) && equals; +} + +nsresult nsOfflineCacheUpdate::UpdateFinished(nsOfflineCacheUpdate* aUpdate) { + // Keep the object alive through a Finish() call. + nsCOMPtr<nsIOfflineCacheUpdate> kungFuDeathGrip(this); + + mImplicitUpdate = nullptr; + + NotifyState(nsIOfflineCacheUpdateObserver::STATE_NOUPDATE); + Finish(); + + return NS_OK; +} + +void nsOfflineCacheUpdate::OnByteProgress(uint64_t byteIncrement) { + mByteProgress += byteIncrement; + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ITEMPROGRESS); +} + +nsresult nsOfflineCacheUpdate::ScheduleImplicit() { + if (mDocumentURIs.Count() == 0) return NS_OK; + + nsresult rv; + + RefPtr<nsOfflineCacheUpdate> update = new nsOfflineCacheUpdate(); + NS_ENSURE_TRUE(update, NS_ERROR_OUT_OF_MEMORY); + + nsAutoCString clientID; + if (mPreviousApplicationCache) { + rv = mPreviousApplicationCache->GetClientID(clientID); + NS_ENSURE_SUCCESS(rv, rv); + } else if (mApplicationCache) { + rv = mApplicationCache->GetClientID(clientID); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NS_ERROR("Offline cache update not having set mApplicationCache?"); + } + + rv = update->InitPartial(mManifestURI, clientID, mDocumentURI, + mLoadingPrincipal, mCookieJarSettings); + NS_ENSURE_SUCCESS(rv, rv); + + for (int32_t i = 0; i < mDocumentURIs.Count(); i++) { + rv = update->AddURI(mDocumentURIs[i], nsIApplicationCache::ITEM_IMPLICIT); + NS_ENSURE_SUCCESS(rv, rv); + } + + update->SetOwner(this); + rv = update->Begin(); + NS_ENSURE_SUCCESS(rv, rv); + + mImplicitUpdate = update; + + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::FinishNoNotify() { + LOG(("nsOfflineCacheUpdate::Finish [%p]", this)); + + mState = STATE_FINISHED; + + if (!mPartialUpdate && !mOnlyCheckUpdate) { + if (mSucceeded) { + nsIArray* namespaces = mManifestItem->GetNamespaces(); + nsresult rv = mApplicationCache->AddNamespaces(namespaces); + if (NS_FAILED(rv)) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + mSucceeded = false; + } + + rv = mApplicationCache->Activate(); + if (NS_FAILED(rv)) { + NotifyState(nsIOfflineCacheUpdateObserver::STATE_ERROR); + mSucceeded = false; + } + + AssociateDocuments(mApplicationCache); + } + + if (mObsolete) { + nsCOMPtr<nsIApplicationCacheService> appCacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID); + if (appCacheService) { + nsAutoCString groupID; + mApplicationCache->GetGroupID(groupID); + appCacheService->DeactivateGroup(groupID); + } + } + + if (!mSucceeded) { + // Update was not merged, mark all the loads as failures + for (uint32_t i = 0; i < mItems.Length(); i++) { + mItems[i]->Cancel(); + } + + mApplicationCache->Discard(); + } + } + + nsresult rv = NS_OK; + + if (mOwner) { + rv = mOwner->UpdateFinished(this); + // mozilla::WeakPtr is missing some key features, like setting it to + // null explicitly. + mOwner = mozilla::WeakPtr<nsOfflineCacheUpdateOwner>(); + } + + return rv; +} + +nsresult nsOfflineCacheUpdate::Finish() { + nsresult rv = FinishNoNotify(); + + NotifyState(nsIOfflineCacheUpdateObserver::STATE_FINISHED); + + return rv; +} + +void nsOfflineCacheUpdate::AsyncFinishWithError() { + NotifyState(nsOfflineCacheUpdate::STATE_ERROR); + Finish(); +} + +static nsresult EvictOneOfCacheGroups(nsIApplicationCacheService* cacheService, + const nsTArray<nsCString>& groups) { + nsresult rv; + + for (auto& group : groups) { + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), group); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCache> cache; + rv = cacheService->GetActiveCache(group, getter_AddRefs(cache)); + // Maybe someone in another thread or process have deleted it. + if (NS_FAILED(rv) || !cache) continue; + + bool pinned; + rv = nsOfflineCacheUpdateService::OfflineAppPinnedForURI(uri, &pinned); + NS_ENSURE_SUCCESS(rv, rv); + + if (!pinned) { + rv = cache->Discard(); + return NS_OK; + } + } + + return NS_ERROR_FILE_NOT_FOUND; +} + +nsresult nsOfflineCacheUpdate::EvictOneNonPinned() { + nsresult rv; + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<nsCString> groups; + rv = cacheService->GetGroupsTimeOrdered(groups); + NS_ENSURE_SUCCESS(rv, rv); + + return EvictOneOfCacheGroups(cacheService, groups); +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate::nsIOfflineCacheUpdate +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetUpdateDomain(nsACString& aUpdateDomain) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + aUpdateDomain = mUpdateDomain; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetStatus(uint16_t* aStatus) { + switch (mState) { + case STATE_CHECKING: + *aStatus = dom::OfflineResourceList_Binding::CHECKING; + return NS_OK; + case STATE_DOWNLOADING: + *aStatus = dom::OfflineResourceList_Binding::DOWNLOADING; + return NS_OK; + default: + *aStatus = dom::OfflineResourceList_Binding::IDLE; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetPartial(bool* aPartial) { + *aPartial = mPartialUpdate || mOnlyCheckUpdate; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetManifestURI(nsIURI** aManifestURI) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + NS_IF_ADDREF(*aManifestURI = mManifestURI); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetLoadingPrincipal(nsIPrincipal** aLoadingPrincipal) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + NS_IF_ADDREF(*aLoadingPrincipal = mLoadingPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetSucceeded(bool* aSucceeded) { + NS_ENSURE_TRUE(mState == STATE_FINISHED, NS_ERROR_NOT_AVAILABLE); + + *aSucceeded = mSucceeded; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetIsUpgrade(bool* aIsUpgrade) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + *aIsUpgrade = (mPreviousApplicationCache != nullptr); + + return NS_OK; +} + +nsresult nsOfflineCacheUpdate::AddURI(nsIURI* aURI, uint32_t aType, + uint32_t aLoadFlags) { + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + if (mState >= STATE_DOWNLOADING) return NS_ERROR_NOT_AVAILABLE; + + // Resource URIs must have the same scheme as the manifest. + nsAutoCString scheme; + aURI->GetScheme(scheme); + + if (!mManifestURI->SchemeIs(scheme.get())) { + return NS_ERROR_FAILURE; + } + + // Don't fetch the same URI twice. + for (uint32_t i = 0; i < mItems.Length(); i++) { + bool equals; + if (NS_SUCCEEDED(mItems[i]->mURI->Equals(aURI, &equals)) && equals && + mItems[i]->mLoadFlags == aLoadFlags) { + // retain both types. + mItems[i]->mItemType |= aType; + return NS_OK; + } + } + + RefPtr<nsOfflineCacheUpdateItem> item = new nsOfflineCacheUpdateItem( + aURI, mDocumentURI, mLoadingPrincipal, mApplicationCache, + mPreviousApplicationCache, aType, aLoadFlags); + if (!item) return NS_ERROR_OUT_OF_MEMORY; + + mItems.AppendElement(item); + mAddedItems = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::AddDynamicURI(nsIURI* aURI) { + if (GeckoProcessType_Default != XRE_GetProcessType()) + return NS_ERROR_NOT_IMPLEMENTED; + + // If this is a partial update and the resource is already in the + // cache, we should only mark the entry, not fetch it again. + if (mPartialUpdate) { + nsAutoCString key; + GetCacheKey(aURI, key); + + uint32_t types; + nsresult rv = mApplicationCache->GetTypes(key, &types); + if (NS_SUCCEEDED(rv)) { + if (!(types & nsIApplicationCache::ITEM_DYNAMIC)) { + mApplicationCache->MarkEntry(key, nsIApplicationCache::ITEM_DYNAMIC); + } + return NS_OK; + } + } + + return AddURI(aURI, nsIApplicationCache::ITEM_DYNAMIC); +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::Cancel() { + LOG(("nsOfflineCacheUpdate::Cancel [%p]", this)); + + if ((mState == STATE_FINISHED) || (mState == STATE_CANCELLED)) { + return NS_ERROR_NOT_AVAILABLE; + } + + mState = STATE_CANCELLED; + mSucceeded = false; + + // Cancel all running downloads + for (uint32_t i = 0; i < mItems.Length(); ++i) { + nsOfflineCacheUpdateItem* item = mItems[i]; + + if (item->IsInProgress()) item->Cancel(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::AddObserver(nsIOfflineCacheUpdateObserver* aObserver, + bool aHoldWeak) { + LOG(("nsOfflineCacheUpdate::AddObserver [%p] to update [%p]", aObserver, + this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + if (aHoldWeak) { + nsWeakPtr weakRef = do_GetWeakReference(aObserver); + mWeakObservers.AppendObject(weakRef); + } else { + mObservers.AppendObject(aObserver); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::RemoveObserver(nsIOfflineCacheUpdateObserver* aObserver) { + LOG(("nsOfflineCacheUpdate::RemoveObserver [%p] from update [%p]", aObserver, + this)); + + NS_ENSURE_TRUE(mState >= STATE_INITIALIZED, NS_ERROR_NOT_INITIALIZED); + + for (int32_t i = 0; i < mWeakObservers.Count(); i++) { + nsCOMPtr<nsIOfflineCacheUpdateObserver> observer = + do_QueryReferent(mWeakObservers[i]); + if (observer == aObserver) { + mWeakObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + for (int32_t i = 0; i < mObservers.Count(); i++) { + if (mObservers[i] == aObserver) { + mObservers.RemoveObjectAt(i); + return NS_OK; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::GetByteProgress(uint64_t* _result) { + NS_ENSURE_ARG(_result); + + *_result = mByteProgress; + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::Schedule() { + LOG(("nsOfflineCacheUpdate::Schedule [%p]", this)); + + nsOfflineCacheUpdateService* service = + nsOfflineCacheUpdateService::EnsureService(); + + if (!service) { + return NS_ERROR_FAILURE; + } + + return service->ScheduleUpdate(this); +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::UpdateStateChanged(nsIOfflineCacheUpdate* aUpdate, + uint32_t aState) { + if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED) { + // Take the mSucceeded flag from the underlying update, we will be + // queried for it soon. mSucceeded of this update is false (manifest + // check failed) but the subsequent re-fetch update might succeed + bool succeeded; + aUpdate->GetSucceeded(&succeeded); + mSucceeded = succeeded; + } + + NotifyState(aState); + if (aState == nsIOfflineCacheUpdateObserver::STATE_FINISHED) + aUpdate->RemoveObserver(this); + + return NS_OK; +} + +void nsOfflineCacheUpdate::SetCookieJarSettings( + nsICookieJarSettings* aCookieJarSettings) { + mCookieJarSettings = aCookieJarSettings; +} + +void nsOfflineCacheUpdate::SetCookieJarSettingsArgs( + const CookieJarSettingsArgs& aCookieJarSettingsArgs) { + MOZ_ASSERT(!mCookieJarSettings); + + CookieJarSettings::Deserialize(aCookieJarSettingsArgs, + getter_AddRefs(mCookieJarSettings)); +} + +NS_IMETHODIMP +nsOfflineCacheUpdate::ApplicationCacheAvailable( + nsIApplicationCache* applicationCache) { + AssociateDocuments(applicationCache); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdate::nsIRunable +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdate::Run() { + ProcessNextURI(); + return NS_OK; +} diff --git a/uriloader/prefetch/nsOfflineCacheUpdate.h b/uriloader/prefetch/nsOfflineCacheUpdate.h new file mode 100644 index 0000000000..bb7699c046 --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdate.h @@ -0,0 +1,369 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsOfflineCacheUpdate_h__ +#define nsOfflineCacheUpdate_h__ + +#include "nsIOfflineCacheUpdate.h" + +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsIChannelEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIMutableArray.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIApplicationCache.h" +#include "nsIRunnable.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsClassHashtable.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWeakReference.h" +#include "nsICryptoHash.h" +#include "mozilla/Attributes.h" +#include "mozilla/WeakPtr.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" + +namespace mozilla { + +namespace net { +class CookieJarSettingsArgs; +} + +} // namespace mozilla + +class nsOfflineCacheUpdate; + +class nsOfflineCacheUpdateItem : public nsIStreamListener, + public nsIRunnable, + public nsIInterfaceRequestor, + public nsIChannelEventSink { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIRUNNABLE + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + nsOfflineCacheUpdateItem(nsIURI* aURI, nsIURI* aReferrerURI, + nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache* aApplicationCache, + nsIApplicationCache* aPreviousApplicationCache, + uint32_t aType, uint32_t aLoadFlags); + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIURI> mReferrerURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsIApplicationCache> mApplicationCache; + nsCOMPtr<nsIApplicationCache> mPreviousApplicationCache; + nsCString mCacheKey; + uint32_t mItemType; + uint32_t mLoadFlags; + + nsresult OpenChannel(nsOfflineCacheUpdate* aUpdate); + nsresult Cancel(); + nsresult GetRequestSucceeded(bool* succeeded); + + bool IsInProgress(); + bool IsScheduled(); + bool IsCompleted(); + + nsresult GetStatus(uint16_t* aStatus); + + private: + enum LoadStatus : uint16_t { + UNINITIALIZED = 0U, + REQUESTED = 1U, + RECEIVING = 2U, + LOADED = 3U + }; + + RefPtr<nsOfflineCacheUpdate> mUpdate; + nsCOMPtr<nsIChannel> mChannel; + uint16_t mState; + + protected: + virtual ~nsOfflineCacheUpdateItem(); + + int64_t mBytesRead; +}; + +class nsOfflineManifestItem : public nsOfflineCacheUpdateItem { + public: + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + nsOfflineManifestItem(nsIURI* aURI, nsIURI* aReferrerURI, + nsIPrincipal* aLoadingPrincipal, + nsIApplicationCache* aApplicationCache, + nsIApplicationCache* aPreviousApplicationCache); + virtual ~nsOfflineManifestItem(); + + nsCOMArray<nsIURI>& GetExplicitURIs() { return mExplicitURIs; } + nsCOMArray<nsIURI>& GetAnonymousURIs() { return mAnonymousURIs; } + nsCOMArray<nsIURI>& GetFallbackURIs() { return mFallbackURIs; } + + nsTArray<nsCString>& GetOpportunisticNamespaces() { + return mOpportunisticNamespaces; + } + nsIArray* GetNamespaces() { return mNamespaces.get(); } + + bool ParseSucceeded() { + return (mParserState != PARSE_INIT && mParserState != PARSE_ERROR); + } + bool NeedsUpdate() { return mParserState != PARSE_INIT && mNeedsUpdate; } + + void GetManifestHash(nsCString& aManifestHash) { + aManifestHash = mManifestHashValue; + } + + private: + static nsresult ReadManifest(nsIInputStream* aInputStream, void* aClosure, + const char* aFromSegment, uint32_t aOffset, + uint32_t aCount, uint32_t* aBytesConsumed); + + nsresult AddNamespace(uint32_t namespaceType, const nsCString& namespaceSpec, + const nsCString& data); + + nsresult HandleManifestLine(const nsCString::const_iterator& aBegin, + const nsCString::const_iterator& aEnd); + + /** + * Saves "offline-manifest-hash" meta data from the old offline cache + * token to mOldManifestHashValue member to be compared on + * successfull load. + */ + nsresult GetOldManifestContentHash(nsIRequest* aRequest); + /** + * This method setups the mNeedsUpdate to false when hash value + * of the just downloaded manifest file is the same as stored in cache's + * "offline-manifest-hash" meta data. Otherwise stores the new value + * to this meta data. + */ + nsresult CheckNewManifestContentHash(nsIRequest* aRequest); + + void ReadStrictFileOriginPolicyPref(); + + enum { + PARSE_INIT, + PARSE_CACHE_ENTRIES, + PARSE_FALLBACK_ENTRIES, + PARSE_BYPASS_ENTRIES, + PARSE_UNKNOWN_SECTION, + PARSE_ERROR + } mParserState; + + nsCString mReadBuf; + + nsCOMArray<nsIURI> mExplicitURIs; + nsCOMArray<nsIURI> mAnonymousURIs; + nsCOMArray<nsIURI> mFallbackURIs; + + // All opportunistic caching namespaces. Used to decide whether + // to include previously-opportunistically-cached entries. + nsTArray<nsCString> mOpportunisticNamespaces; + + // Array of nsIApplicationCacheNamespace objects specified by the + // manifest. + nsCOMPtr<nsIMutableArray> mNamespaces; + + bool mNeedsUpdate; + bool mStrictFileOriginPolicy; + + // manifest hash data + nsCOMPtr<nsICryptoHash> mManifestHash; + bool mManifestHashInitialized; + nsCString mManifestHashValue; + nsCString mOldManifestHashValue; +}; + +class nsOfflineCacheUpdateOwner : public mozilla::SupportsWeakPtr { + public: + virtual ~nsOfflineCacheUpdateOwner() {} + virtual nsresult UpdateFinished(nsOfflineCacheUpdate* aUpdate) = 0; +}; + +class nsOfflineCacheUpdate final : public nsIOfflineCacheUpdate, + public nsIOfflineCacheUpdateObserver, + public nsIRunnable, + public nsOfflineCacheUpdateOwner { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATE + NS_DECL_NSIOFFLINECACHEUPDATEOBSERVER + NS_DECL_NSIRUNNABLE + + nsOfflineCacheUpdate(); + + static nsresult GetCacheKey(nsIURI* aURI, nsACString& aKey); + + nsresult Init(); + + nsresult Begin(); + + void LoadCompleted(nsOfflineCacheUpdateItem* aItem); + void ManifestCheckCompleted(nsresult aStatus, const nsCString& aManifestHash); + void StickDocument(nsIURI* aDocumentURI); + + void SetOwner(nsOfflineCacheUpdateOwner* aOwner); + + bool IsForGroupID(const nsACString& groupID); + bool IsForProfile(nsIFile* aCustomProfileDir); + + virtual nsresult UpdateFinished(nsOfflineCacheUpdate* aUpdate) override; + + nsICookieJarSettings* CookieJarSettings() const { return mCookieJarSettings; } + void SetCookieJarSettings(nsICookieJarSettings* aCookieJarSettings); + void SetCookieJarSettingsArgs( + const mozilla::net::CookieJarSettingsArgs& aCookieJarSettingsArgs); + + protected: + ~nsOfflineCacheUpdate(); + + friend class nsOfflineCacheUpdateItem; + void OnByteProgress(uint64_t byteIncrement); + + private: + nsresult InitInternal(nsIURI* aManifestURI, nsIPrincipal* aPrincipal); + nsresult HandleManifest(bool* aDoUpdate); + nsresult AddURI(nsIURI* aURI, uint32_t aItemType, uint32_t aLoadFlags = 0); + + nsresult ProcessNextURI(); + + // Adds items from the previous cache witha type matching aType. + // If namespaceFilter is non-null, only items matching the + // specified namespaces will be added. + nsresult AddExistingItems(uint32_t aType, + nsTArray<nsCString>* namespaceFilter = nullptr); + nsresult ScheduleImplicit(); + void AssociateDocuments(nsIApplicationCache* cache); + bool CheckUpdateAvailability(); + void NotifyUpdateAvailability(bool updateAvailable); + + void GatherObservers(nsCOMArray<nsIOfflineCacheUpdateObserver>& aObservers); + void NotifyState(uint32_t state); + nsresult Finish(); + nsresult FinishNoNotify(); + + void AsyncFinishWithError(); + + // Find one non-pinned cache group and evict it. + nsresult EvictOneNonPinned(); + + enum { + STATE_UNINITIALIZED, + STATE_INITIALIZED, + STATE_CHECKING, + STATE_DOWNLOADING, + STATE_CANCELLED, + STATE_FINISHED + } mState; + + mozilla::WeakPtr<nsOfflineCacheUpdateOwner> mOwner; + + bool mAddedItems; + bool mPartialUpdate; + bool mOnlyCheckUpdate; + bool mSucceeded; + bool mObsolete; + + nsCString mUpdateDomain; + nsCString mGroupID; + nsCOMPtr<nsIURI> mManifestURI; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsCOMPtr<nsIFile> mCustomProfileDir; + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + + nsCOMPtr<nsIObserver> mUpdateAvailableObserver; + + nsCOMPtr<nsIApplicationCache> mApplicationCache; + nsCOMPtr<nsIApplicationCache> mPreviousApplicationCache; + + nsCOMPtr<nsIObserverService> mObserverService; + + RefPtr<nsOfflineManifestItem> mManifestItem; + + /* Items being updated */ + uint32_t mItemsInProgress; + nsTArray<RefPtr<nsOfflineCacheUpdateItem> > mItems; + + /* Clients watching this update for changes */ + nsCOMArray<nsIWeakReference> mWeakObservers; + nsCOMArray<nsIOfflineCacheUpdateObserver> mObservers; + + /* Documents that requested this update */ + nsCOMArray<nsIURI> mDocumentURIs; + + /* Reschedule count. When an update is rescheduled due to + * mismatched manifests, the reschedule count will be increased. */ + uint32_t mRescheduleCount; + + /* Whena an entry for a pinned app is retried, retries count is + * increaded. */ + uint32_t mPinnedEntryRetriesCount; + + RefPtr<nsOfflineCacheUpdate> mImplicitUpdate; + + bool mPinned; + + uint64_t mByteProgress; +}; + +class nsOfflineCacheUpdateService final : public nsIOfflineCacheUpdateService, + public nsIObserver, + public nsOfflineCacheUpdateOwner, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOFFLINECACHEUPDATESERVICE + NS_DECL_NSIOBSERVER + + nsOfflineCacheUpdateService(); + + nsresult Init(); + + nsresult ScheduleUpdate(nsOfflineCacheUpdate* aUpdate); + nsresult FindUpdate(nsIURI* aManifestURI, nsACString const& aOriginSuffix, + nsIFile* aCustomProfileDir, + nsOfflineCacheUpdate** aUpdate); + + nsresult Schedule(nsIURI* aManifestURI, nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + mozilla::dom::Document* aDocument, + nsPIDOMWindowInner* aWindow, nsIFile* aCustomProfileDir, + nsIOfflineCacheUpdate** aUpdate); + + virtual nsresult UpdateFinished(nsOfflineCacheUpdate* aUpdate) override; + + /** + * Returns the singleton nsOfflineCacheUpdateService without an addref, or + * nullptr if the service couldn't be created. + */ + static nsOfflineCacheUpdateService* EnsureService(); + + static already_AddRefed<nsOfflineCacheUpdateService> GetInstance(); + + static nsresult OfflineAppPinnedForURI(nsIURI* aDocumentURI, bool* aPinned); + + static nsTHashtable<nsCStringHashKey>* AllowedDomains(); + + private: + ~nsOfflineCacheUpdateService(); + + nsresult ProcessNextUpdate(); + + nsTArray<RefPtr<nsOfflineCacheUpdate> > mUpdates; + static nsTHashtable<nsCStringHashKey>* mAllowedDomains; + + bool mDisabled; + bool mUpdateRunning; +}; + +#endif diff --git a/uriloader/prefetch/nsOfflineCacheUpdateService.cpp b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp new file mode 100644 index 0000000000..3f43193e39 --- /dev/null +++ b/uriloader/prefetch/nsOfflineCacheUpdateService.cpp @@ -0,0 +1,646 @@ +/* -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "OfflineCacheUpdateChild.h" +#include "OfflineCacheUpdateParent.h" +#include "nsXULAppAPI.h" +#include "OfflineCacheUpdateGlue.h" +#include "nsOfflineCacheUpdate.h" + +#include "nsCURILoader.h" +#include "nsIApplicationCacheService.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "nsIObserverService.h" +#include "nsIWebProgress.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Logging.h" +#include "mozilla/Components.h" +#include "mozilla/Preferences.h" +#include "mozilla/Attributes.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "nsContentUtils.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static nsOfflineCacheUpdateService* gOfflineCacheUpdateService = nullptr; + +nsTHashtable<nsCStringHashKey>* nsOfflineCacheUpdateService::mAllowedDomains = + nullptr; + +nsTHashtable<nsCStringHashKey>* nsOfflineCacheUpdateService::AllowedDomains() { + if (!mAllowedDomains) mAllowedDomains = new nsTHashtable<nsCStringHashKey>(); + + return mAllowedDomains; +} + +typedef mozilla::docshell::OfflineCacheUpdateParent OfflineCacheUpdateParent; +typedef mozilla::docshell::OfflineCacheUpdateChild OfflineCacheUpdateChild; +typedef mozilla::docshell::OfflineCacheUpdateGlue OfflineCacheUpdateGlue; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsOfflineCacheUpdate:5 +// set MOZ_LOG_FILE=offlineupdate.log +// +// this enables LogLevel::Debug level information and places all output in +// the file offlineupdate.log +// +LazyLogModule gOfflineCacheUpdateLog("nsOfflineCacheUpdate"); + +#undef LOG +#define LOG(args) \ + MOZ_LOG(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() \ + MOZ_LOG_TEST(gOfflineCacheUpdateLog, mozilla::LogLevel::Debug) + +//----------------------------------------------------------------------------- +// nsOfflineCachePendingUpdate +//----------------------------------------------------------------------------- + +class nsOfflineCachePendingUpdate final : public nsIWebProgressListener, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBPROGRESSLISTENER + + nsOfflineCachePendingUpdate(nsOfflineCacheUpdateService* aService, + nsIURI* aManifestURI, nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + Document* aDocument) + : mService(aService), + mManifestURI(aManifestURI), + mDocumentURI(aDocumentURI), + mLoadingPrincipal(aLoadingPrincipal), + mDidReleaseThis(false) { + mDocument = do_GetWeakReference(aDocument); + } + + private: + ~nsOfflineCachePendingUpdate() {} + + RefPtr<nsOfflineCacheUpdateService> mService; + nsCOMPtr<nsIURI> mManifestURI; + nsCOMPtr<nsIURI> mDocumentURI; + nsCOMPtr<nsIPrincipal> mLoadingPrincipal; + nsWeakPtr mDocument; + bool mDidReleaseThis; +}; + +NS_IMPL_ISUPPORTS(nsOfflineCachePendingUpdate, nsIWebProgressListener, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIWebProgressListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnProgressChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, + int32_t curSelfProgress, + int32_t maxSelfProgress, + int32_t curTotalProgress, + int32_t maxTotalProgress) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t progressStateFlags, + nsresult aStatus) { + if (mDidReleaseThis) { + return NS_OK; + } + nsCOMPtr<Document> updateDoc = do_QueryReferent(mDocument); + if (!updateDoc) { + // The document that scheduled this update has gone away, + // we don't need to listen anymore. + aWebProgress->RemoveProgressListener(this); + MOZ_ASSERT(!mDidReleaseThis); + mDidReleaseThis = true; + NS_RELEASE_THIS(); + return NS_OK; + } + + if (!(progressStateFlags & STATE_STOP)) { + return NS_OK; + } + + nsCOMPtr<mozIDOMWindowProxy> windowProxy; + aWebProgress->GetDOMWindow(getter_AddRefs(windowProxy)); + if (!windowProxy) return NS_OK; + + auto* outerWindow = nsPIDOMWindowOuter::From(windowProxy); + nsPIDOMWindowInner* innerWindow = outerWindow->GetCurrentInnerWindow(); + + nsCOMPtr<Document> progressDoc = outerWindow->GetDoc(); + if (!progressDoc || progressDoc != updateDoc) { + return NS_OK; + } + + LOG(("nsOfflineCachePendingUpdate::OnStateChange [%p, doc=%p]", this, + progressDoc.get())); + + // Only schedule the update if the document loaded successfully + if (NS_SUCCEEDED(aStatus)) { + nsCOMPtr<nsIOfflineCacheUpdate> update; + mService->Schedule(mManifestURI, mDocumentURI, mLoadingPrincipal, updateDoc, + innerWindow, nullptr, getter_AddRefs(update)); + if (mDidReleaseThis) { + return NS_OK; + } + } + + aWebProgress->RemoveProgressListener(this); + MOZ_ASSERT(!mDidReleaseThis); + mDidReleaseThis = true; + NS_RELEASE_THIS(); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI* location, + uint32_t aFlags) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCachePendingUpdate::OnContentBlockingEvent( + nsIWebProgress* aWebProgress, nsIRequest* aRequest, uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsOfflineCacheUpdateService, nsIOfflineCacheUpdateService, + nsIObserver, nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService <public> +//----------------------------------------------------------------------------- + +nsOfflineCacheUpdateService::nsOfflineCacheUpdateService() + : mDisabled(false), mUpdateRunning(false) { + MOZ_ASSERT(NS_IsMainThread()); +} + +nsOfflineCacheUpdateService::~nsOfflineCacheUpdateService() { + MOZ_ASSERT(gOfflineCacheUpdateService == this); + gOfflineCacheUpdateService = nullptr; + + delete mAllowedDomains; + mAllowedDomains = nullptr; +} + +nsresult nsOfflineCacheUpdateService::Init() { + // Observe xpcom-shutdown event + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) return NS_ERROR_FAILURE; + + nsresult rv = + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + NS_ENSURE_SUCCESS(rv, rv); + + gOfflineCacheUpdateService = this; + + return NS_OK; +} + +/* static */ +already_AddRefed<nsOfflineCacheUpdateService> +nsOfflineCacheUpdateService::GetInstance() { + if (!gOfflineCacheUpdateService) { + auto serv = MakeRefPtr<nsOfflineCacheUpdateService>(); + if (NS_FAILED(serv->Init())) serv = nullptr; + MOZ_ASSERT(gOfflineCacheUpdateService == serv.get()); + return serv.forget(); + } + + return do_AddRef(gOfflineCacheUpdateService); +} + +/* static */ +nsOfflineCacheUpdateService* nsOfflineCacheUpdateService::EnsureService() { + if (!gOfflineCacheUpdateService) { + // Make the service manager hold a long-lived reference to the service + nsCOMPtr<nsIOfflineCacheUpdateService> service = + components::OfflineCacheUpdate::Service(); + Unused << service; + } + + return gOfflineCacheUpdateService; +} + +nsresult nsOfflineCacheUpdateService::ScheduleUpdate( + nsOfflineCacheUpdate* aUpdate) { + LOG(("nsOfflineCacheUpdateService::Schedule [%p, update=%p]", this, aUpdate)); + + aUpdate->SetOwner(this); + + mUpdates.AppendElement(aUpdate); + ProcessNextUpdate(); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::ScheduleOnDocumentStop( + nsIURI* aManifestURI, nsIURI* aDocumentURI, nsIPrincipal* aLoadingPrincipal, + Document* aDocument) { + LOG( + ("nsOfflineCacheUpdateService::ScheduleOnDocumentStop [%p, " + "manifestURI=%p, documentURI=%p doc=%p]", + this, aManifestURI, aDocumentURI, aDocument)); + + nsCOMPtr<nsIWebProgress> progress = + do_QueryInterface(aDocument->GetContainer()); + NS_ENSURE_TRUE(progress, NS_ERROR_INVALID_ARG); + + // Proceed with cache update + RefPtr<nsOfflineCachePendingUpdate> update = new nsOfflineCachePendingUpdate( + this, aManifestURI, aDocumentURI, aLoadingPrincipal, aDocument); + NS_ENSURE_TRUE(update, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = progress->AddProgressListener( + update, nsIWebProgress::NOTIFY_STATE_DOCUMENT); + NS_ENSURE_SUCCESS(rv, rv); + + // The update will release when it has scheduled itself. + Unused << update.forget(); + + return NS_OK; +} + +nsresult nsOfflineCacheUpdateService::UpdateFinished( + nsOfflineCacheUpdate* aUpdate) { + LOG(("nsOfflineCacheUpdateService::UpdateFinished [%p, update=%p]", this, + aUpdate)); + + NS_ASSERTION(mUpdates.Length() > 0 && mUpdates[0] == aUpdate, + "Unknown update completed"); + + // keep this item alive until we're done notifying observers + RefPtr<nsOfflineCacheUpdate> update = mUpdates[0]; + Unused << update; + mUpdates.RemoveElementAt(0); + mUpdateRunning = false; + + ProcessNextUpdate(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService <private> +//----------------------------------------------------------------------------- + +nsresult nsOfflineCacheUpdateService::ProcessNextUpdate() { + LOG(("nsOfflineCacheUpdateService::ProcessNextUpdate [%p, num=%zu]", this, + mUpdates.Length())); + + if (mDisabled) return NS_ERROR_ABORT; + + if (mUpdateRunning) return NS_OK; + + if (mUpdates.Length() > 0) { + mUpdateRunning = true; + + return mUpdates[0]->Begin(); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIOfflineCacheUpdateService +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateService::GetNumUpdates(uint32_t* aNumUpdates) { + LOG(("nsOfflineCacheUpdateService::GetNumUpdates [%p]", this)); + + *aNumUpdates = mUpdates.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::GetUpdate(uint32_t aIndex, + nsIOfflineCacheUpdate** aUpdate) { + LOG(("nsOfflineCacheUpdateService::GetUpdate [%p, %d]", this, aIndex)); + + if (aIndex < mUpdates.Length()) { + NS_ADDREF(*aUpdate = mUpdates[aIndex]); + } else { + *aUpdate = nullptr; + } + + return NS_OK; +} + +nsresult nsOfflineCacheUpdateService::FindUpdate( + nsIURI* aManifestURI, nsACString const& aOriginSuffix, + nsIFile* aCustomProfileDir, nsOfflineCacheUpdate** aUpdate) { + nsresult rv; + + nsCOMPtr<nsIApplicationCacheService> cacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString groupID; + rv = + cacheService->BuildGroupIDForSuffix(aManifestURI, aOriginSuffix, groupID); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsOfflineCacheUpdate> update; + for (uint32_t i = 0; i < mUpdates.Length(); i++) { + update = mUpdates[i]; + + bool partial; + rv = update->GetPartial(&partial); + NS_ENSURE_SUCCESS(rv, rv); + + if (partial) { + // Partial updates aren't considered + continue; + } + + if (update->IsForGroupID(groupID) && + update->IsForProfile(aCustomProfileDir)) { + update.swap(*aUpdate); + return NS_OK; + } + } + + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsOfflineCacheUpdateService::Schedule( + nsIURI* aManifestURI, nsIURI* aDocumentURI, nsIPrincipal* aLoadingPrincipal, + Document* aDocument, nsPIDOMWindowInner* aWindow, + nsIFile* aCustomProfileDir, nsIOfflineCacheUpdate** aUpdate) { + nsCOMPtr<nsIOfflineCacheUpdate> update; + if (GeckoProcessType_Default != XRE_GetProcessType()) { + update = new OfflineCacheUpdateChild(aWindow); + } else { + update = new OfflineCacheUpdateGlue(); + } + + nsresult rv; + + if (aWindow) { + // Ensure there is window.applicationCache object that is + // responsible for association of the new applicationCache + // with the corresponding document. Just ignore the result. + aWindow->GetApplicationCache(); + } + + rv = update->Init(aManifestURI, aDocumentURI, aLoadingPrincipal, aDocument, + aCustomProfileDir); + NS_ENSURE_SUCCESS(rv, rv); + + rv = update->Schedule(); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*aUpdate = update); + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::ScheduleUpdate(nsIURI* aManifestURI, + nsIURI* aDocumentURI, + nsIPrincipal* aLoadingPrincipal, + mozIDOMWindow* aWindow, + nsIOfflineCacheUpdate** aUpdate) { + return Schedule(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr, + nsPIDOMWindowInner::From(aWindow), nullptr, aUpdate); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::ScheduleAppUpdate( + nsIURI* aManifestURI, nsIURI* aDocumentURI, nsIPrincipal* aLoadingPrincipal, + nsIFile* aProfileDir, nsIOfflineCacheUpdate** aUpdate) { + return Schedule(aManifestURI, aDocumentURI, aLoadingPrincipal, nullptr, + nullptr, aProfileDir, aUpdate); +} + +NS_IMETHODIMP nsOfflineCacheUpdateService::CheckForUpdate( + nsIURI* aManifestURI, nsIPrincipal* aLoadingPrincipal, + nsIObserver* aObserver) { + if (GeckoProcessType_Default != XRE_GetProcessType()) { + // Not intended to support this on child processes + return NS_ERROR_NOT_IMPLEMENTED; + } + + nsCOMPtr<nsIOfflineCacheUpdate> update = new OfflineCacheUpdateGlue(); + + nsresult rv; + + rv = update->InitForUpdateCheck(aManifestURI, aLoadingPrincipal, aObserver); + NS_ENSURE_SUCCESS(rv, rv); + + rv = update->Schedule(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsOfflineCacheUpdateService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + if (mUpdates.Length() > 0) mUpdates[0]->Cancel(); + mDisabled = true; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsOfflineCacheUpdateService::nsIOfflineCacheUpdateService +//----------------------------------------------------------------------------- + +static nsresult OfflineAppPermForPrincipal(nsIPrincipal* aPrincipal, + bool pinned, bool* aAllowed) { + *aAllowed = false; + + if (!StaticPrefs::browser_cache_offline_enable()) { + return NS_OK; + } + + if (!StaticPrefs::browser_cache_offline_storage_enable()) { + return NS_OK; + } + + if (!aPrincipal) return NS_ERROR_INVALID_ARG; + + nsCOMPtr<nsIURI> uri; + // Casting to BasePrincipal, as we can't get InnerMost URI otherwise + auto* basePrincipal = BasePrincipal::Cast(aPrincipal); + basePrincipal->GetURI(getter_AddRefs(uri)); + + if (!uri) return NS_OK; + + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri); + if (!innerURI) return NS_OK; + + // only https applications can use offline APIs. + if (!innerURI->SchemeIs("https")) { + return NS_OK; + } + + nsAutoCString domain; + nsresult rv = innerURI->GetAsciiHost(domain); + NS_ENSURE_SUCCESS(rv, rv); + + if (nsOfflineCacheUpdateService::AllowedDomains()->Contains(domain)) { + *aAllowed = true; + return NS_OK; + } + + nsCOMPtr<nsIPermissionManager> permissionManager = + services::GetPermissionManager(); + if (!permissionManager) { + return NS_OK; + } + + uint32_t perm; + const nsLiteralCString permName = pinned ? "pin-app"_ns : "offline-app"_ns; + permissionManager->TestExactPermissionFromPrincipal(aPrincipal, permName, + &perm); + + if (perm == nsIPermissionManager::ALLOW_ACTION || + perm == nsIOfflineCacheUpdateService::ALLOW_NO_WARN) { + *aAllowed = true; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::OfflineAppAllowed(nsIPrincipal* aPrincipal, + bool* aAllowed) { + return OfflineAppPermForPrincipal(aPrincipal, false, aAllowed); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::OfflineAppAllowedForURI(nsIURI* aURI, + bool* aAllowed) { + OriginAttributes attrs; + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aURI, attrs); + return OfflineAppPermForPrincipal(principal, false, aAllowed); +} + +nsresult nsOfflineCacheUpdateService::OfflineAppPinnedForURI( + nsIURI* aDocumentURI, bool* aPinned) { + OriginAttributes attrs; + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aDocumentURI, attrs); + return OfflineAppPermForPrincipal(principal, true, aPinned); +} + +NS_IMETHODIMP +nsOfflineCacheUpdateService::AllowOfflineApp(nsIPrincipal* aPrincipal) { + nsresult rv; + + if (!StaticPrefs::browser_cache_offline_enable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!StaticPrefs::browser_cache_offline_storage_enable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIURI> uri; + // Casting to BasePrincipal, as we can't get InnerMost URI otherwise + auto* basePrincipal = BasePrincipal::Cast(aPrincipal); + basePrincipal->GetURI(getter_AddRefs(uri)); + + if (!uri) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIURI> innerURI = NS_GetInnermostURI(uri); + if (!innerURI) { + return NS_ERROR_NOT_AVAILABLE; + } + + // if http then we should prevent this cache + if (innerURI->SchemeIs("http")) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (GeckoProcessType_Default != XRE_GetProcessType()) { + ContentChild* child = ContentChild::GetSingleton(); + + if (!child->SendSetOfflinePermission(IPC::Principal(aPrincipal))) { + return NS_ERROR_FAILURE; + } + + nsAutoCString domain; + rv = aPrincipal->GetBaseDomain(domain); + NS_ENSURE_SUCCESS(rv, rv); + + nsOfflineCacheUpdateService::AllowedDomains()->PutEntry(domain); + } else { + nsCOMPtr<nsIPermissionManager> permissionManager = + services::GetPermissionManager(); + if (!permissionManager) return NS_ERROR_NOT_AVAILABLE; + + rv = permissionManager->AddFromPrincipal( + aPrincipal, "offline-app"_ns, nsIPermissionManager::ALLOW_ACTION, + nsIPermissionManager::EXPIRE_NEVER, 0); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} diff --git a/uriloader/prefetch/nsPrefetchService.cpp b/uriloader/prefetch/nsPrefetchService.cpp new file mode 100644 index 0000000000..94762702e6 --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.cpp @@ -0,0 +1,889 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsPrefetchService.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/CORSMode.h" +#include "mozilla/Components.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/Preferences.h" +#include "ReferrerInfo.h" + +#include "nsIObserverService.h" +#include "nsIWebProgress.h" +#include "nsICacheInfoChannel.h" +#include "nsIHttpChannel.h" +#include "nsIURL.h" +#include "nsISupportsPriority.h" +#include "nsNetUtil.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsStreamUtils.h" +#include "prtime.h" +#include "mozilla/Logging.h" +#include "plstr.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsINode.h" +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" +#include "mozilla/AsyncEventDispatcher.h" + +using namespace mozilla; + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=nsPrefetch:5 +// set MOZ_LOG_FILE=prefetch.log +// +// this enables LogLevel::Debug level information and places all output in +// the file prefetch.log +// +static LazyLogModule gPrefetchLog("nsPrefetch"); + +#undef LOG +#define LOG(args) MOZ_LOG(gPrefetchLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gPrefetchLog, mozilla::LogLevel::Debug) + +#define PREFETCH_PREF "network.prefetch-next" +#define PARALLELISM_PREF "network.prefetch-next.parallelism" +#define AGGRESSIVE_PREF "network.prefetch-next.aggressive" + +//----------------------------------------------------------------------------- +// nsPrefetchNode <public> +//----------------------------------------------------------------------------- + +nsPrefetchNode::nsPrefetchNode(nsPrefetchService* aService, nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, nsINode* aSource, + nsContentPolicyType aPolicyType, bool aPreload) + : mURI(aURI), + mReferrerInfo(aReferrerInfo), + mPolicyType(aPolicyType), + mPreload(aPreload), + mService(aService), + mChannel(nullptr), + mBytesRead(0), + mShouldFireLoadEvent(false) { + nsWeakPtr source = do_GetWeakReference(aSource); + mSources.AppendElement(source); +} + +nsresult nsPrefetchNode::OpenChannel() { + if (mSources.IsEmpty()) { + // Don't attempt to prefetch if we don't have a source node + // (which should never happen). + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsINode> source; + while (!mSources.IsEmpty() && + !(source = do_QueryReferent(mSources.ElementAt(0)))) { + // If source is null remove it. + // (which should never happen). + mSources.RemoveElementAt(0); + } + + if (!source) { + // Don't attempt to prefetch if we don't have a source node + // (which should never happen). + + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsILoadGroup> loadGroup = source->OwnerDoc()->GetDocumentLoadGroup(); + CORSMode corsMode = CORS_NONE; + if (auto* link = dom::HTMLLinkElement::FromNode(source)) { + corsMode = link->GetCORSMode(); + } + + uint32_t securityFlags; + if (corsMode == CORS_NONE) { + securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + } else { + securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + if (corsMode == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + } + nsresult rv = NS_NewChannelInternal( + getter_AddRefs(mChannel), mURI, source, source->NodePrincipal(), + nullptr, // aTriggeringPrincipal + Maybe<ClientInfo>(), Maybe<ServiceWorkerDescriptor>(), securityFlags, + mPolicyType, source->OwnerDoc()->CookieJarSettings(), + nullptr, // aPerformanceStorage + loadGroup, // aLoadGroup + this, // aCallbacks + nsIRequest::LOAD_BACKGROUND | nsICachingChannel::LOAD_ONLY_IF_MODIFIED); + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(mReferrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(success)); + success = httpChannel->SetRequestHeader("X-Moz"_ns, "prefetch"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(success)); + } + + // Reduce the priority of prefetch network requests. + nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(mChannel); + if (priorityChannel) { + priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST); + } + + rv = mChannel->AsyncOpen(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Drop the ref to the channel, because we don't want to end up with + // cycles through it. + mChannel = nullptr; + } + return rv; +} + +nsresult nsPrefetchNode::CancelChannel(nsresult error) { + mChannel->Cancel(error); + mChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsPrefetchNode, nsIRequestObserver, nsIStreamListener, + nsIInterfaceRequestor, nsIChannelEventSink, + nsIRedirectResultListener) + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::OnStartRequest(nsIRequest* aRequest) { + nsresult rv; + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest, &rv); + if (NS_FAILED(rv)) return rv; + + // if the load is cross origin without CORS, or the CORS access is rejected, + // always fire load event to avoid leaking site information. + nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo(); + mShouldFireLoadEvent = + loadInfo->GetTainting() == LoadTainting::Opaque || + (loadInfo->GetTainting() == LoadTainting::CORS && + (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv))); + + // no need to prefetch http error page + bool requestSucceeded; + if (NS_FAILED(httpChannel->GetRequestSucceeded(&requestSucceeded)) || + !requestSucceeded) { + return NS_BINDING_ABORTED; + } + + nsCOMPtr<nsICacheInfoChannel> cacheInfoChannel = + do_QueryInterface(aRequest, &rv); + if (NS_FAILED(rv)) return rv; + + // no need to prefetch a document that is already in the cache + bool fromCache; + if (NS_SUCCEEDED(cacheInfoChannel->IsFromCache(&fromCache)) && fromCache) { + LOG(("document is already in the cache; canceling prefetch\n")); + // although it's canceled we still want to fire load event + mShouldFireLoadEvent = true; + return NS_BINDING_ABORTED; + } + + // + // no need to prefetch a document that must be requested fresh each + // and every time. + // + uint32_t expTime; + if (NS_SUCCEEDED(cacheInfoChannel->GetCacheTokenExpirationTime(&expTime))) { + if (NowInSeconds() >= expTime) { + LOG( + ("document cannot be reused from cache; " + "canceling prefetch\n")); + return NS_BINDING_ABORTED; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchNode::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + uint32_t bytesRead = 0; + aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &bytesRead); + mBytesRead += bytesRead; + LOG(("prefetched %u bytes [offset=%" PRIu64 "]\n", bytesRead, aOffset)); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchNode::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + LOG(("done prefetching [status=%" PRIx32 "]\n", + static_cast<uint32_t>(aStatus))); + + if (mBytesRead == 0 && aStatus == NS_OK && mChannel) { + // we didn't need to read (because LOAD_ONLY_IF_MODIFIED was + // specified), but the object should report loadedSize as if it + // did. + mChannel->GetContentLength(&mBytesRead); + } + + mService->NotifyLoadCompleted(this); + mService->DispatchEvent(this, mShouldFireLoadEvent || NS_SUCCEEDED(aStatus)); + mService->RemoveNodeAndMaybeStartNextPrefetchURI(this); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIChannelEventSink*>(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + NS_ADDREF_THIS(); + *aResult = static_cast<nsIRedirectResultListener*>(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + nsCOMPtr<nsIURI> newURI; + nsresult rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) return rv; + + if (!newURI->SchemeIs("http") && !newURI->SchemeIs("https")) { + LOG(("rejected: URL is not of type http/https\n")); + return NS_ERROR_ABORT; + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + rv = httpChannel->SetRequestHeader("X-Moz"_ns, "prefetch"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Assign to mChannel after we get notification about success of the + // redirect in OnRedirectResult. + mRedirectChannel = aNewChannel; + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchNode::nsIRedirectResultListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchNode::OnRedirectResult(bool proceeding) { + if (proceeding && mRedirectChannel) mChannel = mRedirectChannel; + + mRedirectChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService <public> +//----------------------------------------------------------------------------- + +nsPrefetchService::nsPrefetchService() + : mMaxParallelism(6), + mStopCount(0), + mHaveProcessed(false), + mPrefetchDisabled(true), + mAggressive(false) {} + +nsPrefetchService::~nsPrefetchService() { + Preferences::RemoveObserver(this, PREFETCH_PREF); + Preferences::RemoveObserver(this, PARALLELISM_PREF); + Preferences::RemoveObserver(this, AGGRESSIVE_PREF); + // cannot reach destructor if prefetch in progress (listener owns reference + // to this service) + EmptyPrefetchQueue(); +} + +nsresult nsPrefetchService::Init() { + nsresult rv; + + // read prefs and hook up pref observer + mPrefetchDisabled = !Preferences::GetBool(PREFETCH_PREF, !mPrefetchDisabled); + Preferences::AddWeakObserver(this, PREFETCH_PREF); + + mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); + if (mMaxParallelism < 1) { + mMaxParallelism = 1; + } + Preferences::AddWeakObserver(this, PARALLELISM_PREF); + + mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false); + Preferences::AddWeakObserver(this, AGGRESSIVE_PREF); + + // Observe xpcom-shutdown event + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) return NS_ERROR_FAILURE; + + rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mPrefetchDisabled) { + AddProgressListener(); + } + + return NS_OK; +} + +void nsPrefetchService::RemoveNodeAndMaybeStartNextPrefetchURI( + nsPrefetchNode* aFinished) { + if (aFinished) { + mCurrentNodes.RemoveElement(aFinished); + } + + if ((!mStopCount && mHaveProcessed) || mAggressive) { + ProcessNextPrefetchURI(); + } +} + +void nsPrefetchService::ProcessNextPrefetchURI() { + if (mCurrentNodes.Length() >= static_cast<uint32_t>(mMaxParallelism)) { + // We already have enough prefetches going on, so hold off + // for now. + return; + } + + nsresult rv; + + do { + if (mPrefetchQueue.empty()) { + break; + } + RefPtr<nsPrefetchNode> node = std::move(mPrefetchQueue.front()); + mPrefetchQueue.pop_front(); + + if (LOG_ENABLED()) { + LOG(("ProcessNextPrefetchURI [%s]\n", + node->mURI->GetSpecOrDefault().get())); + } + + // + // if opening the channel fails (e.g. security check returns an error), + // send an error event and then just skip to the next uri + // + rv = node->OpenChannel(); + if (NS_SUCCEEDED(rv)) { + mCurrentNodes.AppendElement(node); + } else { + DispatchEvent(node, false); + } + } while (NS_FAILED(rv)); +} + +void nsPrefetchService::NotifyLoadRequested(nsPrefetchNode* node) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) return; + + observerService->NotifyObservers( + static_cast<nsIStreamListener*>(node), + (node->mPreload) ? "preload-load-requested" : "prefetch-load-requested", + nullptr); +} + +void nsPrefetchService::NotifyLoadCompleted(nsPrefetchNode* node) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) return; + + observerService->NotifyObservers( + static_cast<nsIStreamListener*>(node), + (node->mPreload) ? "preload-load-completed" : "prefetch-load-completed", + nullptr); +} + +void nsPrefetchService::DispatchEvent(nsPrefetchNode* node, bool aSuccess) { + for (uint32_t i = 0; i < node->mSources.Length(); i++) { + nsCOMPtr<nsINode> domNode = do_QueryReferent(node->mSources.ElementAt(i)); + if (domNode && domNode->IsInComposedDoc()) { + // We don't dispatch synchronously since |node| might be in a DocGroup + // that we're not allowed to touch. (Our network request happens in the + // DocGroup of one of the mSources nodes--not necessarily this one). + RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher( + domNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo); + dispatcher->RequireNodeInDocument(); + dispatcher->PostDOMEvent(); + } + } +} + +//----------------------------------------------------------------------------- +// nsPrefetchService <private> +//----------------------------------------------------------------------------- + +void nsPrefetchService::AddProgressListener() { + // Register as an observer for the document loader + nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service(); + if (progress) + progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); +} + +void nsPrefetchService::RemoveProgressListener() { + // Register as an observer for the document loader + nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service(); + if (progress) progress->RemoveProgressListener(this); +} + +nsresult nsPrefetchService::EnqueueURI(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, + nsPrefetchNode** aNode) { + RefPtr<nsPrefetchNode> node = new nsPrefetchNode( + this, aURI, aReferrerInfo, aSource, nsIContentPolicy::TYPE_OTHER, false); + mPrefetchQueue.push_back(node); + node.forget(aNode); + return NS_OK; +} + +void nsPrefetchService::EmptyPrefetchQueue() { + while (!mPrefetchQueue.empty()) { + mPrefetchQueue.pop_back(); + } +} + +void nsPrefetchService::StartPrefetching() { + // + // at initialization time we might miss the first DOCUMENT START + // notification, so we have to be careful to avoid letting our + // stop count go negative. + // + if (mStopCount > 0) mStopCount--; + + LOG(("StartPrefetching [stopcount=%d]\n", mStopCount)); + + // only start prefetching after we've received enough DOCUMENT + // STOP notifications. we do this inorder to defer prefetching + // until after all sub-frames have finished loading. + if (!mStopCount) { + mHaveProcessed = true; + while (!mPrefetchQueue.empty() && + mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) { + ProcessNextPrefetchURI(); + } + } +} + +void nsPrefetchService::StopPrefetching() { + mStopCount++; + + LOG(("StopPrefetching [stopcount=%d]\n", mStopCount)); + + // When we start a load, we need to stop all prefetches that has been + // added by the old load, therefore call StopAll only at the moment we + // switch to a new page load (i.e. mStopCount == 1). + // TODO: do not stop prefetches that are relevant for the new load. + if (mStopCount == 1) { + StopAll(); + } +} + +void nsPrefetchService::StopCurrentPrefetchsPreloads(bool aPreload) { + for (int32_t i = mCurrentNodes.Length() - 1; i >= 0; --i) { + if (mCurrentNodes[i]->mPreload == aPreload) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + mCurrentNodes.RemoveElementAt(i); + } + } + + if (!aPreload) { + EmptyPrefetchQueue(); + } +} + +void nsPrefetchService::StopAll() { + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + } + mCurrentNodes.Clear(); + EmptyPrefetchQueue(); +} + +nsresult nsPrefetchService::CheckURIScheme(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo) { + // + // XXX we should really be asking the protocol handler if it supports + // caching, so we can determine if there is any value to prefetching. + // for now, we'll only prefetch http and https links since we know that's + // the most common case. + // + if (!aURI->SchemeIs("http") && !aURI->SchemeIs("https")) { + LOG(("rejected: URL is not of type http/https\n")); + return NS_ERROR_ABORT; + } + + // + // the referrer URI must be http: + // + nsCOMPtr<nsIURI> referrer = aReferrerInfo->GetOriginalReferrer(); + if (!referrer) { + return NS_ERROR_ABORT; + } + + if (!referrer->SchemeIs("http") && !referrer->SchemeIs("https")) { + LOG(("rejected: referrer URL is neither http nor https\n")); + return NS_ERROR_ABORT; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsPrefetchService, nsIPrefetchService, nsIWebProgressListener, + nsIObserver, nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIPrefetchService +//----------------------------------------------------------------------------- + +nsresult nsPrefetchService::Preload(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, + nsContentPolicyType aPolicyType) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aReferrerInfo); + if (LOG_ENABLED()) { + LOG(("PreloadURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + LOG(("rejected: preload service is deprecated\n")); + return NS_ERROR_ABORT; +} + +nsresult nsPrefetchService::Prefetch(nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, bool aExplicit) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aReferrerInfo); + + if (LOG_ENABLED()) { + LOG(("PrefetchURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + if (mPrefetchDisabled) { + LOG(("rejected: prefetch service is disabled\n")); + return NS_ERROR_ABORT; + } + + nsresult rv = CheckURIScheme(aURI, aReferrerInfo); + if (NS_FAILED(rv)) { + return rv; + } + + // XXX we might want to either leverage nsIProtocolHandler::protocolFlags + // or possibly nsIRequest::loadFlags to determine if this URI should be + // prefetched. + // + + // skip URLs that contain query strings, except URLs for which prefetching + // has been explicitly requested. + if (!aExplicit) { + nsCOMPtr<nsIURL> url(do_QueryInterface(aURI, &rv)); + if (NS_FAILED(rv)) return rv; + nsAutoCString query; + rv = url->GetQuery(query); + if (NS_FAILED(rv) || !query.IsEmpty()) { + LOG(("rejected: URL has a query string\n")); + return NS_ERROR_ABORT; + } + } + + // + // Check whether it is being prefetched. + // + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + bool equals; + if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (mCurrentNodes[i]->mSources.IndexOf(source) == + mCurrentNodes[i]->mSources.NoIndex) { + LOG( + ("URL is already being prefetched, add a new reference " + "document\n")); + mCurrentNodes[i]->mSources.AppendElement(source); + return NS_OK; + } else { + LOG(("URL is already being prefetched by this document")); + return NS_ERROR_ABORT; + } + } + } + + // + // Check whether it is on the prefetch queue. + // + for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt = + mPrefetchQueue.begin(); + nodeIt != mPrefetchQueue.end(); nodeIt++) { + bool equals; + RefPtr<nsPrefetchNode> node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (node->mSources.IndexOf(source) == node->mSources.NoIndex) { + LOG( + ("URL is already being prefetched, add a new reference " + "document\n")); + node->mSources.AppendElement(do_GetWeakReference(aSource)); + return NS_OK; + } else { + LOG(("URL is already being prefetched by this document")); + return NS_ERROR_ABORT; + } + } + } + + RefPtr<nsPrefetchNode> enqueuedNode; + rv = EnqueueURI(aURI, aReferrerInfo, aSource, getter_AddRefs(enqueuedNode)); + NS_ENSURE_SUCCESS(rv, rv); + + NotifyLoadRequested(enqueuedNode); + + // if there are no pages loading, kick off the request immediately + if ((!mStopCount && mHaveProcessed) || mAggressive) { + ProcessNextPrefetchURI(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::CancelPrefetchPreloadURI(nsIURI* aURI, nsINode* aSource) { + NS_ENSURE_ARG_POINTER(aURI); + + if (LOG_ENABLED()) { + LOG(("CancelPrefetchURI [%s]\n", aURI->GetSpecOrDefault().get())); + } + + // + // look in current prefetches + // + for (uint32_t i = 0; i < mCurrentNodes.Length(); ++i) { + bool equals; + if (NS_SUCCEEDED(mCurrentNodes[i]->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (mCurrentNodes[i]->mSources.IndexOf(source) != + mCurrentNodes[i]->mSources.NoIndex) { + mCurrentNodes[i]->mSources.RemoveElement(source); + if (mCurrentNodes[i]->mSources.IsEmpty()) { + mCurrentNodes[i]->CancelChannel(NS_BINDING_ABORTED); + mCurrentNodes.RemoveElementAt(i); + } + return NS_OK; + } + return NS_ERROR_FAILURE; + } + } + + // + // look into the prefetch queue + // + for (std::deque<RefPtr<nsPrefetchNode>>::iterator nodeIt = + mPrefetchQueue.begin(); + nodeIt != mPrefetchQueue.end(); nodeIt++) { + bool equals; + RefPtr<nsPrefetchNode> node = nodeIt->get(); + if (NS_SUCCEEDED(node->mURI->Equals(aURI, &equals)) && equals) { + nsWeakPtr source = do_GetWeakReference(aSource); + if (node->mSources.IndexOf(source) != node->mSources.NoIndex) { +#ifdef DEBUG + int32_t inx = node->mSources.IndexOf(source); + nsCOMPtr<nsINode> domNode = + do_QueryReferent(node->mSources.ElementAt(inx)); + MOZ_ASSERT(domNode); +#endif + + node->mSources.RemoveElement(source); + if (node->mSources.IsEmpty()) { + mPrefetchQueue.erase(nodeIt); + } + return NS_OK; + } + return NS_ERROR_FAILURE; + } + } + + // not found! + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsPrefetchService::PreloadURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, + nsContentPolicyType aPolicyType) { + return Preload(aURI, aReferrerInfo, aSource, aPolicyType); +} + +NS_IMETHODIMP +nsPrefetchService::PrefetchURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, bool aExplicit) { + return Prefetch(aURI, aReferrerInfo, aSource, aExplicit); +} + +NS_IMETHODIMP +nsPrefetchService::HasMoreElements(bool* aHasMore) { + *aHasMore = (mCurrentNodes.Length() || !mPrefetchQueue.empty()); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIWebProgressListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchService::OnProgressChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, + int32_t curSelfProgress, + int32_t maxSelfProgress, + int32_t curTotalProgress, + int32_t maxTotalProgress) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t progressStateFlags, + nsresult aStatus) { + if (progressStateFlags & STATE_IS_DOCUMENT) { + if (progressStateFlags & STATE_STOP) + StartPrefetching(); + else if (progressStateFlags & STATE_START) + StopPrefetching(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* location, + uint32_t aFlags) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefetchService::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsPrefetchService::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsPrefetchService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + LOG(("nsPrefetchService::Observe [topic=%s]\n", aTopic)); + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + StopAll(); + mPrefetchDisabled = true; + } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + const nsCString converted = NS_ConvertUTF16toUTF8(aData); + const char* pref = converted.get(); + if (!strcmp(pref, PREFETCH_PREF)) { + if (Preferences::GetBool(PREFETCH_PREF, false)) { + if (mPrefetchDisabled) { + LOG(("enabling prefetching\n")); + mPrefetchDisabled = false; + AddProgressListener(); + } + } else { + if (!mPrefetchDisabled) { + LOG(("disabling prefetching\n")); + StopCurrentPrefetchsPreloads(false); + mPrefetchDisabled = true; + RemoveProgressListener(); + } + } + } else if (!strcmp(pref, PARALLELISM_PREF)) { + mMaxParallelism = Preferences::GetInt(PARALLELISM_PREF, mMaxParallelism); + if (mMaxParallelism < 1) { + mMaxParallelism = 1; + } + // If our parallelism has increased, go ahead and kick off enough + // prefetches to fill up our allowance. If we're now over our + // allowance, we'll just silently let some of them finish to get + // back below our limit. + while (((!mStopCount && mHaveProcessed) || mAggressive) && + !mPrefetchQueue.empty() && + mCurrentNodes.Length() < static_cast<uint32_t>(mMaxParallelism)) { + ProcessNextPrefetchURI(); + } + } else if (!strcmp(pref, AGGRESSIVE_PREF)) { + mAggressive = Preferences::GetBool(AGGRESSIVE_PREF, false); + // in aggressive mode, start prefetching immediately + if (mAggressive) { + while (mStopCount && !mPrefetchQueue.empty() && + mCurrentNodes.Length() < + static_cast<uint32_t>(mMaxParallelism)) { + ProcessNextPrefetchURI(); + } + } + } + } + + return NS_OK; +} + +// vim: ts=4 sw=2 expandtab diff --git a/uriloader/prefetch/nsPrefetchService.h b/uriloader/prefetch/nsPrefetchService.h new file mode 100644 index 0000000000..7b931ba52c --- /dev/null +++ b/uriloader/prefetch/nsPrefetchService.h @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsPrefetchService_h__ +#define nsPrefetchService_h__ + +#include "nsIObserver.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" +#include "nsIPrefetchService.h" +#include "nsIRedirectResultListener.h" +#include "nsIWebProgressListener.h" +#include "nsIStreamListener.h" +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsWeakReference.h" +#include "nsCOMPtr.h" +#include "mozilla/Attributes.h" +#include <deque> + +class nsPrefetchService; +class nsPrefetchNode; +class nsIReferrerInfo; + +//----------------------------------------------------------------------------- +// nsPrefetchService +//----------------------------------------------------------------------------- + +class nsPrefetchService final : public nsIPrefetchService, + public nsIWebProgressListener, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPREFETCHSERVICE + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIOBSERVER + + nsPrefetchService(); + + nsresult Init(); + void RemoveNodeAndMaybeStartNextPrefetchURI(nsPrefetchNode* aFinished); + void ProcessNextPrefetchURI(); + + void NotifyLoadRequested(nsPrefetchNode* node); + void NotifyLoadCompleted(nsPrefetchNode* node); + void DispatchEvent(nsPrefetchNode* node, bool aSuccess); + + private: + ~nsPrefetchService(); + + nsresult Prefetch(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, bool aExplicit); + + nsresult Preload(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, nsContentPolicyType aPolicyType); + + void AddProgressListener(); + void RemoveProgressListener(); + nsresult EnqueueURI(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo, + nsINode* aSource, nsPrefetchNode** node); + void EmptyPrefetchQueue(); + + void StartPrefetching(); + void StopPrefetching(); + void StopCurrentPrefetchsPreloads(bool aPreload); + void StopAll(); + nsresult CheckURIScheme(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo); + + std::deque<RefPtr<nsPrefetchNode>> mPrefetchQueue; + nsTArray<RefPtr<nsPrefetchNode>> mCurrentNodes; + int32_t mMaxParallelism; + int32_t mStopCount; + bool mHaveProcessed; + bool mPrefetchDisabled; + + // In usual case prefetch does not start until all normal loads are done. + // Aggressive mode ignores normal loads and just start prefetch ASAP. + // It's mainly for testing purpose and discoraged for normal use; + // see https://bugzilla.mozilla.org/show_bug.cgi?id=1281415 for details. + bool mAggressive; +}; + +//----------------------------------------------------------------------------- +// nsPreFetchingNode +//----------------------------------------------------------------------------- + +class nsPrefetchNode final : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsIRedirectResultListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + + nsPrefetchNode(nsPrefetchService* aPrefetchService, nsIURI* aURI, + nsIReferrerInfo* aReferrerInfo, nsINode* aSource, + nsContentPolicyType aPolicyType, bool aPreload); + + nsresult OpenChannel(); + nsresult CancelChannel(nsresult error); + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIReferrerInfo> mReferrerInfo; + nsTArray<nsWeakPtr> mSources; + + // The policy type to be used for fetching the resource. + nsContentPolicyType mPolicyType; + // nsPrefetchNode is used for prefetching and preloading resource. + // mPreload is true if a resource is preloaded. Preloads and + // prefetches are fetched in different phases (during load and + // after a page load), therefore we need to distinguish them. + bool mPreload; + + private: + ~nsPrefetchNode() {} + + RefPtr<nsPrefetchService> mService; + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsIChannel> mRedirectChannel; + int64_t mBytesRead; + bool mShouldFireLoadEvent; +}; + +#endif // !nsPrefetchService_h__ diff --git a/uriloader/preload/FetchPreloader.cpp b/uriloader/preload/FetchPreloader.cpp new file mode 100644 index 0000000000..411ca266fc --- /dev/null +++ b/uriloader/preload/FetchPreloader.cpp @@ -0,0 +1,339 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FetchPreloader.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/Document.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIClassOfService.h" +#include "nsIHttpChannel.h" +#include "nsITimedChannel.h" +#include "nsNetUtil.h" +#include "nsStringStream.h" +#include "nsIDocShell.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(FetchPreloader, nsIStreamListener, nsIRequestObserver) + +FetchPreloader::FetchPreloader() + : FetchPreloader(nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD) {} + +FetchPreloader::FetchPreloader(nsContentPolicyType aContentPolicyType) + : mContentPolicyType(aContentPolicyType) {} + +nsresult FetchPreloader::OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument) { + nsresult rv; + nsCOMPtr<nsIChannel> channel; + + auto notify = MakeScopeExit([&]() { + if (NS_FAILED(rv)) { + // Make sure we notify any <link preload> elements when opening fails + // because of various technical or security reasons. + NotifyStart(channel); + // Using the non-channel overload of this method to make it work even + // before NotifyOpen has been called on this preload. We are not + // switching between channels, so it's safe to do so. + NotifyStop(rv); + } + }); + + nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup(); + nsCOMPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow(); + nsCOMPtr<nsIInterfaceRequestor> prompter; + if (window) { + nsIDocShell* docshell = window->GetDocShell(); + prompter = do_QueryInterface(docshell); + } + + rv = CreateChannel(getter_AddRefs(channel), aURI, aCORSMode, aReferrerPolicy, + aDocument, loadGroup, prompter); + NS_ENSURE_SUCCESS(rv, rv); + + // Doing this now so that we have the channel and tainting set on it properly + // to notify the proper event (load or error) on the associated preload tags + // when the CSP check fails. + rv = CheckContentPolicy(aURI, aDocument); + if (NS_FAILED(rv)) { + return rv; + } + + PrioritizeAsPreload(channel); + AddLoadBackgroundFlag(channel); + + NotifyOpen(aKey, channel, aDocument, true); + + return mAsyncConsumeResult = rv = channel->AsyncOpen(this); +} + +nsresult FetchPreloader::CreateChannel( + nsIChannel** aChannel, nsIURI* aURI, const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, dom::Document* aDocument, + nsILoadGroup* aLoadGroup, nsIInterfaceRequestor* aCallbacks) { + nsresult rv; + + nsSecurityFlags securityFlags = + aCORSMode == CORS_NONE + ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL + : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + if (aCORSMode == CORS_ANONYMOUS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else if (aCORSMode == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannelWithTriggeringPrincipal( + getter_AddRefs(channel), aURI, aDocument, aDocument->NodePrincipal(), + securityFlags, nsIContentPolicy::TYPE_FETCH, nullptr, aLoadGroup, + aCallbacks); + if (NS_FAILED(rv)) { + return rv; + } + + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) { + nsCOMPtr<nsIReferrerInfo> referrerInfo = new dom::ReferrerInfo( + aDocument->GetDocumentURIAsReferrer(), aReferrerPolicy); + rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(channel)) { + timedChannel->SetInitiatorType(u"link"_ns); + } + + channel.forget(aChannel); + return NS_OK; +} + +nsresult FetchPreloader::CheckContentPolicy(nsIURI* aURI, + dom::Document* aDocument) { + if (!aDocument) { + return NS_OK; + } + + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo( + aDocument->NodePrincipal(), aDocument->NodePrincipal(), aDocument, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, mContentPolicyType); + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + nsresult rv = + NS_CheckContentLoadPolicy(aURI, secCheckLoadInfo, ""_ns, &shouldLoad, + nsContentUtils::GetContentPolicy()); + if (NS_SUCCEEDED(rv) && NS_CP_ACCEPTED(shouldLoad)) { + return NS_OK; + } + + return NS_ERROR_CONTENT_BLOCKED; +} + +// PreloaderBase + +nsresult FetchPreloader::AsyncConsume(nsIStreamListener* aListener) { + if (NS_FAILED(mAsyncConsumeResult)) { + // Already being consumed or failed to open. + return mAsyncConsumeResult; + } + + // Prevent duplicate calls. + mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE; + + if (!mConsumeListener) { + // Called before we are getting response from the channel. + mConsumeListener = aListener; + } else { + // Channel already started, push cached calls to this listener. + // Can't be anything else than the `Cache`, hence a safe static_cast. + Cache* cache = static_cast<Cache*>(mConsumeListener.get()); + cache->AsyncConsume(aListener); + } + + return NS_OK; +} + +// static +void FetchPreloader::PrioritizeAsPreload(nsIChannel* aChannel) { + if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + } +} + +void FetchPreloader::PrioritizeAsPreload() { PrioritizeAsPreload(Channel()); } + +// nsIRequestObserver + nsIStreamListener + +NS_IMETHODIMP FetchPreloader::OnStartRequest(nsIRequest* request) { + NotifyStart(request); + + if (!mConsumeListener) { + // AsyncConsume not called yet. + mConsumeListener = new Cache(); + } + + return mConsumeListener->OnStartRequest(request); +} + +NS_IMETHODIMP FetchPreloader::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, uint32_t count) { + return mConsumeListener->OnDataAvailable(request, input, offset, count); +} + +NS_IMETHODIMP FetchPreloader::OnStopRequest(nsIRequest* request, + nsresult status) { + mConsumeListener->OnStopRequest(request, status); + + // We want 404 or other types of server responses to be treated as 'error'. + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request)) { + uint32_t responseStatus = 0; + Unused << httpChannel->GetResponseStatus(&responseStatus); + if (responseStatus / 100 != 2) { + status = NS_ERROR_FAILURE; + } + } + + // Fetch preloader wants to keep the channel around so that consumers like XHR + // can access it even after the preload is done. + nsCOMPtr<nsIChannel> channel = mChannel; + NotifyStop(request, status); + mChannel.swap(channel); + return NS_OK; +} + +// FetchPreloader::Cache + +NS_IMPL_ISUPPORTS(FetchPreloader::Cache, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP FetchPreloader::Cache::OnStartRequest(nsIRequest* request) { + mRequest = request; + + if (mFinalListener) { + return mFinalListener->OnStartRequest(mRequest); + } + + mCalls.AppendElement(Call{VariantIndex<0>{}, StartRequest{}}); + return NS_OK; +} + +NS_IMETHODIMP FetchPreloader::Cache::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, + uint32_t count) { + if (mFinalListener) { + return mFinalListener->OnDataAvailable(mRequest, input, offset, count); + } + + DataAvailable data; + if (!data.mData.SetLength(count, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t read; + nsresult rv = input->Read(data.mData.BeginWriting(), count, &read); + if (NS_FAILED(rv)) { + return rv; + } + + mCalls.AppendElement(Call{VariantIndex<1>{}, std::move(data)}); + return NS_OK; +} + +NS_IMETHODIMP FetchPreloader::Cache::OnStopRequest(nsIRequest* request, + nsresult status) { + if (mFinalListener) { + return mFinalListener->OnStopRequest(mRequest, status); + } + + mCalls.AppendElement(Call{VariantIndex<2>{}, StopRequest{status}}); + return NS_OK; +} + +void FetchPreloader::Cache::AsyncConsume(nsIStreamListener* aListener) { + // Must dispatch for two reasons: + // 1. This is called directly from FetchLoader::AsyncConsume, which must + // behave the same way as nsIChannel::AsyncOpen. + // 2. In case there are already stream listener events scheduled on the main + // thread we preserve the order - those will still end up in Cache. + + // * The `Cache` object is fully main thread only for now, doesn't support + // retargeting, but it can be improved to allow it. + + nsCOMPtr<nsIStreamListener> listener(aListener); + NS_DispatchToMainThread(NewRunnableMethod<nsCOMPtr<nsIStreamListener>>( + "FetchPreloader::Cache::Consume", this, &FetchPreloader::Cache::Consume, + listener)); +} + +void FetchPreloader::Cache::Consume(nsCOMPtr<nsIStreamListener> aListener) { + MOZ_ASSERT(!mFinalListener, "Duplicate call"); + + mFinalListener = std::move(aListener); + + // Status of the channel read after each call. + nsresult status = NS_OK; + nsCOMPtr<nsIChannel> channel(do_QueryInterface(mRequest)); + + RefPtr<Cache> self(this); + for (auto& call : mCalls) { + nsresult rv = call.match( + [&](const StartRequest& startRequest) mutable { + return self->mFinalListener->OnStartRequest(self->mRequest); + }, + [&](const DataAvailable& dataAvailable) mutable { + if (NS_FAILED(status)) { + // Channel has been cancelled during this mCalls loop. + return NS_OK; + } + + nsCOMPtr<nsIInputStream> input; + rv = NS_NewCStringInputStream(getter_AddRefs(input), + dataAvailable.mData); + if (NS_FAILED(rv)) { + return rv; + } + + return self->mFinalListener->OnDataAvailable( + self->mRequest, input, 0, dataAvailable.mData.Length()); + }, + [&](const StopRequest& stopRequest) { + // First cancellation overrides mStatus in nsHttpChannel. + nsresult stopStatus = + NS_FAILED(status) ? status : stopRequest.mStatus; + self->mFinalListener->OnStopRequest(self->mRequest, stopStatus); + self->mFinalListener = nullptr; + self->mRequest = nullptr; + return NS_OK; + }); + + if (!mRequest) { + // We are done! + break; + } + + bool isCancelled = false; + Unused << channel->GetCanceled(&isCancelled); + if (isCancelled) { + mRequest->GetStatus(&status); + } else if (NS_FAILED(rv)) { + status = rv; + mRequest->Cancel(status); + } + } + + mCalls.Clear(); +} + +} // namespace mozilla diff --git a/uriloader/preload/FetchPreloader.h b/uriloader/preload/FetchPreloader.h new file mode 100644 index 0000000000..f501a79c97 --- /dev/null +++ b/uriloader/preload/FetchPreloader.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef FetchPreloader_h_ +#define FetchPreloader_h_ + +#include "mozilla/PreloaderBase.h" +#include "mozilla/Variant.h" +#include "nsCOMPtr.h" +#include "nsIAsyncOutputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIContentPolicy.h" +#include "nsIStreamListener.h" + +class nsIChannel; +class nsILoadGroup; +class nsIInterfaceRequestor; + +namespace mozilla { + +namespace dom { +enum class ReferrerPolicy : uint8_t; +} + +class FetchPreloader : public PreloaderBase, public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + FetchPreloader(); + nsresult OpenChannel(const PreloadHashKey& aKey, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument); + + // PreloaderBase + nsresult AsyncConsume(nsIStreamListener* aListener) override; + static void PrioritizeAsPreload(nsIChannel* aChannel); + void PrioritizeAsPreload() override; + + protected: + explicit FetchPreloader(nsContentPolicyType aContentPolicyType); + virtual ~FetchPreloader() = default; + + // Create and setup the channel with necessary security properties. This is + // overridable by subclasses to allow different initial conditions. + virtual nsresult CreateChannel(nsIChannel** aChannel, nsIURI* aURI, + const CORSMode aCORSMode, + const dom::ReferrerPolicy& aReferrerPolicy, + dom::Document* aDocument, + nsILoadGroup* aLoadGroup, + nsIInterfaceRequestor* aCallbacks); + + private: + nsresult CheckContentPolicy(nsIURI* aURI, dom::Document* aDocument); + + class Cache final : public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + void AsyncConsume(nsIStreamListener* aListener); + void Consume(nsCOMPtr<nsIStreamListener> aListener); + + private: + virtual ~Cache() = default; + + struct StartRequest {}; + struct DataAvailable { + nsCString mData; + }; + struct StopRequest { + nsresult mStatus; + }; + + typedef Variant<StartRequest, DataAvailable, StopRequest> Call; + nsCOMPtr<nsIRequest> mRequest; + nsCOMPtr<nsIStreamListener> mFinalListener; + nsTArray<Call> mCalls; + }; + + // The listener passed to AsyncConsume in case we haven't started getting the + // data from the channel yet. + nsCOMPtr<nsIStreamListener> mConsumeListener; + + // Returned by AsyncConsume when a failure. This remembers the result of + // opening the channel and prevents duplicate consumation. + nsresult mAsyncConsumeResult = NS_ERROR_NOT_AVAILABLE; + + // The CP type to check against. Derived classes have to override call to CSP + // constructor of this class. + nsContentPolicyType mContentPolicyType; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloadHashKey.cpp b/uriloader/preload/PreloadHashKey.cpp new file mode 100644 index 0000000000..2096fedf0f --- /dev/null +++ b/uriloader/preload/PreloadHashKey.cpp @@ -0,0 +1,213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PreloadHashKey.h" + +#include "mozilla/dom/Element.h" // StringToCORSMode +#include "mozilla/css/SheetLoadData.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "nsIPrincipal.h" +#include "nsIReferrerInfo.h" + +namespace mozilla { + +PreloadHashKey::PreloadHashKey(const nsIURI* aKey, ResourceType aAs) + : nsURIHashKey(aKey), mAs(aAs) {} + +PreloadHashKey::PreloadHashKey(const PreloadHashKey* aKey) + : nsURIHashKey(aKey->mKey) { + *this = *aKey; +} + +PreloadHashKey::PreloadHashKey(PreloadHashKey&& aToMove) + : nsURIHashKey(std::move(aToMove)) { + mAs = std::move(aToMove.mAs); + mCORSMode = std::move(aToMove.mCORSMode); + mPrincipal = std::move(aToMove.mPrincipal); + + switch (mAs) { + case ResourceType::SCRIPT: + mScript = std::move(aToMove.mScript); + break; + case ResourceType::STYLE: + mStyle = std::move(aToMove.mStyle); + break; + case ResourceType::IMAGE: + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + break; + case ResourceType::NONE: + break; + } +} + +PreloadHashKey& PreloadHashKey::operator=(const PreloadHashKey& aOther) { + MOZ_ASSERT(mAs == ResourceType::NONE || aOther.mAs == ResourceType::NONE, + "Assigning more than once, only reset is allowed"); + + nsURIHashKey::operator=(aOther); + + mAs = aOther.mAs; + mCORSMode = aOther.mCORSMode; + mPrincipal = aOther.mPrincipal; + + switch (mAs) { + case ResourceType::SCRIPT: + mScript = aOther.mScript; + break; + case ResourceType::STYLE: + mStyle = aOther.mStyle; + break; + case ResourceType::IMAGE: + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + break; + case ResourceType::NONE: + break; + } + + return *this; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsScript(nsIURI* aURI, CORSMode aCORSMode, + dom::ScriptKind aScriptKind) { + PreloadHashKey key(aURI, ResourceType::SCRIPT); + key.mCORSMode = aCORSMode; + + key.mScript.mScriptKind = aScriptKind; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsScript(nsIURI* aURI, + const nsAString& aCrossOrigin, + const nsAString& aType) { + dom::ScriptKind scriptKind = dom::ScriptKind::eClassic; + if (aType.LowerCaseEqualsASCII("module")) { + scriptKind = dom::ScriptKind::eModule; + } + CORSMode crossOrigin = dom::Element::StringToCORSMode(aCrossOrigin); + return CreateAsScript(aURI, crossOrigin, scriptKind); +} + +// static +PreloadHashKey PreloadHashKey::CreateAsStyle( + nsIURI* aURI, nsIPrincipal* aPrincipal, CORSMode aCORSMode, + css::SheetParsingMode aParsingMode) { + PreloadHashKey key(aURI, ResourceType::STYLE); + key.mCORSMode = aCORSMode; + key.mPrincipal = aPrincipal; + + key.mStyle.mParsingMode = aParsingMode; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsStyle( + css::SheetLoadData& aSheetLoadData) { + return CreateAsStyle(aSheetLoadData.mURI, aSheetLoadData.mTriggeringPrincipal, + aSheetLoadData.mSheet->GetCORSMode(), + aSheetLoadData.mSheet->ParsingMode()); +} + +// static +PreloadHashKey PreloadHashKey::CreateAsImage(nsIURI* aURI, + nsIPrincipal* aPrincipal, + CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::IMAGE); + key.mCORSMode = aCORSMode; + key.mPrincipal = aPrincipal; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::FETCH); + key.mCORSMode = aCORSMode; + + return key; +} + +// static +PreloadHashKey PreloadHashKey::CreateAsFetch(nsIURI* aURI, + const nsAString& aCrossOrigin) { + return CreateAsFetch(aURI, dom::Element::StringToCORSMode(aCrossOrigin)); +} + +PreloadHashKey PreloadHashKey::CreateAsFont(nsIURI* aURI, CORSMode aCORSMode) { + PreloadHashKey key(aURI, ResourceType::FONT); + key.mCORSMode = aCORSMode; + + return key; +} + +bool PreloadHashKey::KeyEquals(KeyTypePointer aOther) const { + if (mAs != aOther->mAs || mCORSMode != aOther->mCORSMode) { + return false; + } + + if (!mPrincipal != !aOther->mPrincipal) { + // One or the other has a principal, but not both... not equal + return false; + } + + if (mPrincipal && !mPrincipal->Equals(aOther->mPrincipal)) { + return false; + } + + if (!nsURIHashKey::KeyEquals( + static_cast<const nsURIHashKey*>(aOther)->GetKey())) { + return false; + } + + switch (mAs) { + case ResourceType::SCRIPT: + if (mScript.mScriptKind != aOther->mScript.mScriptKind) { + return false; + } + break; + case ResourceType::STYLE: { + if (mStyle.mParsingMode != aOther->mStyle.mParsingMode) { + return false; + } + break; + } + case ResourceType::IMAGE: + // No more checks needed. The image cache key consists of the document + // (which we are scoped into), origin attributes (which we compare as part + // of the principal check) and the URL. imgLoader::ValidateEntry compares + // CORS, referrer info and principal, which we do by default. + break; + case ResourceType::FONT: + break; + case ResourceType::FETCH: + // No more checks needed. + break; + case ResourceType::NONE: + break; + } + + return true; +} + +// static +PLDHashNumber PreloadHashKey::HashKey(KeyTypePointer aKey) { + PLDHashNumber hash = nsURIHashKey::HashKey(aKey->mKey); + + // Enough to use the common attributes + hash = AddToHash(hash, static_cast<uint32_t>(aKey->mAs)); + hash = AddToHash(hash, static_cast<uint32_t>(aKey->mCORSMode)); + + return hash; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloadHashKey.h b/uriloader/preload/PreloadHashKey.h new file mode 100644 index 0000000000..2ab7af6013 --- /dev/null +++ b/uriloader/preload/PreloadHashKey.h @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef PreloadHashKey_h__ +#define PreloadHashKey_h__ + +#include "mozilla/CORSMode.h" +#include "mozilla/css/SheetParsingMode.h" +#include "mozilla/dom/ScriptKind.h" +#include "nsURIHashKey.h" + +class nsIPrincipal; + +namespace mozilla { + +namespace css { +class SheetLoadData; +} + +/** + * This key is used for coalescing and lookup of preloading or regular + * speculative loads. It consists of: + * - the resource type, which is the value of the 'as' attribute + * - the URI of the resource + * - set of attributes that is common to all resource types + * - resource-type-specific attributes that we use to distinguish loads that has + * to be treated separately, some of these attributes may remain at their + * default values + */ +class PreloadHashKey : public nsURIHashKey { + public: + enum class ResourceType : uint8_t { NONE, SCRIPT, STYLE, IMAGE, FONT, FETCH }; + + using KeyType = const PreloadHashKey&; + using KeyTypePointer = const PreloadHashKey*; + + PreloadHashKey() = default; + PreloadHashKey(const nsIURI* aKey, ResourceType aAs); + explicit PreloadHashKey(const PreloadHashKey* aKey); + PreloadHashKey(PreloadHashKey&& aToMove); + + PreloadHashKey& operator=(const PreloadHashKey& aOther); + + // Construct key for "script" + static PreloadHashKey CreateAsScript(nsIURI* aURI, CORSMode aCORSMode, + dom::ScriptKind aScriptKind); + static PreloadHashKey CreateAsScript(nsIURI* aURI, + const nsAString& aCrossOrigin, + const nsAString& aType); + + // Construct key for "style" + static PreloadHashKey CreateAsStyle(nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCORSMode, + css::SheetParsingMode aParsingMode); + static PreloadHashKey CreateAsStyle(css::SheetLoadData&); + + // Construct key for "image" + static PreloadHashKey CreateAsImage(nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCORSMode); + + // Construct key for "fetch" + static PreloadHashKey CreateAsFetch(nsIURI* aURI, CORSMode aCORSMode); + static PreloadHashKey CreateAsFetch(nsIURI* aURI, + const nsAString& aCrossOrigin); + + // Construct key for "font" + static PreloadHashKey CreateAsFont(nsIURI* aURI, CORSMode aCORSMode); + + KeyType GetKey() const { return *this; } + KeyTypePointer GetKeyPointer() const { return this; } + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + + bool KeyEquals(KeyTypePointer aOther) const; + static PLDHashNumber HashKey(KeyTypePointer aKey); + ResourceType As() const { return mAs; } + +#ifdef MOZILLA_INTERNAL_API + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + // Bug 1627752 + return 0; + } +#endif + + enum { ALLOW_MEMMOVE = true }; + + private: + // Attributes common to all resource types + ResourceType mAs = ResourceType::NONE; + + CORSMode mCORSMode = CORS_NONE; + nsCOMPtr<nsIPrincipal> mPrincipal; + + struct { + dom::ScriptKind mScriptKind = dom::ScriptKind::eClassic; + } mScript; + + struct { + css::SheetParsingMode mParsingMode = css::eAuthorSheetFeatures; + } mStyle; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloadService.cpp b/uriloader/preload/PreloadService.cpp new file mode 100644 index 0000000000..5707c3d109 --- /dev/null +++ b/uriloader/preload/PreloadService.cpp @@ -0,0 +1,290 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PreloadService.h" + +#include "FetchPreloader.h" +#include "PreloaderBase.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/Encoding.h" +#include "mozilla/FontPreloader.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsNetUtil.h" + +namespace mozilla { + +PreloadService::PreloadService(dom::Document* aDoc) : mDocument(aDoc) {} +PreloadService::~PreloadService() = default; + +bool PreloadService::RegisterPreload(const PreloadHashKey& aKey, + PreloaderBase* aPreload) { + if (PreloadExists(aKey)) { + return false; + } + + mPreloads.Put(aKey, RefPtr{aPreload}); + return true; +} + +void PreloadService::DeregisterPreload(const PreloadHashKey& aKey) { + mPreloads.Remove(aKey); +} + +void PreloadService::ClearAllPreloads() { mPreloads.Clear(); } + +bool PreloadService::PreloadExists(const PreloadHashKey& aKey) { + bool found; + mPreloads.GetWeak(aKey, &found); + return found; +} + +already_AddRefed<PreloaderBase> PreloadService::LookupPreload( + const PreloadHashKey& aKey) const { + return mPreloads.Get(aKey); +} + +already_AddRefed<nsIURI> PreloadService::GetPreloadURI(const nsAString& aURL) { + nsIURI* base = BaseURIForPreload(); + auto encoding = mDocument->GetDocumentCharacterSet(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, encoding, base); + if (NS_FAILED(rv)) { + return nullptr; + } + + return uri.forget(); +} + +already_AddRefed<PreloaderBase> PreloadService::PreloadLinkElement( + dom::HTMLLinkElement* aLinkElement, nsContentPolicyType aPolicyType) { + // Even if the pref is disabled, we still want to collect telemetry about + // attempted preloads. + const bool preloadEnabled = StaticPrefs::network_preload(); + if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { + MOZ_ASSERT_UNREACHABLE("Caller should check"); + return nullptr; + } + + if (auto* wgc = mDocument->GetWindowGlobalChild()) { + wgc->MaybeSendUpdateDocumentWouldPreloadResources(); + } + + if (!preloadEnabled) { + return nullptr; + } + + nsAutoString as, charset, crossOrigin, integrity, referrerPolicy, srcset, + sizes, type, url; + + nsCOMPtr<nsIURI> uri = aLinkElement->GetURI(); + aLinkElement->GetAs(as); + aLinkElement->GetCharset(charset); + aLinkElement->GetImageSrcset(srcset); + aLinkElement->GetImageSizes(sizes); + aLinkElement->GetHref(url); + aLinkElement->GetCrossOrigin(crossOrigin); + aLinkElement->GetIntegrity(integrity); + aLinkElement->GetReferrerPolicy(referrerPolicy); + aLinkElement->GetType(type); + + auto result = + PreloadOrCoalesce(uri, url, aPolicyType, as, type, charset, srcset, sizes, + integrity, crossOrigin, referrerPolicy); + + if (!result.mPreloader) { + NotifyNodeEvent(aLinkElement, result.mAlreadyComplete); + return nullptr; + } + + result.mPreloader->AddLinkPreloadNode(aLinkElement); + return result.mPreloader.forget(); +} + +void PreloadService::PreloadLinkHeader( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aIntegrity, + const nsAString& aSrcset, const nsAString& aSizes, const nsAString& aCORS, + const nsAString& aReferrerPolicy) { + // Even if the pref is disabled, we still want to collect telemetry about + // attempted preloads. + const bool preloadEnabled = StaticPrefs::network_preload(); + + if (aPolicyType == nsIContentPolicy::TYPE_INVALID) { + MOZ_ASSERT_UNREACHABLE("Caller should check"); + return; + } + + if (auto* wgc = mDocument->GetWindowGlobalChild()) { + wgc->MaybeSendUpdateDocumentWouldPreloadResources(); + } + + if (!preloadEnabled) { + return; + } + + PreloadOrCoalesce(aURI, aURL, aPolicyType, aAs, aType, u""_ns, aSrcset, + aSizes, aIntegrity, aCORS, aReferrerPolicy); +} + +PreloadService::PreloadOrCoalesceResult PreloadService::PreloadOrCoalesce( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aCharset, + const nsAString& aSrcset, const nsAString& aSizes, + const nsAString& aIntegrity, const nsAString& aCORS, + const nsAString& aReferrerPolicy) { + if (!aURI) { + MOZ_ASSERT_UNREACHABLE("Should not pass null nsIURI"); + return {nullptr, false}; + } + + bool isImgSet = false; + PreloadHashKey preloadKey; + nsCOMPtr<nsIURI> uri = aURI; + + if (aAs.LowerCaseEqualsASCII("script")) { + preloadKey = PreloadHashKey::CreateAsScript(uri, aCORS, aType); + } else if (aAs.LowerCaseEqualsASCII("style")) { + preloadKey = PreloadHashKey::CreateAsStyle( + uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS), + css::eAuthorSheetFeatures /* see Loader::LoadSheet */); + } else if (aAs.LowerCaseEqualsASCII("image")) { + uri = mDocument->ResolvePreloadImage(BaseURIForPreload(), aURL, aSrcset, + aSizes, &isImgSet); + if (!uri) { + return {nullptr, false}; + } + + preloadKey = PreloadHashKey::CreateAsImage( + uri, mDocument->NodePrincipal(), dom::Element::StringToCORSMode(aCORS)); + } else if (aAs.LowerCaseEqualsASCII("font")) { + preloadKey = PreloadHashKey::CreateAsFont( + uri, dom::Element::StringToCORSMode(aCORS)); + } else if (aAs.LowerCaseEqualsASCII("fetch")) { + preloadKey = PreloadHashKey::CreateAsFetch( + uri, dom::Element::StringToCORSMode(aCORS)); + } else { + return {nullptr, false}; + } + + if (RefPtr<PreloaderBase> preload = LookupPreload(preloadKey)) { + return {std::move(preload), false}; + } + + if (aAs.LowerCaseEqualsASCII("script")) { + PreloadScript(uri, aType, aCharset, aCORS, aReferrerPolicy, aIntegrity, + true /* isInHead - TODO */); + } else if (aAs.LowerCaseEqualsASCII("style")) { + switch (PreloadStyle(uri, aCharset, aCORS, aReferrerPolicy, aIntegrity)) { + case dom::SheetPreloadStatus::AlreadyComplete: + return {nullptr, /* already_complete = */ true}; + case dom::SheetPreloadStatus::Errored: + case dom::SheetPreloadStatus::InProgress: + break; + } + } else if (aAs.LowerCaseEqualsASCII("image")) { + PreloadImage(uri, aCORS, aReferrerPolicy, isImgSet); + } else if (aAs.LowerCaseEqualsASCII("font")) { + PreloadFont(uri, aCORS, aReferrerPolicy); + } else if (aAs.LowerCaseEqualsASCII("fetch")) { + PreloadFetch(uri, aCORS, aReferrerPolicy); + } + + return {LookupPreload(preloadKey), false}; +} + +void PreloadService::PreloadScript(nsIURI* aURI, const nsAString& aType, + const nsAString& aCharset, + const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + const nsAString& aIntegrity, + bool aScriptFromHead) { + mDocument->ScriptLoader()->PreloadURI( + aURI, aCharset, aType, aCrossOrigin, aIntegrity, aScriptFromHead, false, + false, false, true, PreloadReferrerPolicy(aReferrerPolicy)); +} + +dom::SheetPreloadStatus PreloadService::PreloadStyle( + nsIURI* aURI, const nsAString& aCharset, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, const nsAString& aIntegrity) { + return mDocument->PreloadStyle( + aURI, Encoding::ForLabel(aCharset), aCrossOrigin, + PreloadReferrerPolicy(aReferrerPolicy), aIntegrity, true); +} + +void PreloadService::PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aImageReferrerPolicy, + bool aIsImgSet) { + mDocument->PreLoadImage(aURI, aCrossOrigin, + PreloadReferrerPolicy(aImageReferrerPolicy), + aIsImgSet, true); +} + +void PreloadService::PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy) { + CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); + auto key = PreloadHashKey::CreateAsFont(aURI, cors); + + // * Bug 1618549: Depending on where we decide to do the deduplication, we may + // want to check if the font is already being preloaded here. + + RefPtr<FontPreloader> preloader = new FontPreloader(); + dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); + preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument); +} + +void PreloadService::PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy) { + CORSMode cors = dom::Element::StringToCORSMode(aCrossOrigin); + auto key = PreloadHashKey::CreateAsFetch(aURI, cors); + + // * Bug 1618549: Depending on where we decide to do the deduplication, we may + // want to check if a fetch is already being preloaded here. + + RefPtr<FetchPreloader> preloader = new FetchPreloader(); + dom::ReferrerPolicy referrerPolicy = PreloadReferrerPolicy(aReferrerPolicy); + preloader->OpenChannel(key, aURI, cors, referrerPolicy, mDocument); +} + +// static +void PreloadService::NotifyNodeEvent(nsINode* aNode, bool aSuccess) { + if (!aNode->IsInComposedDoc()) { + return; + } + + // We don't dispatch synchronously since |node| might be in a DocGroup + // that we're not allowed to touch. (Our network request happens in the + // DocGroup of one of the mSources nodes--not necessarily this one). + + RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher( + aNode, aSuccess ? u"load"_ns : u"error"_ns, CanBubble::eNo); + + dispatcher->RequireNodeInDocument(); + dispatcher->PostDOMEvent(); +} + +dom::ReferrerPolicy PreloadService::PreloadReferrerPolicy( + const nsAString& aReferrerPolicy) { + dom::ReferrerPolicy referrerPolicy = + dom::ReferrerInfo::ReferrerPolicyAttributeFromString(aReferrerPolicy); + if (referrerPolicy == dom::ReferrerPolicy::_empty) { + referrerPolicy = mDocument->GetPreloadReferrerInfo()->ReferrerPolicy(); + } + + return referrerPolicy; +} + +nsIURI* PreloadService::BaseURIForPreload() { + nsIURI* documentURI = mDocument->GetDocumentURI(); + nsIURI* documentBaseURI = mDocument->GetDocBaseURI(); + return (documentURI == documentBaseURI) + ? (mSpeculationBaseURI ? mSpeculationBaseURI.get() : documentURI) + : documentBaseURI; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloadService.h b/uriloader/preload/PreloadService.h new file mode 100644 index 0000000000..44077e819a --- /dev/null +++ b/uriloader/preload/PreloadService.h @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef PreloadService_h__ +#define PreloadService_h__ + +#include "nsIContentPolicy.h" +#include "nsIURI.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/PreloadHashKey.h" + +class nsINode; + +namespace mozilla { + +class PreloaderBase; + +namespace dom { + +class HTMLLinkElement; +class Document; +enum class ReferrerPolicy : uint8_t; +enum class SheetPreloadStatus : uint8_t; + +} // namespace dom + +/** + * Intended to scope preloads and speculative loads under one roof. This class + * is intended to be a member of dom::Document. Provides registration of + * speculative loads via a `key` which is defined to consist of the URL, + * resource type, and resource-specific attributes that are further + * distinguishing loads with otherwise same type and url. + */ +class PreloadService { + public: + explicit PreloadService(dom::Document*); + ~PreloadService(); + + // Called by resource loaders to register a running resource load. This is + // called for a speculative load when it's started the first time, being it + // either regular speculative load or a preload. + // + // Returns false and does nothing if a preload is already registered under + // this key, true otherwise. + bool RegisterPreload(const PreloadHashKey& aKey, PreloaderBase* aPreload); + + // Called when the load is about to be cancelled. Exact behavior is to be + // determined yet. + void DeregisterPreload(const PreloadHashKey& aKey); + + // Called when the scope is to go away. + void ClearAllPreloads(); + + // True when there is a preload registered under the key. + bool PreloadExists(const PreloadHashKey& aKey); + + // Returns an existing preload under the key or null, when there is none + // registered under the key. + already_AddRefed<PreloaderBase> LookupPreload( + const PreloadHashKey& aKey) const; + + void SetSpeculationBase(nsIURI* aURI) { mSpeculationBaseURI = aURI; } + already_AddRefed<nsIURI> GetPreloadURI(const nsAString& aURL); + + already_AddRefed<PreloaderBase> PreloadLinkElement( + dom::HTMLLinkElement* aLink, nsContentPolicyType aPolicyType); + + void PreloadLinkHeader(nsIURI* aURI, const nsAString& aURL, + nsContentPolicyType aPolicyType, const nsAString& aAs, + const nsAString& aType, const nsAString& aIntegrity, + const nsAString& aSrcset, const nsAString& aSizes, + const nsAString& aCORS, + const nsAString& aReferrerPolicy); + + void PreloadScript(nsIURI* aURI, const nsAString& aType, + const nsAString& aCharset, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + const nsAString& aIntegrity, bool aScriptFromHead); + + dom::SheetPreloadStatus PreloadStyle(nsIURI* aURI, const nsAString& aCharset, + const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy, + const nsAString& aIntegrity); + + void PreloadImage(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aImageReferrerPolicy, bool aIsImgSet); + + void PreloadFont(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy); + + void PreloadFetch(nsIURI* aURI, const nsAString& aCrossOrigin, + const nsAString& aReferrerPolicy); + + static void NotifyNodeEvent(nsINode* aNode, bool aSuccess); + + private: + dom::ReferrerPolicy PreloadReferrerPolicy(const nsAString& aReferrerPolicy); + nsIURI* BaseURIForPreload(); + + struct PreloadOrCoalesceResult { + RefPtr<PreloaderBase> mPreloader; + bool mAlreadyComplete; + }; + + PreloadOrCoalesceResult PreloadOrCoalesce( + nsIURI* aURI, const nsAString& aURL, nsContentPolicyType aPolicyType, + const nsAString& aAs, const nsAString& aType, const nsAString& aCharset, + const nsAString& aSrcset, const nsAString& aSizes, + const nsAString& aIntegrity, const nsAString& aCORS, + const nsAString& aReferrerPolicy); + + private: + nsRefPtrHashtable<PreloadHashKey, PreloaderBase> mPreloads; + + // Raw pointer only, we are intended to be a direct member of dom::Document + dom::Document* mDocument; + + // Set by `nsHtml5TreeOpExecutor::SetSpeculationBase`. + nsCOMPtr<nsIURI> mSpeculationBaseURI; +}; + +} // namespace mozilla + +#endif diff --git a/uriloader/preload/PreloaderBase.cpp b/uriloader/preload/PreloaderBase.cpp new file mode 100644 index 0000000000..56f8c7e7bf --- /dev/null +++ b/uriloader/preload/PreloaderBase.cpp @@ -0,0 +1,380 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PreloaderBase.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/Telemetry.h" +#include "nsContentUtils.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIChannel.h" +#include "nsILoadGroup.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIRedirectResultListener.h" + +// Change this if we want to cancel and remove the associated preload on removal +// of all <link rel=preload> tags from the tree. +constexpr static bool kCancelAndRemovePreloadOnZeroReferences = false; + +namespace mozilla { + +PreloaderBase::UsageTimer::UsageTimer(PreloaderBase* aPreload, + dom::Document* aDocument) + : mDocument(aDocument), mPreload(aPreload) {} + +class PreloaderBase::RedirectSink final : public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsIRedirectResultListener { + RedirectSink() = delete; + virtual ~RedirectSink(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + + RedirectSink(PreloaderBase* aPreloader, nsIInterfaceRequestor* aCallbacks); + + private: + MainThreadWeakPtr<PreloaderBase> mPreloader; + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsIChannel> mRedirectChannel; +}; + +PreloaderBase::RedirectSink::RedirectSink(PreloaderBase* aPreloader, + nsIInterfaceRequestor* aCallbacks) + : mPreloader(aPreloader), mCallbacks(aCallbacks) {} + +PreloaderBase::RedirectSink::~RedirectSink() = default; + +NS_IMPL_ISUPPORTS(PreloaderBase::RedirectSink, nsIInterfaceRequestor, + nsIChannelEventSink, nsIRedirectResultListener) + +NS_IMETHODIMP PreloaderBase::RedirectSink::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + mRedirectChannel = aNewChannel; + + // Deliberately adding this before confirmation. + nsCOMPtr<nsIURI> uri; + aNewChannel->GetOriginalURI(getter_AddRefs(uri)); + if (mPreloader) { + mPreloader->mRedirectRecords.AppendElement( + RedirectRecord(aFlags, uri.forget())); + } + + if (mCallbacks) { + nsCOMPtr<nsIChannelEventSink> sink(do_GetInterface(mCallbacks)); + if (sink) { + return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, + aCallback); + } + } + + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP PreloaderBase::RedirectSink::OnRedirectResult(bool proceeding) { + if (proceeding && mRedirectChannel) { + mPreloader->mChannel = std::move(mRedirectChannel); + } else { + mRedirectChannel = nullptr; + } + + if (mCallbacks) { + nsCOMPtr<nsIRedirectResultListener> sink(do_GetInterface(mCallbacks)); + if (sink) { + return sink->OnRedirectResult(proceeding); + } + } + + return NS_OK; +} + +NS_IMETHODIMP PreloaderBase::RedirectSink::GetInterface(const nsIID& aIID, + void** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink)) || + aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + return QueryInterface(aIID, aResult); + } + + if (mCallbacks) { + return mCallbacks->GetInterface(aIID, aResult); + } + + *aResult = nullptr; + return NS_ERROR_NO_INTERFACE; +} + +PreloaderBase::~PreloaderBase() { MOZ_ASSERT(NS_IsMainThread()); } + +// static +void PreloaderBase::AddLoadBackgroundFlag(nsIChannel* aChannel) { + nsLoadFlags loadFlags; + aChannel->GetLoadFlags(&loadFlags); + aChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND); +} + +void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, + dom::Document* aDocument, bool aIsPreload) { + if (aDocument && !aDocument->Preloads().RegisterPreload(aKey, this)) { + // This means there is already a preload registered under this key in this + // document. We only allow replacement when this is a regular load. + // Otherwise, this should never happen and is a suspected misuse of the API. + MOZ_ASSERT(!aIsPreload); + aDocument->Preloads().DeregisterPreload(aKey); + aDocument->Preloads().RegisterPreload(aKey, this); + } + + mKey = aKey; + mIsUsed = !aIsPreload; + + if (!mIsUsed && !mUsageTimer) { + auto callback = MakeRefPtr<UsageTimer>(this, aDocument); + NS_NewTimerWithCallback(getter_AddRefs(mUsageTimer), callback, 10000, + nsITimer::TYPE_ONE_SHOT); + } + + ReportUsageTelemetry(); +} + +void PreloaderBase::NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel, + dom::Document* aDocument, bool aIsPreload) { + NotifyOpen(aKey, aDocument, aIsPreload); + mChannel = aChannel; + + nsCOMPtr<nsIInterfaceRequestor> callbacks; + mChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + RefPtr<RedirectSink> sink(new RedirectSink(this, callbacks)); + mChannel->SetNotificationCallbacks(sink); +} + +void PreloaderBase::NotifyUsage(LoadBackground aLoadBackground) { + if (!mIsUsed && mChannel && aLoadBackground == LoadBackground::Drop) { + nsLoadFlags loadFlags; + mChannel->GetLoadFlags(&loadFlags); + + // Preloads are initially set the LOAD_BACKGROUND flag. When becoming + // regular loads by hitting its consuming tag, we need to drop that flag, + // which also means to re-add the request from/to it's loadgroup to reflect + // that flag change. + if (loadFlags & nsIRequest::LOAD_BACKGROUND) { + nsCOMPtr<nsILoadGroup> loadGroup; + mChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + + if (loadGroup) { + nsresult status; + mChannel->GetStatus(&status); + + nsresult rv = loadGroup->RemoveRequest(mChannel, nullptr, status); + mChannel->SetLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND); + if (NS_SUCCEEDED(rv)) { + loadGroup->AddRequest(mChannel, nullptr); + } + } + } + } + + mIsUsed = true; + ReportUsageTelemetry(); + CancelUsageTimer(); +} + +void PreloaderBase::RemoveSelf(dom::Document* aDocument) { + if (aDocument) { + aDocument->Preloads().DeregisterPreload(mKey); + } +} + +void PreloaderBase::NotifyRestart(dom::Document* aDocument, + PreloaderBase* aNewPreloader) { + RemoveSelf(aDocument); + mKey = PreloadHashKey(); + + CancelUsageTimer(); + + if (aNewPreloader) { + aNewPreloader->mNodes = std::move(mNodes); + } +} + +void PreloaderBase::NotifyStart(nsIRequest* aRequest) { + // If there is no channel assigned on this preloader, we are not between + // channel switching, so we can freely update the mShouldFireLoadEvent using + // the given channel. + if (mChannel && !SameCOMIdentity(aRequest, mChannel)) { + return; + } + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + if (!httpChannel) { + return; + } + + // if the load is cross origin without CORS, or the CORS access is rejected, + // always fire load event to avoid leaking site information. + nsresult rv; + nsCOMPtr<nsILoadInfo> loadInfo = httpChannel->LoadInfo(); + mShouldFireLoadEvent = + loadInfo->GetTainting() == LoadTainting::Opaque || + (loadInfo->GetTainting() == LoadTainting::CORS && + (NS_FAILED(httpChannel->GetStatus(&rv)) || NS_FAILED(rv))); +} + +void PreloaderBase::NotifyStop(nsIRequest* aRequest, nsresult aStatus) { + // Filter out notifications that may be arriving from the old channel before + // restarting this request. + if (!SameCOMIdentity(aRequest, mChannel)) { + return; + } + + NotifyStop(aStatus); +} + +void PreloaderBase::NotifyStop(nsresult aStatus) { + mOnStopStatus.emplace(aStatus); + + nsTArray<nsWeakPtr> nodes = std::move(mNodes); + + for (nsWeakPtr& weak : nodes) { + nsCOMPtr<nsINode> node = do_QueryReferent(weak); + if (node) { + NotifyNodeEvent(node); + } + } + + mChannel = nullptr; +} + +void PreloaderBase::NotifyValidating() { mOnStopStatus.reset(); } + +void PreloaderBase::NotifyValidated(nsresult aStatus) { + NotifyStop(nullptr, aStatus); +} + +void PreloaderBase::AddLinkPreloadNode(nsINode* aNode) { + if (mOnStopStatus) { + return NotifyNodeEvent(aNode); + } + + mNodes.AppendElement(do_GetWeakReference(aNode)); +} + +void PreloaderBase::RemoveLinkPreloadNode(nsINode* aNode) { + // Note that do_GetWeakReference returns the internal weak proxy, which is + // always the same, so we can use it to search the array using default + // comparator. + nsWeakPtr node = do_GetWeakReference(aNode); + mNodes.RemoveElement(node); + + if (kCancelAndRemovePreloadOnZeroReferences && mNodes.Length() == 0 && + !mIsUsed) { + // Keep a reference, because the following call may release us. The caller + // may use a WeakPtr to access this. + RefPtr<PreloaderBase> self(this); + RemoveSelf(aNode->OwnerDoc()); + + if (mChannel) { + mChannel->Cancel(NS_BINDING_ABORTED); + } + } +} + +void PreloaderBase::NotifyNodeEvent(nsINode* aNode) { + PreloadService::NotifyNodeEvent( + aNode, mShouldFireLoadEvent || NS_SUCCEEDED(*mOnStopStatus)); +} + +void PreloaderBase::CancelUsageTimer() { + if (mUsageTimer) { + mUsageTimer->Cancel(); + mUsageTimer = nullptr; + } +} + +void PreloaderBase::ReportUsageTelemetry() { + if (mUsageTelementryReported) { + return; + } + mUsageTelementryReported = true; + + if (mKey.As() == PreloadHashKey::ResourceType::NONE) { + return; + } + + // The labels are structured as type1-used, type1-unused, type2-used, ... + // The first "as" resource type is NONE with value 0. + auto index = (static_cast<uint32_t>(mKey.As()) - 1) * 2; + if (!mIsUsed) { + ++index; + } + + auto label = static_cast<Telemetry::LABELS_REL_PRELOAD_MISS_RATIO>(index); + Telemetry::AccumulateCategorical(label); +} + +nsresult PreloaderBase::AsyncConsume(nsIStreamListener* aListener) { + // We want to return an error so that consumers can't ever use a preload to + // consume data unless it's properly implemented. + return NS_ERROR_NOT_IMPLEMENTED; +} + +// PreloaderBase::RedirectRecord + +nsCString PreloaderBase::RedirectRecord::Spec() const { + nsCOMPtr<nsIURI> noFragment; + NS_GetURIWithoutRef(mURI, getter_AddRefs(noFragment)); + MOZ_ASSERT(noFragment); + return noFragment->GetSpecOrDefault(); +} + +nsCString PreloaderBase::RedirectRecord::Fragment() const { + nsCString fragment; + mURI->GetRef(fragment); + return fragment; +} + +// PreloaderBase::UsageTimer + +NS_IMPL_ISUPPORTS(PreloaderBase::UsageTimer, nsITimerCallback) + +NS_IMETHODIMP PreloaderBase::UsageTimer::Notify(nsITimer* aTimer) { + if (!mPreload || !mDocument) { + return NS_OK; + } + + MOZ_ASSERT(aTimer == mPreload->mUsageTimer); + mPreload->mUsageTimer = nullptr; + + if (mPreload->IsUsed()) { + // Left in the hashtable, but marked as used. This is a valid case, and we + // don't want to emit a warning for this preload then. + return NS_OK; + } + + mPreload->ReportUsageTelemetry(); + + // PreloadHashKey overrides GetKey, we need to use the nsURIHashKey one to get + // the URI. + nsIURI* uri = static_cast<nsURIHashKey*>(&mPreload->mKey)->GetKey(); + if (!uri) { + return NS_OK; + } + + nsString spec; + NS_GetSanitizedURIStringFromURI(uri, spec); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + mDocument, nsContentUtils::eDOM_PROPERTIES, + "UnusedLinkPreloadPending", + nsTArray<nsString>({std::move(spec)})); + return NS_OK; +} + +} // namespace mozilla diff --git a/uriloader/preload/PreloaderBase.h b/uriloader/preload/PreloaderBase.h new file mode 100644 index 0000000000..dcd3738217 --- /dev/null +++ b/uriloader/preload/PreloaderBase.h @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef PreloaderBase_h__ +#define PreloaderBase_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/WeakPtr.h" +#include "nsCOMPtr.h" +#include "nsISupports.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIWeakReferenceUtils.h" +#include "nsTArray.h" + +class nsIChannel; +class nsINode; +class nsIRequest; +class nsIStreamListener; + +namespace mozilla { + +namespace dom { + +class Document; + +} // namespace dom + +/** + * A half-abstract base class that resource loaders' respective + * channel-listening classes should derive from. Provides a unified API to + * register the load or preload in a document scoped service, links <link + * rel="preload"> DOM nodes with the load progress and provides API to possibly + * consume the data by later, dynamically discovered consumers. + * + * This class is designed to be used only on the main thread. + */ +class PreloaderBase : public SupportsWeakPtr, public nsISupports { + public: + PreloaderBase() = default; + + // Called by resource loaders to register this preload in the document's + // preload service to provide coalescing, and access to the preload when it + // should be used for an actual load. + void NotifyOpen(const PreloadHashKey& aKey, dom::Document* aDocument, + bool aIsPreload); + void NotifyOpen(const PreloadHashKey& aKey, nsIChannel* aChannel, + dom::Document* aDocument, bool aIsPreload); + + // Called when the load is about to be started all over again and thus this + // PreloaderBase will be registered again with the same key. This method + // taks care to deregister this preload prior to that. + // @param aNewPreloader: If there is a new request and loader created for the + // restarted load, we will pass any necessary information into it. + void NotifyRestart(dom::Document* aDocument, + PreloaderBase* aNewPreloader = nullptr); + + // Called by the loading object when the channel started to load + // (OnStartRequest or equal) and when it finished (OnStopRequest or equal) + void NotifyStart(nsIRequest* aRequest); + void NotifyStop(nsIRequest* aRequest, nsresult aStatus); + // Use this variant only in complement to NotifyOpen without providing a + // channel. + void NotifyStop(nsresult aStatus); + + // Called when this currently existing load has to be asynchronously + // revalidated before it can be used. This prevents link preload DOM nodes + // being notified until the validation is resolved. + void NotifyValidating(); + // Called when the validation process has been done. This will notify + // associated link DOM nodes. + void NotifyValidated(nsresult aStatus); + + // Called by resource loaders or any suitable component to notify the preload + // has been used for an actual load. This is intended to stop any usage + // timers. + // @param aDropLoadBackground: If `Keep` then the loading channel, if still in + // progress, will not be removed the LOAD_BACKGROUND flag, for instance XHR is + // the user here. + enum class LoadBackground { Keep, Drop }; + void NotifyUsage(LoadBackground aLoadBackground = LoadBackground::Drop); + // Whether this preloader has been used for a regular/actual load or not. + bool IsUsed() const { return mIsUsed; } + + // Removes itself from the document's preloads hashtable + void RemoveSelf(dom::Document* aDocument); + + // When a loader starting an actual load finds a preload, the data can be + // delivered using this method. It will deliver stream listener notifications + // as if it were coming from the resource loading channel. The |request| + // argument will be the channel that loaded/loads the resource. + // This method must keep to the nsIChannel.AsyncOpen contract. A loader is + // not obligated to re-implement this method when not necessarily needed. + virtual nsresult AsyncConsume(nsIStreamListener* aListener); + + // Accessor to the resource loading channel. + nsIChannel* Channel() const { return mChannel; } + + // May change priority of the resource loading channel so that it's treated as + // preload when this was initially representing a normal speculative load but + // later <link rel="preload"> was found for this resource. + virtual void PrioritizeAsPreload() = 0; + + // Helper function to set the LOAD_BACKGROUND flag on channel initiated by + // <link rel=preload>. This MUST be used before the channel is AsyncOpen'ed. + static void AddLoadBackgroundFlag(nsIChannel* aChannel); + + // These are linking this preload to <link rel="preload"> DOM nodes. If we + // are already loaded, immediately notify events on the node, otherwise wait + // for NotifyStop() call. + void AddLinkPreloadNode(nsINode* aNode); + void RemoveLinkPreloadNode(nsINode* aNode); + + // A collection of redirects, the main consumer is fetch. + class RedirectRecord { + public: + RedirectRecord(uint32_t aFlags, already_AddRefed<nsIURI> aURI) + : mFlags(aFlags), mURI(aURI) {} + + uint32_t Flags() const { return mFlags; } + nsCString Spec() const; + nsCString Fragment() const; + + private: + uint32_t mFlags; + nsCOMPtr<nsIURI> mURI; + }; + + const nsTArray<RedirectRecord>& Redirects() { return mRedirectRecords; } + + protected: + virtual ~PreloaderBase(); + + // The loading channel. This will update when a redirect occurs. + nsCOMPtr<nsIChannel> mChannel; + + private: + void NotifyNodeEvent(nsINode* aNode); + void CancelUsageTimer(); + + void ReportUsageTelemetry(); + + // A helper class that will update the PreloaderBase.mChannel member when a + // redirect happens, so that we can reprioritize or cancel when needed. + // Having a separate class instead of implementing this on PreloaderBase + // directly is to keep PreloaderBase as simple as possible so that derived + // classes don't have to deal with calling super when implementing these + // interfaces from some reason as well. + class RedirectSink; + + // A timer callback to trigger the unuse warning for this preload + class UsageTimer final : public nsITimerCallback { + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + UsageTimer(PreloaderBase* aPreload, dom::Document* aDocument); + + private: + ~UsageTimer() = default; + + WeakPtr<dom::Document> mDocument; + WeakPtr<PreloaderBase> mPreload; + }; + + private: + // Reference to HTMLLinkElement DOM nodes to deliver onload and onerror + // notifications to. + nsTArray<nsWeakPtr> mNodes; + + // History of redirects. + nsTArray<RedirectRecord> mRedirectRecords; + + // Usage timer, emits warning when the preload is not used in time. Started + // in NotifyOpen and stopped in NotifyUsage. + nsCOMPtr<nsITimer> mUsageTimer; + + // The key this preload has been registered under. We want to remember it to + // be able to deregister itself from the document's preloads. + PreloadHashKey mKey; + + // This overrides the final event we send to DOM nodes to be always 'load'. + // Modified in NotifyStart based on LoadInfo data of the loading channel. + bool mShouldFireLoadEvent = false; + + // True after call to NotifyUsage. + bool mIsUsed = false; + + // True after we have reported the usage telemetry. Prevent duplicates. + bool mUsageTelementryReported = false; + + // Emplaced when the data delivery has finished, in NotifyStop, holds the + // result of the load. + Maybe<nsresult> mOnStopStatus; +}; + +} // namespace mozilla + +#endif // !PreloaderBase_h__ diff --git a/uriloader/preload/gtest/TestFetchPreloader.cpp b/uriloader/preload/gtest/TestFetchPreloader.cpp new file mode 100644 index 0000000000..4d4d2b5103 --- /dev/null +++ b/uriloader/preload/gtest/TestFetchPreloader.cpp @@ -0,0 +1,937 @@ +#include "gtest/gtest.h" + +#include "mozilla/CORSMode.h" +#include "mozilla/dom/XMLDocument.h" +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "mozilla/FetchPreloader.h" +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsIStreamListener.h" +#include "nsThreadUtils.h" +#include "nsStringStream.h" + +namespace { + +auto const ERROR_CANCEL = NS_ERROR_ABORT; +auto const ERROR_ONSTOP = NS_ERROR_NET_INTERRUPT; +auto const ERROR_THROW = NS_ERROR_FAILURE; + +class FakeChannel : public nsIChannel { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUEST + + nsresult Start() { return mListener->OnStartRequest(this); } + nsresult Data(const nsACString& aData) { + if (NS_FAILED(mStatus)) { + return mStatus; + } + nsCOMPtr<nsIInputStream> is; + NS_NewCStringInputStream(getter_AddRefs(is), aData); + return mListener->OnDataAvailable(this, is, 0, aData.Length()); + } + nsresult Stop(nsresult status) { + if (NS_SUCCEEDED(mStatus)) { + mStatus = status; + } + mListener->OnStopRequest(this, mStatus); + mListener = nullptr; + return mStatus; + } + + private: + virtual ~FakeChannel() = default; + bool mIsPending = false; + bool mCanceled = false; + nsresult mStatus = NS_OK; + nsCOMPtr<nsIStreamListener> mListener; +}; + +NS_IMPL_ISUPPORTS(FakeChannel, nsIChannel, nsIRequest) + +NS_IMETHODIMP FakeChannel::GetName(nsACString& result) { return NS_OK; } +NS_IMETHODIMP FakeChannel::IsPending(bool* result) { + *result = mIsPending; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetStatus(nsresult* status) { + *status = mStatus; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::Cancel(nsresult status) { + if (!mCanceled) { + mCanceled = true; + mStatus = status; + } + return NS_OK; +} +NS_IMETHODIMP FakeChannel::Suspend() { return NS_OK; } +NS_IMETHODIMP FakeChannel::Resume() { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = 0; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP FakeChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP FakeChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetOriginalURI(nsIURI** aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetOriginalURI(nsIURI* aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetURI(nsIURI** aURI) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetOwner(nsISupports** aOwner) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetOwner(nsISupports* aOwner) { return NS_OK; } +NS_IMETHODIMP FakeChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { return NS_OK; } +NS_IMETHODIMP FakeChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetIsDocument(bool* aIsDocument) { + *aIsDocument = false; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentType(nsACString& aContentType) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentType(const nsACString& aContentType) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentCharset(nsACString& aContentCharset) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentCharset( + const nsACString& aContentCharset) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDisposition( + uint32_t* aContentDisposition) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} +NS_IMETHODIMP FakeChannel::GetContentLength(int64_t* aContentLength) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::SetContentLength(int64_t aContentLength) { + return NS_OK; +} +NS_IMETHODIMP FakeChannel::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} +NS_IMETHODIMP FakeChannel::Open(nsIInputStream** aStream) { + return NS_ERROR_NOT_IMPLEMENTED; +} +NS_IMETHODIMP +FakeChannel::AsyncOpen(nsIStreamListener* aListener) { + mIsPending = true; + mListener = aListener; + return NS_OK; +} + +class FakePreloader : public mozilla::FetchPreloader { + public: + explicit FakePreloader(FakeChannel* aChannel) : mDrivingChannel(aChannel) {} + + private: + RefPtr<FakeChannel> mDrivingChannel; + + virtual nsresult CreateChannel( + nsIChannel** aChannel, nsIURI* aURI, const mozilla::CORSMode aCORSMode, + const mozilla::dom::ReferrerPolicy& aReferrerPolicy, + mozilla::dom::Document* aDocument, nsILoadGroup* aLoadGroup, + nsIInterfaceRequestor* aCallbacks) override { + mDrivingChannel.forget(aChannel); + return NS_OK; + } +}; + +class FakeListener : public nsIStreamListener { + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + enum { Never, OnStart, OnData, OnStop } mCancelIn = Never; + + nsresult mOnStartResult = NS_OK; + nsresult mOnDataResult = NS_OK; + nsresult mOnStopResult = NS_OK; + + bool mOnStart = false; + nsCString mOnData; + Maybe<nsresult> mOnStop; + + private: + virtual ~FakeListener() = default; +}; + +NS_IMPL_ISUPPORTS(FakeListener, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP FakeListener::OnStartRequest(nsIRequest* request) { + EXPECT_FALSE(mOnStart); + mOnStart = true; + + if (mCancelIn == OnStart) { + request->Cancel(ERROR_CANCEL); + } + + return mOnStartResult; +} +NS_IMETHODIMP FakeListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, uint32_t count) { + nsAutoCString data; + data.SetLength(count); + + uint32_t read; + input->Read(data.BeginWriting(), count, &read); + mOnData += data; + + if (mCancelIn == OnData) { + request->Cancel(ERROR_CANCEL); + } + + return mOnDataResult; +} +NS_IMETHODIMP FakeListener::OnStopRequest(nsIRequest* request, + nsresult status) { + EXPECT_FALSE(mOnStop); + mOnStop.emplace(status); + + if (mCancelIn == OnStop) { + request->Cancel(ERROR_CANCEL); + } + + return mOnStopResult; +} + +bool eventInProgress = true; + +void Await() { + MOZ_ALWAYS_TRUE(mozilla::SpinEventLoopUntil([&]() { + bool yield = !eventInProgress; + eventInProgress = true; // Just for convenience + return yield; + })); +} + +void Yield() { eventInProgress = false; } + +} // namespace + +// **************************************************************************** +// Test body +// **************************************************************************** + +// Caching with all good results (data + NS_OK) +TEST(TestFetchPreloader, CacheNoneBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheStartBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartOfDataBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllDataBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + + // Request consumation of the preload... + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsume) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Get data before the channel fails +TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelError) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_FAILED(channel->Stop(ERROR_ONSTOP))); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_ONSTOP); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Cancel the channel between caching and consuming +TEST(TestFetchPreloader, CacheAllBeforeConsumeWithChannelCancel) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + channel->Cancel(ERROR_CANCEL); + EXPECT_TRUE(NS_FAILED(channel->Stop(ERROR_CANCEL))); + + RefPtr<FakeListener> listener = new FakeListener(); + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + // XXX - This is hard to solve; the data is there but we won't deliver it. + // This is a bit different case than e.g. a network error. We want to + // deliver some data in that case. Cancellation probably happens because of + // navigation or a demand to not consume the channel anyway. + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Let the listener throw while data is already cached +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStartRequest) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnStartResult = ERROR_THROW; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnDataAvailable) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnDataResult = ERROR_THROW; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeThrowFromOnStopRequest) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnStopResult = ERROR_THROW; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("onetwothree")); + // Throwing from OnStopRequest is generally ignored. + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == NS_OK); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Cancel the channel in various callbacks +TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnStartRequest) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnStart; + // check that throwing from OnStartRequest doesn't affect the cancellation + // status. + listener->mOnStartResult = ERROR_THROW; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CacheAllBeforeConsumeCancelInOnDataAvailable) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + // check that throwing from OnStartRequest doesn't affect the cancellation + // status. + listener->mOnDataResult = ERROR_THROW; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +// Corner cases +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailable) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(NS_FAILED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_FAILED(channel->Stop(NS_OK))); + + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnStartRequestAndRace) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + })); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnStart; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get any data and + // propagate the cancellation code and not being called duplicated + // OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeCancelInOnDataAvailableAndRace) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + })); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mCancelIn = FakeListener::OnData; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get anything after + // the first OnData and propagate the cancellation code and not being called + // duplicated OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.EqualsLiteral("one")); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_CANCEL); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} + +TEST(TestFetchPreloader, CachePartlyBeforeConsumeThrowFromOnStartRequestAndRace) +{ + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), "https://example.com"_ns); + auto key = mozilla::PreloadHashKey::CreateAsFetch(uri, mozilla::CORS_NONE); + + RefPtr<FakeChannel> channel = new FakeChannel(); + RefPtr<FakePreloader> preloader = new FakePreloader(channel); + RefPtr<mozilla::dom::Document> doc; + NS_NewXMLDocument(getter_AddRefs(doc)); + + EXPECT_TRUE(NS_SUCCEEDED( + preloader->OpenChannel(key, uri, mozilla::CORS_NONE, + mozilla::dom::ReferrerPolicy::_empty, doc))); + + EXPECT_TRUE(NS_SUCCEEDED(channel->Start())); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("one"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("two"_ns))); + + // This has to simulate a possibiilty when stream listener notifications from + // the channel are already pending in the queue while AsyncConsume is called. + // At this moment the listener has not been notified yet. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(NS_SUCCEEDED(channel->Data("three"_ns))); + EXPECT_TRUE(NS_SUCCEEDED(channel->Stop(NS_OK))); + })); + + RefPtr<FakeListener> listener = new FakeListener(); + listener->mOnStartResult = ERROR_THROW; + + EXPECT_TRUE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); + + // Check listener's been fed properly. Expected is to NOT get any data and + // propagate the throwing code and not being called duplicated OnStopRequest. + NS_DispatchToMainThread(NS_NewRunnableFunction("test", [&]() { + EXPECT_TRUE(listener->mOnStart); + EXPECT_TRUE(listener->mOnData.IsEmpty()); + EXPECT_TRUE(listener->mOnStop && *listener->mOnStop == ERROR_THROW); + + Yield(); + })); + + Await(); + + EXPECT_FALSE(NS_SUCCEEDED(preloader->AsyncConsume(listener))); +} diff --git a/uriloader/preload/gtest/moz.build b/uriloader/preload/gtest/moz.build new file mode 100644 index 0000000000..72c2afa539 --- /dev/null +++ b/uriloader/preload/gtest/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES += [ + "TestFetchPreloader.cpp", +] + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/xpcom/tests/gtest", +] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += ["!/xpcom", "/xpcom/components"] diff --git a/uriloader/preload/moz.build b/uriloader/preload/moz.build new file mode 100644 index 0000000000..5ce2f28d89 --- /dev/null +++ b/uriloader/preload/moz.build @@ -0,0 +1,28 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking") + +TEST_DIRS += ["gtest"] + +EXPORTS.mozilla += [ + "FetchPreloader.h", + "PreloaderBase.h", + "PreloadHashKey.h", + "PreloadService.h", +] + +UNIFIED_SOURCES += [ + "FetchPreloader.cpp", + "PreloaderBase.cpp", + "PreloadHashKey.cpp", + "PreloadService.cpp", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") |