/* -*- 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 #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 mDocument; nsCOMPtr mFile; nsCOMPtr mDataPath; }; // Information about a DOM document struct nsWebBrowserPersist::DocData { nsCOMPtr mBaseURI; nsCOMPtr mDocument; nsCOMPtr 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 mFile; nsCOMPtr mDataPath; nsCOMPtr mRelativeDocumentURI; nsCOMPtr mTriggeringPrincipal; nsCOMPtr 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 mFile; nsCOMPtr mOriginalLocation; nsCOMPtr 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 mFile; int64_t mSelfProgress; int64_t mSelfProgressMax; explicit UploadData(nsIURI* aFile) : mFile(aFile), mSelfProgress(0), mSelfProgressMax(10000) {} }; struct nsWebBrowserPersist::CleanupData { nsCOMPtr 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 mParent; nsCOMPtr mFile; nsCOMPtr 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 mVisitor; nsCOMPtr 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 mParent; nsCOMPtr mFile; nsCOMPtr 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 mMapFrom; nsTArray 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 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 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 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 fileAsURI; nsCOMPtr 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 doc = do_QueryInterface(aDocument); if (!doc) { nsCOMPtr 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 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 destChannel; CreateChannelFromURI(aDestinationURI, getter_AddRefs(destChannel)); nsCOMPtr 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 keyPtr = do_QueryInterface(destChannel); mUploadList.InsertOrUpdate(keyPtr, MakeUnique(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 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 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(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 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 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 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 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 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 channel = do_QueryInterface(request); NS_ENSURE_TRUE(channel, NS_ERROR_FAILURE); nsCOMPtr 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 r = do_QueryInterface(request); // Determine if we're uploading. Only use OMT onDataAvailable if not. nsCOMPtr 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 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 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 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 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 channel = do_QueryInterface(request); NS_ENSURE_TRUE(channel, NS_ERROR_FAILURE); MutexAutoLock lock(mOutputMapMutex); nsCOMPtr 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 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 req = readError ? request : nullptr; nsCOMPtr file = data->mFile; RefPtr 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 endOnMainThread = NewRunnableMethod( "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 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 file; GetLocalFileFromURI(aURI, getter_AddRefs(file)); AutoTArray 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 s = do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && s, NS_ERROR_FAILURE); nsCOMPtr 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 objAsFile = do_QueryInterface(aObject); if (objAsFile) { return NS_NewFileURI(aURI, objAsFile); } nsCOMPtr 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 fileURL = do_QueryInterface(aURI, &rv); if (NS_FAILED(rv)) return rv; nsCOMPtr 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& 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 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 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(this), loadFlags); nsCOMPtr 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 encodedChannel(do_QueryInterface(inputChannel)); if (encodedChannel) { encodedChannel->SetApplyConversion(false); } } nsCOMPtr loadInfo = inputChannel->LoadInfo(); loadInfo->SetIsUserTriggeredSave(true); // Set the referrer, post data and headers if any nsCOMPtr httpChannel(do_QueryInterface(inputChannel)); if (httpChannel) { if (aReferrerInfo) { DebugOnly success = httpChannel->SetReferrerInfo(aReferrerInfo); MOZ_ASSERT(NS_SUCCEEDED(success)); } // Post data if (aPostData) { nsCOMPtr stream(do_QueryInterface(aPostData)); if (stream) { // Rewind the postdata stream stream->Seek(nsISeekableStream::NS_SEEK_SET, 0); nsCOMPtr 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 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 fc(do_QueryInterface(aChannel)); nsCOMPtr fu(do_QueryInterface(aFile)); if (fc && !fu) { nsCOMPtr 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 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 keyPtr = do_QueryInterface(aChannel); mOutputMap.InsertOrUpdate(keyPtr, MakeUnique(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&& 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 localFile; rv = GetLocalFileFromURI(aFile, getter_AddRefs(localFile)); nsCOMPtr 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 baseDir; localFile->GetParent(getter_AddRefs(baseDir)); nsAutoCString relativePathToData; nsCOMPtr 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 newDataDirParent; rv = dataDirParent->GetParent(getter_AddRefs(newDataDirParent)); dataDirParent = newDataDirParent; } } else { // generate a relative path if possible nsCOMPtr 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 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 context = aContext->Canonical(); if (NS_WARN_IF(!context->GetCurrentWindowGlobal())) { EndVisit(nullptr, NS_ERROR_FAILURE); return NS_ERROR_FAILURE; } UniquePtr actor( new WebBrowserPersistDocumentParent()); nsCOMPtr receiver = new OnRemoteWalk(this, aDoc); actor->SetOnReady(receiver); RefPtr 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 toWalk = mWalkStack.PopLastElement(); // Bounce this off the event loop to avoid stack overflow. using WalkStorage = StoreCopyPassByRRef; auto saveMethod = &nsWebBrowserPersist::SaveDocumentDeferred; nsCOMPtr saveLater = NewRunnableMethod( "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 outputMapCopy; { MutexAutoLock lock(mOutputMapMutex); mOutputMap.SwapElements(outputMapCopy); } for (const auto& key : outputMapCopy.Keys()) { nsCOMPtr channel = do_QueryInterface(key); if (channel) { channel->Cancel(NS_BINDING_ABORTED); } } outputMapCopy.Clear(); for (const auto& key : mUploadList.Keys()) { nsCOMPtr 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 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 dirStack; int32_t stackSize = 0; // Push the top level enum onto the stack nsCOMPtr pos; if (NS_SUCCEEDED(file->GetDirectoryEntries(getter_AddRefs(pos)))) dirStack.AppendObject(pos); while (isEmptyDirectory && (stackSize = dirStack.Count())) { // Pop the last element nsCOMPtr curPos; curPos = dirStack[stackSize - 1]; dirStack.RemoveObjectAt(stackSize - 1); nsCOMPtr 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 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& aOutURI) { nsCOMPtr 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 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 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& 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 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 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 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 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 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 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 originalURI; aNewChannel->GetOriginalURI(getter_AddRefs(originalURI)); nsISupports* matchingKey = nullptr; for (nsISupports* key : mOutputMap.Keys()) { nsCOMPtr thisChannel = do_QueryInterface(key); nsCOMPtr 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; 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 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 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 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 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 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 dataFile; rv = GetLocalFileFromURI(mFile, getter_AddRefs(dataFile)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr docFile; rv = GetLocalFileFromURI(targetBaseURI, getter_AddRefs(docFile)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 docURI; rv = NS_NewURI(getter_AddRefs(docURI), aURISpec, mCurrentCharset.get()); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 frameURI = mCurrentDataPath; rv = AppendPathToURI(frameURI, filenameWithExt, frameURI); NS_ENSURE_SUCCESS(rv, rv); // Work out the path for the subframe data nsCOMPtr 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 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(); 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(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(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 encChannel = do_QueryInterface(aChannel, &rv); if (NS_FAILED(rv)) return; // Set the default conversion preference: encChannel->SetApplyConversion(false); nsCOMPtr thisURI; aChannel->GetURI(getter_AddRefs(thisURI)); nsCOMPtr sourceURL(do_QueryInterface(thisURI)); if (!sourceURL) return; nsAutoCString extension; sourceURL->GetFileExtension(extension); nsCOMPtr encEnum; encChannel->GetContentEncodings(getter_AddRefs(encEnum)); if (!encEnum) return; nsCOMPtr 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); } } }