/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "GetFilesHelper.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/Directory.h" #include "mozilla/dom/FileBlobImpl.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/UnionTypes.h" #include "mozilla/dom/IPCBlobUtils.h" #include "mozilla/ipc/IPCStreamUtils.h" #include "FileSystemUtils.h" #include "nsNetCID.h" #include "nsProxyRelease.h" namespace mozilla::dom { namespace { // This class is used in the DTOR of GetFilesHelper to release resources in the // correct thread. class ReleaseRunnable final : public Runnable { public: static void MaybeReleaseOnMainThread( nsTArray>&& aPromises, nsTArray>&& aCallbacks) { if (NS_IsMainThread()) { return; } RefPtr runnable = new ReleaseRunnable(std::move(aPromises), std::move(aCallbacks)); FileSystemUtils::DispatchRunnable(nullptr, runnable.forget()); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); mPromises.Clear(); mCallbacks.Clear(); return NS_OK; } private: ReleaseRunnable(nsTArray>&& aPromises, nsTArray>&& aCallbacks) : Runnable("dom::ReleaseRunnable"), mPromises(std::move(aPromises)), mCallbacks(std::move(aCallbacks)) {} nsTArray> mPromises; nsTArray> mCallbacks; }; } // namespace /////////////////////////////////////////////////////////////////////////////// // GetFilesHelper Base class already_AddRefed GetFilesHelper::Create( const nsTArray& aFilesOrDirectory, bool aRecursiveFlag, ErrorResult& aRv) { RefPtr helper; if (XRE_IsParentProcess()) { helper = new GetFilesHelper(aRecursiveFlag); } else { helper = new GetFilesHelperChild(aRecursiveFlag); } nsAutoString directoryPath; for (uint32_t i = 0; i < aFilesOrDirectory.Length(); ++i) { const OwningFileOrDirectory& data = aFilesOrDirectory[i]; if (data.IsFile()) { if (!helper->mTargetBlobImplArray.AppendElement(data.GetAsFile()->Impl(), fallible)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } } else { MOZ_ASSERT(data.IsDirectory()); // We support the upload of only 1 top-level directory from our // directory picker. This means that we cannot have more than 1 // Directory object in aFilesOrDirectory array. MOZ_ASSERT(directoryPath.IsEmpty()); RefPtr directory = data.GetAsDirectory(); MOZ_ASSERT(directory); aRv = directory->GetFullRealPath(directoryPath); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } } } // No directories to explore. if (directoryPath.IsEmpty()) { helper->mListingCompleted = true; return helper.forget(); } MOZ_ASSERT(helper->mTargetBlobImplArray.IsEmpty()); helper->SetDirectoryPath(directoryPath); helper->Work(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return helper.forget(); } GetFilesHelper::GetFilesHelper(bool aRecursiveFlag) : Runnable("GetFilesHelper"), GetFilesHelperBase(aRecursiveFlag), mListingCompleted(false), mErrorResult(NS_OK), mMutex("GetFilesHelper::mMutex"), mCanceled(false) {} GetFilesHelper::~GetFilesHelper() { ReleaseRunnable::MaybeReleaseOnMainThread(std::move(mPromises), std::move(mCallbacks)); } void GetFilesHelper::AddPromise(Promise* aPromise) { MOZ_ASSERT(aPromise); // Still working. if (!mListingCompleted) { mPromises.AppendElement(aPromise); return; } MOZ_ASSERT(mPromises.IsEmpty()); ResolveOrRejectPromise(aPromise); } void GetFilesHelper::AddCallback(GetFilesCallback* aCallback) { MOZ_ASSERT(aCallback); // Still working. if (!mListingCompleted) { mCallbacks.AppendElement(aCallback); return; } MOZ_ASSERT(mCallbacks.IsEmpty()); RunCallback(aCallback); } void GetFilesHelper::Unlink() { mPromises.Clear(); mCallbacks.Clear(); { MutexAutoLock lock(mMutex); mCanceled = true; } Cancel(); } void GetFilesHelper::Traverse(nsCycleCollectionTraversalCallback& cb) { GetFilesHelper* tmp = this; NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises); } void GetFilesHelper::Work(ErrorResult& aRv) { nsCOMPtr target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); MOZ_ASSERT(target); aRv = target->Dispatch(this, NS_DISPATCH_NORMAL); } NS_IMETHODIMP GetFilesHelper::Run() { MOZ_ASSERT(!mDirectoryPath.IsEmpty()); MOZ_ASSERT(!mListingCompleted); // First step is to retrieve the list of file paths. // This happens in the I/O thread. if (!NS_IsMainThread()) { RunIO(); // If this operation has been canceled, we don't have to go back to // main-thread. if (IsCanceled()) { return NS_OK; } RefPtr runnable = this; return FileSystemUtils::DispatchRunnable(nullptr, runnable.forget()); } // We are here, but we should not do anything on this thread because, in the // meantime, the operation has been canceled. if (IsCanceled()) { return NS_OK; } OperationCompleted(); return NS_OK; } void GetFilesHelper::OperationCompleted() { // We mark the operation as completed here. mListingCompleted = true; // Let's process the pending promises. nsTArray> promises = std::move(mPromises); for (uint32_t i = 0; i < promises.Length(); ++i) { ResolveOrRejectPromise(promises[i]); } // Let's process the pending callbacks. nsTArray> callbacks = std::move(mCallbacks); for (uint32_t i = 0; i < callbacks.Length(); ++i) { RunCallback(callbacks[i]); } } void GetFilesHelper::RunIO() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!mDirectoryPath.IsEmpty()); MOZ_ASSERT(!mListingCompleted); nsCOMPtr file; mErrorResult = NS_NewLocalFile(mDirectoryPath, true, getter_AddRefs(file)); if (NS_WARN_IF(NS_FAILED(mErrorResult))) { return; } nsAutoString leafName; mErrorResult = file->GetLeafName(leafName); if (NS_WARN_IF(NS_FAILED(mErrorResult))) { return; } nsAutoString domPath; domPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL); domPath.Append(leafName); mErrorResult = ExploreDirectory(domPath, file); } nsresult GetFilesHelperBase::ExploreDirectory(const nsAString& aDOMPath, nsIFile* aFile) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aFile); // We check if this operation has to be terminated at each recursion. if (IsCanceled()) { return NS_OK; } nsCOMPtr entries; nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(entries)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } for (;;) { nsCOMPtr currFile; if (NS_WARN_IF(NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile)))) || !currFile) { break; } bool isLink, isSpecial, isFile, isDir; if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) || NS_FAILED(currFile->IsSpecial(&isSpecial))) || isSpecial || // Although we allow explicit individual selection of symlinks via the // file picker, we do not process symlinks in directory traversal. Our // specific policy decision is documented at // https://bugzilla.mozilla.org/show_bug.cgi?id=1813299#c20 isLink) { continue; } if (NS_WARN_IF(NS_FAILED(currFile->IsFile(&isFile)) || NS_FAILED(currFile->IsDirectory(&isDir))) || !(isFile || isDir)) { continue; } // The new domPath nsAutoString domPath; domPath.Assign(aDOMPath); if (!aDOMPath.EqualsLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL)) { domPath.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL); } nsAutoString leafName; if (NS_WARN_IF(NS_FAILED(currFile->GetLeafName(leafName)))) { continue; } domPath.Append(leafName); if (isFile) { RefPtr blobImpl = new FileBlobImpl(currFile); blobImpl->SetDOMPath(domPath); if (!mTargetBlobImplArray.AppendElement(blobImpl, fallible)) { return NS_ERROR_OUT_OF_MEMORY; } continue; } MOZ_ASSERT(isDir); if (!mRecursiveFlag) { continue; } // Recursive. rv = ExploreDirectory(domPath, currFile); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } return NS_OK; } void GetFilesHelper::ResolveOrRejectPromise(Promise* aPromise) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mListingCompleted); MOZ_ASSERT(aPromise); Sequence> files; if (NS_SUCCEEDED(mErrorResult)) { for (uint32_t i = 0; i < mTargetBlobImplArray.Length(); ++i) { RefPtr domFile = File::Create(aPromise->GetParentObject(), mTargetBlobImplArray[i]); if (NS_WARN_IF(!domFile)) { mErrorResult = NS_ERROR_FAILURE; files.Clear(); break; } if (!files.AppendElement(domFile, fallible)) { mErrorResult = NS_ERROR_OUT_OF_MEMORY; files.Clear(); break; } } } // Error propagation. if (NS_FAILED(mErrorResult)) { aPromise->MaybeReject(mErrorResult); return; } aPromise->MaybeResolve(files); } void GetFilesHelper::RunCallback(GetFilesCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mListingCompleted); MOZ_ASSERT(aCallback); aCallback->Callback(mErrorResult, mTargetBlobImplArray); } /////////////////////////////////////////////////////////////////////////////// // GetFilesHelperChild class void GetFilesHelperChild::Work(ErrorResult& aRv) { ContentChild* cc = ContentChild::GetSingleton(); if (NS_WARN_IF(!cc)) { aRv.Throw(NS_ERROR_FAILURE); return; } aRv = nsID::GenerateUUIDInPlace(mUUID); if (NS_WARN_IF(aRv.Failed())) { return; } mPendingOperation = true; cc->CreateGetFilesRequest(mDirectoryPath, mRecursiveFlag, mUUID, this); } void GetFilesHelperChild::Cancel() { if (!mPendingOperation) { return; } ContentChild* cc = ContentChild::GetSingleton(); if (NS_WARN_IF(!cc)) { return; } mPendingOperation = false; cc->DeleteGetFilesRequest(mUUID, this); } bool GetFilesHelperChild::AppendBlobImpl(BlobImpl* aBlobImpl) { MOZ_ASSERT(mPendingOperation); MOZ_ASSERT(aBlobImpl); MOZ_ASSERT(aBlobImpl->IsFile()); return mTargetBlobImplArray.AppendElement(aBlobImpl, fallible); } void GetFilesHelperChild::Finished(nsresult aError) { MOZ_ASSERT(mPendingOperation); MOZ_ASSERT(NS_SUCCEEDED(mErrorResult)); mPendingOperation = false; mErrorResult = aError; OperationCompleted(); } /////////////////////////////////////////////////////////////////////////////// // GetFilesHelperParent class class GetFilesHelperParentCallback final : public GetFilesCallback { public: explicit GetFilesHelperParentCallback(GetFilesHelperParent* aParent) : mParent(aParent) { MOZ_ASSERT(aParent); } void Callback(nsresult aStatus, const FallibleTArray>& aBlobImpls) override { if (NS_FAILED(aStatus)) { mParent->mContentParent->SendGetFilesResponseAndForget( mParent->mUUID, GetFilesResponseFailure(aStatus)); return; } GetFilesResponseSuccess success; nsTArray& ipcBlobs = success.blobs(); ipcBlobs.SetLength(aBlobImpls.Length()); for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) { nsresult rv = IPCBlobUtils::Serialize(aBlobImpls[i], ipcBlobs[i]); if (NS_WARN_IF(NS_FAILED(rv))) { mParent->mContentParent->SendGetFilesResponseAndForget( mParent->mUUID, GetFilesResponseFailure(NS_ERROR_OUT_OF_MEMORY)); return; } } mParent->mContentParent->SendGetFilesResponseAndForget(mParent->mUUID, success); } private: // Raw pointer because this callback is kept alive by this parent object. GetFilesHelperParent* mParent; }; GetFilesHelperParent::GetFilesHelperParent(const nsID& aUUID, ContentParent* aContentParent, bool aRecursiveFlag) : GetFilesHelper(aRecursiveFlag), mContentParent(aContentParent), mUUID(aUUID) {} GetFilesHelperParent::~GetFilesHelperParent() { NS_ReleaseOnMainThread("GetFilesHelperParent::mContentParent", mContentParent.forget()); } /* static */ already_AddRefed GetFilesHelperParent::Create( const nsID& aUUID, const nsAString& aDirectoryPath, bool aRecursiveFlag, ContentParent* aContentParent, ErrorResult& aRv) { MOZ_ASSERT(aContentParent); RefPtr helper = new GetFilesHelperParent(aUUID, aContentParent, aRecursiveFlag); helper->SetDirectoryPath(aDirectoryPath); helper->Work(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } RefPtr callback = new GetFilesHelperParentCallback(helper); helper->AddCallback(callback); return helper.forget(); } } // namespace mozilla::dom