diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /uriloader/prefetch | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'uriloader/prefetch')
-rw-r--r-- | uriloader/prefetch/OfflineCacheUpdateChild.cpp | 472 | ||||
-rw-r--r-- | uriloader/prefetch/OfflineCacheUpdateChild.h | 93 | ||||
-rw-r--r-- | uriloader/prefetch/OfflineCacheUpdateGlue.cpp | 220 | ||||
-rw-r--r-- | uriloader/prefetch/OfflineCacheUpdateGlue.h | 124 | ||||
-rw-r--r-- | uriloader/prefetch/OfflineCacheUpdateParent.cpp | 287 | ||||
-rw-r--r-- | uriloader/prefetch/OfflineCacheUpdateParent.h | 64 | ||||
-rw-r--r-- | uriloader/prefetch/POfflineCacheUpdate.ipdl | 28 | ||||
-rw-r--r-- | uriloader/prefetch/moz.build | 44 | ||||
-rw-r--r-- | uriloader/prefetch/nsIOfflineCacheUpdate.idl | 291 | ||||
-rw-r--r-- | uriloader/prefetch/nsIPrefetchService.idl | 54 | ||||
-rw-r--r-- | uriloader/prefetch/nsOfflineCacheUpdate.cpp | 2332 | ||||
-rw-r--r-- | uriloader/prefetch/nsOfflineCacheUpdate.h | 369 | ||||
-rw-r--r-- | uriloader/prefetch/nsOfflineCacheUpdateService.cpp | 646 | ||||
-rw-r--r-- | uriloader/prefetch/nsPrefetchService.cpp | 889 | ||||
-rw-r--r-- | uriloader/prefetch/nsPrefetchService.h | 130 |
15 files changed, 6043 insertions, 0 deletions
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__ |