diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/webbrowserpersist | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webbrowserpersist')
24 files changed, 6221 insertions, 0 deletions
diff --git a/dom/webbrowserpersist/PWebBrowserPersistDocument.ipdl b/dom/webbrowserpersist/PWebBrowserPersistDocument.ipdl new file mode 100644 index 0000000000..ee2e50185f --- /dev/null +++ b/dom/webbrowserpersist/PWebBrowserPersistDocument.ipdl @@ -0,0 +1,100 @@ +/* -*- Mode: IDL; tab-width: 8; 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 protocol PContent; +include protocol PWebBrowserPersistResources; +include protocol PWebBrowserPersistSerialize; + +include PBackgroundSharedTypes; +include IPCStream; +include NeckoChannelParams; + +include "mozilla/dom/ReferrerInfoUtils.h"; + +[RefCounted] using class nsIReferrerInfo from "nsIReferrerInfo.h"; + +namespace mozilla { + +// nsIWebBrowserPersistDocument has attributes which can be read +// synchronously. To avoid using sync IPC for them, the actor sends +// this structure from the child to the parent before the parent actor +// is exposed to XPCOM. +struct WebBrowserPersistDocumentAttrs { + bool isPrivate; + nsCString documentURI; + nsCString baseURI; + nsCString contentType; + nsCString characterSet; + nsString title; + nsIReferrerInfo referrerInfo; + CookieJarSettingsArgs cookieJarSettings; + nsString contentDisposition; + uint32_t sessionHistoryCacheKey; + uint32_t persistFlags; + PrincipalInfo principal; +}; + +// IPDL doesn't have tuples, so this gives the pair of strings from +// nsIWebBrowserPersistURIMap::getURIMapping a name. +struct WebBrowserPersistURIMapEntry { + nsCString mapFrom; + nsCString mapTo; +}; + +// nsIWebBrowserPersistURIMap is just copied over IPC as one of these, +// not proxied, to simplify the protocol. +struct WebBrowserPersistURIMap { + WebBrowserPersistURIMapEntry[] mapURIs; + nsCString targetBaseURI; +}; + +// This remotes nsIWebBrowserPersistDocument and its visitors. The +// lifecycle is a little complicated: the initial document is +// constructed parent->child, but subdocuments are constructed +// child->parent and then passed back. Subdocuments aren't subactors, +// because that would impose a lifetime relationship that doesn't +// exist in the XPIDL; instead they're all managed by the enclosing +// PContent. +[ManualDealloc] +protocol PWebBrowserPersistDocument { + manager PContent; + manages PWebBrowserPersistResources; + manages PWebBrowserPersistSerialize; + +parent: + // The actor isn't exposed to XPCOM until after it gets one of these + // two messages; see also the state transition rules. The message + // is either a response to the constructor (if it was parent->child) + // or sent after it (if it was child->parent). + async Attributes(WebBrowserPersistDocumentAttrs aAttrs, + IPCStream? stream); + async InitFailure(nsresult aStatus); + +child: + async SetPersistFlags(uint32_t aNewFlags); + async PWebBrowserPersistResources(); + async PWebBrowserPersistSerialize(WebBrowserPersistURIMap aMap, + nsCString aRequestedContentType, + uint32_t aEncoderFlags, + uint32_t aWrapColumn); + async __delete__(); + +/* +state START: + recv Attributes goto MAIN; + recv InitFailure goto FAILED; + +state MAIN: + send SetPersistFlags goto MAIN; + send PWebBrowserPersistResources goto MAIN; + send PWebBrowserPersistSerialize goto MAIN; + send __delete__; + +state FAILED: + send __delete__; +*/ +}; + +} // namespace mozilla diff --git a/dom/webbrowserpersist/PWebBrowserPersistResources.ipdl b/dom/webbrowserpersist/PWebBrowserPersistResources.ipdl new file mode 100644 index 0000000000..87b880c296 --- /dev/null +++ b/dom/webbrowserpersist/PWebBrowserPersistResources.ipdl @@ -0,0 +1,32 @@ +/* -*- Mode: IDL; tab-width: 8; 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 protocol PWebBrowserPersistDocument; + +using mozilla::dom::MaybeDiscardedBrowsingContext from "mozilla/dom/BrowsingContext.h"; +using nsContentPolicyType from "nsIContentPolicy.h"; + +namespace mozilla { + +// == nsIWebBrowserPersistResourceVisitor +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PWebBrowserPersistResources { + manager PWebBrowserPersistDocument; + +parent: + async VisitResource(nsCString aURI, nsContentPolicyType aContentPolicyType); + + // The actor sent here is in the START state; the parent-side + // receiver will have to wait for it to enter the MAIN state + // before exposing it with a visitDocument call. + async VisitDocument(PWebBrowserPersistDocument aSubDocument); + + async VisitBrowsingContext(MaybeDiscardedBrowsingContext aContext); + + // This reflects the endVisit method. + async __delete__(nsresult aStatus); +}; + +} // namespace mozilla diff --git a/dom/webbrowserpersist/PWebBrowserPersistSerialize.ipdl b/dom/webbrowserpersist/PWebBrowserPersistSerialize.ipdl new file mode 100644 index 0000000000..11fbe530c0 --- /dev/null +++ b/dom/webbrowserpersist/PWebBrowserPersistSerialize.ipdl @@ -0,0 +1,30 @@ +/* -*- Mode: IDL; tab-width: 8; 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 protocol PWebBrowserPersistDocument; + +namespace mozilla { + +// This actor represents both an nsIWebBrowserPersistWriteCompletion +// and the nsIOutputStream passed with it to the writeContent method. +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +protocol PWebBrowserPersistSerialize { + manager PWebBrowserPersistDocument; + +parent: + // This sends the data with no flow control, so the parent could + // wind up buffering an arbitrarily large amount of data... but + // it's a serialized DOM that's already in memory as DOM nodes, so + // this is at worst just a constant-factor increase in memory usage. + // Also, Chromium does the same thing; see + // content::RenderViewImpl::didSerializeDataForFrame. + async WriteData(uint8_t[] aData); + + // This is the onFinish method. + async __delete__(nsCString aContentType, + nsresult aStatus); +}; + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistDocumentChild.cpp b/dom/webbrowserpersist/WebBrowserPersistDocumentChild.cpp new file mode 100644 index 0000000000..00dbbef08f --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistDocumentChild.cpp @@ -0,0 +1,157 @@ +/* -*- 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 "WebBrowserPersistDocumentChild.h" + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/dom/Document.h" +#include "nsIInputStream.h" +#include "WebBrowserPersistLocalDocument.h" +#include "WebBrowserPersistResourcesChild.h" +#include "WebBrowserPersistSerializeChild.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/net/CookieJarSettings.h" + +namespace mozilla { + +WebBrowserPersistDocumentChild::WebBrowserPersistDocumentChild() = default; + +WebBrowserPersistDocumentChild::~WebBrowserPersistDocumentChild() = default; + +void WebBrowserPersistDocumentChild::Start(dom::Document* aDocument) { + RefPtr<WebBrowserPersistLocalDocument> doc; + if (aDocument) { + doc = new WebBrowserPersistLocalDocument(aDocument); + } + Start(doc); +} + +void WebBrowserPersistDocumentChild::Start( + nsIWebBrowserPersistDocument* aDocument) { + MOZ_ASSERT(!mDocument); + if (!aDocument) { + SendInitFailure(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIReferrerInfo> referrerInfo; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + WebBrowserPersistDocumentAttrs attrs; + nsCOMPtr<nsIInputStream> postDataStream; +#define ENSURE(e) \ + do { \ + nsresult rv = (e); \ + if (NS_FAILED(rv)) { \ + SendInitFailure(rv); \ + return; \ + } \ + } while (0) + ENSURE(aDocument->GetIsPrivate(&(attrs.isPrivate()))); + ENSURE(aDocument->GetDocumentURI(attrs.documentURI())); + ENSURE(aDocument->GetBaseURI(attrs.baseURI())); + ENSURE(aDocument->GetContentType(attrs.contentType())); + ENSURE(aDocument->GetCharacterSet(attrs.characterSet())); + ENSURE(aDocument->GetTitle(attrs.title())); + ENSURE(aDocument->GetContentDisposition(attrs.contentDisposition())); + + attrs.sessionHistoryCacheKey() = aDocument->GetCacheKey(); + + ENSURE(aDocument->GetPersistFlags(&(attrs.persistFlags()))); + + ENSURE(aDocument->GetPrincipal(getter_AddRefs(principal))); + ENSURE(ipc::PrincipalToPrincipalInfo(principal, &(attrs.principal()))); + + ENSURE(aDocument->GetReferrerInfo(getter_AddRefs(referrerInfo))); + attrs.referrerInfo() = referrerInfo; + + ENSURE(aDocument->GetCookieJarSettings(getter_AddRefs(cookieJarSettings))); + net::CookieJarSettings::Cast(cookieJarSettings) + ->Serialize(attrs.cookieJarSettings()); + + ENSURE(aDocument->GetPostData(getter_AddRefs(postDataStream))); +#undef ENSURE + + Maybe<mozilla::ipc::IPCStream> stream; + mozilla::ipc::SerializeIPCStream(postDataStream.forget(), stream, + /* aAllowLazy */ false); + + mDocument = aDocument; + SendAttributes(attrs, stream); +} + +mozilla::ipc::IPCResult WebBrowserPersistDocumentChild::RecvSetPersistFlags( + const uint32_t& aNewFlags) { + mDocument->SetPersistFlags(aNewFlags); + return IPC_OK(); +} + +PWebBrowserPersistResourcesChild* +WebBrowserPersistDocumentChild::AllocPWebBrowserPersistResourcesChild() { + auto* actor = new WebBrowserPersistResourcesChild(); + NS_ADDREF(actor); + return actor; +} + +mozilla::ipc::IPCResult +WebBrowserPersistDocumentChild::RecvPWebBrowserPersistResourcesConstructor( + PWebBrowserPersistResourcesChild* aActor) { + RefPtr<WebBrowserPersistResourcesChild> visitor = + static_cast<WebBrowserPersistResourcesChild*>(aActor); + nsresult rv = mDocument->ReadResources(visitor); + if (NS_FAILED(rv)) { + // This is a sync failure on the child side but an async + // failure on the parent side -- it already got NS_OK from + // ReadResources, so the error has to be reported via the + // visitor instead. + visitor->EndVisit(mDocument, rv); + } + return IPC_OK(); +} + +bool WebBrowserPersistDocumentChild::DeallocPWebBrowserPersistResourcesChild( + PWebBrowserPersistResourcesChild* aActor) { + auto* castActor = static_cast<WebBrowserPersistResourcesChild*>(aActor); + NS_RELEASE(castActor); + return true; +} + +PWebBrowserPersistSerializeChild* +WebBrowserPersistDocumentChild::AllocPWebBrowserPersistSerializeChild( + const WebBrowserPersistURIMap& aMap, + const nsACString& aRequestedContentType, const uint32_t& aEncoderFlags, + const uint32_t& aWrapColumn) { + auto* actor = new WebBrowserPersistSerializeChild(aMap); + NS_ADDREF(actor); + return actor; +} + +mozilla::ipc::IPCResult +WebBrowserPersistDocumentChild::RecvPWebBrowserPersistSerializeConstructor( + PWebBrowserPersistSerializeChild* aActor, + const WebBrowserPersistURIMap& aMap, + const nsACString& aRequestedContentType, const uint32_t& aEncoderFlags, + const uint32_t& aWrapColumn) { + auto* castActor = static_cast<WebBrowserPersistSerializeChild*>(aActor); + // This actor performs the roles of: completion, URI map, and output stream. + nsresult rv = + mDocument->WriteContent(castActor, castActor, aRequestedContentType, + aEncoderFlags, aWrapColumn, castActor); + if (NS_FAILED(rv)) { + castActor->OnFinish(mDocument, castActor, aRequestedContentType, rv); + } + return IPC_OK(); +} + +bool WebBrowserPersistDocumentChild::DeallocPWebBrowserPersistSerializeChild( + PWebBrowserPersistSerializeChild* aActor) { + auto* castActor = static_cast<WebBrowserPersistSerializeChild*>(aActor); + NS_RELEASE(castActor); + return true; +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistDocumentChild.h b/dom/webbrowserpersist/WebBrowserPersistDocumentChild.h new file mode 100644 index 0000000000..220811fb6f --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistDocumentChild.h @@ -0,0 +1,53 @@ +/* -*- 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 WebBrowserPersistDocumentChild_h__ +#define WebBrowserPersistDocumentChild_h__ + +#include "mozilla/PWebBrowserPersistDocumentChild.h" +#include "nsCOMPtr.h" +#include "nsIWebBrowserPersistDocument.h" + +namespace mozilla { + +class WebBrowserPersistDocumentChild final + : public PWebBrowserPersistDocumentChild { + public: + WebBrowserPersistDocumentChild(); + ~WebBrowserPersistDocumentChild(); + + // This sends either Attributes or InitFailure and thereby causes + // the actor to leave the START state. + void Start(nsIWebBrowserPersistDocument* aDocument); + void Start(dom::Document* aDocument); + + mozilla::ipc::IPCResult RecvSetPersistFlags(const uint32_t& aNewFlags); + + PWebBrowserPersistResourcesChild* AllocPWebBrowserPersistResourcesChild(); + virtual mozilla::ipc::IPCResult RecvPWebBrowserPersistResourcesConstructor( + PWebBrowserPersistResourcesChild* aActor) override; + bool DeallocPWebBrowserPersistResourcesChild( + PWebBrowserPersistResourcesChild* aActor); + + PWebBrowserPersistSerializeChild* AllocPWebBrowserPersistSerializeChild( + const WebBrowserPersistURIMap& aMap, + const nsACString& aRequestedContentType, const uint32_t& aEncoderFlags, + const uint32_t& aWrapColumn); + virtual mozilla::ipc::IPCResult RecvPWebBrowserPersistSerializeConstructor( + PWebBrowserPersistSerializeChild* aActor, + const WebBrowserPersistURIMap& aMap, + const nsACString& aRequestedContentType, const uint32_t& aEncoderFlags, + const uint32_t& aWrapColumn) override; + bool DeallocPWebBrowserPersistSerializeChild( + PWebBrowserPersistSerializeChild* aActor); + + private: + nsCOMPtr<nsIWebBrowserPersistDocument> mDocument; +}; + +} // namespace mozilla + +#endif // WebBrowserPersistDocumentChild_h__ diff --git a/dom/webbrowserpersist/WebBrowserPersistDocumentParent.cpp b/dom/webbrowserpersist/WebBrowserPersistDocumentParent.cpp new file mode 100644 index 0000000000..8d08ae5fc5 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistDocumentParent.cpp @@ -0,0 +1,115 @@ +/* -*- 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 "WebBrowserPersistDocumentParent.h" + +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/dom/PContentParent.h" +#include "nsIInputStream.h" +#include "nsThreadUtils.h" +#include "WebBrowserPersistResourcesParent.h" +#include "WebBrowserPersistSerializeParent.h" +#include "WebBrowserPersistRemoteDocument.h" + +namespace mozilla { + +WebBrowserPersistDocumentParent::WebBrowserPersistDocumentParent() + : mReflection(nullptr) {} + +void WebBrowserPersistDocumentParent::SetOnReady( + nsIWebBrowserPersistDocumentReceiver* aOnReady) { + MOZ_ASSERT(aOnReady); + MOZ_ASSERT(!mOnReady); + MOZ_ASSERT(!mReflection); + mOnReady = aOnReady; +} + +void WebBrowserPersistDocumentParent::ActorDestroy(ActorDestroyReason aWhy) { + if (mReflection) { + mReflection->ActorDestroy(); + mReflection = nullptr; + } + if (mOnReady) { + // Bug 1202887: If this is part of a subtree destruction, then + // anything which could cause another actor in that subtree to + // be Send__delete__()ed will cause use-after-free -- such as + // dropping the last reference to another document's + // WebBrowserPersistRemoteDocument. To avoid that, defer the + // callback until after the entire subtree is destroyed. + nsCOMPtr<nsIRunnable> errorLater = NewRunnableMethod<nsresult>( + "nsIWebBrowserPersistDocumentReceiver::OnError", mOnReady, + &nsIWebBrowserPersistDocumentReceiver::OnError, NS_ERROR_FAILURE); + NS_DispatchToCurrentThread(errorLater); + mOnReady = nullptr; + } +} + +WebBrowserPersistDocumentParent::~WebBrowserPersistDocumentParent() { + MOZ_RELEASE_ASSERT(!mReflection); + MOZ_ASSERT(!mOnReady); +} + +mozilla::ipc::IPCResult WebBrowserPersistDocumentParent::RecvAttributes( + const Attrs& aAttrs, const Maybe<IPCStream>& aPostStream) { + // Deserialize the postData unconditionally so that fds aren't leaked. + nsCOMPtr<nsIInputStream> postData = + mozilla::ipc::DeserializeIPCStream(aPostStream); + if (!mOnReady || mReflection) { + return IPC_FAIL_NO_REASON(this); + } + mReflection = new WebBrowserPersistRemoteDocument(this, aAttrs, postData); + RefPtr<WebBrowserPersistRemoteDocument> reflection = mReflection; + mOnReady->OnDocumentReady(reflection); + mOnReady = nullptr; + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebBrowserPersistDocumentParent::RecvInitFailure( + const nsresult& aFailure) { + if (!mOnReady || mReflection) { + return IPC_FAIL_NO_REASON(this); + } + mOnReady->OnError(aFailure); + mOnReady = nullptr; + // Warning: Send__delete__ deallocates this object. + IProtocol* mgr = Manager(); + if (!Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +PWebBrowserPersistResourcesParent* +WebBrowserPersistDocumentParent::AllocPWebBrowserPersistResourcesParent() { + MOZ_CRASH("Don't use this; construct the actor directly and AddRef."); + return nullptr; +} + +bool WebBrowserPersistDocumentParent::DeallocPWebBrowserPersistResourcesParent( + PWebBrowserPersistResourcesParent* aActor) { + // Turn the ref held by IPC back into an nsRefPtr. + RefPtr<WebBrowserPersistResourcesParent> actor = + already_AddRefed<WebBrowserPersistResourcesParent>( + static_cast<WebBrowserPersistResourcesParent*>(aActor)); + return true; +} + +PWebBrowserPersistSerializeParent* +WebBrowserPersistDocumentParent::AllocPWebBrowserPersistSerializeParent( + const WebBrowserPersistURIMap& aMap, + const nsACString& aRequestedContentType, const uint32_t& aEncoderFlags, + const uint32_t& aWrapColumn) { + MOZ_CRASH("Don't use this; construct the actor directly."); + return nullptr; +} + +bool WebBrowserPersistDocumentParent::DeallocPWebBrowserPersistSerializeParent( + PWebBrowserPersistSerializeParent* aActor) { + delete aActor; + return true; +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistDocumentParent.h b/dom/webbrowserpersist/WebBrowserPersistDocumentParent.h new file mode 100644 index 0000000000..5f656abbbb --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistDocumentParent.h @@ -0,0 +1,72 @@ +/* -*- 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 WebBrowserPersistDocumentParent_h__ +#define WebBrowserPersistDocumentParent_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/PWebBrowserPersistDocumentParent.h" +#include "nsCOMPtr.h" +#include "nsIWebBrowserPersistDocument.h" + +// This class is the IPC half of the glue between the +// nsIWebBrowserPersistDocument interface and a remote document. When +// (and if) it receives the Attributes message it constructs an +// WebBrowserPersistRemoteDocument and releases it into the XPCOM +// universe; otherwise, it invokes the document receiver's error +// callback. +// +// This object's lifetime is the normal IPC lifetime; on destruction, +// it calls its XPCOM reflection (if it exists yet) to remove that +// reference. Normal deletion occurs when the XPCOM object is being +// destroyed or after an InitFailure is received and handled. +// +// See also: BrowserParent::StartPersistence. + +namespace mozilla { + +class WebBrowserPersistRemoteDocument; + +class WebBrowserPersistDocumentParent final + : public PWebBrowserPersistDocumentParent { + public: + WebBrowserPersistDocumentParent(); + virtual ~WebBrowserPersistDocumentParent(); + + // Set a callback to be invoked when the actor leaves the START + // state. This method must be called exactly once while the actor + // is still in the START state (or is unconstructed). + void SetOnReady(nsIWebBrowserPersistDocumentReceiver* aOnReady); + + using Attrs = WebBrowserPersistDocumentAttrs; + + // IPDL methods: + mozilla::ipc::IPCResult RecvAttributes(const Attrs& aAttrs, + const Maybe<IPCStream>& aPostStream); + mozilla::ipc::IPCResult RecvInitFailure(const nsresult& aFailure); + + PWebBrowserPersistResourcesParent* AllocPWebBrowserPersistResourcesParent(); + bool DeallocPWebBrowserPersistResourcesParent( + PWebBrowserPersistResourcesParent* aActor); + + PWebBrowserPersistSerializeParent* AllocPWebBrowserPersistSerializeParent( + const WebBrowserPersistURIMap& aMap, + const nsACString& aRequestedContentType, const uint32_t& aEncoderFlags, + const uint32_t& aWrapColumn); + bool DeallocPWebBrowserPersistSerializeParent( + PWebBrowserPersistSerializeParent* aActor); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + // This is reset to nullptr when the callback is invoked. + nsCOMPtr<nsIWebBrowserPersistDocumentReceiver> mOnReady; + WebBrowserPersistRemoteDocument* mReflection; +}; + +} // namespace mozilla + +#endif // WebBrowserPersistDocumentParent_h__ diff --git a/dom/webbrowserpersist/WebBrowserPersistLocalDocument.cpp b/dom/webbrowserpersist/WebBrowserPersistLocalDocument.cpp new file mode 100644 index 0000000000..524f496d35 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistLocalDocument.cpp @@ -0,0 +1,1319 @@ +/* -*- 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 "WebBrowserPersistLocalDocument.h" +#include "WebBrowserPersistDocumentParent.h" + +#include "mozilla/dom/Attr.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Comment.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLAnchorElement.h" +#include "mozilla/dom/HTMLAreaElement.h" +#include "mozilla/dom/HTMLImageElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "mozilla/dom/HTMLObjectElement.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/dom/HTMLSharedElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/NodeFilterBinding.h" +#include "mozilla/dom/ProcessingInstruction.h" +#include "mozilla/dom/ResponsiveImageSelector.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/TreeWalker.h" +#include "mozilla/Encoding.h" +#include "mozilla/Unused.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsContentCID.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDOMAttributeMap.h" +#include "nsFrameLoader.h" +#include "nsGlobalWindowOuter.h" +#include "nsIContent.h" +#include "nsICookieJarSettings.h" +#include "nsIDOMWindowUtils.h" +#include "mozilla/dom/Document.h" +#include "nsIDocumentEncoder.h" +#include "nsILoadContext.h" +#include "nsIProtocolHandler.h" +#include "nsISHEntry.h" +#include "nsIURIMutator.h" +#include "nsIWebBrowserPersist.h" +#include "nsIWebNavigation.h" +#include "nsIWebPageDescriptor.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WebBrowserPersistLocalDocument) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WebBrowserPersistLocalDocument) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebBrowserPersistLocalDocument) + NS_INTERFACE_MAP_ENTRY(nsIWebBrowserPersistDocument) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(WebBrowserPersistLocalDocument, mDocument) + +WebBrowserPersistLocalDocument::WebBrowserPersistLocalDocument( + dom::Document* aDocument) + : mDocument(aDocument), mPersistFlags(0) { + MOZ_ASSERT(mDocument); +} + +WebBrowserPersistLocalDocument::~WebBrowserPersistLocalDocument() = default; + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::SetPersistFlags(uint32_t aFlags) { + mPersistFlags = aFlags; + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetPersistFlags(uint32_t* aFlags) { + *aFlags = mPersistFlags; + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetIsClosed(bool* aIsClosed) { + *aIsClosed = false; + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetIsPrivate(bool* aIsPrivate) { + nsCOMPtr<nsILoadContext> privacyContext = mDocument->GetLoadContext(); + *aIsPrivate = privacyContext && privacyContext->UsePrivateBrowsing(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetDocumentURI(nsACString& aURISpec) { + nsCOMPtr<nsIURI> uri = mDocument->GetDocumentURI(); + if (!uri) { + return NS_ERROR_UNEXPECTED; + } + return uri->GetSpec(aURISpec); +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetBaseURI(nsACString& aURISpec) { + nsCOMPtr<nsIURI> uri = GetBaseURI(); + if (!uri) { + return NS_ERROR_UNEXPECTED; + } + return uri->GetSpec(aURISpec); +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetContentType(nsACString& aContentType) { + nsAutoString utf16Type; + mDocument->GetContentType(utf16Type); + CopyUTF16toUTF8(utf16Type, aContentType); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetCharacterSet(nsACString& aCharSet) { + GetCharacterSet()->Name(aCharSet); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetTitle(nsAString& aTitle) { + nsAutoString titleBuffer; + mDocument->GetTitle(titleBuffer); + aTitle = titleBuffer; + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetReferrerInfo( + nsIReferrerInfo** aReferrerInfo) { + *aReferrerInfo = mDocument->GetReferrerInfo(); + NS_IF_ADDREF(*aReferrerInfo); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetCookieJarSettings( + nsICookieJarSettings** aCookieJarSettings) { + *aCookieJarSettings = mDocument->CookieJarSettings(); + NS_ADDREF(*aCookieJarSettings); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetContentDisposition(nsAString& aCD) { + nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow(); + if (NS_WARN_IF(!window)) { + aCD.SetIsVoid(true); + return NS_OK; + } + nsCOMPtr<nsIDOMWindowUtils> utils = + nsGlobalWindowOuter::Cast(window)->WindowUtils(); + nsresult rv = utils->GetDocumentMetadata(u"content-disposition"_ns, aCD); + if (NS_WARN_IF(NS_FAILED(rv))) { + aCD.SetIsVoid(true); + } + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetCacheKey(uint32_t* aKey) { + Maybe<uint32_t> cacheKey; + + if (nsDocShell* docShell = nsDocShell::Cast(mDocument->GetDocShell())) { + cacheKey = docShell->GetCacheKeyFromCurrentEntry(); + } + *aKey = cacheKey.valueOr(0); + + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetPostData(nsIInputStream** aStream) { + nsCOMPtr<nsIInputStream> postData; + if (nsDocShell* docShell = nsDocShell::Cast(mDocument->GetDocShell())) { + postData = docShell->GetPostDataFromCurrentEntry(); + } + + postData.forget(aStream); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::GetPrincipal(nsIPrincipal** aPrincipal) { + nsCOMPtr<nsIPrincipal> nodePrincipal = mDocument->NodePrincipal(); + nodePrincipal.forget(aPrincipal); + return NS_OK; +} + +already_AddRefed<nsISHEntry> WebBrowserPersistLocalDocument::GetHistory() { + nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow(); + if (NS_WARN_IF(!window)) { + return nullptr; + } + nsCOMPtr<nsIWebNavigation> webNav = do_GetInterface(window); + if (NS_WARN_IF(!webNav)) { + return nullptr; + } + nsCOMPtr<nsIWebPageDescriptor> desc = do_QueryInterface(webNav); + if (NS_WARN_IF(!desc)) { + return nullptr; + } + nsCOMPtr<nsISupports> curDesc; + nsresult rv = desc->GetCurrentDescriptor(getter_AddRefs(curDesc)); + // This can fail if, e.g., the document is a Print Preview. + if (NS_FAILED(rv) || NS_WARN_IF(!curDesc)) { + return nullptr; + } + nsCOMPtr<nsISHEntry> history = do_QueryInterface(curDesc); + return history.forget(); +} + +NotNull<const Encoding*> WebBrowserPersistLocalDocument::GetCharacterSet() + const { + return mDocument->GetDocumentCharacterSet(); +} + +uint32_t WebBrowserPersistLocalDocument::GetPersistFlags() const { + return mPersistFlags; +} + +nsIURI* WebBrowserPersistLocalDocument::GetBaseURI() const { + return mDocument->GetBaseURI(); +} + +namespace { + +// Helper class for ReadResources(). +class ResourceReader final : public nsIWebBrowserPersistDocumentReceiver { + public: + ResourceReader(WebBrowserPersistLocalDocument* aParent, + nsIWebBrowserPersistResourceVisitor* aVisitor); + nsresult OnWalkDOMNode(nsINode* aNode); + + // This is called both to indicate the end of the document walk + // and when a subdocument is (maybe asynchronously) sent to the + // visitor. The call to EndVisit needs to happen after both of + // those have finished. + void DocumentDone(nsresult aStatus); + + NS_DECL_NSIWEBBROWSERPERSISTDOCUMENTRECEIVER + NS_DECL_ISUPPORTS + + private: + RefPtr<WebBrowserPersistLocalDocument> mParent; + nsCOMPtr<nsIWebBrowserPersistResourceVisitor> mVisitor; + nsCOMPtr<nsIURI> mCurrentBaseURI; + uint32_t mPersistFlags; + + // The number of DocumentDone calls after which EndVisit will be + // called on the visitor. Counts the main document if it's still + // being walked and any outstanding asynchronous subdocument + // StartPersistence calls. + size_t mOutstandingDocuments; + // Collects the status parameters to DocumentDone calls. + nsresult mEndStatus; + + nsresult OnWalkURI(const nsACString& aURISpec, + nsContentPolicyType aContentPolicyType); + nsresult OnWalkURI(nsIURI* aURI, nsContentPolicyType aContentPolicyType); + nsresult OnWalkAttribute(dom::Element* aElement, + nsContentPolicyType aContentPolicyType, + const char* aAttribute, + const char* aNamespaceURI = ""); + nsresult OnWalkSubframe(nsINode* aNode); + nsresult OnWalkSrcSet(dom::Element* aElement); + + ~ResourceReader(); + + using IWBP = nsIWebBrowserPersist; +}; + +NS_IMPL_ISUPPORTS(ResourceReader, nsIWebBrowserPersistDocumentReceiver) + +ResourceReader::ResourceReader(WebBrowserPersistLocalDocument* aParent, + nsIWebBrowserPersistResourceVisitor* aVisitor) + : mParent(aParent), + mVisitor(aVisitor), + mCurrentBaseURI(aParent->GetBaseURI()), + mPersistFlags(aParent->GetPersistFlags()), + mOutstandingDocuments(1), + mEndStatus(NS_OK) { + MOZ_ASSERT(mCurrentBaseURI); +} + +ResourceReader::~ResourceReader() { MOZ_ASSERT(mOutstandingDocuments == 0); } + +void ResourceReader::DocumentDone(nsresult aStatus) { + MOZ_ASSERT(mOutstandingDocuments > 0); + if (NS_SUCCEEDED(mEndStatus)) { + mEndStatus = aStatus; + } + if (--mOutstandingDocuments == 0) { + mVisitor->EndVisit(mParent, mEndStatus); + } +} + +nsresult ResourceReader::OnWalkSubframe(nsINode* aNode) { + RefPtr<nsFrameLoaderOwner> loaderOwner = do_QueryObject(aNode); + NS_ENSURE_STATE(loaderOwner); + RefPtr<nsFrameLoader> loader = loaderOwner->GetFrameLoader(); + NS_ENSURE_STATE(loader); + + RefPtr<dom::BrowsingContext> context = loader->GetBrowsingContext(); + NS_ENSURE_STATE(context); + + if (loader->IsRemoteFrame()) { + mVisitor->VisitBrowsingContext(mParent, context); + return NS_OK; + } + + ++mOutstandingDocuments; + ErrorResult err; + loader->StartPersistence(context, this, err); + nsresult rv = err.StealNSResult(); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_NO_CONTENT) { + // Just ignore frames with no content document. + rv = NS_OK; + } + // StartPersistence won't eventually call this if it failed, + // so this does so (to keep mOutstandingDocuments correct). + DocumentDone(rv); + } + return rv; +} + +NS_IMETHODIMP +ResourceReader::OnDocumentReady(nsIWebBrowserPersistDocument* aDocument) { + mVisitor->VisitDocument(mParent, aDocument); + DocumentDone(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +ResourceReader::OnError(nsresult aFailure) { + DocumentDone(aFailure); + return NS_OK; +} + +nsresult ResourceReader::OnWalkURI(nsIURI* aURI, + nsContentPolicyType aContentPolicyType) { + // Test if this URI should be persisted. By default + // we should assume the URI is persistable. + bool doNotPersistURI; + nsresult rv = NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_NON_PERSISTABLE, &doNotPersistURI); + if (NS_SUCCEEDED(rv) && doNotPersistURI) { + return NS_OK; + } + + nsAutoCString stringURI; + rv = aURI->GetSpec(stringURI); + NS_ENSURE_SUCCESS(rv, rv); + return mVisitor->VisitResource(mParent, stringURI, aContentPolicyType); +} + +nsresult ResourceReader::OnWalkURI(const nsACString& aURISpec, + nsContentPolicyType aContentPolicyType) { + nsresult rv; + nsCOMPtr<nsIURI> uri; + + rv = NS_NewURI(getter_AddRefs(uri), aURISpec, mParent->GetCharacterSet(), + mCurrentBaseURI); + if (NS_FAILED(rv)) { + // We don't want to break saving a page in case of a malformed URI. + return NS_OK; + } + return OnWalkURI(uri, aContentPolicyType); +} + +static void ExtractAttribute(dom::Element* aElement, const char* aAttribute, + const char* aNamespaceURI, nsCString& aValue) { + // Find the named URI attribute on the (element) node and store + // a reference to the URI that maps onto a local file name + + RefPtr<nsDOMAttributeMap> attrMap = aElement->Attributes(); + + NS_ConvertASCIItoUTF16 namespaceURI(aNamespaceURI); + NS_ConvertASCIItoUTF16 attribute(aAttribute); + RefPtr<dom::Attr> attr = attrMap->GetNamedItemNS(namespaceURI, attribute); + if (attr) { + nsAutoString value; + attr->GetValue(value); + CopyUTF16toUTF8(value, aValue); + } else { + aValue.Truncate(); + } +} + +nsresult ResourceReader::OnWalkAttribute(dom::Element* aElement, + nsContentPolicyType aContentPolicyType, + const char* aAttribute, + const char* aNamespaceURI) { + nsAutoCString uriSpec; + ExtractAttribute(aElement, aAttribute, aNamespaceURI, uriSpec); + if (uriSpec.IsEmpty()) { + return NS_OK; + } + return OnWalkURI(uriSpec, aContentPolicyType); +} + +nsresult ResourceReader::OnWalkSrcSet(dom::Element* aElement) { + nsAutoString srcSet; + if (!aElement->GetAttr(nsGkAtoms::srcset, srcSet)) { + return NS_OK; + } + + nsresult rv = NS_OK; + auto eachCandidate = [&](dom::ResponsiveImageCandidate&& aCandidate) { + if (!aCandidate.IsValid() || NS_FAILED(rv)) { + return; + } + rv = OnWalkURI(NS_ConvertUTF16toUTF8(aCandidate.URLString()), + nsIContentPolicy::TYPE_IMAGE); + }; + dom::ResponsiveImageSelector::ParseSourceSet(srcSet, eachCandidate); + return rv; +} + +static nsresult GetXMLStyleSheetLink(dom::ProcessingInstruction* aPI, + nsAString& aHref) { + nsAutoString data; + aPI->GetData(data); + + nsContentUtils::GetPseudoAttributeValue(data, nsGkAtoms::href, aHref); + return NS_OK; +} + +nsresult ResourceReader::OnWalkDOMNode(nsINode* aNode) { + // Fixup xml-stylesheet processing instructions + if (auto nodeAsPI = dom::ProcessingInstruction::FromNode(aNode)) { + nsAutoString target; + nodeAsPI->GetTarget(target); + if (target.EqualsLiteral("xml-stylesheet")) { + nsAutoString href; + GetXMLStyleSheetLink(nodeAsPI, href); + if (!href.IsEmpty()) { + return OnWalkURI(NS_ConvertUTF16toUTF8(href), + nsIContentPolicy::TYPE_STYLESHEET); + } + } + return NS_OK; + } + + // Test the node to see if it's an image, frame, iframe, css, js + if (auto* img = dom::HTMLImageElement::FromNode(*aNode)) { + MOZ_TRY(OnWalkAttribute(img, nsIContentPolicy::TYPE_IMAGE, "src")); + MOZ_TRY(OnWalkSrcSet(img)); + return NS_OK; + } + + if (aNode->IsSVGElement(nsGkAtoms::image)) { + MOZ_TRY(OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_IMAGE, + "href")); + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_IMAGE, + "href", "http://www.w3.org/1999/xlink"); + } + + if (aNode->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_MEDIA, + "src"); + } + + if (aNode->IsHTMLElement(nsGkAtoms::source)) { + MOZ_TRY(OnWalkSrcSet(aNode->AsElement())); + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_MEDIA, + "src"); + } + + if (aNode->IsHTMLElement(nsGkAtoms::body)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_IMAGE, + "background"); + } + + if (aNode->IsHTMLElement(nsGkAtoms::table)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_IMAGE, + "background"); + } + + if (aNode->IsHTMLElement(nsGkAtoms::tr)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_IMAGE, + "background"); + } + + if (aNode->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_IMAGE, + "background"); + } + + if (aNode->IsHTMLElement(nsGkAtoms::script)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_SCRIPT, + "src"); + } + + if (aNode->IsSVGElement(nsGkAtoms::script)) { + MOZ_TRY(OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_SCRIPT, + "href")); + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_SCRIPT, + "href", "http://www.w3.org/1999/xlink"); + } + + if (aNode->IsHTMLElement(nsGkAtoms::embed)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_OBJECT, + "src"); + } + + if (aNode->IsHTMLElement(nsGkAtoms::object)) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_OBJECT, + "data"); + } + + if (auto nodeAsLink = dom::HTMLLinkElement::FromNode(aNode)) { + // Test if the link has a rel value indicating it to be a stylesheet + nsAutoString linkRel; + nodeAsLink->GetRel(linkRel); + if (!linkRel.IsEmpty()) { + nsReadingIterator<char16_t> start; + nsReadingIterator<char16_t> end; + nsReadingIterator<char16_t> current; + + linkRel.BeginReading(start); + linkRel.EndReading(end); + + // Walk through space delimited string looking for "stylesheet" + for (current = start; current != end; ++current) { + // Ignore whitespace + if (nsCRT::IsAsciiSpace(*current)) { + continue; + } + + // Grab the next space delimited word + nsReadingIterator<char16_t> startWord = current; + do { + ++current; + } while (current != end && !nsCRT::IsAsciiSpace(*current)); + + // Store the link for fix up if it says "stylesheet" + if (Substring(startWord, current) + .LowerCaseEqualsLiteral("stylesheet")) { + OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_STYLESHEET, + "href"); + return NS_OK; + } + if (current == end) { + break; + } + } + } + return NS_OK; + } + + if (aNode->IsHTMLElement(nsGkAtoms::frame)) { + return OnWalkSubframe(aNode); + } + + if (aNode->IsHTMLElement(nsGkAtoms::iframe) && + !(mPersistFlags & IWBP::PERSIST_FLAGS_IGNORE_IFRAMES)) { + return OnWalkSubframe(aNode); + } + + auto nodeAsInput = dom::HTMLInputElement::FromNode(aNode); + if (nodeAsInput) { + return OnWalkAttribute(aNode->AsElement(), nsIContentPolicy::TYPE_IMAGE, + "src"); + } + + return NS_OK; +} + +// Helper class for node rewriting in writeContent(). +class PersistNodeFixup final : public nsIDocumentEncoderNodeFixup { + public: + PersistNodeFixup(WebBrowserPersistLocalDocument* aParent, + nsIWebBrowserPersistURIMap* aMap, nsIURI* aTargetURI); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOCUMENTENCODERNODEFIXUP + private: + virtual ~PersistNodeFixup() = default; + RefPtr<WebBrowserPersistLocalDocument> mParent; + nsClassHashtable<nsCStringHashKey, nsCString> mMap; + nsCOMPtr<nsIURI> mCurrentBaseURI; + nsCOMPtr<nsIURI> mTargetBaseURI; + + bool IsFlagSet(uint32_t aFlag) const { + return mParent->GetPersistFlags() & aFlag; + } + + nsresult GetNodeToFixup(nsINode* aNodeIn, nsINode** aNodeOut); + nsresult FixupURI(nsAString& aURI); + nsresult FixupAttribute(nsINode* aNode, const char* aAttribute, + const char* aNamespaceURI = ""); + nsresult FixupAnchor(nsINode* aNode); + nsresult FixupXMLStyleSheetLink(dom::ProcessingInstruction* aPI, + const nsAString& aHref); + + nsresult FixupSrcSet(nsINode*); + + using IWBP = nsIWebBrowserPersist; +}; + +NS_IMPL_ISUPPORTS(PersistNodeFixup, nsIDocumentEncoderNodeFixup) + +PersistNodeFixup::PersistNodeFixup(WebBrowserPersistLocalDocument* aParent, + nsIWebBrowserPersistURIMap* aMap, + nsIURI* aTargetURI) + : mParent(aParent), + mCurrentBaseURI(aParent->GetBaseURI()), + mTargetBaseURI(aTargetURI) { + if (aMap) { + uint32_t mapSize; + nsresult rv = aMap->GetNumMappedURIs(&mapSize); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + NS_ENSURE_SUCCESS_VOID(rv); + for (uint32_t i = 0; i < mapSize; ++i) { + nsAutoCString urlFrom; + auto urlTo = MakeUnique<nsCString>(); + + rv = aMap->GetURIMapping(i, urlFrom, *urlTo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_SUCCEEDED(rv)) { + mMap.InsertOrUpdate(urlFrom, std::move(urlTo)); + } + } + } +} + +nsresult PersistNodeFixup::GetNodeToFixup(nsINode* aNodeIn, + nsINode** aNodeOut) { + // Avoid mixups in FixupNode that could leak objects; this goes + // against the usual out parameter convention, but it's a private + // method so shouldn't be a problem. + MOZ_ASSERT(!*aNodeOut); + + if (!IsFlagSet(IWBP::PERSIST_FLAGS_FIXUP_ORIGINAL_DOM)) { + ErrorResult rv; + *aNodeOut = aNodeIn->CloneNode(false, rv).take(); + return rv.StealNSResult(); + } + + NS_ADDREF(*aNodeOut = aNodeIn); + return NS_OK; +} + +nsresult PersistNodeFixup::FixupURI(nsAString& aURI) { + // get the current location of the file (absolutized) + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURI, mParent->GetCharacterSet(), + mCurrentBaseURI); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString spec; + rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + const nsCString* replacement = mMap.Get(spec); + if (!replacement) { + // Note that most callers ignore this "failure". + return NS_ERROR_FAILURE; + } + if (!replacement->IsEmpty()) { + CopyUTF8toUTF16(*replacement, aURI); + } + return NS_OK; +} + +nsresult PersistNodeFixup::FixupSrcSet(nsINode* aNode) { + dom::Element* element = aNode->AsElement(); + nsAutoString originalSrcSet; + if (!element->GetAttr(nsGkAtoms::srcset, originalSrcSet)) { + return NS_OK; + } + nsAutoString newSrcSet; + bool first = true; + auto eachCandidate = [&](dom::ResponsiveImageCandidate&& aCandidate) { + if (!aCandidate.IsValid()) { + return; + } + if (!first) { + newSrcSet.AppendLiteral(", "); + } + first = false; + nsAutoString uri(aCandidate.URLString()); + FixupURI(uri); + newSrcSet.Append(uri); + aCandidate.AppendDescriptors(newSrcSet); + }; + dom::ResponsiveImageSelector::ParseSourceSet(originalSrcSet, eachCandidate); + element->SetAttr(nsGkAtoms::srcset, newSrcSet, IgnoreErrors()); + return NS_OK; +} + +nsresult PersistNodeFixup::FixupAttribute(nsINode* aNode, + const char* aAttribute, + const char* aNamespaceURI) { + MOZ_ASSERT(aNode->IsElement()); + dom::Element* element = aNode->AsElement(); + + RefPtr<nsDOMAttributeMap> attrMap = element->Attributes(); + + NS_ConvertASCIItoUTF16 attribute(aAttribute); + NS_ConvertASCIItoUTF16 namespaceURI(aNamespaceURI); + RefPtr<dom::Attr> attr = attrMap->GetNamedItemNS(namespaceURI, attribute); + nsresult rv = NS_OK; + if (attr) { + nsString uri; + attr->GetValue(uri); + rv = FixupURI(uri); + if (NS_SUCCEEDED(rv)) { + attr->SetValue(uri, IgnoreErrors()); + } + } + + return rv; +} + +nsresult PersistNodeFixup::FixupAnchor(nsINode* aNode) { + if (IsFlagSet(IWBP::PERSIST_FLAGS_DONT_FIXUP_LINKS)) { + return NS_OK; + } + + MOZ_ASSERT(aNode->IsElement()); + dom::Element* element = aNode->AsElement(); + + RefPtr<nsDOMAttributeMap> attrMap = element->Attributes(); + + // Make all anchor links absolute so they point off onto the Internet + nsString attribute(u"href"_ns); + RefPtr<dom::Attr> attr = attrMap->GetNamedItem(attribute); + if (attr) { + nsString oldValue; + attr->GetValue(oldValue); + NS_ConvertUTF16toUTF8 oldCValue(oldValue); + + // Skip empty values and self-referencing bookmarks + if (oldCValue.IsEmpty() || oldCValue.CharAt(0) == '#') { + return NS_OK; + } + + // if saving file to same location, we don't need to do any fixup + bool isEqual; + if (mTargetBaseURI && + NS_SUCCEEDED(mCurrentBaseURI->Equals(mTargetBaseURI, &isEqual)) && + isEqual) { + return NS_OK; + } + + nsCOMPtr<nsIURI> relativeURI; + relativeURI = IsFlagSet(IWBP::PERSIST_FLAGS_FIXUP_LINKS_TO_DESTINATION) + ? mTargetBaseURI + : mCurrentBaseURI; + // Make a new URI to replace the current one + nsCOMPtr<nsIURI> newURI; + nsresult rv = NS_NewURI(getter_AddRefs(newURI), oldCValue, + mParent->GetCharacterSet(), relativeURI); + if (NS_SUCCEEDED(rv) && newURI) { + Unused << NS_MutateURI(newURI).SetUserPass(""_ns).Finalize(newURI); + nsAutoCString uriSpec; + rv = newURI->GetSpec(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + attr->SetValue(NS_ConvertUTF8toUTF16(uriSpec), IgnoreErrors()); + } + } + + return NS_OK; +} + +static void AppendXMLAttr(const nsAString& key, const nsAString& aValue, + nsAString& aBuffer) { + if (!aBuffer.IsEmpty()) { + aBuffer.Append(' '); + } + aBuffer.Append(key); + aBuffer.AppendLiteral(R"(=")"); + for (size_t i = 0; i < aValue.Length(); ++i) { + switch (aValue[i]) { + case '&': + aBuffer.AppendLiteral("&"); + break; + case '<': + aBuffer.AppendLiteral("<"); + break; + case '>': + aBuffer.AppendLiteral(">"); + break; + case '"': + aBuffer.AppendLiteral("""); + break; + default: + aBuffer.Append(aValue[i]); + break; + } + } + aBuffer.Append('"'); +} + +nsresult PersistNodeFixup::FixupXMLStyleSheetLink( + dom::ProcessingInstruction* aPI, const nsAString& aHref) { + NS_ENSURE_ARG_POINTER(aPI); + + nsAutoString data; + aPI->GetData(data); + + nsAutoString href; + nsContentUtils::GetPseudoAttributeValue(data, nsGkAtoms::href, href); + + // Construct and set a new data value for the xml-stylesheet + if (!aHref.IsEmpty() && !href.IsEmpty()) { + nsAutoString alternate; + nsAutoString charset; + nsAutoString title; + nsAutoString type; + nsAutoString media; + + nsContentUtils::GetPseudoAttributeValue(data, nsGkAtoms::alternate, + alternate); + nsContentUtils::GetPseudoAttributeValue(data, nsGkAtoms::charset, charset); + nsContentUtils::GetPseudoAttributeValue(data, nsGkAtoms::title, title); + nsContentUtils::GetPseudoAttributeValue(data, nsGkAtoms::type, type); + nsContentUtils::GetPseudoAttributeValue(data, nsGkAtoms::media, media); + + nsAutoString newData; + AppendXMLAttr(u"href"_ns, aHref, newData); + if (!title.IsEmpty()) { + AppendXMLAttr(u"title"_ns, title, newData); + } + if (!media.IsEmpty()) { + AppendXMLAttr(u"media"_ns, media, newData); + } + if (!type.IsEmpty()) { + AppendXMLAttr(u"type"_ns, type, newData); + } + if (!charset.IsEmpty()) { + AppendXMLAttr(u"charset"_ns, charset, newData); + } + if (!alternate.IsEmpty()) { + AppendXMLAttr(u"alternate"_ns, alternate, newData); + } + aPI->SetData(newData, IgnoreErrors()); + } + + return NS_OK; +} + +NS_IMETHODIMP +PersistNodeFixup::FixupNode(nsINode* aNodeIn, bool* aSerializeCloneKids, + nsINode** aNodeOut) { + *aNodeOut = nullptr; + *aSerializeCloneKids = false; + + uint16_t type = aNodeIn->NodeType(); + if (type != nsINode::ELEMENT_NODE && + type != nsINode::PROCESSING_INSTRUCTION_NODE) { + return NS_OK; + } + + MOZ_ASSERT(aNodeIn->IsContent()); + + // Fixup xml-stylesheet processing instructions + if (auto nodeAsPI = dom::ProcessingInstruction::FromNode(aNodeIn)) { + nsAutoString target; + nodeAsPI->GetTarget(target); + if (target.EqualsLiteral("xml-stylesheet")) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + MOZ_ASSERT((*aNodeOut)->IsProcessingInstruction()); + auto nodeAsPI = static_cast<dom::ProcessingInstruction*>(*aNodeOut); + nsAutoString href; + GetXMLStyleSheetLink(nodeAsPI, href); + if (!href.IsEmpty()) { + FixupURI(href); + FixupXMLStyleSheetLink(nodeAsPI, href); + } + } + } + return NS_OK; + } + + nsCOMPtr<nsIContent> content = do_QueryInterface(aNodeIn); + if (!content) { + return NS_OK; + } + + // BASE elements are replaced by a comment so relative links are not hosed. + if (!IsFlagSet(IWBP::PERSIST_FLAGS_NO_BASE_TAG_MODIFICATIONS) && + content->IsHTMLElement(nsGkAtoms::base)) { + // Base uses HTMLSharedElement, which would be awkward to implement + // FromContent on, since it represents multiple elements. Since we've + // already checked IsHTMLElement here, just cast as we were doing. + auto* base = static_cast<dom::HTMLSharedElement*>(content.get()); + dom::Document* ownerDoc = base->OwnerDoc(); + + nsAutoString href; + base->GetHref(href); // Doesn't matter if this fails + nsAutoString commentText; + commentText.AssignLiteral(" base "); + if (!href.IsEmpty()) { + commentText += u"href=\""_ns + href + u"\" "_ns; + } + *aNodeOut = ownerDoc->CreateComment(commentText).take(); + return NS_OK; + } + + // Fix up href and file links in the elements + RefPtr<dom::HTMLAnchorElement> nodeAsAnchor = + dom::HTMLAnchorElement::FromNode(content); + if (nodeAsAnchor) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAnchor(*aNodeOut); + } + return rv; + } + + RefPtr<dom::HTMLAreaElement> nodeAsArea = + dom::HTMLAreaElement::FromNode(content); + if (nodeAsArea) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAnchor(*aNodeOut); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::body)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "background"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::table)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "background"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::tr)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "background"); + } + return rv; + } + + if (content->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "background"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::img)) { + MOZ_TRY(GetNodeToFixup(aNodeIn, aNodeOut)); + if (!*aNodeOut) { + return NS_OK; + } + + // Disable image loads + nsCOMPtr<nsIImageLoadingContent> imgCon = do_QueryInterface(*aNodeOut); + if (imgCon) { + imgCon->SetLoadingEnabled(false); + } + // FIXME(emilio): Why fixing up <img href>? Looks bogus + FixupAnchor(*aNodeOut); + FixupAttribute(*aNodeOut, "src"); + FixupSrcSet(*aNodeOut); + return NS_OK; + } + + if (content->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "src"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::source)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "src"); + FixupSrcSet(*aNodeOut); + } + return rv; + } + + if (content->IsSVGElement(nsGkAtoms::image)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + // Disable image loads + nsCOMPtr<nsIImageLoadingContent> imgCon = do_QueryInterface(*aNodeOut); + if (imgCon) imgCon->SetLoadingEnabled(false); + + // FixupAnchor(*aNodeOut); // XXXjwatt: is this line needed? + FixupAttribute(*aNodeOut, "href"); + FixupAttribute(*aNodeOut, "href", "http://www.w3.org/1999/xlink"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::script)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "src"); + } + return rv; + } + + if (content->IsSVGElement(nsGkAtoms::script)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "href"); + FixupAttribute(*aNodeOut, "href", "http://www.w3.org/1999/xlink"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::embed)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "src"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::object)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "data"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::link)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + // First see if the link represents linked content + rv = FixupAttribute(*aNodeOut, "href"); + if (NS_FAILED(rv)) { + // Perhaps this link is actually an anchor to related content + FixupAnchor(*aNodeOut); + } + // TODO if "type" attribute == "text/css" + // fixup stylesheet + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::frame)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "src"); + } + return rv; + } + + if (content->IsHTMLElement(nsGkAtoms::iframe)) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + FixupAttribute(*aNodeOut, "src"); + } + return rv; + } + + RefPtr<dom::HTMLInputElement> nodeAsInput = + dom::HTMLInputElement::FromNodeOrNull(content); + if (nodeAsInput) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + // Disable image loads + nsCOMPtr<nsIImageLoadingContent> imgCon = do_QueryInterface(*aNodeOut); + if (imgCon) { + imgCon->SetLoadingEnabled(false); + } + + FixupAttribute(*aNodeOut, "src"); + + nsAutoString valueStr; + constexpr auto valueAttr = u"value"_ns; + // Update element node attributes with user-entered form state + RefPtr<dom::HTMLInputElement> outElt = + dom::HTMLInputElement::FromNode((*aNodeOut)->AsContent()); + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(*aNodeOut); + switch (formControl->ControlType()) { + case FormControlType::InputEmail: + case FormControlType::InputSearch: + case FormControlType::InputText: + case FormControlType::InputTel: + case FormControlType::InputUrl: + case FormControlType::InputNumber: + case FormControlType::InputRange: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputColor: + nodeAsInput->GetValue(valueStr, dom::CallerType::System); + // Avoid superfluous value="" serialization + if (valueStr.IsEmpty()) { + outElt->RemoveAttribute(valueAttr, IgnoreErrors()); + } else { + outElt->SetAttribute(valueAttr, valueStr, IgnoreErrors()); + } + break; + case FormControlType::InputCheckbox: + case FormControlType::InputRadio: + outElt->SetDefaultChecked(nodeAsInput->Checked(), IgnoreErrors()); + break; + default: + break; + } + } + return rv; + } + + dom::HTMLTextAreaElement* nodeAsTextArea = + dom::HTMLTextAreaElement::FromNode(content); + if (nodeAsTextArea) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + // Tell the document encoder to serialize the text child we create below + *aSerializeCloneKids = true; + + nsAutoString valueStr; + nodeAsTextArea->GetValue(valueStr); + + (*aNodeOut)->SetTextContent(valueStr, IgnoreErrors()); + } + return rv; + } + + dom::HTMLOptionElement* nodeAsOption = + dom::HTMLOptionElement::FromNode(content); + if (nodeAsOption) { + nsresult rv = GetNodeToFixup(aNodeIn, aNodeOut); + if (NS_SUCCEEDED(rv) && *aNodeOut) { + dom::HTMLOptionElement* outElt = + dom::HTMLOptionElement::FromNode((*aNodeOut)->AsContent()); + bool selected = nodeAsOption->Selected(); + outElt->SetDefaultSelected(selected, IgnoreErrors()); + } + return rv; + } + + return NS_OK; +} + +} // unnamed namespace + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::ReadResources( + nsIWebBrowserPersistResourceVisitor* aVisitor) { + nsresult rv = NS_OK; + nsCOMPtr<nsIWebBrowserPersistResourceVisitor> visitor = aVisitor; + + NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE); + + ErrorResult err; + RefPtr<dom::TreeWalker> walker = mDocument->CreateTreeWalker( + *mDocument, + dom::NodeFilter_Binding::SHOW_ELEMENT | + dom::NodeFilter_Binding::SHOW_DOCUMENT | + dom::NodeFilter_Binding::SHOW_PROCESSING_INSTRUCTION, + nullptr, err); + + if (NS_WARN_IF(err.Failed())) { + return err.StealNSResult(); + } + MOZ_ASSERT(walker); + + RefPtr<ResourceReader> reader = new ResourceReader(this, aVisitor); + nsCOMPtr<nsINode> currentNode = walker->CurrentNode(); + do { + rv = reader->OnWalkDOMNode(currentNode); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + ErrorResult err; + currentNode = walker->NextNode(err); + if (NS_WARN_IF(err.Failed())) { + err.SuppressException(); + break; + } + } while (currentNode); + reader->DocumentDone(rv); + // If NS_FAILED(rv), it was / will be reported by an EndVisit call + // via DocumentDone. This method must return a failure if and + // only if visitor won't be invoked. + return NS_OK; +} + +static uint32_t ConvertEncoderFlags(uint32_t aEncoderFlags) { + uint32_t encoderFlags = 0; + + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_SELECTION_ONLY) + encoderFlags |= nsIDocumentEncoder::OutputSelectionOnly; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_FORMATTED) + encoderFlags |= nsIDocumentEncoder::OutputFormatted; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_RAW) + encoderFlags |= nsIDocumentEncoder::OutputRaw; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_BODY_ONLY) + encoderFlags |= nsIDocumentEncoder::OutputBodyOnly; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_PREFORMATTED) + encoderFlags |= nsIDocumentEncoder::OutputPreformatted; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_WRAP) + encoderFlags |= nsIDocumentEncoder::OutputWrap; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_FORMAT_FLOWED) + encoderFlags |= nsIDocumentEncoder::OutputFormatFlowed; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_ABSOLUTE_LINKS) + encoderFlags |= nsIDocumentEncoder::OutputAbsoluteLinks; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_ENCODE_BASIC_ENTITIES) + encoderFlags |= nsIDocumentEncoder::OutputEncodeBasicEntities; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_CR_LINEBREAKS) + encoderFlags |= nsIDocumentEncoder::OutputCRLineBreak; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_LF_LINEBREAKS) + encoderFlags |= nsIDocumentEncoder::OutputLFLineBreak; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_NOSCRIPT_CONTENT) + encoderFlags |= nsIDocumentEncoder::OutputNoScriptContent; + if (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_NOFRAMES_CONTENT) + encoderFlags |= nsIDocumentEncoder::OutputNoFramesContent; + + return encoderFlags; +} + +static bool ContentTypeEncoderExists(const nsACString& aType) { + return do_getDocumentTypeSupportedForEncoding( + PromiseFlatCString(aType).get()); +} + +void WebBrowserPersistLocalDocument::DecideContentType( + nsACString& aContentType) { + if (aContentType.IsEmpty()) { + if (NS_WARN_IF(NS_FAILED(GetContentType(aContentType)))) { + aContentType.Truncate(); + } + } + if (!aContentType.IsEmpty() && !ContentTypeEncoderExists(aContentType)) { + aContentType.Truncate(); + } + if (aContentType.IsEmpty()) { + aContentType.AssignLiteral("text/html"); + } +} + +nsresult WebBrowserPersistLocalDocument::GetDocEncoder( + const nsACString& aContentType, uint32_t aEncoderFlags, + nsIDocumentEncoder** aEncoder) { + nsCOMPtr<nsIDocumentEncoder> encoder = + do_createDocumentEncoder(PromiseFlatCString(aContentType).get()); + NS_ENSURE_TRUE(encoder, NS_ERROR_FAILURE); + + nsresult rv = + encoder->NativeInit(mDocument, NS_ConvertASCIItoUTF16(aContentType), + ConvertEncoderFlags(aEncoderFlags)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + nsAutoCString charSet; + rv = GetCharacterSet(charSet); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + rv = encoder->SetCharset(charSet); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + encoder.forget(aEncoder); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistLocalDocument::WriteContent( + nsIOutputStream* aStream, nsIWebBrowserPersistURIMap* aMap, + const nsACString& aRequestedContentType, uint32_t aEncoderFlags, + uint32_t aWrapColumn, nsIWebBrowserPersistWriteCompletion* aCompletion) { + NS_ENSURE_ARG_POINTER(aStream); + NS_ENSURE_ARG_POINTER(aCompletion); + nsAutoCString contentType(aRequestedContentType); + DecideContentType(contentType); + + nsCOMPtr<nsIDocumentEncoder> encoder; + nsresult rv = + GetDocEncoder(contentType, aEncoderFlags, getter_AddRefs(encoder)); + NS_ENSURE_SUCCESS(rv, rv); + + if (aWrapColumn != 0 && + (aEncoderFlags & nsIWebBrowserPersist::ENCODE_FLAGS_WRAP)) { + encoder->SetWrapColumn(aWrapColumn); + } + + nsCOMPtr<nsIURI> targetURI; + if (aMap) { + nsAutoCString targetURISpec; + rv = aMap->GetTargetBaseURI(targetURISpec); + if (NS_SUCCEEDED(rv) && !targetURISpec.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(targetURI), targetURISpec); + NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED); + } else if (mPersistFlags & + nsIWebBrowserPersist::PERSIST_FLAGS_FIXUP_LINKS_TO_DESTINATION) { + return NS_ERROR_UNEXPECTED; + } + } + rv = encoder->SetNodeFixup(new PersistNodeFixup(this, aMap, targetURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + rv = encoder->EncodeToStream(aStream); + aCompletion->OnFinish(this, aStream, contentType, rv); + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistLocalDocument.h b/dom/webbrowserpersist/WebBrowserPersistLocalDocument.h new file mode 100644 index 0000000000..c1af8acd54 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistLocalDocument.h @@ -0,0 +1,52 @@ +/* -*- 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 WebBrowserPersistLocalDocument_h__ +#define WebBrowserPersistLocalDocument_h__ + +#include "mozilla/NotNull.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIURI.h" +#include "nsIWebBrowserPersistDocument.h" + +class nsIDocumentEncoder; +class nsISHEntry; + +namespace mozilla { + +namespace dom { +class Document; +} + +class WebBrowserPersistLocalDocument final + : public nsIWebBrowserPersistDocument { + public: + explicit WebBrowserPersistLocalDocument(dom::Document* aDocument); + + NotNull<const Encoding*> GetCharacterSet() const; + uint32_t GetPersistFlags() const; + nsIURI* GetBaseURI() const; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIWEBBROWSERPERSISTDOCUMENT + + NS_DECL_CYCLE_COLLECTION_CLASS(WebBrowserPersistLocalDocument) + + private: + RefPtr<dom::Document> mDocument; + uint32_t mPersistFlags; + + void DecideContentType(nsACString& aContentType); + nsresult GetDocEncoder(const nsACString& aContentType, uint32_t aEncoderFlags, + nsIDocumentEncoder** aEncoder); + + virtual ~WebBrowserPersistLocalDocument(); +}; + +} // namespace mozilla + +#endif // WebBrowserPersistLocalDocument_h__ diff --git a/dom/webbrowserpersist/WebBrowserPersistRemoteDocument.cpp b/dom/webbrowserpersist/WebBrowserPersistRemoteDocument.cpp new file mode 100644 index 0000000000..a4e7c96c85 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistRemoteDocument.cpp @@ -0,0 +1,207 @@ +/* -*- 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 "WebBrowserPersistRemoteDocument.h" +#include "WebBrowserPersistDocumentParent.h" +#include "WebBrowserPersistResourcesParent.h" +#include "WebBrowserPersistSerializeParent.h" +#include "mozilla/Unused.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/net/CookieJarSettings.h" + +#include "nsDebug.h" +#include "nsIPrincipal.h" +#include "nsISHEntry.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(WebBrowserPersistRemoteDocument, nsIWebBrowserPersistDocument) + +WebBrowserPersistRemoteDocument ::WebBrowserPersistRemoteDocument( + WebBrowserPersistDocumentParent* aActor, const Attrs& aAttrs, + nsIInputStream* aPostData) + : mActor(aActor), mAttrs(aAttrs), mPostData(aPostData) { + auto principalOrErr = ipc::PrincipalInfoToPrincipal(mAttrs.principal()); + if (principalOrErr.isOk()) { + mPrincipal = principalOrErr.unwrap(); + } else { + NS_WARNING("Failed to obtain principal!"); + } + + net::CookieJarSettings::Deserialize(mAttrs.cookieJarSettings(), + getter_AddRefs(mCookieJarSettings)); +} + +WebBrowserPersistRemoteDocument::~WebBrowserPersistRemoteDocument() { + if (mActor) { + Unused << WebBrowserPersistDocumentParent::Send__delete__(mActor); + // That will call mActor->ActorDestroy, which calls this->ActorDestroy + // (whether or not the IPC send succeeds). + } + MOZ_ASSERT(!mActor); +} + +void WebBrowserPersistRemoteDocument::ActorDestroy(void) { mActor = nullptr; } + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetIsClosed(bool* aIsClosed) { + *aIsClosed = !mActor; + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetIsPrivate(bool* aIsPrivate) { + *aIsPrivate = mAttrs.isPrivate(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetDocumentURI(nsACString& aURISpec) { + aURISpec = mAttrs.documentURI(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetBaseURI(nsACString& aURISpec) { + aURISpec = mAttrs.baseURI(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetContentType(nsACString& aContentType) { + aContentType = mAttrs.contentType(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetCharacterSet(nsACString& aCharSet) { + aCharSet = mAttrs.characterSet(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetTitle(nsAString& aTitle) { + aTitle = mAttrs.title(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetReferrerInfo( + nsIReferrerInfo** aReferrerInfo) { + *aReferrerInfo = mAttrs.referrerInfo(); + NS_IF_ADDREF(*aReferrerInfo); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetCookieJarSettings( + nsICookieJarSettings** aCookieJarSettings) { + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = mCookieJarSettings; + cookieJarSettings.forget(aCookieJarSettings); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetContentDisposition(nsAString& aDisp) { + aDisp = mAttrs.contentDisposition(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetCacheKey(uint32_t* aCacheKey) { + *aCacheKey = mAttrs.sessionHistoryCacheKey(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetPersistFlags(uint32_t* aFlags) { + *aFlags = mAttrs.persistFlags(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::SetPersistFlags(uint32_t aFlags) { + if (!mActor) { + return NS_ERROR_FAILURE; + } + if (!mActor->SendSetPersistFlags(aFlags)) { + return NS_ERROR_FAILURE; + } + mAttrs.persistFlags() = aFlags; + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetPostData(nsIInputStream** aStream) { + nsCOMPtr<nsIInputStream> stream = mPostData; + stream.forget(aStream); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::GetPrincipal(nsIPrincipal** aPrincipal) { + nsCOMPtr<nsIPrincipal> nodePrincipal = mPrincipal; + nodePrincipal.forget(aPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::ReadResources( + nsIWebBrowserPersistResourceVisitor* aVisitor) { + if (!mActor) { + return NS_ERROR_FAILURE; + } + RefPtr<WebBrowserPersistResourcesParent> subActor = + new WebBrowserPersistResourcesParent(this, aVisitor); + return mActor->SendPWebBrowserPersistResourcesConstructor( + subActor.forget().take()) + ? NS_OK + : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +WebBrowserPersistRemoteDocument::WriteContent( + nsIOutputStream* aStream, nsIWebBrowserPersistURIMap* aMap, + const nsACString& aRequestedContentType, uint32_t aEncoderFlags, + uint32_t aWrapColumn, nsIWebBrowserPersistWriteCompletion* aCompletion) { + if (!mActor) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + WebBrowserPersistURIMap map; + uint32_t numMappedURIs; + if (aMap) { + rv = aMap->GetTargetBaseURI(map.targetBaseURI()); + NS_ENSURE_SUCCESS(rv, rv); + rv = aMap->GetNumMappedURIs(&numMappedURIs); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < numMappedURIs; ++i) { + WebBrowserPersistURIMapEntry& nextEntry = + *(map.mapURIs().AppendElement()); + rv = aMap->GetURIMapping(i, nextEntry.mapFrom(), nextEntry.mapTo()); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + auto* subActor = + new WebBrowserPersistSerializeParent(this, aStream, aCompletion); + nsCString requestedContentType(aRequestedContentType); // Sigh. + return mActor->SendPWebBrowserPersistSerializeConstructor( + subActor, map, requestedContentType, aEncoderFlags, aWrapColumn) + ? NS_OK + : NS_ERROR_FAILURE; +} + +// Forcing WebBrowserPersistRemoteDocument to implement GetHistory is the +// easiest way to ensure that we can call GetHistory in +// WebBrowserPersistDocumentChild::Start +already_AddRefed<nsISHEntry> WebBrowserPersistRemoteDocument::GetHistory() { + MOZ_CRASH("We should not call GetHistory on WebBrowserPersistRemoteDocument"); + return nullptr; +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistRemoteDocument.h b/dom/webbrowserpersist/WebBrowserPersistRemoteDocument.h new file mode 100644 index 0000000000..e33976b3d2 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistRemoteDocument.h @@ -0,0 +1,59 @@ +/* -*- 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 WebBrowserPersistRemoteDocument_h__ +#define WebBrowserPersistRemoteDocument_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/PWebBrowserPersistDocumentParent.h" +#include "nsCOMPtr.h" +#include "nsIWebBrowserPersistDocument.h" +#include "nsIInputStream.h" + +class nsIPrincipal; + +// This class is the XPCOM half of the glue between the +// nsIWebBrowserPersistDocument interface and a remote document; it is +// created by WebBrowserPersistDocumentParent when (and if) it +// receives the information needed to populate the interface's +// properties. +// +// This object has a normal refcounted lifetime. The corresponding +// IPC actor holds a weak reference to this class; when the last +// strong reference is released, it sends an IPC delete message and +// thereby removes that reference. + +namespace mozilla { + +class WebBrowserPersistDocumentParent; + +class WebBrowserPersistRemoteDocument final + : public nsIWebBrowserPersistDocument { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBBROWSERPERSISTDOCUMENT + + private: + using Attrs = WebBrowserPersistDocumentAttrs; + WebBrowserPersistDocumentParent* mActor; + Attrs mAttrs; + nsCOMPtr<nsISHEntry> mSHEntry; + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + nsCOMPtr<nsIInputStream> mPostData; + nsCOMPtr<nsIPrincipal> mPrincipal; + + friend class WebBrowserPersistDocumentParent; + WebBrowserPersistRemoteDocument(WebBrowserPersistDocumentParent* aActor, + const Attrs& aAttrs, + nsIInputStream* aPostData); + ~WebBrowserPersistRemoteDocument(); + + void ActorDestroy(void); +}; + +} // namespace mozilla + +#endif // WebBrowserPersistRemoteDocument_h__ diff --git a/dom/webbrowserpersist/WebBrowserPersistResourcesChild.cpp b/dom/webbrowserpersist/WebBrowserPersistResourcesChild.cpp new file mode 100644 index 0000000000..b77b8fe25a --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistResourcesChild.cpp @@ -0,0 +1,76 @@ +/* -*- 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 "WebBrowserPersistResourcesChild.h" + +#include "WebBrowserPersistDocumentChild.h" +#include "mozilla/dom/PContentChild.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(WebBrowserPersistResourcesChild, + nsIWebBrowserPersistResourceVisitor) + +WebBrowserPersistResourcesChild::WebBrowserPersistResourcesChild() = default; + +WebBrowserPersistResourcesChild::~WebBrowserPersistResourcesChild() = default; + +NS_IMETHODIMP +WebBrowserPersistResourcesChild::VisitResource( + nsIWebBrowserPersistDocument* aDocument, const nsACString& aURI, + nsContentPolicyType aContentPolicyType) { + nsCString copiedURI(aURI); // Yay, XPIDL/IPDL mismatch. + SendVisitResource(copiedURI, aContentPolicyType); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistResourcesChild::VisitDocument( + nsIWebBrowserPersistDocument* aDocument, + nsIWebBrowserPersistDocument* aSubDocument) { + auto* subActor = new WebBrowserPersistDocumentChild(); + // As a consequence of how PWebBrowserPersistDocumentConstructor + // can be sent by both the parent and the child, we must pass the + // aBrowser and outerWindowID arguments here, but the values are + // ignored by the parent. In particular, the BrowserChild in which + // persistence started does not necessarily exist at this point; + // see bug 1203602. + if (!Manager()->Manager()->SendPWebBrowserPersistDocumentConstructor( + subActor, nullptr, nullptr)) { + // NOTE: subActor is freed at this point. + return NS_ERROR_FAILURE; + } + // ...but here, IPC won't free subActor until after this returns + // to the event loop. + + // The order of these two messages will be preserved, because + // they're the same toplevel protocol and priority. + // + // With this ordering, it's always the transition out of START + // state that causes a document's parent actor to be exposed to + // XPCOM (for both parent->child and child->parent construction), + // which simplifies the lifetime management. + SendVisitDocument(subActor); + subActor->Start(aSubDocument); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistResourcesChild::VisitBrowsingContext( + nsIWebBrowserPersistDocument* aDocument, + dom::BrowsingContext* aBrowsingContext) { + SendVisitBrowsingContext(aBrowsingContext); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistResourcesChild::EndVisit( + nsIWebBrowserPersistDocument* aDocument, nsresult aStatus) { + Send__delete__(this, aStatus); + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistResourcesChild.h b/dom/webbrowserpersist/WebBrowserPersistResourcesChild.h new file mode 100644 index 0000000000..ff0a11ee8f --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistResourcesChild.h @@ -0,0 +1,30 @@ +/* -*- 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 WebBrowserPersistResourcesChild_h__ +#define WebBrowserPersistResourcesChild_h__ + +#include "mozilla/PWebBrowserPersistResourcesChild.h" + +#include "nsIWebBrowserPersistDocument.h" + +namespace mozilla { + +class WebBrowserPersistResourcesChild final + : public PWebBrowserPersistResourcesChild, + public nsIWebBrowserPersistResourceVisitor { + public: + WebBrowserPersistResourcesChild(); + + NS_DECL_NSIWEBBROWSERPERSISTRESOURCEVISITOR + NS_DECL_ISUPPORTS + private: + virtual ~WebBrowserPersistResourcesChild(); +}; + +} // namespace mozilla + +#endif // WebBrowserPersistDocumentChild_h__ diff --git a/dom/webbrowserpersist/WebBrowserPersistResourcesParent.cpp b/dom/webbrowserpersist/WebBrowserPersistResourcesParent.cpp new file mode 100644 index 0000000000..2ebfb4b28e --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistResourcesParent.cpp @@ -0,0 +1,94 @@ +/* -*- 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 "WebBrowserPersistResourcesParent.h" + +#include "nsThreadUtils.h" + +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/WindowGlobalParent.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(WebBrowserPersistResourcesParent, + nsIWebBrowserPersistDocumentReceiver) + +WebBrowserPersistResourcesParent::WebBrowserPersistResourcesParent( + nsIWebBrowserPersistDocument* aDocument, + nsIWebBrowserPersistResourceVisitor* aVisitor) + : mDocument(aDocument), mVisitor(aVisitor) { + MOZ_ASSERT(aDocument); + MOZ_ASSERT(aVisitor); +} + +WebBrowserPersistResourcesParent::~WebBrowserPersistResourcesParent() = default; + +void WebBrowserPersistResourcesParent::ActorDestroy(ActorDestroyReason aWhy) { + if (aWhy != Deletion && mVisitor) { + // See comment in WebBrowserPersistDocumentParent::ActorDestroy + // (or bug 1202887) for why this is deferred. + nsCOMPtr<nsIRunnable> errorLater = + NewRunnableMethod<nsCOMPtr<nsIWebBrowserPersistDocument>, nsresult>( + "nsIWebBrowserPersistResourceVisitor::EndVisit", mVisitor, + &nsIWebBrowserPersistResourceVisitor::EndVisit, mDocument, + NS_ERROR_FAILURE); + NS_DispatchToCurrentThread(errorLater); + } + mVisitor = nullptr; +} + +mozilla::ipc::IPCResult WebBrowserPersistResourcesParent::Recv__delete__( + const nsresult& aStatus) { + mVisitor->EndVisit(mDocument, aStatus); + mVisitor = nullptr; + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebBrowserPersistResourcesParent::RecvVisitResource( + const nsACString& aURI, const nsContentPolicyType& aContentPolicyType) { + mVisitor->VisitResource(mDocument, aURI, aContentPolicyType); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebBrowserPersistResourcesParent::RecvVisitDocument( + PWebBrowserPersistDocumentParent* aSubDocument) { + // Don't expose the subdocument to the visitor until it's ready + // (until the actor isn't in START state). + static_cast<WebBrowserPersistDocumentParent*>(aSubDocument)->SetOnReady(this); + return IPC_OK(); +} + +mozilla::ipc::IPCResult +WebBrowserPersistResourcesParent::RecvVisitBrowsingContext( + const dom::MaybeDiscarded<dom::BrowsingContext>& aContext) { + if (aContext.IsNullOrDiscarded()) { + // Nothing useful to do but ignore the discarded context. + return IPC_OK(); + } + + mVisitor->VisitBrowsingContext(mDocument, aContext.get()); + return IPC_OK(); +} + +NS_IMETHODIMP +WebBrowserPersistResourcesParent::OnDocumentReady( + nsIWebBrowserPersistDocument* aSubDocument) { + if (!mVisitor) { + return NS_ERROR_FAILURE; + } + mVisitor->VisitDocument(mDocument, aSubDocument); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistResourcesParent::OnError(nsresult aFailure) { + // Nothing useful to do but ignore the failed document. + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistResourcesParent.h b/dom/webbrowserpersist/WebBrowserPersistResourcesParent.h new file mode 100644 index 0000000000..fce0a59d97 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistResourcesParent.h @@ -0,0 +1,57 @@ +/* -*- 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 WebBrowserPersistResourcesParent_h__ +#define WebBrowserPersistResourcesParent_h__ + +#include "mozilla/PWebBrowserPersistResourcesParent.h" + +#include "WebBrowserPersistDocumentParent.h" +#include "nsCOMPtr.h" +#include "nsIWebBrowserPersistDocument.h" + +namespace mozilla { + +class WebBrowserPersistResourcesParent final + : public PWebBrowserPersistResourcesParent, + public nsIWebBrowserPersistDocumentReceiver { + public: + WebBrowserPersistResourcesParent( + nsIWebBrowserPersistDocument* aDocument, + nsIWebBrowserPersistResourceVisitor* aVisitor); + + virtual mozilla::ipc::IPCResult RecvVisitResource( + const nsACString& aURI, + const nsContentPolicyType& aContentPolicyType) override; + + virtual mozilla::ipc::IPCResult RecvVisitDocument( + PWebBrowserPersistDocumentParent* aSubDocument) override; + + virtual mozilla::ipc::IPCResult RecvVisitBrowsingContext( + const dom::MaybeDiscarded<dom::BrowsingContext>& aContext) override; + + virtual mozilla::ipc::IPCResult Recv__delete__( + const nsresult& aStatus) override; + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + NS_DECL_NSIWEBBROWSERPERSISTDOCUMENTRECEIVER + NS_DECL_ISUPPORTS + + private: + // Note: even if the XPIDL didn't need mDocument for visitor + // callbacks, this object still needs to hold a strong reference + // to it to defer actor subtree deletion until after the + // visitation is finished. + nsCOMPtr<nsIWebBrowserPersistDocument> mDocument; + nsCOMPtr<nsIWebBrowserPersistResourceVisitor> mVisitor; + + virtual ~WebBrowserPersistResourcesParent(); +}; + +} // namespace mozilla + +#endif // WebBrowserPersistResourcesParent_h__ diff --git a/dom/webbrowserpersist/WebBrowserPersistSerializeChild.cpp b/dom/webbrowserpersist/WebBrowserPersistSerializeChild.cpp new file mode 100644 index 0000000000..cdfb2d8472 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistSerializeChild.cpp @@ -0,0 +1,130 @@ +/* -*- 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 "WebBrowserPersistSerializeChild.h" + +#include <algorithm> + +#include "nsThreadUtils.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(WebBrowserPersistSerializeChild, + nsIWebBrowserPersistWriteCompletion, + nsIWebBrowserPersistURIMap, nsIOutputStream) + +WebBrowserPersistSerializeChild::WebBrowserPersistSerializeChild( + const WebBrowserPersistURIMap& aMap) + : mMap(aMap) {} + +WebBrowserPersistSerializeChild::~WebBrowserPersistSerializeChild() = default; + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::OnFinish( + nsIWebBrowserPersistDocument* aDocument, nsIOutputStream* aStream, + const nsACString& aContentType, nsresult aStatus) { + MOZ_ASSERT(aStream == this); + nsCString contentType(aContentType); + Send__delete__(this, contentType, aStatus); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::GetNumMappedURIs(uint32_t* aNum) { + *aNum = static_cast<uint32_t>(mMap.mapURIs().Length()); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::GetURIMapping(uint32_t aIndex, + nsACString& aMapFrom, + nsACString& aMapTo) { + if (aIndex >= mMap.mapURIs().Length()) { + return NS_ERROR_INVALID_ARG; + } + aMapFrom = mMap.mapURIs()[aIndex].mapFrom(); + aMapTo = mMap.mapURIs()[aIndex].mapTo(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::GetTargetBaseURI(nsACString& aURI) { + aURI = mMap.targetBaseURI(); + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::Close() { + NS_WARNING("WebBrowserPersistSerializeChild::Close()"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::Flush() { + NS_WARNING("WebBrowserPersistSerializeChild::Flush()"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::Write(const char* aBuf, uint32_t aCount, + uint32_t* aWritten) { + // Normally an nsIOutputStream would have to be thread-safe, but + // nsDocumentEncoder currently doesn't call this off the main + // thread (which also means it's difficult to test the + // thread-safety code this class doesn't yet have). + // + // This is *not* an NS_ERROR_NOT_IMPLEMENTED, because at this + // point we've probably already misused the non-thread-safe + // refcounting. + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Fix this class to be thread-safe."); + + // Limit the message size to 64k because large messages are + // potentially bad for the latency of other messages on the same channel. + static const uint32_t kMaxWrite = 65536; + + // Work around bug 1181433 by sending multiple messages if + // necessary to write the entire aCount bytes, even though + // nsIOutputStream.idl says we're allowed to do a short write. + const char* buf = aBuf; + uint32_t count = aCount; + *aWritten = 0; + while (count > 0) { + uint32_t toWrite = std::min(kMaxWrite, count); + nsTArray<uint8_t> arrayBuf; + // It would be nice if this extra copy could be avoided. + arrayBuf.AppendElements(buf, toWrite); + SendWriteData(std::move(arrayBuf)); + *aWritten += toWrite; + buf += toWrite; + count -= toWrite; + } + return NS_OK; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::WriteFrom(nsIInputStream* aFrom, + uint32_t aCount, + uint32_t* aWritten) { + NS_WARNING("WebBrowserPersistSerializeChild::WriteFrom()"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::WriteSegments(nsReadSegmentFun aFun, + void* aCtx, uint32_t aCount, + uint32_t* aWritten) { + NS_WARNING("WebBrowserPersistSerializeChild::WriteSegments()"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +WebBrowserPersistSerializeChild::IsNonBlocking(bool* aNonBlocking) { + // Writes will never fail with NS_BASE_STREAM_WOULD_BLOCK, so: + *aNonBlocking = false; + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistSerializeChild.h b/dom/webbrowserpersist/WebBrowserPersistSerializeChild.h new file mode 100644 index 0000000000..86bcf0e1e7 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistSerializeChild.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef WebBrowserPersistSerializeChild_h__ +#define WebBrowserPersistSerializeChild_h__ + +#include "mozilla/PWebBrowserPersistSerializeChild.h" + +#include "mozilla/PWebBrowserPersistDocument.h" +#include "nsIWebBrowserPersistDocument.h" +#include "nsIOutputStream.h" + +namespace mozilla { + +class WebBrowserPersistSerializeChild final + : public PWebBrowserPersistSerializeChild, + public nsIWebBrowserPersistWriteCompletion, + public nsIWebBrowserPersistURIMap, + public nsIOutputStream { + public: + explicit WebBrowserPersistSerializeChild(const WebBrowserPersistURIMap& aMap); + + NS_DECL_NSIWEBBROWSERPERSISTWRITECOMPLETION + NS_DECL_NSIWEBBROWSERPERSISTURIMAP + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_ISUPPORTS + private: + WebBrowserPersistURIMap mMap; + + virtual ~WebBrowserPersistSerializeChild(); +}; + +} // namespace mozilla + +#endif // WebBrowserPersistSerializeChild_h__ diff --git a/dom/webbrowserpersist/WebBrowserPersistSerializeParent.cpp b/dom/webbrowserpersist/WebBrowserPersistSerializeParent.cpp new file mode 100644 index 0000000000..1fe1af95ff --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistSerializeParent.cpp @@ -0,0 +1,78 @@ +/* -*- 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 "WebBrowserPersistSerializeParent.h" + +#include "nsReadableUtils.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +WebBrowserPersistSerializeParent::WebBrowserPersistSerializeParent( + nsIWebBrowserPersistDocument* aDocument, nsIOutputStream* aStream, + nsIWebBrowserPersistWriteCompletion* aFinish) + : mDocument(aDocument), + mStream(aStream), + mFinish(aFinish), + mOutputError(NS_OK) { + MOZ_ASSERT(aDocument); + MOZ_ASSERT(aStream); + MOZ_ASSERT(aFinish); +} + +WebBrowserPersistSerializeParent::~WebBrowserPersistSerializeParent() = default; + +mozilla::ipc::IPCResult WebBrowserPersistSerializeParent::RecvWriteData( + nsTArray<uint8_t>&& aData) { + if (NS_FAILED(mOutputError)) { + return IPC_OK(); + } + + uint32_t written = 0; + static_assert(sizeof(char) == sizeof(uint8_t), + "char must be (at least?) 8 bits"); + const char* data = reinterpret_cast<const char*>(aData.Elements()); + // nsIOutputStream::Write is allowed to return short writes. + while (written < aData.Length()) { + uint32_t writeReturn; + nsresult rv = + mStream->Write(data + written, aData.Length() - written, &writeReturn); + if (NS_FAILED(rv)) { + mOutputError = rv; + return IPC_OK(); + } + written += writeReturn; + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebBrowserPersistSerializeParent::Recv__delete__( + const nsACString& aContentType, const nsresult& aStatus) { + if (NS_SUCCEEDED(mOutputError)) { + mOutputError = aStatus; + } + mFinish->OnFinish(mDocument, mStream, aContentType, mOutputError); + mFinish = nullptr; + return IPC_OK(); +} + +void WebBrowserPersistSerializeParent::ActorDestroy(ActorDestroyReason aWhy) { + if (mFinish) { + MOZ_ASSERT(aWhy != Deletion); + // See comment in WebBrowserPersistDocumentParent::ActorDestroy + // (or bug 1202887) for why this is deferred. + nsCOMPtr<nsIRunnable> errorLater = + NewRunnableMethod<nsCOMPtr<nsIWebBrowserPersistDocument>, + nsCOMPtr<nsIOutputStream>, nsCString, nsresult>( + "nsIWebBrowserPersistWriteCompletion::OnFinish", mFinish, + &nsIWebBrowserPersistWriteCompletion::OnFinish, mDocument, mStream, + ""_ns, NS_ERROR_FAILURE); + NS_DispatchToCurrentThread(errorLater); + mFinish = nullptr; + } +} + +} // namespace mozilla diff --git a/dom/webbrowserpersist/WebBrowserPersistSerializeParent.h b/dom/webbrowserpersist/WebBrowserPersistSerializeParent.h new file mode 100644 index 0000000000..56bd789c55 --- /dev/null +++ b/dom/webbrowserpersist/WebBrowserPersistSerializeParent.h @@ -0,0 +1,45 @@ +/* -*- 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 WebBrowserPersistSerializeParent_h__ +#define WebBrowserPersistSerializeParent_h__ + +#include "mozilla/PWebBrowserPersistSerializeParent.h" + +#include "nsCOMPtr.h" +#include "nsIOutputStream.h" +#include "nsIWebBrowserPersistDocument.h" + +namespace mozilla { + +class WebBrowserPersistSerializeParent + : public PWebBrowserPersistSerializeParent { + public: + WebBrowserPersistSerializeParent( + nsIWebBrowserPersistDocument* aDocument, nsIOutputStream* aStream, + nsIWebBrowserPersistWriteCompletion* aFinish); + virtual ~WebBrowserPersistSerializeParent(); + + virtual mozilla::ipc::IPCResult RecvWriteData( + nsTArray<uint8_t>&& aData) override; + + virtual mozilla::ipc::IPCResult Recv__delete__( + const nsACString& aContentType, const nsresult& aStatus) override; + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + // See also ...ReadParent::mDocument for the other reason this + // strong reference needs to be here. + nsCOMPtr<nsIWebBrowserPersistDocument> mDocument; + nsCOMPtr<nsIOutputStream> mStream; + nsCOMPtr<nsIWebBrowserPersistWriteCompletion> mFinish; + nsresult mOutputError; +}; + +} // namespace mozilla + +#endif // WebBrowserPersistSerializeParent_h__ diff --git a/dom/webbrowserpersist/moz.build b/dom/webbrowserpersist/moz.build new file mode 100644 index 0000000000..2c3e6a4d0f --- /dev/null +++ b/dom/webbrowserpersist/moz.build @@ -0,0 +1,49 @@ +# -*- 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/. + +# moved from embedding/components/webbrowserpersist Jan 13, 2017 +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +XPIDL_SOURCES += [ + "nsIWebBrowserPersist.idl", + "nsIWebBrowserPersistDocument.idl", +] + +XPIDL_MODULE = "webbrowserpersist" + +IPDL_SOURCES += [ + "PWebBrowserPersistDocument.ipdl", + "PWebBrowserPersistResources.ipdl", + "PWebBrowserPersistSerialize.ipdl", +] + +UNIFIED_SOURCES += [ + "nsWebBrowserPersist.cpp", + "WebBrowserPersistDocumentChild.cpp", + "WebBrowserPersistDocumentParent.cpp", + "WebBrowserPersistLocalDocument.cpp", + "WebBrowserPersistRemoteDocument.cpp", + "WebBrowserPersistResourcesChild.cpp", + "WebBrowserPersistResourcesParent.cpp", + "WebBrowserPersistSerializeChild.cpp", + "WebBrowserPersistSerializeParent.cpp", +] + +EXPORTS.mozilla += [ + "WebBrowserPersistDocumentChild.h", + "WebBrowserPersistDocumentParent.h", + "WebBrowserPersistLocalDocument.h", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/docshell/shistory", + "/dom/base", + "/dom/html", +] diff --git a/dom/webbrowserpersist/nsIWebBrowserPersist.idl b/dom/webbrowserpersist/nsIWebBrowserPersist.idl new file mode 100644 index 0000000000..0cfd021142 --- /dev/null +++ b/dom/webbrowserpersist/nsIWebBrowserPersist.idl @@ -0,0 +1,282 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsICancelable.idl" +#include "nsIContentPolicy.idl" + +interface nsIURI; +interface nsIInputStream; +interface nsIWebProgressListener; +interface nsIFile; +interface nsIChannel; +interface nsILoadContext; +interface nsIPrincipal; +interface nsIReferrerInfo; +interface nsICookieJarSettings; + +/** + * Interface for persisting DOM documents and URIs to local or remote storage. + */ +[scriptable, uuid(8cd752a4-60b1-42c3-a819-65c7a1138a28)] +interface nsIWebBrowserPersist : nsICancelable +{ + /** No special persistence behaviour. */ + const unsigned long PERSIST_FLAGS_NONE = 0; + /** Use cached data if present (skipping validation), else load from network */ + const unsigned long PERSIST_FLAGS_FROM_CACHE = 1; + /** Bypass the cached data. */ + const unsigned long PERSIST_FLAGS_BYPASS_CACHE = 2; + /** Ignore any redirected data (usually adverts). */ + const unsigned long PERSIST_FLAGS_IGNORE_REDIRECTED_DATA = 4; + /** Ignore IFRAME content (usually adverts). */ + const unsigned long PERSIST_FLAGS_IGNORE_IFRAMES = 8; + /** Do not run the incoming data through a content converter e.g. to decompress it */ + const unsigned long PERSIST_FLAGS_NO_CONVERSION = 16; + /** Replace existing files on the disk (use with due diligence!) */ + const unsigned long PERSIST_FLAGS_REPLACE_EXISTING_FILES = 32; + /** Don't modify or add base tags */ + const unsigned long PERSIST_FLAGS_NO_BASE_TAG_MODIFICATIONS = 64; + /** Make changes to original dom rather than cloning nodes */ + const unsigned long PERSIST_FLAGS_FIXUP_ORIGINAL_DOM = 128; + /** Fix links relative to destination location (not origin) */ + const unsigned long PERSIST_FLAGS_FIXUP_LINKS_TO_DESTINATION = 256; + /** Don't make any adjustments to links */ + const unsigned long PERSIST_FLAGS_DONT_FIXUP_LINKS = 512; + /** Force serialization of output (one file at a time; not concurrent) */ + const unsigned long PERSIST_FLAGS_SERIALIZE_OUTPUT = 1024; + /** Don't make any adjustments to filenames */ + const unsigned long PERSIST_FLAGS_DONT_CHANGE_FILENAMES = 2048; + /** Fail on broken inline links */ + const unsigned long PERSIST_FLAGS_FAIL_ON_BROKEN_LINKS = 4096; + /** + * Automatically cleanup after a failed or cancelled operation, deleting all + * created files and directories. This flag does nothing for failed upload + * operations to remote servers. + */ + const unsigned long PERSIST_FLAGS_CLEANUP_ON_FAILURE = 8192; + /** + * Let the WebBrowserPersist decide whether the incoming data is encoded + * and whether it needs to go through a content converter e.g. to + * decompress it. + */ + const unsigned long PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION = 16384; + /** + * Append the downloaded data to the target file. + * This can only be used when persisting to a local file. + */ + const unsigned long PERSIST_FLAGS_APPEND_TO_FILE = 32768; + + /** + * Flags governing how data is fetched and saved from the network. + * It is best to set this value explicitly unless you are prepared + * to accept the default values. + */ + attribute unsigned long persistFlags; + + /** Persister is ready to save data */ + const unsigned long PERSIST_STATE_READY = 1; + /** Persister is saving data */ + const unsigned long PERSIST_STATE_SAVING = 2; + /** Persister has finished saving data */ + const unsigned long PERSIST_STATE_FINISHED = 3; + + /** + * Current state of the persister object. + */ + readonly attribute unsigned long currentState; + + /** + * Value indicating the success or failure of the persist + * operation. + * + * @throws NS_BINDING_ABORTED Operation cancelled. + * @throws NS_ERROR_FAILURE Non-specific failure. + */ + readonly attribute nsresult result; + + /** + * Callback listener for progress notifications. The object that the + * embbedder supplies may also implement nsIInterfaceRequestor and be + * prepared to return nsIAuthPrompt or other interfaces that may be required + * to download data. + * + * @see nsIAuthPrompt + * @see nsIInterfaceRequestor + */ + attribute nsIWebProgressListener progressListener; + + /** + * Save the specified URI to file. + * + * @param aURI URI to save to file. Some implementations of this interface + * may also support <CODE>nullptr</CODE> to imply the currently + * loaded URI. + * @param aTriggeringPrincipal + * The triggering principal for the URI we're saving. + * @param aCacheKey The necko cache key integer. + * @param aReferrerInfo The referrer info for compute and send referrer via + * HTTP Referer header. + * @param aCookieJarSettings The cookieJarSettings for the HTTP channel which + * is saving the URI. + * @param aPostData Post data to pass with an HTTP request or + * <CODE>nullptr</CODE>. + * @param aExtraHeaders Additional headers to supply with an HTTP request + * or <CODE>nullptr</CODE>. + * @param aFile Target file. This may be a nsIFile object or an + * nsIURI object with a file scheme or a scheme that + * supports uploading (e.g. ftp). + * @param aContentPolicyType The type of content we're saving. + * @param aPrivacyContext A context from which the privacy status of this + * save operation can be determined. Must only be null + * in situations in which no such context is available + * (eg. the operation has no logical association with any + * window or document) + * + * @see nsIFile + * @see nsIURI + * @see nsIInputStream + * + * @throws NS_ERROR_INVALID_ARG One or more arguments was invalid. + */ + void saveURI(in nsIURI aURI, in nsIPrincipal aTriggeringPrincipal, + in unsigned long aCacheKey, + in nsIReferrerInfo aReferrerInfo, + in nsICookieJarSettings aCookieJarSettings, + in nsIInputStream aPostData, + in string aExtraHeaders, in nsISupports aFile, + in nsContentPolicyType aContentPolicyType, + in nsILoadContext aPrivacyContext); + + /** + * @param aIsPrivate Treat the save operation as private (ie. with + * regards to networking operations and persistence + * of intermediate data, etc.) + * @see saveURI for all other parameter descriptions + */ + void savePrivacyAwareURI(in nsIURI aURI, + in nsIPrincipal aTriggeringPrincipal, in unsigned long aCacheKey, + in nsIReferrerInfo aReferrerInfo, + in nsICookieJarSettings aCookieJarSettings, + in nsIInputStream aPostData, + in string aExtraHeaders, in nsISupports aFile, + in nsContentPolicyType aContentPolicyType, + in boolean aIsPrivate); + + /** + * Save a channel to a file. It must not be opened yet. + * @see saveURI + */ + void saveChannel(in nsIChannel aChannel, in nsISupports aFile); + + /** Output only the current selection as opposed to the whole document. */ + const unsigned long ENCODE_FLAGS_SELECTION_ONLY = 1; + /** + * For plaintext output. Convert html to plaintext that looks like the html. + * Implies wrap (except inside <pre>), since html wraps. + * HTML output: always do prettyprinting, ignoring existing formatting. + */ + const unsigned long ENCODE_FLAGS_FORMATTED = 2; + /** + * Output without formatting or wrapping the content. This flag + * may be used to preserve the original formatting as much as possible. + */ + const unsigned long ENCODE_FLAGS_RAW = 4; + /** Output only the body section, no HTML tags. */ + const unsigned long ENCODE_FLAGS_BODY_ONLY = 8; + /** Wrap even if when not doing formatted output (e.g. for text fields). */ + const unsigned long ENCODE_FLAGS_PREFORMATTED = 16; + /** Wrap documents at the specified column. */ + const unsigned long ENCODE_FLAGS_WRAP = 32; + /** + * For plaintext output. Output for format flowed (RFC 2646). This is used + * when converting to text for mail sending. This differs just slightly + * but in an important way from normal formatted, and that is that + * lines are space stuffed. This can't (correctly) be done later. + */ + const unsigned long ENCODE_FLAGS_FORMAT_FLOWED = 64; + /** Convert links to absolute links where possible. */ + const unsigned long ENCODE_FLAGS_ABSOLUTE_LINKS = 128; + + /** + * Output with carriage return line breaks. May also be combined with + * ENCODE_FLAGS_LF_LINEBREAKS and if neither is specified, the platform + * default format is used. + */ + const unsigned long ENCODE_FLAGS_CR_LINEBREAKS = 512; + /** + * Output with linefeed line breaks. May also be combined with + * ENCODE_FLAGS_CR_LINEBREAKS and if neither is specified, the platform + * default format is used. + */ + const unsigned long ENCODE_FLAGS_LF_LINEBREAKS = 1024; + /** For plaintext output. Output the content of noscript elements. */ + const unsigned long ENCODE_FLAGS_NOSCRIPT_CONTENT = 2048; + /** For plaintext output. Output the content of noframes elements. */ + const unsigned long ENCODE_FLAGS_NOFRAMES_CONTENT = 4096; + + /** + * Encode basic entities, e.g. output instead of character code 0xa0. + * The basic set is just & < > " for interoperability + * with older products that don't support α and friends. + */ + const unsigned long ENCODE_FLAGS_ENCODE_BASIC_ENTITIES = 8192; + + /** + * Save the specified DOM document to file and optionally all linked files + * (e.g. images, CSS, JS & subframes). Do not call this method until the + * document has finished loading! + * + * @param aDocument Document to save to file. Some implementations of + * this interface may also support <CODE>nullptr</CODE> + * to imply the currently loaded document. Can be an + * nsIWebBrowserPersistDocument or Document. + * @param aFile Target local file. This may be a nsIFile object or an + * nsIURI object with a file scheme or a scheme that + * supports uploading (e.g. ftp). + * @param aDataPath Path to directory where URIs linked to the document + * are saved or nullptr if no linked URIs should be saved. + * This may be a nsIFile object or an nsIURI object + * with a file scheme. + * @param aOutputContentType The desired MIME type format to save the + * document and all subdocuments into or nullptr to use + * the default behaviour. + * @param aEncodingFlags Flags to pass to the encoder. + * @param aWrapColumn For text documents, indicates the desired width to + * wrap text at. Parameter is ignored if wrapping is not + * specified by the encoding flags. + * + * @see nsIWebBrowserPersistDocument + * @see WebBrowserPersistable + * @see nsIFile + * @see nsIURI + * + * @throws NS_ERROR_INVALID_ARG One or more arguments was invalid. + */ + void saveDocument(in nsISupports aDocument, + in nsISupports aFile, in nsISupports aDataPath, + in string aOutputContentType, in unsigned long aEncodingFlags, + in unsigned long aWrapColumn); + + /** + * Cancels the current operation. The caller is responsible for cleaning up + * partially written files or directories. This has the same effect as calling + * cancel with an argument of NS_BINDING_ABORTED. + */ + void cancelSave(); +}; + +/** + * We don't export nsWebBrowserPersist.h as a public header, so we need a place + * to put the CID/ContractID. All places uses the WebBrowserPersist include + * nsIWebBrowserPersist.h, so we define our contract IDs here for now. + */ +%{ C++ +// {7E677795-C582-4cd1-9E8D-8271B3474D2A} +#define NS_WEBBROWSERPERSIST_CID \ + { 0x7e677795, 0xc582, 0x4cd1, { 0x9e, 0x8d, 0x82, 0x71, 0xb3, 0x47, 0x4d, 0x2a } } +#define NS_WEBBROWSERPERSIST_CONTRACTID \ + "@mozilla.org/embedding/browser/nsWebBrowserPersist;1" +%} diff --git a/dom/webbrowserpersist/nsIWebBrowserPersistDocument.idl b/dom/webbrowserpersist/nsIWebBrowserPersistDocument.idl new file mode 100644 index 0000000000..9f1f77fad5 --- /dev/null +++ b/dom/webbrowserpersist/nsIWebBrowserPersistDocument.idl @@ -0,0 +1,227 @@ +/* -*- Mode: IDL; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIContentPolicy.idl" + +interface nsIInputStream; +interface nsIOutputStream; +interface nsIPrincipal; +interface nsIRemoteTab; +interface nsIWebBrowserPersistResourceVisitor; +interface nsIWebBrowserPersistWriteCompletion; +interface nsIReferrerInfo; +interface nsISHEntry; +interface nsICookieJarSettings; + +webidl BrowsingContext; + +native SHEntryRef(already_AddRefed<nsISHEntry>); + +/** + * Interface for the URI-mapping information that can be supplied when + * serializing the DOM of an nsIWebBrowserPersistDocument. + * + * @see nsIWebBrowserPersistDocument + */ +[scriptable, uuid(d52e8b93-2771-45e8-a5b0-6e12b667046b)] +interface nsIWebBrowserPersistURIMap : nsISupports +{ + /** + * The number of URI mappings. + */ + readonly attribute unsigned long numMappedURIs; + + /** + * Obtain the URI mapping at the given index, which must be less than + * numMappedURIs, as a pair of URI spec strings. + */ + void getURIMapping(in unsigned long aIndex, + out AUTF8String aMapFrom, + out AUTF8String aMapTo); + + /** + * The spec of the base URI that the document will have after it is + * serialized. + */ + readonly attribute AUTF8String targetBaseURI; +}; + +/** + * Interface representing a document that can be serialized with + * nsIWebBrowserPersist; it may or may not be in this process. Some + * information is exposed as attributes, which may or may not reflect + * changes made to the underlying document; most of these are + * self-explanatory from their names and types. + */ +[scriptable, builtinclass, uuid(74aa4918-5d15-46b6-9ccf-74f9696d721d)] +interface nsIWebBrowserPersistDocument : nsISupports +{ + readonly attribute boolean isClosed; + readonly attribute boolean isPrivate; + readonly attribute AUTF8String documentURI; + readonly attribute AUTF8String baseURI; + readonly attribute ACString contentType; + readonly attribute ACString characterSet; + readonly attribute AString title; + readonly attribute nsIReferrerInfo referrerInfo; + readonly attribute nsICookieJarSettings cookieJarSettings; + readonly attribute AString contentDisposition; + readonly attribute nsIInputStream postData; + readonly attribute nsIPrincipal principal; + + /** + * The cache key. Unlike in nsISHEntry, where it's wrapped in an + * nsISupportsPRUint32, this is just the integer. + */ + [infallible] + readonly attribute unsigned long cacheKey; + + /** + * This attribute is set by nsIWebBrowserPersist implementations to + * propagate persist flags that apply to the DOM traversal and + * serialization (rather than to managing file I/O). + */ + attribute unsigned long persistFlags; + + /** + * Walk the DOM searching for external resources needed to render it. + * The visitor callbacks may be called either before or after + * readResources returns. + * + * @see nsIWebBrowserPersistResourceVisitor + */ + void readResources(in nsIWebBrowserPersistResourceVisitor aVisitor); + + /** + * Serialize the document's DOM. + * + * @param aStream The output stream to write the document to. + * + * @param aURIMap Optional; specifies URI rewriting to perform on + * external references (as read by readResources). + * If given, also causes relative hyperlinks to be + * converted to absolute in the written text. + * + * @param aRequestedContentType + * The desired MIME type to save the document as; + * optional and defaults to the document's type. + * (If no encoder exists for that type, "text/html" + * is used instead.) + * + * @param aEncoderFlags Flags to pass to the encoder. + * + * @param aWrapColumn Desired text width, ignored if wrapping is not + * specified by the encoding flags, or if 0. + * + * @param aCompletion Callback invoked when writing is complete. + * It may be called either before or after writeContent + * returns. + * + * @see nsIDocumentEncoder + */ + void writeContent(in nsIOutputStream aStream, + in nsIWebBrowserPersistURIMap aURIMap, + in ACString aRequestedContentType, + in unsigned long aEncoderFlags, + in unsigned long aWrapColumn, + in nsIWebBrowserPersistWriteCompletion aCompletion); + + [notxpcom, nostdcall] SHEntryRef GetHistory(); +}; + +/** + * Asynchronous visitor that receives external resources linked by an + * nsIWebBrowserPersistDocument and which are needed to render the + * document. + */ +[scriptable, uuid(8ce37706-b7d3-481a-be68-54f174fc0d0a)] +interface nsIWebBrowserPersistResourceVisitor : nsISupports +{ + /** + * Indicates a resource that is not a document; e.g., an image, script, + * or stylesheet. + * + * @param aDocument The document containing the reference. + * @param aURI The absolute URI spec for the referenced resource. + * @param aContentPolicyType The type of resource. + */ + void visitResource(in nsIWebBrowserPersistDocument aDocument, + in AUTF8String aURI, + in nsContentPolicyType aContentPolicyType); + + /** + * Indicates a subdocument resource; e.g., a frame or iframe. + * + * @param aDocument The document containing the reference. + * @param aSubDocument The referenced document. + */ + void visitDocument(in nsIWebBrowserPersistDocument aDocument, + in nsIWebBrowserPersistDocument aSubDocument); + + /** + * Indicates a cross origin subdocument resource; e.g., a frame + * or iframe loaded in another process. + * + * @param aDocument The document containing the reference. + * @param aContext The referenced document's browsing context. + */ + void visitBrowsingContext(in nsIWebBrowserPersistDocument aDocument, + in BrowsingContext aContext); + + /** + * Indicates that the document traversal is complete. + * + * @param aDocument The document that was being traversed. + * @param aStatus Indicates whether the traversal encountered an error. + */ + void endVisit(in nsIWebBrowserPersistDocument aDocument, + in nsresult aStatus); +}; + +/** + * Asynchronous callback for when nsIWebBrowserPersistDocument is finished + * serializing the document's DOM. + */ +[scriptable, function, uuid(a07e6892-38ae-4207-8340-7fa6ec446ed6)] +interface nsIWebBrowserPersistWriteCompletion : nsISupports +{ + /** + * Indicates that serialization is finished. + * + * @param aDocument The document that was being serialized. + * + * @param aStream The stream that was being written to. If it + * needs to be closed, the callback must do that; + * the serialization process leaves it open. + * + * @param aContentType The content type with which the document was + * actually serialized; this may be useful to set + * metadata on the result, or if uploading it. + * + * @param aStatus Indicates whether serialization encountered an error. + */ + void onFinish(in nsIWebBrowserPersistDocument aDocument, + in nsIOutputStream aStream, + in ACString aContentType, + in nsresult aStatus); +}; + +/** + * Asynchronous callback for creating a persistable document from some + * other object. + * + * XXXbz This should really be changed to just return a promise that + * then gets resolved or rejected... + * + * @see WebBrowserPersistable in FrameLoader.webidl. + */ +[scriptable, uuid(321e3174-594f-4036-b7be-791b821bd376)] +interface nsIWebBrowserPersistDocumentReceiver : nsISupports +{ + void onDocumentReady(in nsIWebBrowserPersistDocument aDocument); + void onError(in nsresult aFailure); +}; diff --git a/dom/webbrowserpersist/nsWebBrowserPersist.cpp b/dom/webbrowserpersist/nsWebBrowserPersist.cpp new file mode 100644 index 0000000000..eac9681f2c --- /dev/null +++ b/dom/webbrowserpersist/nsWebBrowserPersist.cpp @@ -0,0 +1,2728 @@ +/* -*- 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 "mozilla/ArrayUtils.h" +#include "mozilla/TextUtils.h" + +#include "nspr.h" + +#include "nsIFileStreams.h" // New Necko file streams +#include <algorithm> + +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIClassOfService.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsILoadContext.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsComponentManagerUtils.h" +#include "nsIStorageStream.h" +#include "nsISeekableStream.h" +#include "nsIHttpChannel.h" +#include "nsIEncodedChannel.h" +#include "nsIUploadChannel.h" +#include "nsICacheInfoChannel.h" +#include "nsIFileChannel.h" +#include "nsEscape.h" +#include "nsUnicharUtils.h" +#include "nsIStringEnumerator.h" +#include "nsContentCID.h" +#include "nsStreamUtils.h" + +#include "nsCExternalHandlerService.h" + +#include "nsIURL.h" +#include "nsIFileURL.h" +#include "nsIWebProgressListener.h" +#include "nsIAuthPrompt.h" +#include "nsIPrompt.h" +#include "nsIFormControl.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsContentUtils.h" + +#include "nsIStringBundle.h" +#include "nsIProtocolHandler.h" + +#include "nsWebBrowserPersist.h" +#include "WebBrowserPersistLocalDocument.h" + +#include "nsIContent.h" +#include "nsIMIMEInfo.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLSharedElement.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/Mutex.h" +#include "mozilla/Printf.h" +#include "ReferrerInfo.h" +#include "nsIURIMutator.h" +#include "mozilla/WebBrowserPersistDocumentParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/PContentParent.h" +#include "mozilla/dom/BrowserParent.h" +#include "nsIDocumentEncoder.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// Buffer file writes in 32kb chunks +#define BUFFERED_OUTPUT_SIZE (1024 * 32) + +struct nsWebBrowserPersist::WalkData { + nsCOMPtr<nsIWebBrowserPersistDocument> mDocument; + nsCOMPtr<nsIURI> mFile; + nsCOMPtr<nsIURI> mDataPath; +}; + +// Information about a DOM document +struct nsWebBrowserPersist::DocData { + nsCOMPtr<nsIURI> mBaseURI; + nsCOMPtr<nsIWebBrowserPersistDocument> mDocument; + nsCOMPtr<nsIURI> mFile; + nsCString mCharset; +}; + +// Information about a URI +struct nsWebBrowserPersist::URIData { + bool mNeedsPersisting; + bool mSaved; + bool mIsSubFrame; + bool mDataPathIsRelative; + bool mNeedsFixup; + nsString mFilename; + nsString mSubFrameExt; + nsCOMPtr<nsIURI> mFile; + nsCOMPtr<nsIURI> mDataPath; + nsCOMPtr<nsIURI> mRelativeDocumentURI; + nsCOMPtr<nsIPrincipal> mTriggeringPrincipal; + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + nsContentPolicyType mContentPolicyType; + nsCString mRelativePathToData; + nsCString mCharset; + + nsresult GetLocalURI(nsIURI* targetBaseURI, nsCString& aSpecOut); +}; + +// Information about the output stream +// Note that this data structure (and the map that nsWebBrowserPersist keeps, +// where these are values) is used from two threads: the main thread, +// and the background task thread. +// The background thread only writes to mStream (from OnDataAvailable), and +// this access is guarded using mStreamMutex. It reads the mFile member, which +// is only written to on the main thread when the object is constructed and +// from OnStartRequest (if mCalcFileExt), both guaranteed to happen before +// OnDataAvailable is fired. +// The main thread gets OnStartRequest, OnStopRequest, and progress sink events, +// and accesses the other members. +struct nsWebBrowserPersist::OutputData { + nsCOMPtr<nsIURI> mFile; + nsCOMPtr<nsIURI> mOriginalLocation; + nsCOMPtr<nsIOutputStream> mStream; + Mutex mStreamMutex MOZ_UNANNOTATED; + int64_t mSelfProgress; + int64_t mSelfProgressMax; + bool mCalcFileExt; + + OutputData(nsIURI* aFile, nsIURI* aOriginalLocation, bool aCalcFileExt) + : mFile(aFile), + mOriginalLocation(aOriginalLocation), + mStreamMutex("nsWebBrowserPersist::OutputData::mStreamMutex"), + mSelfProgress(0), + mSelfProgressMax(10000), + mCalcFileExt(aCalcFileExt) {} + ~OutputData() { + // Gaining this lock in the destructor is pretty icky. It should be OK + // because the only other place we lock the mutex is in OnDataAvailable, + // which will never itself cause the OutputData instance to be + // destroyed. + MutexAutoLock lock(mStreamMutex); + if (mStream) { + mStream->Close(); + } + } +}; + +struct nsWebBrowserPersist::UploadData { + nsCOMPtr<nsIURI> mFile; + int64_t mSelfProgress; + int64_t mSelfProgressMax; + + explicit UploadData(nsIURI* aFile) + : mFile(aFile), mSelfProgress(0), mSelfProgressMax(10000) {} +}; + +struct nsWebBrowserPersist::CleanupData { + nsCOMPtr<nsIFile> mFile; + // Snapshot of what the file actually is at the time of creation so that if + // it transmutes into something else later on it can be ignored. For example, + // catch files that turn into dirs or vice versa. + bool mIsDirectory; +}; + +class nsWebBrowserPersist::OnWalk final + : public nsIWebBrowserPersistResourceVisitor { + public: + OnWalk(nsWebBrowserPersist* aParent, nsIURI* aFile, nsIFile* aDataPath) + : mParent(aParent), + mFile(aFile), + mDataPath(aDataPath), + mPendingDocuments(1), + mStatus(NS_OK) {} + + NS_DECL_NSIWEBBROWSERPERSISTRESOURCEVISITOR + NS_DECL_ISUPPORTS + private: + RefPtr<nsWebBrowserPersist> mParent; + nsCOMPtr<nsIURI> mFile; + nsCOMPtr<nsIFile> mDataPath; + + uint32_t mPendingDocuments; + nsresult mStatus; + + virtual ~OnWalk() = default; +}; + +NS_IMPL_ISUPPORTS(nsWebBrowserPersist::OnWalk, + nsIWebBrowserPersistResourceVisitor) + +class nsWebBrowserPersist::OnRemoteWalk final + : public nsIWebBrowserPersistDocumentReceiver { + public: + OnRemoteWalk(nsIWebBrowserPersistResourceVisitor* aVisitor, + nsIWebBrowserPersistDocument* aDocument) + : mVisitor(aVisitor), mDocument(aDocument) {} + + NS_DECL_NSIWEBBROWSERPERSISTDOCUMENTRECEIVER + NS_DECL_ISUPPORTS + private: + nsCOMPtr<nsIWebBrowserPersistResourceVisitor> mVisitor; + nsCOMPtr<nsIWebBrowserPersistDocument> mDocument; + + virtual ~OnRemoteWalk() = default; +}; + +NS_IMPL_ISUPPORTS(nsWebBrowserPersist::OnRemoteWalk, + nsIWebBrowserPersistDocumentReceiver) + +class nsWebBrowserPersist::OnWrite final + : public nsIWebBrowserPersistWriteCompletion { + public: + OnWrite(nsWebBrowserPersist* aParent, nsIURI* aFile, nsIFile* aLocalFile) + : mParent(aParent), mFile(aFile), mLocalFile(aLocalFile) {} + + NS_DECL_NSIWEBBROWSERPERSISTWRITECOMPLETION + NS_DECL_ISUPPORTS + private: + RefPtr<nsWebBrowserPersist> mParent; + nsCOMPtr<nsIURI> mFile; + nsCOMPtr<nsIFile> mLocalFile; + + virtual ~OnWrite() = default; +}; + +NS_IMPL_ISUPPORTS(nsWebBrowserPersist::OnWrite, + nsIWebBrowserPersistWriteCompletion) + +class nsWebBrowserPersist::FlatURIMap final + : public nsIWebBrowserPersistURIMap { + public: + explicit FlatURIMap(const nsACString& aTargetBase) + : mTargetBase(aTargetBase) {} + + void Add(const nsACString& aMapFrom, const nsACString& aMapTo) { + mMapFrom.AppendElement(aMapFrom); + mMapTo.AppendElement(aMapTo); + } + + NS_DECL_NSIWEBBROWSERPERSISTURIMAP + NS_DECL_ISUPPORTS + + private: + nsTArray<nsCString> mMapFrom; + nsTArray<nsCString> mMapTo; + nsCString mTargetBase; + + virtual ~FlatURIMap() = default; +}; + +NS_IMPL_ISUPPORTS(nsWebBrowserPersist::FlatURIMap, nsIWebBrowserPersistURIMap) + +NS_IMETHODIMP +nsWebBrowserPersist::FlatURIMap::GetNumMappedURIs(uint32_t* aNum) { + MOZ_ASSERT(mMapFrom.Length() == mMapTo.Length()); + *aNum = mMapTo.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserPersist::FlatURIMap::GetTargetBaseURI(nsACString& aTargetBase) { + aTargetBase = mTargetBase; + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserPersist::FlatURIMap::GetURIMapping(uint32_t aIndex, + nsACString& aMapFrom, + nsACString& aMapTo) { + MOZ_ASSERT(mMapFrom.Length() == mMapTo.Length()); + if (aIndex >= mMapTo.Length()) { + return NS_ERROR_INVALID_ARG; + } + aMapFrom = mMapFrom[aIndex]; + aMapTo = mMapTo[aIndex]; + return NS_OK; +} + +// Maximum file length constant. The max file name length is +// volume / server dependent but it is difficult to obtain +// that information. Instead this constant is a reasonable value that +// modern systems should able to cope with. +const uint32_t kDefaultMaxFilenameLength = 64; + +// Default flags for persistence +const uint32_t kDefaultPersistFlags = + nsIWebBrowserPersist::PERSIST_FLAGS_NO_CONVERSION | + nsIWebBrowserPersist::PERSIST_FLAGS_REPLACE_EXISTING_FILES; + +// String bundle where error messages come from +const char* kWebBrowserPersistStringBundle = + "chrome://global/locale/nsWebBrowserPersist.properties"; + +nsWebBrowserPersist::nsWebBrowserPersist() + : mCurrentDataPathIsRelative(false), + mCurrentThingsToPersist(0), + mOutputMapMutex("nsWebBrowserPersist::mOutputMapMutex"), + mFirstAndOnlyUse(true), + mSavingDocument(false), + mCancel(false), + mEndCalled(false), + mCompleted(false), + mStartSaving(false), + mReplaceExisting(true), + mSerializingOutput(false), + mIsPrivate(false), + mPersistFlags(kDefaultPersistFlags), + mPersistResult(NS_OK), + mTotalCurrentProgress(0), + mTotalMaxProgress(0), + mWrapColumn(72), + mEncodingFlags(0) {} + +nsWebBrowserPersist::~nsWebBrowserPersist() { Cleanup(); } + +//***************************************************************************** +// nsWebBrowserPersist::nsISupports +//***************************************************************************** + +NS_IMPL_ADDREF(nsWebBrowserPersist) +NS_IMPL_RELEASE(nsWebBrowserPersist) + +NS_INTERFACE_MAP_BEGIN(nsWebBrowserPersist) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebBrowserPersist) + NS_INTERFACE_MAP_ENTRY(nsIWebBrowserPersist) + NS_INTERFACE_MAP_ENTRY(nsICancelable) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink) +NS_INTERFACE_MAP_END + +//***************************************************************************** +// nsWebBrowserPersist::nsIInterfaceRequestor +//***************************************************************************** + +NS_IMETHODIMP nsWebBrowserPersist::GetInterface(const nsIID& aIID, + void** aIFace) { + NS_ENSURE_ARG_POINTER(aIFace); + + *aIFace = nullptr; + + nsresult rv = QueryInterface(aIID, aIFace); + if (NS_SUCCEEDED(rv)) { + return rv; + } + + if (mProgressListener && (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) || + aIID.Equals(NS_GET_IID(nsIPrompt)))) { + mProgressListener->QueryInterface(aIID, aIFace); + if (*aIFace) return NS_OK; + } + + nsCOMPtr<nsIInterfaceRequestor> req = do_QueryInterface(mProgressListener); + if (req) { + return req->GetInterface(aIID, aIFace); + } + + return NS_ERROR_NO_INTERFACE; +} + +//***************************************************************************** +// nsWebBrowserPersist::nsIWebBrowserPersist +//***************************************************************************** + +NS_IMETHODIMP nsWebBrowserPersist::GetPersistFlags(uint32_t* aPersistFlags) { + NS_ENSURE_ARG_POINTER(aPersistFlags); + *aPersistFlags = mPersistFlags; + return NS_OK; +} +NS_IMETHODIMP nsWebBrowserPersist::SetPersistFlags(uint32_t aPersistFlags) { + mPersistFlags = aPersistFlags; + mReplaceExisting = (mPersistFlags & PERSIST_FLAGS_REPLACE_EXISTING_FILES); + mSerializingOutput = (mPersistFlags & PERSIST_FLAGS_SERIALIZE_OUTPUT); + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::GetCurrentState(uint32_t* aCurrentState) { + NS_ENSURE_ARG_POINTER(aCurrentState); + if (mCompleted) { + *aCurrentState = PERSIST_STATE_FINISHED; + } else if (mFirstAndOnlyUse) { + *aCurrentState = PERSIST_STATE_SAVING; + } else { + *aCurrentState = PERSIST_STATE_READY; + } + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::GetResult(nsresult* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = mPersistResult; + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::GetProgressListener( + nsIWebProgressListener** aProgressListener) { + NS_ENSURE_ARG_POINTER(aProgressListener); + *aProgressListener = mProgressListener; + NS_IF_ADDREF(*aProgressListener); + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::SetProgressListener( + nsIWebProgressListener* aProgressListener) { + mProgressListener = aProgressListener; + mProgressListener2 = do_QueryInterface(aProgressListener); + mEventSink = do_GetInterface(aProgressListener); + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::SaveURI( + nsIURI* aURI, nsIPrincipal* aPrincipal, uint32_t aCacheKey, + nsIReferrerInfo* aReferrerInfo, nsICookieJarSettings* aCookieJarSettings, + nsIInputStream* aPostData, const char* aExtraHeaders, nsISupports* aFile, + nsContentPolicyType aContentPolicyType, nsILoadContext* aPrivacyContext) { + bool isPrivate = aPrivacyContext && aPrivacyContext->UsePrivateBrowsing(); + return SavePrivacyAwareURI(aURI, aPrincipal, aCacheKey, aReferrerInfo, + aCookieJarSettings, aPostData, aExtraHeaders, + aFile, aContentPolicyType, isPrivate); +} + +NS_IMETHODIMP nsWebBrowserPersist::SavePrivacyAwareURI( + nsIURI* aURI, nsIPrincipal* aPrincipal, uint32_t aCacheKey, + nsIReferrerInfo* aReferrerInfo, nsICookieJarSettings* aCookieJarSettings, + nsIInputStream* aPostData, const char* aExtraHeaders, nsISupports* aFile, + nsContentPolicyType aContentPolicy, bool aIsPrivate) { + NS_ENSURE_TRUE(mFirstAndOnlyUse, NS_ERROR_FAILURE); + mFirstAndOnlyUse = false; // Stop people from reusing this object! + + nsCOMPtr<nsIURI> fileAsURI; + nsresult rv; + rv = GetValidURIFromObject(aFile, getter_AddRefs(fileAsURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_ARG); + + // SaveURI doesn't like broken uris. + mPersistFlags |= PERSIST_FLAGS_FAIL_ON_BROKEN_LINKS; + rv = SaveURIInternal(aURI, aPrincipal, aContentPolicy, aCacheKey, + aReferrerInfo, aCookieJarSettings, aPostData, + aExtraHeaders, fileAsURI, false, aIsPrivate); + return NS_FAILED(rv) ? rv : NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::SaveChannel(nsIChannel* aChannel, + nsISupports* aFile) { + NS_ENSURE_TRUE(mFirstAndOnlyUse, NS_ERROR_FAILURE); + mFirstAndOnlyUse = false; // Stop people from reusing this object! + + nsCOMPtr<nsIURI> fileAsURI; + nsresult rv; + rv = GetValidURIFromObject(aFile, getter_AddRefs(fileAsURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_ARG); + + rv = aChannel->GetURI(getter_AddRefs(mURI)); + NS_ENSURE_SUCCESS(rv, rv); + + // SaveURI doesn't like broken uris. + mPersistFlags |= PERSIST_FLAGS_FAIL_ON_BROKEN_LINKS; + rv = SaveChannelInternal(aChannel, fileAsURI, false); + return NS_FAILED(rv) ? rv : NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::SaveDocument(nsISupports* aDocument, + nsISupports* aFile, + nsISupports* aDataPath, + const char* aOutputContentType, + uint32_t aEncodingFlags, + uint32_t aWrapColumn) { + NS_ENSURE_TRUE(mFirstAndOnlyUse, NS_ERROR_FAILURE); + mFirstAndOnlyUse = false; // Stop people from reusing this object! + + // We need a STATE_IS_NETWORK start/stop pair to bracket the + // notification callbacks. For a whole document we generate those + // here and in EndDownload(), but for the single-request methods + // that's done in On{Start,Stop}Request instead. + mSavingDocument = true; + + NS_ENSURE_ARG_POINTER(aDocument); + NS_ENSURE_ARG_POINTER(aFile); + + nsCOMPtr<nsIURI> fileAsURI; + nsCOMPtr<nsIURI> datapathAsURI; + nsresult rv; + + rv = GetValidURIFromObject(aFile, getter_AddRefs(fileAsURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_ARG); + if (aDataPath) { + rv = GetValidURIFromObject(aDataPath, getter_AddRefs(datapathAsURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_ARG); + } + + mWrapColumn = aWrapColumn; + mEncodingFlags = aEncodingFlags; + + if (aOutputContentType) { + mContentType.AssignASCII(aOutputContentType); + } + + // State start notification + if (mProgressListener) { + mProgressListener->OnStateChange( + nullptr, nullptr, + nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); + } + + nsCOMPtr<nsIWebBrowserPersistDocument> doc = do_QueryInterface(aDocument); + if (!doc) { + nsCOMPtr<Document> localDoc = do_QueryInterface(aDocument); + if (localDoc) { + doc = new mozilla::WebBrowserPersistLocalDocument(localDoc); + } else { + rv = NS_ERROR_NO_INTERFACE; + } + } + + bool closed = false; + if (doc && NS_SUCCEEDED(doc->GetIsClosed(&closed)) && !closed) { + rv = SaveDocumentInternal(doc, fileAsURI, datapathAsURI); + } + + if (NS_FAILED(rv) || closed) { + SendErrorStatusChange(true, rv, nullptr, mURI); + EndDownload(rv); + } + return rv; +} + +NS_IMETHODIMP nsWebBrowserPersist::Cancel(nsresult aReason) { + // No point cancelling if we're already complete. + if (mEndCalled) { + return NS_OK; + } + mCancel = true; + EndDownload(aReason); + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::CancelSave() { + return Cancel(NS_BINDING_ABORTED); +} + +nsresult nsWebBrowserPersist::StartUpload(nsIStorageStream* storStream, + nsIURI* aDestinationURI, + const nsACString& aContentType) { + // setup the upload channel if the destination is not local + nsCOMPtr<nsIInputStream> inputstream; + nsresult rv = storStream->NewInputStream(0, getter_AddRefs(inputstream)); + NS_ENSURE_TRUE(inputstream, NS_ERROR_FAILURE); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + return StartUpload(inputstream, aDestinationURI, aContentType); +} + +nsresult nsWebBrowserPersist::StartUpload(nsIInputStream* aInputStream, + nsIURI* aDestinationURI, + const nsACString& aContentType) { + nsCOMPtr<nsIChannel> destChannel; + CreateChannelFromURI(aDestinationURI, getter_AddRefs(destChannel)); + nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(destChannel)); + NS_ENSURE_TRUE(uploadChannel, NS_ERROR_FAILURE); + + // Set the upload stream + // NOTE: ALL data must be available in "inputstream" + nsresult rv = uploadChannel->SetUploadStream(aInputStream, aContentType, -1); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + rv = destChannel->AsyncOpen(this); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // add this to the upload list + nsCOMPtr<nsISupports> keyPtr = do_QueryInterface(destChannel); + mUploadList.InsertOrUpdate(keyPtr, MakeUnique<UploadData>(aDestinationURI)); + + return NS_OK; +} + +void nsWebBrowserPersist::SerializeNextFile() { + nsresult rv = NS_OK; + MOZ_ASSERT(mWalkStack.Length() == 0); + + // First, handle gathered URIs. + // This is potentially O(n^2), when taking into account the + // number of times this method is called. If it becomes a + // bottleneck, the count of not-yet-persisted URIs could be + // maintained separately, and we can skip iterating mURIMap if there are none. + + // Persist each file in the uri map. The document(s) + // will be saved after the last one of these is saved. + for (const auto& entry : mURIMap) { + URIData* data = entry.GetWeak(); + + if (!data->mNeedsPersisting || data->mSaved) { + continue; + } + + // Create a URI from the key. + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), entry.GetKey(), data->mCharset.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + // Make a URI to save the data to. + nsCOMPtr<nsIURI> fileAsURI = data->mDataPath; + rv = AppendPathToURI(fileAsURI, data->mFilename, fileAsURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + rv = SaveURIInternal(uri, data->mTriggeringPrincipal, + data->mContentPolicyType, 0, nullptr, + data->mCookieJarSettings, nullptr, nullptr, fileAsURI, + true, mIsPrivate); + // If SaveURIInternal fails, then it will have called EndDownload, + // which means that |data| is no longer valid memory. We MUST bail. + if (NS_WARN_IF(NS_FAILED(rv))) { + break; + } + + if (rv == NS_OK) { + // URIData.mFile will be updated to point to the correct + // URI object when it is fixed up with the right file extension + // in OnStartRequest + data->mFile = fileAsURI; + data->mSaved = true; + } else { + data->mNeedsFixup = false; + } + + if (mSerializingOutput) { + break; + } + } + + // If there are downloads happening, wait until they're done; the + // OnStopRequest handler will call this method again. + if (mOutputMap.Count() > 0) { + return; + } + + // If serializing, also wait until last upload is done. + if (mSerializingOutput && mUploadList.Count() > 0) { + return; + } + + // If there are also no more documents, then we're done. + if (mDocList.Length() == 0) { + // ...or not quite done, if there are still uploads. + if (mUploadList.Count() > 0) { + return; + } + // Finish and clean things up. Defer this because the caller + // may have been expecting to use the listeners that that + // method will clear. + NS_DispatchToCurrentThread( + NewRunnableMethod("nsWebBrowserPersist::FinishDownload", this, + &nsWebBrowserPersist::FinishDownload)); + return; + } + + // There are no URIs to save, so just save the next document. + mStartSaving = true; + mozilla::UniquePtr<DocData> docData(mDocList.ElementAt(0)); + mDocList.RemoveElementAt(0); // O(n^2) but probably doesn't matter. + MOZ_ASSERT(docData); + if (!docData) { + EndDownload(NS_ERROR_FAILURE); + return; + } + + mCurrentBaseURI = docData->mBaseURI; + mCurrentCharset = docData->mCharset; + mTargetBaseURI = docData->mFile; + + // Save the document, fixing it up with the new URIs as we do + + nsAutoCString targetBaseSpec; + if (mTargetBaseURI) { + rv = mTargetBaseURI->GetSpec(targetBaseSpec); + if (NS_FAILED(rv)) { + SendErrorStatusChange(true, rv, nullptr, nullptr); + EndDownload(rv); + return; + } + } + + // mFlatURIMap must be rebuilt each time through SerializeNextFile, as + // mTargetBaseURI is used to create the relative URLs and will be different + // with each serialized document. + RefPtr<FlatURIMap> flatMap = new FlatURIMap(targetBaseSpec); + for (const auto& uriEntry : mURIMap) { + nsAutoCString mapTo; + nsresult rv = uriEntry.GetWeak()->GetLocalURI(mTargetBaseURI, mapTo); + if (NS_SUCCEEDED(rv) || !mapTo.IsVoid()) { + flatMap->Add(uriEntry.GetKey(), mapTo); + } + } + mFlatURIMap = std::move(flatMap); + + nsCOMPtr<nsIFile> localFile; + GetLocalFileFromURI(docData->mFile, getter_AddRefs(localFile)); + if (localFile) { + // if we're not replacing an existing file but the file + // exists, something is wrong + bool fileExists = false; + rv = localFile->Exists(&fileExists); + if (NS_SUCCEEDED(rv) && !mReplaceExisting && fileExists) { + rv = NS_ERROR_FILE_ALREADY_EXISTS; + } + if (NS_FAILED(rv)) { + SendErrorStatusChange(false, rv, nullptr, docData->mFile); + EndDownload(rv); + return; + } + } + nsCOMPtr<nsIOutputStream> outputStream; + rv = MakeOutputStream(docData->mFile, getter_AddRefs(outputStream)); + if (NS_SUCCEEDED(rv) && !outputStream) { + rv = NS_ERROR_FAILURE; + } + if (NS_FAILED(rv)) { + SendErrorStatusChange(false, rv, nullptr, docData->mFile); + EndDownload(rv); + return; + } + + RefPtr<OnWrite> finish = new OnWrite(this, docData->mFile, localFile); + rv = docData->mDocument->WriteContent(outputStream, mFlatURIMap, + NS_ConvertUTF16toUTF8(mContentType), + mEncodingFlags, mWrapColumn, finish); + if (NS_FAILED(rv)) { + SendErrorStatusChange(false, rv, nullptr, docData->mFile); + EndDownload(rv); + } +} + +NS_IMETHODIMP +nsWebBrowserPersist::OnWrite::OnFinish(nsIWebBrowserPersistDocument* aDoc, + nsIOutputStream* aStream, + const nsACString& aContentType, + nsresult aStatus) { + nsresult rv = aStatus; + + if (NS_FAILED(rv)) { + mParent->SendErrorStatusChange(false, rv, nullptr, mFile); + mParent->EndDownload(rv); + return NS_OK; + } + if (!mLocalFile) { + nsCOMPtr<nsIStorageStream> storStream(do_QueryInterface(aStream)); + if (storStream) { + aStream->Close(); + rv = mParent->StartUpload(storStream, mFile, aContentType); + if (NS_FAILED(rv)) { + mParent->SendErrorStatusChange(false, rv, nullptr, mFile); + mParent->EndDownload(rv); + } + // Either we failed and we're done, or we're uploading and + // the OnStopRequest callback is responsible for the next + // SerializeNextFile(). + return NS_OK; + } + } + NS_DispatchToCurrentThread( + NewRunnableMethod("nsWebBrowserPersist::SerializeNextFile", mParent, + &nsWebBrowserPersist::SerializeNextFile)); + return NS_OK; +} + +//***************************************************************************** +// nsWebBrowserPersist::nsIRequestObserver +//***************************************************************************** + +NS_IMETHODIMP nsWebBrowserPersist::OnStartRequest(nsIRequest* request) { + if (mProgressListener) { + uint32_t stateFlags = nsIWebProgressListener::STATE_START | + nsIWebProgressListener::STATE_IS_REQUEST; + if (!mSavingDocument) { + stateFlags |= nsIWebProgressListener::STATE_IS_NETWORK; + } + mProgressListener->OnStateChange(nullptr, request, stateFlags, NS_OK); + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + NS_ENSURE_TRUE(channel, NS_ERROR_FAILURE); + + nsCOMPtr<nsISupports> keyPtr = do_QueryInterface(request); + OutputData* data = mOutputMap.Get(keyPtr); + + // NOTE: This code uses the channel as a hash key so it will not + // recognize redirected channels because the key is not the same. + // When that happens we remove and add the data entry to use the + // new channel as the hash key. + if (!data) { + UploadData* upData = mUploadList.Get(keyPtr); + if (!upData) { + // Redirect? Try and fixup the output table + nsresult rv = FixRedirectedChannelEntry(channel); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // Should be able to find the data after fixup unless redirects + // are disabled. + data = mOutputMap.Get(keyPtr); + if (!data) { + return NS_ERROR_FAILURE; + } + } + } + + if (data && data->mFile) { + nsCOMPtr<nsIThreadRetargetableRequest> r = do_QueryInterface(request); + // Determine if we're uploading. Only use OMT onDataAvailable if not. + nsCOMPtr<nsIFile> localFile; + GetLocalFileFromURI(data->mFile, getter_AddRefs(localFile)); + if (r && localFile) { + if (!mBackgroundQueue) { + NS_CreateBackgroundTaskQueue("WebBrowserPersist", + getter_AddRefs(mBackgroundQueue)); + } + if (mBackgroundQueue) { + r->RetargetDeliveryTo(mBackgroundQueue); + } + } + + // If PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION is set in mPersistFlags, + // try to determine whether this channel needs to apply Content-Encoding + // conversions. + NS_ASSERTION( + !((mPersistFlags & PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION) && + (mPersistFlags & PERSIST_FLAGS_NO_CONVERSION)), + "Conflict in persist flags: both AUTODETECT and NO_CONVERSION set"); + if (mPersistFlags & PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION) + SetApplyConversionIfNeeded(channel); + + if (data->mCalcFileExt && + !(mPersistFlags & PERSIST_FLAGS_DONT_CHANGE_FILENAMES)) { + nsCOMPtr<nsIURI> uriWithExt; + // this is the first point at which the server can tell us the mimetype + nsresult rv = CalculateAndAppendFileExt( + data->mFile, channel, data->mOriginalLocation, uriWithExt); + if (NS_SUCCEEDED(rv)) { + data->mFile = uriWithExt; + } + + // now make filename conformant and unique + nsCOMPtr<nsIURI> uniqueFilenameURI; + rv = CalculateUniqueFilename(data->mFile, uniqueFilenameURI); + if (NS_SUCCEEDED(rv)) { + data->mFile = uniqueFilenameURI; + } + + // The URIData entry is pointing to the old unfixed URI, so we need + // to update it. + nsCOMPtr<nsIURI> chanURI; + rv = channel->GetOriginalURI(getter_AddRefs(chanURI)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString spec; + chanURI->GetSpec(spec); + URIData* uridata; + if (mURIMap.Get(spec, &uridata)) { + uridata->mFile = data->mFile; + } + } + } + + // compare uris and bail before we add to output map if they are equal + bool isEqual = false; + if (NS_SUCCEEDED(data->mFile->Equals(data->mOriginalLocation, &isEqual)) && + isEqual) { + { + MutexAutoLock lock(mOutputMapMutex); + // remove from output map + mOutputMap.Remove(keyPtr); + } + + // cancel; we don't need to know any more + // stop request will get called + request->Cancel(NS_BINDING_ABORTED); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::OnStopRequest(nsIRequest* request, + nsresult status) { + nsCOMPtr<nsISupports> keyPtr = do_QueryInterface(request); + OutputData* data = mOutputMap.Get(keyPtr); + if (data) { + if (NS_SUCCEEDED(mPersistResult) && NS_FAILED(status)) { + SendErrorStatusChange(true, status, request, data->mFile); + } + + // If there is a stream ref and we weren't canceled, + // close it away from the main thread. + // We don't do this when there's an error/cancelation, + // because our consumer may try to delete the file, which will error + // if we're still holding on to it, so we have to close it pronto. + { + MutexAutoLock lock(data->mStreamMutex); + if (data->mStream && NS_SUCCEEDED(status) && !mCancel) { + if (!mBackgroundQueue) { + nsresult rv = NS_CreateBackgroundTaskQueue( + "WebBrowserPersist", getter_AddRefs(mBackgroundQueue)); + if (NS_FAILED(rv)) { + return rv; + } + } + // Now steal the stream ref and close it away from the main thread, + // keeping the promise around so we don't finish before all files + // are flushed and closed. + mFileClosePromises.AppendElement(InvokeAsync( + mBackgroundQueue, __func__, [stream = std::move(data->mStream)]() { + nsresult rv = stream->Close(); + // We don't care if closing failed; we don't care in the + // destructor either... + return ClosePromise::CreateAndResolve(rv, __func__); + })); + } + } + MutexAutoLock lock(mOutputMapMutex); + mOutputMap.Remove(keyPtr); + } else { + // if we didn't find the data in mOutputMap, try mUploadList + UploadData* upData = mUploadList.Get(keyPtr); + if (upData) { + mUploadList.Remove(keyPtr); + } + } + + // Do more work. + SerializeNextFile(); + + if (mProgressListener) { + uint32_t stateFlags = nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_REQUEST; + if (!mSavingDocument) { + stateFlags |= nsIWebProgressListener::STATE_IS_NETWORK; + } + mProgressListener->OnStateChange(nullptr, request, stateFlags, status); + } + + return NS_OK; +} + +//***************************************************************************** +// nsWebBrowserPersist::nsIStreamListener +//***************************************************************************** + +// Note: this is supposed to (but not guaranteed to) fire on a background +// thread when used to save to local disk (channels not using local files will +// use the main thread). +// (Read) Access to mOutputMap is guarded via mOutputMapMutex. +// Access to individual OutputData::mStream is guarded via its mStreamMutex. +// mCancel is atomic, as is mPersistFlags (accessed via MakeOutputStream). +// If you end up touching this method and needing other member access, bear +// this in mind. +NS_IMETHODIMP +nsWebBrowserPersist::OnDataAvailable(nsIRequest* request, + nsIInputStream* aIStream, uint64_t aOffset, + uint32_t aLength) { + // MOZ_ASSERT(!NS_IsMainThread()); // no guarantees, but it's likely. + + bool cancel = mCancel; + if (!cancel) { + nsresult rv = NS_OK; + uint32_t bytesRemaining = aLength; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + NS_ENSURE_TRUE(channel, NS_ERROR_FAILURE); + + MutexAutoLock lock(mOutputMapMutex); + nsCOMPtr<nsISupports> keyPtr = do_QueryInterface(request); + OutputData* data = mOutputMap.Get(keyPtr); + if (!data) { + // might be uploadData; consume necko's buffer and bail... + uint32_t n; + return aIStream->ReadSegments(NS_DiscardSegment, nullptr, aLength, &n); + } + + bool readError = true; + + MutexAutoLock streamLock(data->mStreamMutex); + // Make the output stream + if (!data->mStream) { + rv = MakeOutputStream(data->mFile, getter_AddRefs(data->mStream)); + if (NS_FAILED(rv)) { + readError = false; + cancel = true; + } + } + + // Read data from the input and write to the output + char buffer[8192]; + uint32_t bytesRead; + while (!cancel && bytesRemaining) { + readError = true; + rv = aIStream->Read(buffer, + std::min(uint32_t(sizeof(buffer)), bytesRemaining), + &bytesRead); + if (NS_SUCCEEDED(rv)) { + readError = false; + // Write out the data until something goes wrong, or, it is + // all written. We loop because for some errors (e.g., disk + // full), we get NS_OK with some bytes written, then an error. + // So, we want to write again in that case to get the actual + // error code. + const char* bufPtr = buffer; // Where to write from. + while (NS_SUCCEEDED(rv) && bytesRead) { + uint32_t bytesWritten = 0; + rv = data->mStream->Write(bufPtr, bytesRead, &bytesWritten); + if (NS_SUCCEEDED(rv)) { + bytesRead -= bytesWritten; + bufPtr += bytesWritten; + bytesRemaining -= bytesWritten; + // Force an error if (for some reason) we get NS_OK but + // no bytes written. + if (!bytesWritten) { + rv = NS_ERROR_FAILURE; + cancel = true; + } + } else { + // Disaster - can't write out the bytes - disk full / permission? + cancel = true; + } + } + } else { + // Disaster - can't read the bytes - broken link / file error? + cancel = true; + } + } + + int64_t channelContentLength = -1; + if (!cancel && + NS_SUCCEEDED(channel->GetContentLength(&channelContentLength))) { + // if we get -1 at this point, we didn't get content-length header + // assume that we got all of the data and push what we have; + // that's the best we can do now + if ((-1 == channelContentLength) || + ((channelContentLength - (aOffset + aLength)) == 0)) { + NS_WARNING_ASSERTION( + channelContentLength != -1, + "nsWebBrowserPersist::OnDataAvailable() no content length " + "header, pushing what we have"); + // we're done with this pass; see if we need to do upload + nsAutoCString contentType; + channel->GetContentType(contentType); + // if we don't have the right type of output stream then it's a local + // file + nsCOMPtr<nsIStorageStream> storStream(do_QueryInterface(data->mStream)); + if (storStream) { + data->mStream->Close(); + data->mStream = + nullptr; // null out stream so we don't close it later + MOZ_ASSERT(NS_IsMainThread(), + "Uploads should be on the main thread."); + rv = StartUpload(storStream, data->mFile, contentType); + if (NS_FAILED(rv)) { + readError = false; + cancel = true; + } + } + } + } + + // Notify listener if an error occurred. + if (cancel) { + RefPtr<nsIRequest> req = readError ? request : nullptr; + nsCOMPtr<nsIURI> file = data->mFile; + RefPtr<Runnable> errorOnMainThread = NS_NewRunnableFunction( + "nsWebBrowserPersist::SendErrorStatusChange", + [self = RefPtr{this}, req, file, readError, rv]() { + self->SendErrorStatusChange(readError, rv, req, file); + }); + NS_DispatchToMainThread(errorOnMainThread); + + // And end the download on the main thread. + nsCOMPtr<nsIRunnable> endOnMainThread = NewRunnableMethod<nsresult>( + "nsWebBrowserPersist::EndDownload", this, + &nsWebBrowserPersist::EndDownload, NS_BINDING_ABORTED); + NS_DispatchToMainThread(endOnMainThread); + } + } + + return cancel ? NS_BINDING_ABORTED : NS_OK; +} + +//***************************************************************************** +// nsWebBrowserPersist::nsIThreadRetargetableStreamListener +//***************************************************************************** + +NS_IMETHODIMP nsWebBrowserPersist::CheckListenerChain() { return NS_OK; } + +//***************************************************************************** +// nsWebBrowserPersist::nsIProgressEventSink +//***************************************************************************** + +NS_IMETHODIMP nsWebBrowserPersist::OnProgress(nsIRequest* request, + int64_t aProgress, + int64_t aProgressMax) { + if (!mProgressListener) { + return NS_OK; + } + + // Store the progress of this request + nsCOMPtr<nsISupports> keyPtr = do_QueryInterface(request); + OutputData* data = mOutputMap.Get(keyPtr); + if (data) { + data->mSelfProgress = aProgress; + data->mSelfProgressMax = aProgressMax; + } else { + UploadData* upData = mUploadList.Get(keyPtr); + if (upData) { + upData->mSelfProgress = aProgress; + upData->mSelfProgressMax = aProgressMax; + } + } + + // Notify listener of total progress + CalcTotalProgress(); + if (mProgressListener2) { + mProgressListener2->OnProgressChange64(nullptr, request, aProgress, + aProgressMax, mTotalCurrentProgress, + mTotalMaxProgress); + } else { + // have to truncate 64-bit to 32bit + mProgressListener->OnProgressChange( + nullptr, request, uint64_t(aProgress), uint64_t(aProgressMax), + mTotalCurrentProgress, mTotalMaxProgress); + } + + // If our progress listener implements nsIProgressEventSink, + // forward the notification + if (mEventSink) { + mEventSink->OnProgress(request, aProgress, aProgressMax); + } + + return NS_OK; +} + +NS_IMETHODIMP nsWebBrowserPersist::OnStatus(nsIRequest* request, + nsresult status, + const char16_t* statusArg) { + if (mProgressListener) { + // We need to filter out non-error error codes. + // Is the only NS_SUCCEEDED value NS_OK? + switch (status) { + case NS_NET_STATUS_RESOLVING_HOST: + case NS_NET_STATUS_RESOLVED_HOST: + case NS_NET_STATUS_CONNECTING_TO: + case NS_NET_STATUS_CONNECTED_TO: + case NS_NET_STATUS_TLS_HANDSHAKE_STARTING: + case NS_NET_STATUS_TLS_HANDSHAKE_ENDED: + case NS_NET_STATUS_SENDING_TO: + case NS_NET_STATUS_RECEIVING_FROM: + case NS_NET_STATUS_WAITING_FOR: + case NS_NET_STATUS_READING: + case NS_NET_STATUS_WRITING: + break; + + default: + // Pass other notifications (for legitimate errors) along. + mProgressListener->OnStatusChange(nullptr, request, status, statusArg); + break; + } + } + + // If our progress listener implements nsIProgressEventSink, + // forward the notification + if (mEventSink) { + mEventSink->OnStatus(request, status, statusArg); + } + + return NS_OK; +} + +//***************************************************************************** +// nsWebBrowserPersist private methods +//***************************************************************************** + +// Convert error info into proper message text and send OnStatusChange +// notification to the web progress listener. +nsresult nsWebBrowserPersist::SendErrorStatusChange(bool aIsReadError, + nsresult aResult, + nsIRequest* aRequest, + nsIURI* aURI) { + NS_ENSURE_ARG_POINTER(aURI); + + if (!mProgressListener) { + // Do nothing + return NS_OK; + } + + // Get the file path or spec from the supplied URI + nsCOMPtr<nsIFile> file; + GetLocalFileFromURI(aURI, getter_AddRefs(file)); + AutoTArray<nsString, 1> strings; + nsresult rv; + if (file) { + file->GetPath(*strings.AppendElement()); + } else { + nsAutoCString fileurl; + rv = aURI->GetSpec(fileurl); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF8toUTF16(fileurl, *strings.AppendElement()); + } + + const char* msgId; + switch (aResult) { + case NS_ERROR_FILE_NAME_TOO_LONG: + // File name too long. + msgId = "fileNameTooLongError"; + break; + case NS_ERROR_FILE_ALREADY_EXISTS: + // File exists with same name as directory. + msgId = "fileAlreadyExistsError"; + break; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + // Out of space on target volume. + msgId = "diskFull"; + break; + + case NS_ERROR_FILE_READ_ONLY: + // Attempt to write to read/only file. + msgId = "readOnly"; + break; + + case NS_ERROR_FILE_ACCESS_DENIED: + // Attempt to write without sufficient permissions. + msgId = "accessError"; + break; + + default: + // Generic read/write error message. + if (aIsReadError) + msgId = "readError"; + else + msgId = "writeError"; + break; + } + // Get properties file bundle and extract status string. + nsCOMPtr<nsIStringBundleService> s = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && s, NS_ERROR_FAILURE); + + nsCOMPtr<nsIStringBundle> bundle; + rv = s->CreateBundle(kWebBrowserPersistStringBundle, getter_AddRefs(bundle)); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && bundle, NS_ERROR_FAILURE); + + nsAutoString msgText; + rv = bundle->FormatStringFromName(msgId, strings, msgText); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + mProgressListener->OnStatusChange(nullptr, aRequest, aResult, msgText.get()); + + return NS_OK; +} + +nsresult nsWebBrowserPersist::GetValidURIFromObject(nsISupports* aObject, + nsIURI** aURI) const { + NS_ENSURE_ARG_POINTER(aObject); + NS_ENSURE_ARG_POINTER(aURI); + + nsCOMPtr<nsIFile> objAsFile = do_QueryInterface(aObject); + if (objAsFile) { + return NS_NewFileURI(aURI, objAsFile); + } + nsCOMPtr<nsIURI> objAsURI = do_QueryInterface(aObject); + if (objAsURI) { + *aURI = objAsURI; + NS_ADDREF(*aURI); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +/* static */ +nsresult nsWebBrowserPersist::GetLocalFileFromURI(nsIURI* aURI, + nsIFile** aLocalFile) { + nsresult rv; + + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(aURI, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFile> file; + rv = fileURL->GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + file.forget(aLocalFile); + return NS_OK; +} + +/* static */ +nsresult nsWebBrowserPersist::AppendPathToURI(nsIURI* aURI, + const nsAString& aPath, + nsCOMPtr<nsIURI>& aOutURI) { + NS_ENSURE_ARG_POINTER(aURI); + + nsAutoCString newPath; + nsresult rv = aURI->GetPathQueryRef(newPath); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // Append a forward slash if necessary + int32_t len = newPath.Length(); + if (len > 0 && newPath.CharAt(len - 1) != '/') { + newPath.Append('/'); + } + + // Store the path back on the URI + AppendUTF16toUTF8(aPath, newPath); + + return NS_MutateURI(aURI).SetPathQueryRef(newPath).Finalize(aOutURI); +} + +nsresult nsWebBrowserPersist::SaveURIInternal( + nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal, + nsContentPolicyType aContentPolicyType, uint32_t aCacheKey, + nsIReferrerInfo* aReferrerInfo, nsICookieJarSettings* aCookieJarSettings, + nsIInputStream* aPostData, const char* aExtraHeaders, nsIURI* aFile, + bool aCalcFileExt, bool aIsPrivate) { + NS_ENSURE_ARG_POINTER(aURI); + NS_ENSURE_ARG_POINTER(aFile); + NS_ENSURE_ARG_POINTER(aTriggeringPrincipal); + + nsresult rv = NS_OK; + + mURI = aURI; + + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; + if (mPersistFlags & PERSIST_FLAGS_BYPASS_CACHE) { + loadFlags |= nsIRequest::LOAD_BYPASS_CACHE; + } else if (mPersistFlags & PERSIST_FLAGS_FROM_CACHE) { + loadFlags |= nsIRequest::LOAD_FROM_CACHE; + } + + // If there is no cookieJarSetting given, we need to create a new + // cookieJarSettings for this download in order to send cookies based on the + // current state of the prefs/permissions. + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = aCookieJarSettings; + if (!cookieJarSettings) { + // Although the variable is called 'triggering principal', it is used as the + // loading principal in the download channel, so we treat it as a loading + // principal also. + bool shouldResistFingerprinting = + nsContentUtils::ShouldResistFingerprinting_dangerous( + aTriggeringPrincipal, + "We are creating a new CookieJar Settings, so none exists " + "currently. Although the variable is called 'triggering principal'," + "it is used as the loading principal in the download channel, so we" + "treat it as a loading principal also."); + cookieJarSettings = + aIsPrivate + ? net::CookieJarSettings::Create(net::CookieJarSettings::ePrivate, + shouldResistFingerprinting) + : net::CookieJarSettings::Create(net::CookieJarSettings::eRegular, + shouldResistFingerprinting); + } + + // Open a channel to the URI + nsCOMPtr<nsIChannel> inputChannel; + rv = NS_NewChannel(getter_AddRefs(inputChannel), aURI, aTriggeringPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + aContentPolicyType, cookieJarSettings, + nullptr, // aPerformanceStorage + nullptr, // aLoadGroup + static_cast<nsIInterfaceRequestor*>(this), loadFlags); + + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = + do_QueryInterface(inputChannel); + if (pbChannel) { + pbChannel->SetPrivate(aIsPrivate); + } + + if (NS_FAILED(rv) || inputChannel == nullptr) { + EndDownload(NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + // Disable content conversion + if (mPersistFlags & PERSIST_FLAGS_NO_CONVERSION) { + nsCOMPtr<nsIEncodedChannel> encodedChannel(do_QueryInterface(inputChannel)); + if (encodedChannel) { + encodedChannel->SetApplyConversion(false); + } + } + + nsCOMPtr<nsILoadInfo> loadInfo = inputChannel->LoadInfo(); + loadInfo->SetIsUserTriggeredSave(true); + + // Set the referrer, post data and headers if any + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(inputChannel)); + if (httpChannel) { + if (aReferrerInfo) { + DebugOnly<nsresult> success = httpChannel->SetReferrerInfo(aReferrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(success)); + } + + // Post data + if (aPostData) { + nsCOMPtr<nsISeekableStream> stream(do_QueryInterface(aPostData)); + if (stream) { + // Rewind the postdata stream + stream->Seek(nsISeekableStream::NS_SEEK_SET, 0); + nsCOMPtr<nsIUploadChannel> uploadChannel( + do_QueryInterface(httpChannel)); + NS_ASSERTION(uploadChannel, "http must support nsIUploadChannel"); + // Attach the postdata to the http channel + uploadChannel->SetUploadStream(aPostData, ""_ns, -1); + } + } + + // Cache key + nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(httpChannel)); + if (cacheChannel && aCacheKey != 0) { + cacheChannel->SetCacheKey(aCacheKey); + } + + // Headers + if (aExtraHeaders) { + nsAutoCString oneHeader; + nsAutoCString headerName; + nsAutoCString headerValue; + int32_t crlf = 0; + int32_t colon = 0; + const char* kWhitespace = "\b\t\r\n "; + nsAutoCString extraHeaders(aExtraHeaders); + while (true) { + crlf = extraHeaders.Find("\r\n"); + if (crlf == -1) break; + extraHeaders.Mid(oneHeader, 0, crlf); + extraHeaders.Cut(0, crlf + 2); + colon = oneHeader.Find(":"); + if (colon == -1) break; // Should have a colon + oneHeader.Left(headerName, colon); + colon++; + oneHeader.Mid(headerValue, colon, oneHeader.Length() - colon); + headerName.Trim(kWhitespace); + headerValue.Trim(kWhitespace); + // Add the header (merging if required) + rv = httpChannel->SetRequestHeader(headerName, headerValue, true); + if (NS_FAILED(rv)) { + EndDownload(NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + } + } + } + return SaveChannelInternal(inputChannel, aFile, aCalcFileExt); +} + +nsresult nsWebBrowserPersist::SaveChannelInternal(nsIChannel* aChannel, + nsIURI* aFile, + bool aCalcFileExt) { + NS_ENSURE_ARG_POINTER(aChannel); + NS_ENSURE_ARG_POINTER(aFile); + + // The default behaviour of SaveChannelInternal is to download the source + // into a storage stream and upload that to the target. MakeOutputStream + // special-cases a file target and creates a file output stream directly. + // We want to special-case a file source and create a file input stream, + // but we don't need to do this in the case of a file target. + nsCOMPtr<nsIFileChannel> fc(do_QueryInterface(aChannel)); + nsCOMPtr<nsIFileURL> fu(do_QueryInterface(aFile)); + + if (fc && !fu) { + nsCOMPtr<nsIInputStream> fileInputStream, bufferedInputStream; + nsresult rv = aChannel->Open(getter_AddRefs(fileInputStream)); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedInputStream), + fileInputStream.forget(), + BUFFERED_OUTPUT_SIZE); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString contentType; + aChannel->GetContentType(contentType); + return StartUpload(bufferedInputStream, aFile, contentType); + } + + // Mark save channel as throttleable. + nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(aChannel)); + if (cos) { + cos->AddClassFlags(nsIClassOfService::Throttleable); + } + + // Read from the input channel + nsresult rv = aChannel->AsyncOpen(this); + if (rv == NS_ERROR_NO_CONTENT) { + // Assume this is a protocol such as mailto: which does not feed out + // data and just ignore it. + return NS_SUCCESS_DONT_FIXUP; + } + + if (NS_FAILED(rv)) { + // Opening failed, but do we care? + if (mPersistFlags & PERSIST_FLAGS_FAIL_ON_BROKEN_LINKS) { + SendErrorStatusChange(true, rv, aChannel, aFile); + EndDownload(NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + return NS_SUCCESS_DONT_FIXUP; + } + + MutexAutoLock lock(mOutputMapMutex); + // Add the output transport to the output map with the channel as the key + nsCOMPtr<nsISupports> keyPtr = do_QueryInterface(aChannel); + mOutputMap.InsertOrUpdate(keyPtr, + MakeUnique<OutputData>(aFile, mURI, aCalcFileExt)); + + return NS_OK; +} + +nsresult nsWebBrowserPersist::GetExtensionForContentType( + const char16_t* aContentType, char16_t** aExt) { + NS_ENSURE_ARG_POINTER(aContentType); + NS_ENSURE_ARG_POINTER(aExt); + + *aExt = nullptr; + + nsresult rv; + if (!mMIMEService) { + mMIMEService = do_GetService(NS_MIMESERVICE_CONTRACTID, &rv); + NS_ENSURE_TRUE(mMIMEService, NS_ERROR_FAILURE); + } + + nsAutoCString contentType; + LossyCopyUTF16toASCII(MakeStringSpan(aContentType), contentType); + nsAutoCString ext; + rv = mMIMEService->GetPrimaryExtension(contentType, ""_ns, ext); + if (NS_SUCCEEDED(rv)) { + *aExt = UTF8ToNewUnicode(ext); + NS_ENSURE_TRUE(*aExt, NS_ERROR_OUT_OF_MEMORY); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +nsresult nsWebBrowserPersist::SaveDocumentDeferred( + mozilla::UniquePtr<WalkData>&& aData) { + nsresult rv = + SaveDocumentInternal(aData->mDocument, aData->mFile, aData->mDataPath); + if (NS_FAILED(rv)) { + SendErrorStatusChange(true, rv, nullptr, mURI); + EndDownload(rv); + } + return rv; +} + +nsresult nsWebBrowserPersist::SaveDocumentInternal( + nsIWebBrowserPersistDocument* aDocument, nsIURI* aFile, nsIURI* aDataPath) { + mURI = nullptr; + NS_ENSURE_ARG_POINTER(aDocument); + NS_ENSURE_ARG_POINTER(aFile); + + nsresult rv = aDocument->SetPersistFlags(mPersistFlags); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aDocument->GetIsPrivate(&mIsPrivate); + NS_ENSURE_SUCCESS(rv, rv); + + // See if we can get the local file representation of this URI + nsCOMPtr<nsIFile> localFile; + rv = GetLocalFileFromURI(aFile, getter_AddRefs(localFile)); + + nsCOMPtr<nsIFile> localDataPath; + if (NS_SUCCEEDED(rv) && aDataPath) { + // See if we can get the local file representation of this URI + rv = GetLocalFileFromURI(aDataPath, getter_AddRefs(localDataPath)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + } + + // Persist the main document + rv = aDocument->GetCharacterSet(mCurrentCharset); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString uriSpec; + rv = aDocument->GetDocumentURI(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewURI(getter_AddRefs(mURI), uriSpec, mCurrentCharset.get()); + NS_ENSURE_SUCCESS(rv, rv); + rv = aDocument->GetBaseURI(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + rv = NS_NewURI(getter_AddRefs(mCurrentBaseURI), uriSpec, + mCurrentCharset.get()); + NS_ENSURE_SUCCESS(rv, rv); + + // Does the caller want to fixup the referenced URIs and save those too? + if (aDataPath) { + // Basic steps are these. + // + // 1. Iterate through the document (and subdocuments) building a list + // of unique URIs. + // 2. For each URI create an OutputData entry and open a channel to save + // it. As each URI is saved, discover the mime type and fix up the + // local filename with the correct extension. + // 3. Store the document in a list and wait for URI persistence to finish + // 4. After URI persistence completes save the list of documents, + // fixing it up as it goes out to file. + + mCurrentDataPathIsRelative = false; + mCurrentDataPath = aDataPath; + mCurrentRelativePathToData = ""; + mCurrentThingsToPersist = 0; + mTargetBaseURI = aFile; + + // Determine if the specified data path is relative to the + // specified file, (e.g. c:\docs\htmldata is relative to + // c:\docs\myfile.htm, but not to d:\foo\data. + + // Starting with the data dir work back through its parents + // checking if one of them matches the base directory. + + if (localDataPath && localFile) { + nsCOMPtr<nsIFile> baseDir; + localFile->GetParent(getter_AddRefs(baseDir)); + + nsAutoCString relativePathToData; + nsCOMPtr<nsIFile> dataDirParent; + dataDirParent = localDataPath; + while (dataDirParent) { + bool sameDir = false; + dataDirParent->Equals(baseDir, &sameDir); + if (sameDir) { + mCurrentRelativePathToData = relativePathToData; + mCurrentDataPathIsRelative = true; + break; + } + + nsAutoString dirName; + dataDirParent->GetLeafName(dirName); + + nsAutoCString newRelativePathToData; + newRelativePathToData = + NS_ConvertUTF16toUTF8(dirName) + "/"_ns + relativePathToData; + relativePathToData = newRelativePathToData; + + nsCOMPtr<nsIFile> newDataDirParent; + rv = dataDirParent->GetParent(getter_AddRefs(newDataDirParent)); + dataDirParent = newDataDirParent; + } + } else { + // generate a relative path if possible + nsCOMPtr<nsIURL> pathToBaseURL(do_QueryInterface(aFile)); + if (pathToBaseURL) { + nsAutoCString relativePath; // nsACString + if (NS_SUCCEEDED( + pathToBaseURL->GetRelativeSpec(aDataPath, relativePath))) { + mCurrentDataPathIsRelative = true; + mCurrentRelativePathToData = relativePath; + } + } + } + + // Store the document in a list so when URI persistence is done and the + // filenames of saved URIs are known, the documents can be fixed up and + // saved + + auto* docData = new DocData; + docData->mBaseURI = mCurrentBaseURI; + docData->mCharset = mCurrentCharset; + docData->mDocument = aDocument; + docData->mFile = aFile; + mDocList.AppendElement(docData); + + // Walk the DOM gathering a list of externally referenced URIs in the uri + // map + nsCOMPtr<nsIWebBrowserPersistResourceVisitor> visit = + new OnWalk(this, aFile, localDataPath); + return aDocument->ReadResources(visit); + } else { + auto* docData = new DocData; + docData->mBaseURI = mCurrentBaseURI; + docData->mCharset = mCurrentCharset; + docData->mDocument = aDocument; + docData->mFile = aFile; + mDocList.AppendElement(docData); + + // Not walking DOMs, so go directly to serialization. + SerializeNextFile(); + return NS_OK; + } +} + +NS_IMETHODIMP +nsWebBrowserPersist::OnWalk::VisitResource( + nsIWebBrowserPersistDocument* aDoc, const nsACString& aURI, + nsContentPolicyType aContentPolicyType) { + return mParent->StoreURI(aURI, aDoc, aContentPolicyType); +} + +NS_IMETHODIMP +nsWebBrowserPersist::OnWalk::VisitDocument( + nsIWebBrowserPersistDocument* aDoc, nsIWebBrowserPersistDocument* aSubDoc) { + URIData* data = nullptr; + nsAutoCString uriSpec; + nsresult rv = aSubDoc->GetDocumentURI(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + rv = mParent->StoreURI(uriSpec, aDoc, nsIContentPolicy::TYPE_SUBDOCUMENT, + false, &data); + NS_ENSURE_SUCCESS(rv, rv); + if (!data) { + // If the URI scheme isn't persistable, then don't persist. + return NS_OK; + } + data->mIsSubFrame = true; + return mParent->SaveSubframeContent(aSubDoc, aDoc, uriSpec, data); +} + +NS_IMETHODIMP +nsWebBrowserPersist::OnWalk::VisitBrowsingContext( + nsIWebBrowserPersistDocument* aDoc, BrowsingContext* aContext) { + RefPtr<dom::CanonicalBrowsingContext> context = aContext->Canonical(); + + if (NS_WARN_IF(!context->GetCurrentWindowGlobal())) { + EndVisit(nullptr, NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + UniquePtr<WebBrowserPersistDocumentParent> actor( + new WebBrowserPersistDocumentParent()); + + nsCOMPtr<nsIWebBrowserPersistDocumentReceiver> receiver = + new OnRemoteWalk(this, aDoc); + actor->SetOnReady(receiver); + + RefPtr<dom::BrowserParent> browserParent = + context->GetCurrentWindowGlobal()->GetBrowserParent(); + + bool ok = + context->GetContentParent()->SendPWebBrowserPersistDocumentConstructor( + actor.release(), browserParent, context); + + if (NS_WARN_IF(!ok)) { + // (The actor will be destroyed on constructor failure.) + EndVisit(nullptr, NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + ++mPendingDocuments; + + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserPersist::OnWalk::EndVisit(nsIWebBrowserPersistDocument* aDoc, + nsresult aStatus) { + if (NS_FAILED(mStatus)) { + return mStatus; + } + + if (NS_FAILED(aStatus)) { + mStatus = aStatus; + mParent->SendErrorStatusChange(true, aStatus, nullptr, mFile); + mParent->EndDownload(aStatus); + return aStatus; + } + + if (--mPendingDocuments) { + // We're not done yet, wait for more. + return NS_OK; + } + + mParent->FinishSaveDocumentInternal(mFile, mDataPath); + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserPersist::OnRemoteWalk::OnDocumentReady( + nsIWebBrowserPersistDocument* aSubDocument) { + mVisitor->VisitDocument(mDocument, aSubDocument); + mVisitor->EndVisit(mDocument, NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +nsWebBrowserPersist::OnRemoteWalk::OnError(nsresult aFailure) { + mVisitor->EndVisit(nullptr, aFailure); + return NS_OK; +} + +void nsWebBrowserPersist::FinishSaveDocumentInternal(nsIURI* aFile, + nsIFile* aDataPath) { + // If there are things to persist, create a directory to hold them + if (mCurrentThingsToPersist > 0) { + if (aDataPath) { + bool exists = false; + bool haveDir = false; + + aDataPath->Exists(&exists); + if (exists) { + aDataPath->IsDirectory(&haveDir); + } + if (!haveDir) { + nsresult rv = aDataPath->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_SUCCEEDED(rv)) { + haveDir = true; + } else { + SendErrorStatusChange(false, rv, nullptr, aFile); + } + } + if (!haveDir) { + EndDownload(NS_ERROR_FAILURE); + return; + } + if (mPersistFlags & PERSIST_FLAGS_CLEANUP_ON_FAILURE) { + // Add to list of things to delete later if all goes wrong + auto* cleanupData = new CleanupData; + cleanupData->mFile = aDataPath; + cleanupData->mIsDirectory = true; + mCleanupList.AppendElement(cleanupData); + } + } + } + + if (mWalkStack.Length() > 0) { + mozilla::UniquePtr<WalkData> toWalk = mWalkStack.PopLastElement(); + // Bounce this off the event loop to avoid stack overflow. + using WalkStorage = StoreCopyPassByRRef<decltype(toWalk)>; + auto saveMethod = &nsWebBrowserPersist::SaveDocumentDeferred; + nsCOMPtr<nsIRunnable> saveLater = NewRunnableMethod<WalkStorage>( + "nsWebBrowserPersist::FinishSaveDocumentInternal", this, saveMethod, + std::move(toWalk)); + NS_DispatchToCurrentThread(saveLater); + } else { + // Done walking DOMs; on to the serialization phase. + SerializeNextFile(); + } +} + +void nsWebBrowserPersist::Cleanup() { + mURIMap.Clear(); + nsClassHashtable<nsISupportsHashKey, OutputData> outputMapCopy; + { + MutexAutoLock lock(mOutputMapMutex); + mOutputMap.SwapElements(outputMapCopy); + } + for (const auto& key : outputMapCopy.Keys()) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(key); + if (channel) { + channel->Cancel(NS_BINDING_ABORTED); + } + } + outputMapCopy.Clear(); + + for (const auto& key : mUploadList.Keys()) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(key); + if (channel) { + channel->Cancel(NS_BINDING_ABORTED); + } + } + mUploadList.Clear(); + + uint32_t i; + for (i = 0; i < mDocList.Length(); i++) { + DocData* docData = mDocList.ElementAt(i); + delete docData; + } + mDocList.Clear(); + + for (i = 0; i < mCleanupList.Length(); i++) { + CleanupData* cleanupData = mCleanupList.ElementAt(i); + delete cleanupData; + } + mCleanupList.Clear(); + + mFilenameList.Clear(); +} + +void nsWebBrowserPersist::CleanupLocalFiles() { + // Two passes, the first pass cleans up files, the second pass tests + // for and then deletes empty directories. Directories that are not + // empty after the first pass must contain files from something else + // and are not deleted. + int pass; + for (pass = 0; pass < 2; pass++) { + uint32_t i; + for (i = 0; i < mCleanupList.Length(); i++) { + CleanupData* cleanupData = mCleanupList.ElementAt(i); + nsCOMPtr<nsIFile> file = cleanupData->mFile; + + // Test if the dir / file exists (something in an earlier loop + // may have already removed it) + bool exists = false; + file->Exists(&exists); + if (!exists) continue; + + // Test if the file has changed in between creation and deletion + // in some way that means it should be ignored + bool isDirectory = false; + file->IsDirectory(&isDirectory); + if (isDirectory != cleanupData->mIsDirectory) + continue; // A file has become a dir or vice versa ! + + if (pass == 0 && !isDirectory) { + file->Remove(false); + } else if (pass == 1 && isDirectory) // Directory + { + // Directories are more complicated. Enumerate through + // children looking for files. Any files created by the + // persist object would have been deleted by the first + // pass so if there are any there at this stage, the dir + // cannot be deleted because it has someone else's files + // in it. Empty child dirs are deleted but they must be + // recursed through to ensure they are actually empty. + + bool isEmptyDirectory = true; + nsCOMArray<nsIDirectoryEnumerator> dirStack; + int32_t stackSize = 0; + + // Push the top level enum onto the stack + nsCOMPtr<nsIDirectoryEnumerator> pos; + if (NS_SUCCEEDED(file->GetDirectoryEntries(getter_AddRefs(pos)))) + dirStack.AppendObject(pos); + + while (isEmptyDirectory && (stackSize = dirStack.Count())) { + // Pop the last element + nsCOMPtr<nsIDirectoryEnumerator> curPos; + curPos = dirStack[stackSize - 1]; + dirStack.RemoveObjectAt(stackSize - 1); + + nsCOMPtr<nsIFile> child; + if (NS_FAILED(curPos->GetNextFile(getter_AddRefs(child))) || !child) { + continue; + } + + bool childIsSymlink = false; + child->IsSymlink(&childIsSymlink); + bool childIsDir = false; + child->IsDirectory(&childIsDir); + if (!childIsDir || childIsSymlink) { + // Some kind of file or symlink which means dir + // is not empty so just drop out. + isEmptyDirectory = false; + break; + } + // Push parent enumerator followed by child enumerator + nsCOMPtr<nsIDirectoryEnumerator> childPos; + child->GetDirectoryEntries(getter_AddRefs(childPos)); + dirStack.AppendObject(curPos); + if (childPos) dirStack.AppendObject(childPos); + } + dirStack.Clear(); + + // If after all that walking the dir is deemed empty, delete it + if (isEmptyDirectory) { + file->Remove(true); + } + } + } + } +} + +nsresult nsWebBrowserPersist::CalculateUniqueFilename( + nsIURI* aURI, nsCOMPtr<nsIURI>& aOutURI) { + nsCOMPtr<nsIURL> url(do_QueryInterface(aURI)); + NS_ENSURE_TRUE(url, NS_ERROR_FAILURE); + + bool nameHasChanged = false; + nsresult rv; + + // Get the old filename + nsAutoCString filename; + rv = url->GetFileName(filename); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + nsAutoCString directory; + rv = url->GetDirectory(directory); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // Split the filename into a base and an extension. + // e.g. "foo.html" becomes "foo" & ".html" + // + // The nsIURL methods GetFileBaseName & GetFileExtension don't + // preserve the dot whereas this code does to save some effort + // later when everything is put back together. + int32_t lastDot = filename.RFind("."); + nsAutoCString base; + nsAutoCString ext; + if (lastDot >= 0) { + filename.Mid(base, 0, lastDot); + filename.Mid(ext, lastDot, filename.Length() - lastDot); // includes dot + } else { + // filename contains no dot + base = filename; + } + + // Test if the filename is longer than allowed by the OS + int32_t needToChop = filename.Length() - kDefaultMaxFilenameLength; + if (needToChop > 0) { + // Truncate the base first and then the ext if necessary + if (base.Length() > (uint32_t)needToChop) { + base.Truncate(base.Length() - needToChop); + } else { + needToChop -= base.Length() - 1; + base.Truncate(1); + if (ext.Length() > (uint32_t)needToChop) { + ext.Truncate(ext.Length() - needToChop); + } else { + ext.Truncate(0); + } + // If kDefaultMaxFilenameLength were 1 we'd be in trouble here, + // but that won't happen because it will be set to a sensible + // value. + } + + filename.Assign(base); + filename.Append(ext); + nameHasChanged = true; + } + + // Ensure the filename is unique + // Create a filename if it's empty, or if the filename / datapath is + // already taken by another URI and create an alternate name. + + if (base.IsEmpty() || !mFilenameList.IsEmpty()) { + nsAutoCString tmpPath; + nsAutoCString tmpBase; + uint32_t duplicateCounter = 1; + while (true) { + // Make a file name, + // Foo become foo_001, foo_002, etc. + // Empty files become _001, _002 etc. + + if (base.IsEmpty() || duplicateCounter > 1) { + SmprintfPointer tmp = mozilla::Smprintf("_%03d", duplicateCounter); + NS_ENSURE_TRUE(tmp, NS_ERROR_OUT_OF_MEMORY); + if (filename.Length() < kDefaultMaxFilenameLength - 4) { + tmpBase = base; + } else { + base.Mid(tmpBase, 0, base.Length() - 4); + } + tmpBase.Append(tmp.get()); + } else { + tmpBase = base; + } + + tmpPath.Assign(directory); + tmpPath.Append(tmpBase); + tmpPath.Append(ext); + + // Test if the name is a duplicate + if (!mFilenameList.Contains(tmpPath)) { + if (!base.Equals(tmpBase)) { + filename.Assign(tmpBase); + filename.Append(ext); + nameHasChanged = true; + } + break; + } + duplicateCounter++; + } + } + + // Add name to list of those already used + nsAutoCString newFilepath(directory); + newFilepath.Append(filename); + mFilenameList.AppendElement(newFilepath); + + // Update the uri accordingly if the filename actually changed + if (nameHasChanged) { + // Final sanity test + if (filename.Length() > kDefaultMaxFilenameLength) { + NS_WARNING( + "Filename wasn't truncated less than the max file length - how can " + "that be?"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> localFile; + GetLocalFileFromURI(aURI, getter_AddRefs(localFile)); + + if (localFile) { + nsAutoString filenameAsUnichar; + CopyASCIItoUTF16(filename, filenameAsUnichar); + localFile->SetLeafName(filenameAsUnichar); + + // Resync the URI with the file after the extension has been appended + return NS_MutateURI(aURI) + .Apply(&nsIFileURLMutator::SetFile, localFile) + .Finalize(aOutURI); + } + return NS_MutateURI(url) + .Apply(&nsIURLMutator::SetFileName, filename, nullptr) + .Finalize(aOutURI); + } + + // TODO (:valentin) This method should always clone aURI + aOutURI = aURI; + return NS_OK; +} + +nsresult nsWebBrowserPersist::MakeFilenameFromURI(nsIURI* aURI, + nsString& aFilename) { + // Try to get filename from the URI. + nsAutoString fileName; + + // Get a suggested file name from the URL but strip it of characters + // likely to cause the name to be illegal. + + nsCOMPtr<nsIURL> url(do_QueryInterface(aURI)); + if (url) { + nsAutoCString nameFromURL; + url->GetFileName(nameFromURL); + if (mPersistFlags & PERSIST_FLAGS_DONT_CHANGE_FILENAMES) { + CopyASCIItoUTF16(NS_UnescapeURL(nameFromURL), fileName); + aFilename = fileName; + return NS_OK; + } + if (!nameFromURL.IsEmpty()) { + // Unescape the file name (GetFileName escapes it) + NS_UnescapeURL(nameFromURL); + uint32_t nameLength = 0; + const char* p = nameFromURL.get(); + for (; *p && *p != ';' && *p != '?' && *p != '#' && *p != '.'; p++) { + if (IsAsciiAlpha(*p) || IsAsciiDigit(*p) || *p == '.' || *p == '-' || + *p == '_' || (*p == ' ')) { + fileName.Append(char16_t(*p)); + if (++nameLength == kDefaultMaxFilenameLength) { + // Note: + // There is no point going any further since it will be + // truncated in CalculateUniqueFilename anyway. + // More importantly, certain implementations of + // nsIFile (e.g. the Mac impl) might truncate + // names in undesirable ways, such as truncating from + // the middle, inserting ellipsis and so on. + break; + } + } + } + } + } + + // Empty filenames can confuse the local file object later + // when it attempts to set the leaf name in CalculateUniqueFilename + // for duplicates and ends up replacing the parent dir. To avoid + // the problem, all filenames are made at least one character long. + if (fileName.IsEmpty()) { + fileName.Append(char16_t('a')); // 'a' is for arbitrary + } + + aFilename = fileName; + return NS_OK; +} + +nsresult nsWebBrowserPersist::CalculateAndAppendFileExt( + nsIURI* aURI, nsIChannel* aChannel, nsIURI* aOriginalURIWithExtension, + nsCOMPtr<nsIURI>& aOutURI) { + nsresult rv = NS_OK; + + if (!mMIMEService) { + mMIMEService = do_GetService(NS_MIMESERVICE_CONTRACTID, &rv); + NS_ENSURE_TRUE(mMIMEService, NS_ERROR_FAILURE); + } + + nsAutoCString contentType; + + // Get the content type from the channel + aChannel->GetContentType(contentType); + + // Get the content type from the MIME service + if (contentType.IsEmpty()) { + nsCOMPtr<nsIURI> uri; + aChannel->GetOriginalURI(getter_AddRefs(uri)); + mMIMEService->GetTypeFromURI(uri, contentType); + } + + // Validate the filename + if (!contentType.IsEmpty()) { + nsAutoString newFileName; + if (NS_SUCCEEDED(mMIMEService->GetValidFileName( + aChannel, contentType, aOriginalURIWithExtension, + nsIMIMEService::VALIDATE_DEFAULT, newFileName))) { + nsCOMPtr<nsIFile> localFile; + GetLocalFileFromURI(aURI, getter_AddRefs(localFile)); + if (localFile) { + localFile->SetLeafName(newFileName); + + // Resync the URI with the file after the extension has been appended + return NS_MutateURI(aURI) + .Apply(&nsIFileURLMutator::SetFile, localFile) + .Finalize(aOutURI); + } + return NS_MutateURI(aURI) + .Apply(&nsIURLMutator::SetFileName, + NS_ConvertUTF16toUTF8(newFileName), nullptr) + .Finalize(aOutURI); + } + } + + // TODO (:valentin) This method should always clone aURI + aOutURI = aURI; + return NS_OK; +} + +// Note: the MakeOutputStream helpers can be called from a background thread. +nsresult nsWebBrowserPersist::MakeOutputStream( + nsIURI* aURI, nsIOutputStream** aOutputStream) { + nsresult rv; + + nsCOMPtr<nsIFile> localFile; + GetLocalFileFromURI(aURI, getter_AddRefs(localFile)); + if (localFile) + rv = MakeOutputStreamFromFile(localFile, aOutputStream); + else + rv = MakeOutputStreamFromURI(aURI, aOutputStream); + + return rv; +} + +nsresult nsWebBrowserPersist::MakeOutputStreamFromFile( + nsIFile* aFile, nsIOutputStream** aOutputStream) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIFileOutputStream> fileOutputStream = + do_CreateInstance(NS_LOCALFILEOUTPUTSTREAM_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // XXX brade: get the right flags here! + int32_t ioFlags = -1; + if (mPersistFlags & nsIWebBrowserPersist::PERSIST_FLAGS_APPEND_TO_FILE) + ioFlags = PR_APPEND | PR_CREATE_FILE | PR_WRONLY; + rv = fileOutputStream->Init(aFile, ioFlags, -1, 0); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewBufferedOutputStream(aOutputStream, fileOutputStream.forget(), + BUFFERED_OUTPUT_SIZE); + NS_ENSURE_SUCCESS(rv, rv); + + if (mPersistFlags & PERSIST_FLAGS_CLEANUP_ON_FAILURE) { + // Add to cleanup list in event of failure + auto* cleanupData = new CleanupData; + cleanupData->mFile = aFile; + cleanupData->mIsDirectory = false; + if (NS_IsMainThread()) { + mCleanupList.AppendElement(cleanupData); + } else { + // If we're on a background thread, add the cleanup back on the main + // thread. + RefPtr<Runnable> addCleanup = NS_NewRunnableFunction( + "nsWebBrowserPersist::AddCleanupToList", + [self = RefPtr{this}, cleanup = std::move(cleanupData)]() { + self->mCleanupList.AppendElement(cleanup); + }); + NS_DispatchToMainThread(addCleanup); + } + } + + return NS_OK; +} + +nsresult nsWebBrowserPersist::MakeOutputStreamFromURI( + nsIURI* aURI, nsIOutputStream** aOutputStream) { + uint32_t segsize = 8192; + uint32_t maxsize = uint32_t(-1); + nsCOMPtr<nsIStorageStream> storStream; + nsresult rv = + NS_NewStorageStream(segsize, maxsize, getter_AddRefs(storStream)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_SUCCESS(CallQueryInterface(storStream, aOutputStream), + NS_ERROR_FAILURE); + return NS_OK; +} + +void nsWebBrowserPersist::FinishDownload() { + // We call FinishDownload when we run out of things to download for this + // persist operation, by dispatching this method to the main thread. By now, + // it's possible that we have been canceled or encountered an error earlier + // in the download, or something else called EndDownload. In that case, don't + // re-run EndDownload. + if (mEndCalled) { + return; + } + EndDownload(NS_OK); +} + +void nsWebBrowserPersist::EndDownload(nsresult aResult) { + MOZ_ASSERT(NS_IsMainThread(), "Should end download on the main thread."); + + // Really this should just never happen, but if it does, at least avoid + // no-op notifications or pretending we succeeded if we already failed. + if (mEndCalled && (NS_SUCCEEDED(aResult) || mPersistResult == aResult)) { + return; + } + + // Store the error code in the result if it is an error + if (NS_SUCCEEDED(mPersistResult) && NS_FAILED(aResult)) { + mPersistResult = aResult; + } + + if (mEndCalled) { + MOZ_ASSERT(!mEndCalled, "Should only end the download once."); + return; + } + mEndCalled = true; + + ClosePromise::All(GetCurrentSerialEventTarget(), mFileClosePromises) + ->Then(GetCurrentSerialEventTarget(), __func__, + [self = RefPtr{this}, aResult]() { + self->EndDownloadInternal(aResult); + }); +} + +void nsWebBrowserPersist::EndDownloadInternal(nsresult aResult) { + // mCompleted needs to be set before issuing the stop notification. + // (Bug 1224437) + mCompleted = true; + // State stop notification + if (mProgressListener) { + mProgressListener->OnStateChange( + nullptr, nullptr, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_NETWORK, + mPersistResult); + } + + // Do file cleanup if required + if (NS_FAILED(aResult) && + (mPersistFlags & PERSIST_FLAGS_CLEANUP_ON_FAILURE)) { + CleanupLocalFiles(); + } + + // Cleanup the channels + Cleanup(); + + mProgressListener = nullptr; + mProgressListener2 = nullptr; + mEventSink = nullptr; +} + +nsresult nsWebBrowserPersist::FixRedirectedChannelEntry( + nsIChannel* aNewChannel) { + NS_ENSURE_ARG_POINTER(aNewChannel); + + // Iterate through existing open channels looking for one with a URI + // matching the one specified. + nsCOMPtr<nsIURI> originalURI; + aNewChannel->GetOriginalURI(getter_AddRefs(originalURI)); + nsISupports* matchingKey = nullptr; + for (nsISupports* key : mOutputMap.Keys()) { + nsCOMPtr<nsIChannel> thisChannel = do_QueryInterface(key); + nsCOMPtr<nsIURI> thisURI; + + thisChannel->GetOriginalURI(getter_AddRefs(thisURI)); + + // Compare this channel's URI to the one passed in. + bool matchingURI = false; + thisURI->Equals(originalURI, &matchingURI); + if (matchingURI) { + matchingKey = key; + break; + } + } + + if (matchingKey) { + // We only get called from OnStartRequest, so this is always on the + // main thread. Make sure we don't pull the rug from under anything else. + MutexAutoLock lock(mOutputMapMutex); + // If a match was found, remove the data entry with the old channel + // key and re-add it with the new channel key. + mozilla::UniquePtr<OutputData> outputData; + mOutputMap.Remove(matchingKey, &outputData); + NS_ENSURE_TRUE(outputData, NS_ERROR_FAILURE); + + // Store data again with new channel unless told to ignore redirects. + if (!(mPersistFlags & PERSIST_FLAGS_IGNORE_REDIRECTED_DATA)) { + nsCOMPtr<nsISupports> keyPtr = do_QueryInterface(aNewChannel); + mOutputMap.InsertOrUpdate(keyPtr, std::move(outputData)); + } + } + + return NS_OK; +} + +void nsWebBrowserPersist::CalcTotalProgress() { + mTotalCurrentProgress = 0; + mTotalMaxProgress = 0; + + if (mOutputMap.Count() > 0) { + // Total up the progress of each output stream + for (const auto& data : mOutputMap.Values()) { + // Only count toward total progress if destination file is local. + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(data->mFile); + if (fileURL) { + mTotalCurrentProgress += data->mSelfProgress; + mTotalMaxProgress += data->mSelfProgressMax; + } + } + } + + if (mUploadList.Count() > 0) { + // Total up the progress of each upload + for (const auto& data : mUploadList.Values()) { + if (data) { + mTotalCurrentProgress += data->mSelfProgress; + mTotalMaxProgress += data->mSelfProgressMax; + } + } + } + + // XXX this code seems pretty bogus and pointless + if (mTotalCurrentProgress == 0 && mTotalMaxProgress == 0) { + // No output streams so we must be complete + mTotalCurrentProgress = 10000; + mTotalMaxProgress = 10000; + } +} + +nsresult nsWebBrowserPersist::StoreURI(const nsACString& aURI, + nsIWebBrowserPersistDocument* aDoc, + nsContentPolicyType aContentPolicyType, + bool aNeedsPersisting, URIData** aData) { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURI, mCurrentCharset.get(), + mCurrentBaseURI); + NS_ENSURE_SUCCESS(rv, rv); + + return StoreURI(uri, aDoc, aContentPolicyType, aNeedsPersisting, aData); +} + +nsresult nsWebBrowserPersist::StoreURI(nsIURI* aURI, + nsIWebBrowserPersistDocument* aDoc, + nsContentPolicyType aContentPolicyType, + bool aNeedsPersisting, URIData** aData) { + NS_ENSURE_ARG_POINTER(aURI); + if (aData) { + *aData = nullptr; + } + + // Test if this URI should be persisted. By default + // we should assume the URI is persistable. + bool doNotPersistURI; + nsresult rv = NS_URIChainHasFlags( + aURI, nsIProtocolHandler::URI_NON_PERSISTABLE, &doNotPersistURI); + if (NS_FAILED(rv)) { + doNotPersistURI = false; + } + + if (doNotPersistURI) { + return NS_OK; + } + + URIData* data = nullptr; + MakeAndStoreLocalFilenameInURIMap(aURI, aDoc, aContentPolicyType, + aNeedsPersisting, &data); + if (aData) { + *aData = data; + } + + return NS_OK; +} + +nsresult nsWebBrowserPersist::URIData::GetLocalURI(nsIURI* targetBaseURI, + nsCString& aSpecOut) { + aSpecOut.SetIsVoid(true); + if (!mNeedsFixup) { + return NS_OK; + } + nsresult rv; + nsCOMPtr<nsIURI> fileAsURI; + if (mFile) { + fileAsURI = mFile; + } else { + fileAsURI = mDataPath; + rv = AppendPathToURI(fileAsURI, mFilename, fileAsURI); + NS_ENSURE_SUCCESS(rv, rv); + } + + // remove username/password if present + Unused << NS_MutateURI(fileAsURI).SetUserPass(""_ns).Finalize(fileAsURI); + + // reset node attribute + // Use relative or absolute links + if (mDataPathIsRelative) { + bool isEqual = false; + if (NS_SUCCEEDED(mRelativeDocumentURI->Equals(targetBaseURI, &isEqual)) && + isEqual) { + nsCOMPtr<nsIURL> url(do_QueryInterface(fileAsURI)); + if (!url) { + return NS_ERROR_FAILURE; + } + + nsAutoCString filename; + url->GetFileName(filename); + + nsAutoCString rawPathURL(mRelativePathToData); + rawPathURL.Append(filename); + + rv = NS_EscapeURL(rawPathURL, esc_FilePath, aSpecOut, fallible); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsAutoCString rawPathURL; + + nsCOMPtr<nsIFile> dataFile; + rv = GetLocalFileFromURI(mFile, getter_AddRefs(dataFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> docFile; + rv = GetLocalFileFromURI(targetBaseURI, getter_AddRefs(docFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> parentDir; + rv = docFile->GetParent(getter_AddRefs(parentDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = dataFile->GetRelativePath(parentDir, rawPathURL); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_EscapeURL(rawPathURL, esc_FilePath, aSpecOut, fallible); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + fileAsURI->GetSpec(aSpecOut); + } + if (mIsSubFrame) { + AppendUTF16toUTF8(mSubFrameExt, aSpecOut); + } + + return NS_OK; +} + +bool nsWebBrowserPersist::DocumentEncoderExists(const char* aContentType) { + return do_getDocumentTypeSupportedForEncoding(aContentType); +} + +nsresult nsWebBrowserPersist::SaveSubframeContent( + nsIWebBrowserPersistDocument* aFrameContent, + nsIWebBrowserPersistDocument* aParentDocument, const nsCString& aURISpec, + URIData* aData) { + NS_ENSURE_ARG_POINTER(aData); + + // Extract the content type for the frame's contents. + nsAutoCString contentType; + nsresult rv = aFrameContent->GetContentType(contentType); + NS_ENSURE_SUCCESS(rv, rv); + + nsString ext; + GetExtensionForContentType(NS_ConvertASCIItoUTF16(contentType).get(), + getter_Copies(ext)); + + // We must always have an extension so we will try to re-assign + // the original extension if GetExtensionForContentType fails. + if (ext.IsEmpty()) { + nsCOMPtr<nsIURI> docURI; + rv = NS_NewURI(getter_AddRefs(docURI), aURISpec, mCurrentCharset.get()); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURL> url(do_QueryInterface(docURI, &rv)); + nsAutoCString extension; + if (NS_SUCCEEDED(rv)) { + url->GetFileExtension(extension); + } else { + extension.AssignLiteral("htm"); + } + aData->mSubFrameExt.Assign(char16_t('.')); + AppendUTF8toUTF16(extension, aData->mSubFrameExt); + } else { + aData->mSubFrameExt.Assign(char16_t('.')); + aData->mSubFrameExt.Append(ext); + } + + nsString filenameWithExt = aData->mFilename; + filenameWithExt.Append(aData->mSubFrameExt); + + // Work out the path for the subframe + nsCOMPtr<nsIURI> frameURI = mCurrentDataPath; + rv = AppendPathToURI(frameURI, filenameWithExt, frameURI); + NS_ENSURE_SUCCESS(rv, rv); + + // Work out the path for the subframe data + nsCOMPtr<nsIURI> frameDataURI = mCurrentDataPath; + nsAutoString newFrameDataPath(aData->mFilename); + + // Append _data + newFrameDataPath.AppendLiteral("_data"); + rv = AppendPathToURI(frameDataURI, newFrameDataPath, frameDataURI); + NS_ENSURE_SUCCESS(rv, rv); + + // Make frame document & data path conformant and unique + nsCOMPtr<nsIURI> out; + rv = CalculateUniqueFilename(frameURI, out); + NS_ENSURE_SUCCESS(rv, rv); + frameURI = out; + + rv = CalculateUniqueFilename(frameDataURI, out); + NS_ENSURE_SUCCESS(rv, rv); + frameDataURI = out; + + mCurrentThingsToPersist++; + + // We shouldn't use SaveDocumentInternal for the contents + // of frames that are not documents, e.g. images. + if (DocumentEncoderExists(contentType.get())) { + auto toWalk = mozilla::MakeUnique<WalkData>(); + toWalk->mDocument = aFrameContent; + toWalk->mFile = frameURI; + toWalk->mDataPath = frameDataURI; + mWalkStack.AppendElement(std::move(toWalk)); + } else { + nsContentPolicyType policyType = nsIContentPolicy::TYPE_OTHER; + if (StringBeginsWith(contentType, "image/"_ns)) { + policyType = nsIContentPolicy::TYPE_IMAGE; + } else if (StringBeginsWith(contentType, "audio/"_ns) || + StringBeginsWith(contentType, "video/"_ns)) { + policyType = nsIContentPolicy::TYPE_MEDIA; + } + rv = StoreURI(aURISpec, aParentDocument, policyType); + } + NS_ENSURE_SUCCESS(rv, rv); + + // Store the updated uri to the frame + aData->mFile = frameURI; + aData->mSubFrameExt.Truncate(); // we already put this in frameURI + + return NS_OK; +} + +nsresult nsWebBrowserPersist::CreateChannelFromURI(nsIURI* aURI, + nsIChannel** aChannel) { + nsresult rv = NS_OK; + *aChannel = nullptr; + + rv = NS_NewChannel(aChannel, aURI, nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_ARG_POINTER(*aChannel); + + rv = (*aChannel)->SetNotificationCallbacks( + static_cast<nsIInterfaceRequestor*>(this)); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +// we store the current location as the key (absolutized version of domnode's +// attribute's value) +nsresult nsWebBrowserPersist::MakeAndStoreLocalFilenameInURIMap( + nsIURI* aURI, nsIWebBrowserPersistDocument* aDoc, + nsContentPolicyType aContentPolicyType, bool aNeedsPersisting, + URIData** aData) { + NS_ENSURE_ARG_POINTER(aURI); + + nsAutoCString spec; + nsresult rv = aURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // Create a sensibly named filename for the URI and store in the URI map + URIData* data; + if (mURIMap.Get(spec, &data)) { + if (aNeedsPersisting) { + data->mNeedsPersisting = true; + } + if (aData) { + *aData = data; + } + return NS_OK; + } + + // Create a unique file name for the uri + nsString filename; + rv = MakeFilenameFromURI(aURI, filename); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // Store the file name + data = new URIData; + + data->mContentPolicyType = aContentPolicyType; + data->mNeedsPersisting = aNeedsPersisting; + data->mNeedsFixup = true; + data->mFilename = filename; + data->mSaved = false; + data->mIsSubFrame = false; + data->mDataPath = mCurrentDataPath; + data->mDataPathIsRelative = mCurrentDataPathIsRelative; + data->mRelativePathToData = mCurrentRelativePathToData; + data->mRelativeDocumentURI = mTargetBaseURI; + data->mCharset = mCurrentCharset; + + aDoc->GetPrincipal(getter_AddRefs(data->mTriggeringPrincipal)); + aDoc->GetCookieJarSettings(getter_AddRefs(data->mCookieJarSettings)); + + if (aNeedsPersisting) mCurrentThingsToPersist++; + + mURIMap.InsertOrUpdate(spec, UniquePtr<URIData>(data)); + if (aData) { + *aData = data; + } + + return NS_OK; +} + +// Decide if we need to apply conversion to the passed channel. +void nsWebBrowserPersist::SetApplyConversionIfNeeded(nsIChannel* aChannel) { + nsresult rv = NS_OK; + nsCOMPtr<nsIEncodedChannel> encChannel = do_QueryInterface(aChannel, &rv); + if (NS_FAILED(rv)) return; + + // Set the default conversion preference: + encChannel->SetApplyConversion(false); + + nsCOMPtr<nsIURI> thisURI; + aChannel->GetURI(getter_AddRefs(thisURI)); + nsCOMPtr<nsIURL> sourceURL(do_QueryInterface(thisURI)); + if (!sourceURL) return; + nsAutoCString extension; + sourceURL->GetFileExtension(extension); + + nsCOMPtr<nsIUTF8StringEnumerator> encEnum; + encChannel->GetContentEncodings(getter_AddRefs(encEnum)); + if (!encEnum) return; + nsCOMPtr<nsIExternalHelperAppService> helperAppService = + do_GetService(NS_EXTERNALHELPERAPPSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return; + bool hasMore; + rv = encEnum->HasMore(&hasMore); + if (NS_SUCCEEDED(rv) && hasMore) { + nsAutoCString encType; + rv = encEnum->GetNext(encType); + if (NS_SUCCEEDED(rv)) { + bool applyConversion = false; + rv = helperAppService->ApplyDecodingForExtension(extension, encType, + &applyConversion); + if (NS_SUCCEEDED(rv)) encChannel->SetApplyConversion(applyConversion); + } + } +} diff --git a/dom/webbrowserpersist/nsWebBrowserPersist.h b/dom/webbrowserpersist/nsWebBrowserPersist.h new file mode 100644 index 0000000000..47f13b1cb4 --- /dev/null +++ b/dom/webbrowserpersist/nsWebBrowserPersist.h @@ -0,0 +1,191 @@ +/* -*- 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 nsWebBrowserPersist_h__ +#define nsWebBrowserPersist_h__ + +#include "nsCOMPtr.h" +#include "nsWeakReference.h" + +#include "nsIInterfaceRequestor.h" +#include "nsIMIMEService.h" +#include "nsIStreamListener.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsIChannel.h" +#include "nsIProgressEventSink.h" +#include "nsIFile.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsIWebProgressListener2.h" +#include "nsIWebBrowserPersist.h" +#include "nsIWebBrowserPersistDocument.h" + +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/UniquePtr.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsTArray.h" + +class nsIStorageStream; +class nsIWebBrowserPersistDocument; + +using ClosePromise = mozilla::MozPromise<nsresult, nsresult, true>; + +class nsWebBrowserPersist final : public nsIInterfaceRequestor, + public nsIWebBrowserPersist, + public nsIStreamListener, + public nsIThreadRetargetableStreamListener, + public nsIProgressEventSink, + public nsSupportsWeakReference { + friend class nsEncoderNodeFixup; + + // Public members + public: + nsWebBrowserPersist(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICANCELABLE + NS_DECL_NSIWEBBROWSERPERSIST + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSIPROGRESSEVENTSINK + + // Private members + private: + virtual ~nsWebBrowserPersist(); + nsresult SaveURIInternal(nsIURI* aURI, nsIPrincipal* aTriggeringPrincipal, + nsContentPolicyType aContentPolicyType, + uint32_t aCacheKey, nsIReferrerInfo* aReferrerInfo, + nsICookieJarSettings* aCookieJarSettings, + nsIInputStream* aPostData, const char* aExtraHeaders, + nsIURI* aFile, bool aCalcFileExt, bool aIsPrivate); + nsresult SaveChannelInternal(nsIChannel* aChannel, nsIURI* aFile, + bool aCalcFileExt); + nsresult SaveDocumentInternal(nsIWebBrowserPersistDocument* aDocument, + nsIURI* aFile, nsIURI* aDataPath); + nsresult SaveDocuments(); + void FinishSaveDocumentInternal(nsIURI* aFile, nsIFile* aDataPath); + nsresult GetExtensionForContentType(const char16_t* aContentType, + char16_t** aExt); + + struct CleanupData; + struct DocData; + struct OutputData; + struct UploadData; + struct URIData; + struct WalkData; + + class OnWalk; + class OnRemoteWalk; + class OnWrite; + class FlatURIMap; + friend class OnWalk; + friend class OnWrite; + + nsresult SaveDocumentDeferred(mozilla::UniquePtr<WalkData>&& aData); + void Cleanup(); + void CleanupLocalFiles(); + nsresult GetValidURIFromObject(nsISupports* aObject, nsIURI** aURI) const; + static nsresult GetLocalFileFromURI(nsIURI* aURI, nsIFile** aLocalFile); + static nsresult AppendPathToURI(nsIURI* aURI, const nsAString& aPath, + nsCOMPtr<nsIURI>& aOutURI); + nsresult MakeAndStoreLocalFilenameInURIMap( + nsIURI* aURI, nsIWebBrowserPersistDocument* aDoc, + nsContentPolicyType aContentPolicyType, bool aNeedsPersisting, + URIData** aData); + nsresult MakeOutputStream(nsIURI* aFile, nsIOutputStream** aOutputStream); + nsresult MakeOutputStreamFromFile(nsIFile* aFile, + nsIOutputStream** aOutputStream); + nsresult MakeOutputStreamFromURI(nsIURI* aURI, nsIOutputStream** aOutStream); + nsresult CreateChannelFromURI(nsIURI* aURI, nsIChannel** aChannel); + nsresult StartUpload(nsIStorageStream* aOutStream, nsIURI* aDestinationURI, + const nsACString& aContentType); + nsresult StartUpload(nsIInputStream* aInputStream, nsIURI* aDestinationURI, + const nsACString& aContentType); + nsresult CalculateAndAppendFileExt(nsIURI* aURI, nsIChannel* aChannel, + nsIURI* aOriginalURIWithExtension, + nsCOMPtr<nsIURI>& aOutURI); + nsresult CalculateUniqueFilename(nsIURI* aURI, nsCOMPtr<nsIURI>& aOutURI); + nsresult MakeFilenameFromURI(nsIURI* aURI, nsString& aFilename); + nsresult StoreURI(const nsACString& aURI, nsIWebBrowserPersistDocument* aDoc, + nsContentPolicyType aContentPolicyType, + bool aNeedsPersisting = true, URIData** aData = nullptr); + nsresult StoreURI(nsIURI* aURI, nsIWebBrowserPersistDocument* aDoc, + nsContentPolicyType aContentPolicyType, + bool aNeedsPersisting = true, URIData** aData = nullptr); + bool DocumentEncoderExists(const char* aContentType); + + nsresult SaveSubframeContent(nsIWebBrowserPersistDocument* aFrameContent, + nsIWebBrowserPersistDocument* aParentDocument, + const nsCString& aURISpec, URIData* aData); + nsresult SendErrorStatusChange(bool aIsReadError, nsresult aResult, + nsIRequest* aRequest, nsIURI* aURI); + + nsresult FixRedirectedChannelEntry(nsIChannel* aNewChannel); + + void FinishDownload(); + void EndDownload(nsresult aResult); + void EndDownloadInternal(nsresult aResult); + void SerializeNextFile(); + void CalcTotalProgress(); + + void SetApplyConversionIfNeeded(nsIChannel* aChannel); + + nsCOMPtr<nsIURI> mCurrentDataPath; + bool mCurrentDataPathIsRelative; + nsCString mCurrentRelativePathToData; + nsCOMPtr<nsIURI> mCurrentBaseURI; + nsCString mCurrentCharset; + nsCOMPtr<nsIURI> mTargetBaseURI; + uint32_t mCurrentThingsToPersist; + + nsCOMPtr<nsIMIMEService> mMIMEService; + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIWebProgressListener> mProgressListener; + /** + * Progress listener for 64-bit values; this is the same object as + * mProgressListener, but is a member to avoid having to qi it for each + * progress notification. + */ + nsCOMPtr<nsIWebProgressListener2> mProgressListener2; + nsCOMPtr<nsIProgressEventSink> mEventSink; + mozilla::Mutex mOutputMapMutex MOZ_UNANNOTATED; + nsClassHashtable<nsISupportsHashKey, OutputData> mOutputMap; + nsClassHashtable<nsISupportsHashKey, UploadData> mUploadList; + nsCOMPtr<nsISerialEventTarget> mBackgroundQueue; + nsTArray<RefPtr<ClosePromise>> mFileClosePromises; + nsClassHashtable<nsCStringHashKey, URIData> mURIMap; + nsCOMPtr<nsIWebBrowserPersistURIMap> mFlatURIMap; + nsTArray<mozilla::UniquePtr<WalkData>> mWalkStack; + nsTArray<DocData*> mDocList; + nsTArray<CleanupData*> mCleanupList; + nsTArray<nsCString> mFilenameList; + bool mFirstAndOnlyUse; + bool mSavingDocument; + // mCancel is used from both the main thread, and (inside OnDataAvailable) + // from a background thread. + mozilla::Atomic<bool> mCancel; + bool mEndCalled; + bool mCompleted; + bool mStartSaving; + bool mReplaceExisting; + bool mSerializingOutput; + bool mIsPrivate; + // mPersistFlags can be modified on the main thread, and can be read from + // a background thread when OnDataAvailable calls MakeOutputStreamFromFile. + mozilla::Atomic<uint32_t> mPersistFlags; + nsresult mPersistResult; + int64_t mTotalCurrentProgress; + int64_t mTotalMaxProgress; + int16_t mWrapColumn; + uint32_t mEncodingFlags; + nsString mContentType; +}; + +#endif |