diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/filesystem | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/filesystem')
58 files changed, 7238 insertions, 0 deletions
diff --git a/dom/filesystem/Directory.cpp b/dom/filesystem/Directory.cpp new file mode 100644 index 0000000000..da5d6e5515 --- /dev/null +++ b/dom/filesystem/Directory.cpp @@ -0,0 +1,198 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/Directory.h" + +#include "GetDirectoryListingTask.h" +#include "GetFilesTask.h" + +#include "nsIFile.h" +#include "nsString.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/DirectoryBinding.h" +#include "mozilla/dom/FileSystemBase.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/OSFileSystem.h" +#include "mozilla/dom/WorkerPrivate.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Directory) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Directory) + if (tmp->mFileSystem) { + tmp->mFileSystem->Unlink(); + tmp->mFileSystem = nullptr; + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Directory) + if (tmp->mFileSystem) { + tmp->mFileSystem->Traverse(cb); + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Directory) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Directory) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Directory) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed<Directory> Directory::Constructor(const GlobalObject& aGlobal, + const nsAString& aRealPath, + ErrorResult& aRv) { + nsCOMPtr<nsIFile> path; + aRv = NS_NewLocalFile(aRealPath, true, getter_AddRefs(path)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return Create(global, path); +} + +/* static */ +already_AddRefed<Directory> Directory::Create(nsIGlobalObject* aGlobal, + nsIFile* aFile, + FileSystemBase* aFileSystem) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aFile); + + RefPtr<Directory> directory = new Directory(aGlobal, aFile, aFileSystem); + return directory.forget(); +} + +Directory::Directory(nsIGlobalObject* aGlobal, nsIFile* aFile, + FileSystemBase* aFileSystem) + : mGlobal(aGlobal), mFile(aFile) { + MOZ_ASSERT(aFile); + + // aFileSystem can be null. In this case we create a OSFileSystem when needed. + if (aFileSystem) { + // More likely, this is a OSFileSystem. This object keeps a reference of + // mGlobal but it's not cycle collectable and to avoid manual + // addref/release, it's better to have 1 object per directory. For this + // reason we clone it here. + mFileSystem = aFileSystem->Clone(); + } +} + +Directory::~Directory() = default; + +nsIGlobalObject* Directory::GetParentObject() const { return mGlobal; } + +JSObject* Directory::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Directory_Binding::Wrap(aCx, this, aGivenProto); +} + +void Directory::GetName(nsAString& aRetval, ErrorResult& aRv) { + aRetval.Truncate(); + + RefPtr<FileSystemBase> fs = GetFileSystem(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + fs->GetDirectoryName(mFile, aRetval, aRv); +} + +void Directory::GetPath(nsAString& aRetval, ErrorResult& aRv) { + // This operation is expensive. Better to cache the result. + if (mPath.IsEmpty()) { + RefPtr<FileSystemBase> fs = GetFileSystem(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + fs->GetDOMPath(mFile, mPath, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + aRetval = mPath; +} + +nsresult Directory::GetFullRealPath(nsAString& aPath) { + nsresult rv = mFile->GetPath(aPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +already_AddRefed<Promise> Directory::GetFilesAndDirectories(ErrorResult& aRv) { + RefPtr<FileSystemBase> fs = GetFileSystem(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<GetDirectoryListingTaskChild> task = + GetDirectoryListingTaskChild::Create(fs, this, mFile, mFilters, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + task->Start(); + + return task->GetPromise(); +} + +already_AddRefed<Promise> Directory::GetFiles(bool aRecursiveFlag, + ErrorResult& aRv) { + ErrorResult rv; + RefPtr<FileSystemBase> fs = GetFileSystem(rv); + if (NS_WARN_IF(rv.Failed())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + RefPtr<GetFilesTaskChild> task = + GetFilesTaskChild::Create(fs, this, mFile, aRecursiveFlag, rv); + if (NS_WARN_IF(rv.Failed())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + task->Start(); + + return task->GetPromise(); +} + +void Directory::SetContentFilters(const nsAString& aFilters) { + mFilters = aFilters; +} + +FileSystemBase* Directory::GetFileSystem(ErrorResult& aRv) { + if (!mFileSystem) { + nsAutoString path; + aRv = mFile->GetPath(path); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<OSFileSystem> fs = new OSFileSystem(path); + fs->Init(mGlobal); + + mFileSystem = fs; + } + + return mFileSystem; +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/Directory.h b/dom/filesystem/Directory.h new file mode 100644 index 0000000000..b8ee0e68e1 --- /dev/null +++ b/dom/filesystem/Directory.h @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_Directory_h +#define mozilla_dom_Directory_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/File.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class FileSystemBase; +class Promise; +class StringOrFileOrDirectory; + +class Directory final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Directory) + + static already_AddRefed<Directory> Constructor(const GlobalObject& aGlobal, + const nsAString& aRealPath, + ErrorResult& aRv); + + static already_AddRefed<Directory> Create(nsIGlobalObject* aGlobal, + nsIFile* aDirectory, + FileSystemBase* aFileSystem = 0); + + // ========= Begin WebIDL bindings. =========== + + nsIGlobalObject* GetParentObject() const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetName(nsAString& aRetval, ErrorResult& aRv); + + // From + // https://microsoftedge.github.io/directory-upload/proposal.html#directory-interface + // : + + void GetPath(nsAString& aRetval, ErrorResult& aRv); + + nsresult GetFullRealPath(nsAString& aPath); + + already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv); + + already_AddRefed<Promise> GetFiles(bool aRecursiveFlag, ErrorResult& aRv); + + // =========== End WebIDL bindings.============ + + /** + * Sets a semi-colon separated list of filters to filter-in or filter-out + * certain types of files when the contents of this directory are requested + * via a GetFilesAndDirectories() call. + * + * Currently supported keywords: + * + * * filter-out-sensitive + * This keyword filters out files or directories that we don't wish to + * make available to Web content because we are concerned that there is + * a risk that users may unwittingly give Web content access to them + * and suffer undesirable consequences. The details of what is + * filtered out can be found in GetDirectoryListingTask::Work. + * + * In future, we will likely support filtering based on filename extensions + * (for example, aFilters could be "*.jpg; *.jpeg; *.gif"), but that isn't + * supported yet. Once supported, files that don't match a specified + * extension (if any are specified) would be filtered out. This + * functionality would allow us to apply the 'accept' attribute from + * <input type=file directory accept="..."> to the results of a directory + * picker operation. + */ + void SetContentFilters(const nsAString& aFilters); + + FileSystemBase* GetFileSystem(ErrorResult& aRv); + + nsIFile* GetInternalNsIFile() const { return mFile; } + + private: + Directory(nsIGlobalObject* aGlobal, nsIFile* aFile, + FileSystemBase* aFileSystem = nullptr); + ~Directory(); + + /* + * Convert relative DOM path to the absolute real path. + */ + nsresult DOMPathToRealPath(const nsAString& aPath, nsIFile** aFile) const; + + nsCOMPtr<nsIGlobalObject> mGlobal; + RefPtr<FileSystemBase> mFileSystem; + nsCOMPtr<nsIFile> mFile; + + nsString mFilters; + nsString mPath; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Directory_h diff --git a/dom/filesystem/FileSystemBase.cpp b/dom/filesystem/FileSystemBase.cpp new file mode 100644 index 0000000000..776ecc9db5 --- /dev/null +++ b/dom/filesystem/FileSystemBase.cpp @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/FileSystemBase.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "nsIFile.h" +#include "OSFileSystem.h" + +namespace mozilla::dom { + +FileSystemBase::FileSystemBase() : mShutdown(false) {} + +FileSystemBase::~FileSystemBase() { AssertIsOnOwningThread(); } + +void FileSystemBase::Shutdown() { + AssertIsOnOwningThread(); + mShutdown = true; +} + +nsIGlobalObject* FileSystemBase::GetParentObject() const { + AssertIsOnOwningThread(); + return nullptr; +} + +bool FileSystemBase::GetRealPath(BlobImpl* aFile, nsIFile** aPath) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aFile, "aFile Should not be null."); + MOZ_ASSERT(aPath); + + nsAutoString filePath; + ErrorResult rv; + aFile->GetMozFullPathInternal(filePath, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return false; + } + + rv = NS_NewLocalFile(filePath, true, aPath); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return false; + } + + return true; +} + +bool FileSystemBase::IsSafeFile(nsIFile* aFile) const { + AssertIsOnOwningThread(); + return false; +} + +bool FileSystemBase::IsSafeDirectory(Directory* aDir) const { + AssertIsOnOwningThread(); + return false; +} + +void FileSystemBase::GetDirectoryName(nsIFile* aFile, nsAString& aRetval, + ErrorResult& aRv) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aFile); + + aRv = aFile->GetLeafName(aRetval); + NS_WARNING_ASSERTION(!aRv.Failed(), "GetLeafName failed"); +} + +void FileSystemBase::GetDOMPath(nsIFile* aFile, nsAString& aRetval, + ErrorResult& aRv) const { + AssertIsOnOwningThread(); + MOZ_ASSERT(aFile); + + aRetval.Truncate(); + + nsCOMPtr<nsIFile> fileSystemPath; + aRv = NS_NewLocalFile(LocalRootPath(), true, getter_AddRefs(fileSystemPath)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIFile> path; + aRv = aFile->Clone(getter_AddRefs(path)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsTArray<nsString> parts; + + while (true) { + nsAutoString leafName; + aRv = path->GetLeafName(leafName); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (!leafName.IsEmpty()) { + parts.AppendElement(leafName); + } + + bool equal = false; + aRv = fileSystemPath->Equals(path, &equal); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (equal) { + break; + } + + nsCOMPtr<nsIFile> parentPath; + aRv = path->GetParent(getter_AddRefs(parentPath)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_ASSERT(parentPath); + + aRv = parentPath->Clone(getter_AddRefs(path)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + if (parts.IsEmpty()) { + aRetval.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL); + return; + } + + for (int32_t i = parts.Length() - 1; i >= 0; --i) { + aRetval.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL); + aRetval.Append(parts[i]); + } +} + +void FileSystemBase::AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(FileSystemBase); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/FileSystemBase.h b/dom/filesystem/FileSystemBase.h new file mode 100644 index 0000000000..b78fe1736b --- /dev/null +++ b/dom/filesystem/FileSystemBase.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemBase_h +#define mozilla_dom_FileSystemBase_h + +#include "nsString.h" +#include "Directory.h" + +namespace mozilla::dom { + +class BlobImpl; + +class FileSystemBase { + public: + NS_INLINE_DECL_REFCOUNTING(FileSystemBase) + + FileSystemBase(); + + virtual void Shutdown(); + + // SerializeDOMPath the FileSystem to string. + virtual void SerializeDOMPath(nsAString& aOutput) const = 0; + + virtual already_AddRefed<FileSystemBase> Clone() = 0; + + virtual bool ShouldCreateDirectory() = 0; + + virtual nsIGlobalObject* GetParentObject() const; + + virtual void GetDirectoryName(nsIFile* aFile, nsAString& aRetval, + ErrorResult& aRv) const; + + void GetDOMPath(nsIFile* aFile, nsAString& aRetval, ErrorResult& aRv) const; + + /* + * Return the local root path of the FileSystem implementation. + * For OSFileSystem, this is equal to the path of the root Directory; + * For DeviceStorageFileSystem, this is the path of the SDCard, parent + * directory of the exposed root Directory (per type). + */ + const nsAString& LocalRootPath() const { return mLocalRootPath; } + + bool IsShutdown() const { return mShutdown; } + + virtual bool IsSafeFile(nsIFile* aFile) const; + + virtual bool IsSafeDirectory(Directory* aDir) const; + + bool GetRealPath(BlobImpl* aFile, nsIFile** aPath) const; + + // CC methods + virtual void Unlink() {} + virtual void Traverse(nsCycleCollectionTraversalCallback& cb) {} + + void AssertIsOnOwningThread() const; + + protected: + virtual ~FileSystemBase(); + + // The local path of the root (i.e. the OS path, with OS path separators, of + // the OS directory that acts as the root of this OSFileSystem). + // This path must be set by the FileSystem implementation immediately + // because it will be used for the validation of any FileSystemTaskChildBase. + // The concept of this path is that, any task will never go out of it and this + // must be considered the OS 'root' of the current FileSystem. Different + // Directory object can have different OS 'root' path. + // To be more clear, any path managed by this FileSystem implementation must + // be discendant of this local root path. + nsString mLocalRootPath; + + bool mShutdown; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemBase_h diff --git a/dom/filesystem/FileSystemRequestParent.cpp b/dom/filesystem/FileSystemRequestParent.cpp new file mode 100644 index 0000000000..25211dbd23 --- /dev/null +++ b/dom/filesystem/FileSystemRequestParent.cpp @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/FileSystemRequestParent.h" +#include "mozilla/dom/PFileSystemParams.h" + +#include "GetDirectoryListingTask.h" +#include "GetFileOrDirectoryTask.h" +#include "GetFilesTask.h" + +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/FileSystemBase.h" +#include "mozilla/dom/FileSystemSecurity.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/dom/OSFileSystem.h" +#include "mozilla/Preferences.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "nsProxyRelease.h" + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +FileSystemRequestParent::FileSystemRequestParent() : mDestroyed(false) { + AssertIsOnBackgroundThread(); +} + +FileSystemRequestParent::~FileSystemRequestParent() { + AssertIsOnBackgroundThread(); +} + +#define FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(name) \ + case FileSystemParams::TFileSystem##name##Params: { \ + const FileSystem##name##Params& p = aParams; \ + mFileSystem = new OSFileSystemParent(p.filesystem()); \ + MOZ_ASSERT(mFileSystem); \ + mTask = name##TaskParent::Create(mFileSystem, p, this, rv); \ + if (NS_WARN_IF(rv.Failed())) { \ + rv.SuppressException(); \ + return false; \ + } \ + break; \ + } + +bool FileSystemRequestParent::Initialize(const FileSystemParams& aParams) { + AssertIsOnBackgroundThread(); + + ErrorResult rv; + + switch (aParams.type()) { + FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(GetDirectoryListing) + FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(GetFileOrDirectory) + FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(GetFiles) + + default: { + MOZ_CRASH("not reached"); + break; + } + } + + if (NS_WARN_IF(!mTask || !mFileSystem)) { + // Should never reach here. + return false; + } + + return true; +} + +namespace { + +class CheckPermissionRunnable final : public Runnable { + public: + CheckPermissionRunnable( + already_AddRefed<ThreadsafeContentParentHandle> aParent, + FileSystemRequestParent* aActor, FileSystemTaskParentBase* aTask, + const nsAString& aPath) + : Runnable("dom::CheckPermissionRunnable"), + mContentHandle(aParent), + mActor(aActor), + mTask(aTask), + mPath(aPath), + mBackgroundEventTarget(GetCurrentSerialEventTarget()) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(mContentHandle); + MOZ_ASSERT(mActor); + MOZ_ASSERT(mTask); + MOZ_ASSERT(mBackgroundEventTarget); + } + + NS_IMETHOD + Run() override { + if (NS_IsMainThread()) { + if (!mozilla::Preferences::GetBool("dom.filesystem.pathcheck.disabled", + false)) { + RefPtr<FileSystemSecurity> fss = FileSystemSecurity::Get(); + if (NS_WARN_IF(!fss || !fss->ContentProcessHasAccessTo( + mContentHandle->ChildID(), mPath))) { + AssertIsOnMainThread(); + if (RefPtr<ContentParent> contentParent = + mContentHandle->GetContentParent()) { + contentParent->KillHard("This path is not allowed."); + } + return NS_OK; + } + } + + return mBackgroundEventTarget->Dispatch(this, NS_DISPATCH_NORMAL); + } + + AssertIsOnBackgroundThread(); + + // It can happen that this actor has been destroyed in the meantime we were + // on the main-thread. + if (!mActor->Destroyed()) { + mTask->Start(); + } + + return NS_OK; + } + + private: + ~CheckPermissionRunnable() { + NS_ProxyRelease("CheckPermissionRunnable::mActor", mBackgroundEventTarget, + mActor.forget()); + } + + RefPtr<ThreadsafeContentParentHandle> mContentHandle; + RefPtr<FileSystemRequestParent> mActor; + RefPtr<FileSystemTaskParentBase> mTask; + const nsString mPath; + + nsCOMPtr<nsIEventTarget> mBackgroundEventTarget; +}; + +} // namespace + +void FileSystemRequestParent::Start() { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(!mDestroyed); + MOZ_ASSERT(mFileSystem); + MOZ_ASSERT(mTask); + + nsAutoString path; + if (NS_WARN_IF(NS_FAILED(mTask->GetTargetPath(path)))) { + (void)Send__delete__(this, + FileSystemErrorResponse(NS_ERROR_DOM_SECURITY_ERR)); + return; + } + + RefPtr<ThreadsafeContentParentHandle> parent = + BackgroundParent::GetContentParentHandle(Manager()); + + // If the ThreadsafeContentParentHandle is null we are dealing with a + // same-process actor. + if (!parent) { + mTask->Start(); + return; + } + + RefPtr<Runnable> runnable = + new CheckPermissionRunnable(parent.forget(), this, mTask, path); + NS_DispatchToMainThread(runnable); +} + +void FileSystemRequestParent::ActorDestroy(ActorDestroyReason aWhy) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mDestroyed); + + if (!mFileSystem) { + return; + } + + mFileSystem->Shutdown(); + mFileSystem = nullptr; + mTask = nullptr; + mDestroyed = true; +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/FileSystemRequestParent.h b/dom/filesystem/FileSystemRequestParent.h new file mode 100644 index 0000000000..7ac1547d27 --- /dev/null +++ b/dom/filesystem/FileSystemRequestParent.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemRequestParent_h +#define mozilla_dom_FileSystemRequestParent_h + +#include "mozilla/dom/PFileSystemRequestParent.h" +#include "mozilla/dom/FileSystemBase.h" + +namespace mozilla::dom { + +class FileSystemParams; +class FileSystemTaskParentBase; + +class FileSystemRequestParent final : public PFileSystemRequestParent { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FileSystemRequestParent, final) + + public: + FileSystemRequestParent(); + + bool Initialize(const FileSystemParams& aParams); + + void Start(); + + bool Destroyed() const { return mDestroyed; } + + virtual void ActorDestroy(ActorDestroyReason why) override; + + private: + ~FileSystemRequestParent(); + + RefPtr<FileSystemBase> mFileSystem; + RefPtr<FileSystemTaskParentBase> mTask; + + bool mDestroyed; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemRequestParent_h diff --git a/dom/filesystem/FileSystemSecurity.cpp b/dom/filesystem/FileSystemSecurity.cpp new file mode 100644 index 0000000000..b9468270d4 --- /dev/null +++ b/dom/filesystem/FileSystemSecurity.cpp @@ -0,0 +1,106 @@ +/* -*- 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 "FileSystemSecurity.h" +#include "FileSystemUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla::dom { + +namespace { + +StaticRefPtr<FileSystemSecurity> gFileSystemSecurity; + +} // namespace + +/* static */ +already_AddRefed<FileSystemSecurity> FileSystemSecurity::Get() { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::ipc::AssertIsInMainProcess(); + + RefPtr<FileSystemSecurity> service = gFileSystemSecurity.get(); + return service.forget(); +} + +/* static */ +already_AddRefed<FileSystemSecurity> FileSystemSecurity::GetOrCreate() { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::ipc::AssertIsInMainProcess(); + + if (!gFileSystemSecurity) { + gFileSystemSecurity = new FileSystemSecurity(); + ClearOnShutdown(&gFileSystemSecurity); + } + + RefPtr<FileSystemSecurity> service = gFileSystemSecurity.get(); + return service.forget(); +} + +FileSystemSecurity::FileSystemSecurity() { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::ipc::AssertIsInMainProcess(); +} + +FileSystemSecurity::~FileSystemSecurity() { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::ipc::AssertIsInMainProcess(); +} + +void FileSystemSecurity::GrantAccessToContentProcess( + ContentParentId aId, const nsAString& aDirectoryPath) { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::ipc::AssertIsInMainProcess(); + + mPaths.WithEntryHandle(aId, [&](auto&& entry) { + if (entry && entry.Data()->Contains(aDirectoryPath)) { + return; + } + + entry.OrInsertWith([] { return MakeUnique<nsTArray<nsString>>(); }) + ->AppendElement(aDirectoryPath); + }); +} + +void FileSystemSecurity::Forget(ContentParentId aId) { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::ipc::AssertIsInMainProcess(); + + mPaths.Remove(aId); +} + +bool FileSystemSecurity::ContentProcessHasAccessTo(ContentParentId aId, + const nsAString& aPath) { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::ipc::AssertIsInMainProcess(); + +#if defined(XP_WIN) + if (StringBeginsWith(aPath, u"..\\"_ns) || + FindInReadable(u"\\..\\"_ns, aPath)) { + return false; + } +#elif defined(XP_UNIX) + if (StringBeginsWith(aPath, u"../"_ns) || FindInReadable(u"/../"_ns, aPath)) { + return false; + } +#endif + + nsTArray<nsString>* paths; + if (!mPaths.Get(aId, &paths)) { + return false; + } + + for (uint32_t i = 0, len = paths->Length(); i < len; ++i) { + if (FileSystemUtils::IsDescendantPath(paths->ElementAt(i), aPath)) { + return true; + } + } + + return false; +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/FileSystemSecurity.h b/dom/filesystem/FileSystemSecurity.h new file mode 100644 index 0000000000..381d156777 --- /dev/null +++ b/dom/filesystem/FileSystemSecurity.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemSecurity_h +#define mozilla_dom_FileSystemSecurity_h + +#include "mozilla/dom/ipc/IdType.h" +#include "nsClassHashtable.h" +#include "nsISupportsImpl.h" + +namespace mozilla::dom { + +class FileSystemSecurity final { + public: + NS_INLINE_DECL_REFCOUNTING(FileSystemSecurity) + + static already_AddRefed<FileSystemSecurity> Get(); + + static already_AddRefed<FileSystemSecurity> GetOrCreate(); + + void GrantAccessToContentProcess(ContentParentId aId, + const nsAString& aDirectoryPath); + + void Forget(ContentParentId aId); + + bool ContentProcessHasAccessTo(ContentParentId aId, const nsAString& aPath); + + private: + FileSystemSecurity(); + ~FileSystemSecurity(); + + nsClassHashtable<nsUint64HashKey, nsTArray<nsString>> mPaths; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemSecurity_h diff --git a/dom/filesystem/FileSystemTaskBase.cpp b/dom/filesystem/FileSystemTaskBase.cpp new file mode 100644 index 0000000000..34cfd8f018 --- /dev/null +++ b/dom/filesystem/FileSystemTaskBase.cpp @@ -0,0 +1,246 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/FileSystemTaskBase.h" + +#include "nsNetCID.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileSystemBase.h" +#include "mozilla/dom/FileSystemRequestParent.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/Unused.h" +#include "nsProxyRelease.h" + +namespace mozilla::dom { + +namespace { + +nsresult FileSystemErrorFromNsError(const nsresult& aErrorValue) { + uint16_t module = NS_ERROR_GET_MODULE(aErrorValue); + if (module == NS_ERROR_MODULE_DOM_FILESYSTEM || + module == NS_ERROR_MODULE_DOM_FILE || module == NS_ERROR_MODULE_DOM) { + return aErrorValue; + } + + switch (aErrorValue) { + case NS_OK: + return NS_OK; + + case NS_ERROR_FILE_INVALID_PATH: + case NS_ERROR_FILE_UNRECOGNIZED_PATH: + return NS_ERROR_DOM_FILESYSTEM_INVALID_PATH_ERR; + + case NS_ERROR_FILE_DESTINATION_NOT_DIR: + return NS_ERROR_DOM_FILESYSTEM_INVALID_MODIFICATION_ERR; + + case NS_ERROR_FILE_ACCESS_DENIED: + case NS_ERROR_FILE_DIR_NOT_EMPTY: + return NS_ERROR_DOM_FILESYSTEM_NO_MODIFICATION_ALLOWED_ERR; + + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_NOT_AVAILABLE: + return NS_ERROR_DOM_FILE_NOT_FOUND_ERR; + + case NS_ERROR_FILE_ALREADY_EXISTS: + return NS_ERROR_DOM_FILESYSTEM_PATH_EXISTS_ERR; + + case NS_ERROR_FILE_NOT_DIRECTORY: + return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR; + + case NS_ERROR_UNEXPECTED: + default: + return NS_ERROR_DOM_FILESYSTEM_UNKNOWN_ERR; + } +} + +nsresult DispatchToIOThread(nsIRunnable* aRunnable) { + MOZ_ASSERT(aRunnable); + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target); + + return target->Dispatch(aRunnable, NS_DISPATCH_NORMAL); +} + +} // anonymous namespace + +/** + * FileSystemTaskBase class + */ + +FileSystemTaskChildBase::FileSystemTaskChildBase(nsIGlobalObject* aGlobalObject, + FileSystemBase* aFileSystem) + : mErrorValue(NS_OK), + mFileSystem(aFileSystem), + mGlobalObject(aGlobalObject) { + MOZ_ASSERT(aFileSystem, "aFileSystem should not be null."); + aFileSystem->AssertIsOnOwningThread(); + MOZ_ASSERT(aGlobalObject); +} + +FileSystemTaskChildBase::~FileSystemTaskChildBase() { + mFileSystem->AssertIsOnOwningThread(); +} + +FileSystemBase* FileSystemTaskChildBase::GetFileSystem() const { + mFileSystem->AssertIsOnOwningThread(); + return mFileSystem.get(); +} + +void FileSystemTaskChildBase::Start() { + mFileSystem->AssertIsOnOwningThread(); + + mozilla::ipc::PBackgroundChild* actor = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actor)) { + // We are probably shutting down. + return; + } + + nsAutoString serialization; + mFileSystem->SerializeDOMPath(serialization); + + ErrorResult rv; + FileSystemParams params = GetRequestParams(serialization, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return; + } + + actor->SendPFileSystemRequestConstructor(this, params); +} + +void FileSystemTaskChildBase::SetRequestResult( + const FileSystemResponseValue& aValue) { + mFileSystem->AssertIsOnOwningThread(); + + if (aValue.type() == FileSystemResponseValue::TFileSystemErrorResponse) { + FileSystemErrorResponse r = aValue; + mErrorValue = r.error(); + } else { + ErrorResult rv; + SetSuccessRequestResult(aValue, rv); + mErrorValue = rv.StealNSResult(); + } +} + +mozilla::ipc::IPCResult FileSystemTaskChildBase::Recv__delete__( + const FileSystemResponseValue& aValue) { + mFileSystem->AssertIsOnOwningThread(); + + SetRequestResult(aValue); + HandlerCallback(); + return IPC_OK(); +} + +void FileSystemTaskChildBase::SetError(const nsresult& aErrorValue) { + mErrorValue = FileSystemErrorFromNsError(aErrorValue); +} + +/** + * FileSystemTaskParentBase class + */ + +FileSystemTaskParentBase::FileSystemTaskParentBase( + FileSystemBase* aFileSystem, const FileSystemParams& aParam, + FileSystemRequestParent* aParent) + : Runnable("dom::FileSystemTaskParentBase"), + mErrorValue(NS_OK), + mFileSystem(aFileSystem), + mRequestParent(aParent), + mBackgroundEventTarget(GetCurrentSerialEventTarget()) { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + MOZ_ASSERT(aFileSystem, "aFileSystem should not be null."); + MOZ_ASSERT(aParent); + MOZ_ASSERT(mBackgroundEventTarget); + mozilla::ipc::AssertIsOnBackgroundThread(); +} + +FileSystemTaskParentBase::~FileSystemTaskParentBase() { + // This task can be released on different threads because we dispatch it (as + // runnable) to main-thread, I/O and then back to the PBackground thread. + NS_ProxyRelease("FileSystemTaskParentBase::mFileSystem", + mBackgroundEventTarget, mFileSystem.forget()); + NS_ProxyRelease("FileSystemTaskParentBase::mRequestParent", + mBackgroundEventTarget, mRequestParent.forget()); +} + +void FileSystemTaskParentBase::Start() { + mozilla::ipc::AssertIsOnBackgroundThread(); + mFileSystem->AssertIsOnOwningThread(); + + DebugOnly<nsresult> rv = DispatchToIOThread(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "DispatchToIOThread failed"); +} + +void FileSystemTaskParentBase::HandleResult() { + mozilla::ipc::AssertIsOnBackgroundThread(); + mFileSystem->AssertIsOnOwningThread(); + + if (mFileSystem->IsShutdown()) { + return; + } + + MOZ_ASSERT(mRequestParent); + (void)mRequestParent->Send__delete__(mRequestParent, GetRequestResult()); +} + +FileSystemResponseValue FileSystemTaskParentBase::GetRequestResult() const { + mozilla::ipc::AssertIsOnBackgroundThread(); + mFileSystem->AssertIsOnOwningThread(); + + if (HasError()) { + return FileSystemErrorResponse(mErrorValue); + } + + ErrorResult rv; + FileSystemResponseValue value = GetSuccessRequestResult(rv); + if (NS_WARN_IF(rv.Failed())) { + return FileSystemErrorResponse(rv.StealNSResult()); + } + + return value; +} + +void FileSystemTaskParentBase::SetError(const nsresult& aErrorValue) { + mErrorValue = FileSystemErrorFromNsError(aErrorValue); +} + +NS_IMETHODIMP +FileSystemTaskParentBase::Run() { + // This method can run in 2 different threads. Here why: + // 1. We are are on the I/O thread and we call IOWork(). + // 2. After step 1, it returns back to the PBackground thread. + + // Run I/O thread tasks + if (!mozilla::ipc::IsOnBackgroundThread()) { + nsresult rv = IOWork(); + if (NS_WARN_IF(NS_FAILED(rv))) { + SetError(rv); + } + + // Let's go back to PBackground thread to finish the work. + rv = mBackgroundEventTarget->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + // If we are here, it's because the I/O work has been done and we have to + // handle the result back via IPC. + mozilla::ipc::AssertIsOnBackgroundThread(); + HandleResult(); + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/FileSystemTaskBase.h b/dom/filesystem/FileSystemTaskBase.h new file mode 100644 index 0000000000..3d440403cc --- /dev/null +++ b/dom/filesystem/FileSystemTaskBase.h @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemTaskBase_h +#define mozilla_dom_FileSystemTaskBase_h + +#include "mozilla/dom/FileSystemRequestParent.h" +#include "mozilla/dom/PFileSystemRequestChild.h" +#include "nsIGlobalObject.h" +#include "nsThreadUtils.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BlobImpl; +class FileSystemBase; +class FileSystemParams; + +/* + * The base class to implement a Task class. + * The file system operations can only be performed in the parent process. In + * order to avoid duplicated code, we used PBackground for child-parent and + * parent-parent communications. + * + * The following diagram illustrates the how a API call from the content page + * starts a task and gets call back results. + * + * The left block is the call sequence inside any process loading content, while + * the right block is the call sequence only inside the parent process. + * + * Page + * | + * | (1) + * ______|_________________________ | _________________________________ + * | | | | | | + * | | | | | | + * | V | IPC | PBackground thread on | + * | [new FileSystemTaskChildBase()] | | | the parent process | + * | | | | | | + * | | (2) | | | + * | V | (3) | | + * | [GetRequestParams]------------------->[new FileSystemTaskParentBase()] | + * | | | | | + * | | | | | (4) _____________ | + * | | | | | | | | + * | | | | | | I/O Thread | | + * | | | | | | | | + * | | | | ---------> [IOWork] | | + * | | IPC | | | | | + * | | | | | | (5) | | + * | | | | -------------- | | + * | | | | | |_____________| | + * | | | | | | + * | | | | V | + * | | | | [HandleResult] | + * | | | | | | + * | | | | (6) | + * | | (7) | V | + * | [SetRequestResult]<---------------------[GetRequestResult] | + * | | | | | + * | | (8) | | | | + * | V | | | | + * |[HandlerCallback] | IPC | | + * |_______|_________________________| | |_________________________________| + * | | + * V + * Page + * + * 1. From the process that is handling the request + * Child/Parent (it can be in any process): + * (1) Call FileSystem API from content page with JS. Create a task and run. + * The base constructor [FileSystemTaskChildBase()] of the task should be + * called. + * (2) Forward the task to the parent process through the IPC and call + * [GetRequestParams] to prepare the parameters of the IPC. + * Parent: + * (3) The parent process receives IPC and handle it in + * FileystemRequestParent. Get the IPC parameters and create a task to run the + * IPC task. + * (4) The task operation will be performed in the member function of + * [IOWork]. A I/O thread will be created to run that function. If error occurs + * during the operation, call [SetError] to record the error and then abort. + * (5) After finishing the task operation, call [HandleResult] to send the + * result back to the child process though the IPC. + * (6) Call [GetRequestResult] request result to prepare the parameters of the + * IPC. Because the formats of the error result for different task are the + * same, FileSystemTaskChildBase can handle the error message without + * interfering. + * Each task only needs to implement its specific success result preparation + * function -[GetSuccessRequestResult]. + * Child/Parent: + * (7) The process receives IPC and calls [SetRequestResult] to get the + * task result. Each task needs to implement its specific success result + * parsing function [SetSuccessRequestResult] to get the success result. + * (8) Call [HandlerCallback] to send the task result to the content page. + */ +class FileSystemTaskChildBase : public PFileSystemRequestChild { + friend class PFileSystemRequestChild; + + public: + NS_INLINE_DECL_REFCOUNTING(FileSystemTaskChildBase, final) + + /* + * Start the task. It will dispatch all the information to the parent process, + * PBackground thread. This method must be called from the owning thread. + */ + void Start(); + + /* + * The error codes are defined in xpcom/base/ErrorList.h and their + * corresponding error name and message are defined in dom/base/domerr.msg. + */ + void SetError(const nsresult& aErrorCode); + + FileSystemBase* GetFileSystem() const; + + /* + * After the task is completed, this function will be called to pass the task + * result to the content page. This method is called in the owning thread. + * Override this function to handle the call back to the content page. + */ + virtual void HandlerCallback() = 0; + + bool HasError() const { return NS_FAILED(mErrorValue); } + + protected: + /* + * To create a task to handle the page content request. + */ + FileSystemTaskChildBase(nsIGlobalObject* aGlobalObject, + FileSystemBase* aFileSystem); + + virtual ~FileSystemTaskChildBase(); + + /* + * Wrap the task parameter to FileSystemParams for sending it through IPC. + * It will be called when we need to forward a task from the child process to + * the parent process. This method runs in the owning thread. + * @param filesystem The string representation of the file system. + */ + virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath, + ErrorResult& aRv) const = 0; + + /* + * Unwrap the IPC message to get the task success result. + * It will be called when the task is completed successfully and an IPC + * message is received in the child process and we want to get the task + * success result. This method runs in the owning thread. + */ + virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue, + ErrorResult& aRv) = 0; + + // Overrides PFileSystemRequestChild + virtual mozilla::ipc::IPCResult Recv__delete__( + const FileSystemResponseValue& value) final; + + nsresult mErrorValue; + RefPtr<FileSystemBase> mFileSystem; + nsCOMPtr<nsIGlobalObject> mGlobalObject; + + private: + /* + * Unwrap the IPC message to get the task result. + * It will be called when the task is completed and an IPC message is received + * in the content process and we want to get the task result. This runs on the + * owning thread. + */ + void SetRequestResult(const FileSystemResponseValue& aValue); +}; + +// This class is the 'alter ego' of FileSystemTaskChildBase in the PBackground +// world. +class FileSystemTaskParentBase : public Runnable { + public: + FileSystemTaskParentBase() + : Runnable("FileSystemTaskParentBase"), + mErrorValue(NS_ERROR_NOT_INITIALIZED) {} + + /* + * Start the task. This must be called from the PBackground thread only. + */ + void Start(); + + /* + * The error codes are defined in xpcom/base/ErrorList.h and their + * corresponding error name and message are defined in dom/base/domerr.msg. + */ + void SetError(const nsresult& aErrorCode); + + /* + * The function to perform task operation. It will be run on the I/O + * thread of the parent process. + * Overrides this function to define the task operation for individual task. + */ + virtual nsresult IOWork() = 0; + + /* + * Wrap the task success result to FileSystemResponseValue for sending it + * through IPC. This method runs in the PBackground thread. + * It will be called when the task is completed successfully and we need to + * send the task success result back to the child process. + */ + virtual FileSystemResponseValue GetSuccessRequestResult( + ErrorResult& aRv) const = 0; + + /* + * After finishing the task operation, handle the task result. + * If it is an IPC task, send back the IPC result. It runs on the PBackground + * thread. + */ + void HandleResult(); + + bool HasError() const { return NS_FAILED(mErrorValue); } + + NS_IMETHOD + Run() override; + + virtual nsresult GetTargetPath(nsAString& aPath) const = 0; + + private: + /* + * Wrap the task result to FileSystemResponseValue for sending it through IPC. + * It will be called when the task is completed and we need to + * send the task result back to the content. This runs on the PBackground + * thread. + */ + FileSystemResponseValue GetRequestResult() const; + + protected: + /* + * To create a parent process task delivered from the child process through + * IPC. + */ + FileSystemTaskParentBase(FileSystemBase* aFileSystem, + const FileSystemParams& aParam, + FileSystemRequestParent* aParent); + + virtual ~FileSystemTaskParentBase(); + + nsresult mErrorValue; + RefPtr<FileSystemBase> mFileSystem; + RefPtr<FileSystemRequestParent> mRequestParent; + nsCOMPtr<nsIEventTarget> mBackgroundEventTarget; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FileSystemTaskBase_h diff --git a/dom/filesystem/FileSystemUtils.cpp b/dom/filesystem/FileSystemUtils.cpp new file mode 100644 index 0000000000..5d4b391596 --- /dev/null +++ b/dom/filesystem/FileSystemUtils.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/FileSystemUtils.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsIEventTarget.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +/* static */ +bool FileSystemUtils::IsDescendantPath(const nsAString& aPath, + const nsAString& aDescendantPath) { + // Check the sub-directory path to see if it has the parent path as prefix. + if (!aDescendantPath.Equals(aPath) && + !StringBeginsWith(aDescendantPath, aPath)) { + return false; + } + + return true; +} + +/* static */ +bool FileSystemUtils::IsValidRelativeDOMPath(const nsAString& aPath, + nsTArray<nsString>& aParts) { + // We don't allow empty relative path to access the root. + if (aPath.IsEmpty()) { + return false; + } + + // Leading and trailing "/" are not allowed. + if (aPath.First() == FILESYSTEM_DOM_PATH_SEPARATOR_CHAR || + aPath.Last() == FILESYSTEM_DOM_PATH_SEPARATOR_CHAR) { + return false; + } + + constexpr auto kCurrentDir = u"."_ns; + constexpr auto kParentDir = u".."_ns; + + // Split path and check each path component. + for (const nsAString& pathComponent : + nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>{ + aPath, FILESYSTEM_DOM_PATH_SEPARATOR_CHAR} + .ToRange()) { + // The path containing empty components, such as "foo//bar", is invalid. + // We don't allow paths, such as "../foo", "foo/./bar" and "foo/../bar", + // to walk up the directory. + if (pathComponent.IsEmpty() || pathComponent.Equals(kCurrentDir) || + pathComponent.Equals(kParentDir)) { + return false; + } + + aParts.AppendElement(pathComponent); + } + + return true; +} + +/* static */ +nsresult FileSystemUtils::DispatchRunnable( + nsIGlobalObject* aGlobal, already_AddRefed<nsIRunnable>&& aRunnable) { + nsCOMPtr<nsIRunnable> runnable = aRunnable; + + nsCOMPtr<nsIEventTarget> target; + if (!aGlobal) { + target = GetMainThreadSerialEventTarget(); + } else { + target = aGlobal->EventTargetFor(TaskCategory::Other); + } + + MOZ_ASSERT(target); + + nsresult rv = target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/FileSystemUtils.h b/dom/filesystem/FileSystemUtils.h new file mode 100644 index 0000000000..7f82e8474f --- /dev/null +++ b/dom/filesystem/FileSystemUtils.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemUtils_h +#define mozilla_dom_FileSystemUtils_h + +#include "nsIGlobalObject.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +class nsIFile; +class nsIRunnable; + +namespace mozilla::dom { + +#define FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL "/" +#define FILESYSTEM_DOM_PATH_SEPARATOR_CHAR '/' + +/* + * This class is for error handling. + * All methods in this class are static. + */ +class FileSystemUtils { + public: + /* + * Return true if aDescendantPath is a descendant of aPath. + */ + static bool IsDescendantPath(const nsAString& aPath, + const nsAString& aDescendantPath); + + /** + * Return true if this is valid DOMPath. It also splits the path in + * subdirectories and stores them in aParts. + */ + static bool IsValidRelativeDOMPath(const nsAString& aPath, + nsTArray<nsString>& aParts); + + /** + * Helper method. If aGlobal is null, the SystemGroup EventTarget will be + * used. + */ + static nsresult DispatchRunnable(nsIGlobalObject* aGlobal, + already_AddRefed<nsIRunnable>&& aRunnable); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemUtils_h diff --git a/dom/filesystem/GetDirectoryListingTask.cpp b/dom/filesystem/GetDirectoryListingTask.cpp new file mode 100644 index 0000000000..f78a04a9d0 --- /dev/null +++ b/dom/filesystem/GetDirectoryListingTask.cpp @@ -0,0 +1,371 @@ +/* -*- 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 "GetDirectoryListingTask.h" + +#include "HTMLSplitOnSpacesTokenizer.h" +#include "js/Value.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "mozilla/dom/FileSystemBase.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/PFileSystemParams.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIFile.h" +#include "nsString.h" + +namespace mozilla::dom { + +/** + * GetDirectoryListingTaskChild + */ + +/* static */ +already_AddRefed<GetDirectoryListingTaskChild> +GetDirectoryListingTaskChild::Create(FileSystemBase* aFileSystem, + Directory* aDirectory, + nsIFile* aTargetPath, + const nsAString& aFilters, + ErrorResult& aRv) { + MOZ_ASSERT(aFileSystem); + MOZ_ASSERT(aDirectory); + aFileSystem->AssertIsOnOwningThread(); + + nsCOMPtr<nsIGlobalObject> globalObject = aFileSystem->GetParentObject(); + MOZ_ASSERT(globalObject); + + RefPtr<GetDirectoryListingTaskChild> task = new GetDirectoryListingTaskChild( + globalObject, aFileSystem, aDirectory, aTargetPath, aFilters); + + // aTargetPath can be null. In this case SetError will be called. + + task->mPromise = Promise::Create(globalObject, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return task.forget(); +} + +GetDirectoryListingTaskChild::GetDirectoryListingTaskChild( + nsIGlobalObject* aGlobalObject, FileSystemBase* aFileSystem, + Directory* aDirectory, nsIFile* aTargetPath, const nsAString& aFilters) + : FileSystemTaskChildBase(aGlobalObject, aFileSystem), + mDirectory(aDirectory), + mTargetPath(aTargetPath), + mFilters(aFilters) { + MOZ_ASSERT(aFileSystem); + aFileSystem->AssertIsOnOwningThread(); +} + +GetDirectoryListingTaskChild::~GetDirectoryListingTaskChild() { + mFileSystem->AssertIsOnOwningThread(); +} + +already_AddRefed<Promise> GetDirectoryListingTaskChild::GetPromise() { + mFileSystem->AssertIsOnOwningThread(); + return RefPtr<Promise>(mPromise).forget(); +} + +FileSystemParams GetDirectoryListingTaskChild::GetRequestParams( + const nsString& aSerializedDOMPath, ErrorResult& aRv) const { + mFileSystem->AssertIsOnOwningThread(); + + // this is the real path. + nsAutoString path; + aRv = mTargetPath->GetPath(path); + if (NS_WARN_IF(aRv.Failed())) { + return FileSystemGetDirectoryListingParams(); + } + + // this is the dom path. + nsAutoString directoryPath; + mDirectory->GetPath(directoryPath, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return FileSystemGetDirectoryListingParams(); + } + + return FileSystemGetDirectoryListingParams(aSerializedDOMPath, path, + directoryPath, mFilters); +} + +void GetDirectoryListingTaskChild::SetSuccessRequestResult( + const FileSystemResponseValue& aValue, ErrorResult& aRv) { + mFileSystem->AssertIsOnOwningThread(); + MOZ_ASSERT(aValue.type() == + FileSystemResponseValue::TFileSystemDirectoryListingResponse); + + FileSystemDirectoryListingResponse r = aValue; + for (uint32_t i = 0; i < r.data().Length(); ++i) { + const FileSystemDirectoryListingResponseData& data = r.data()[i]; + + OwningFileOrDirectory* ofd = mTargetData.AppendElement(fallible); + if (!ofd) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (data.type() == FileSystemDirectoryListingResponseData:: + TFileSystemDirectoryListingResponseFile) { + const FileSystemDirectoryListingResponseFile& d = + data.get_FileSystemDirectoryListingResponseFile(); + + RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(d.blob()); + MOZ_ASSERT(blobImpl); + + nsCOMPtr<nsIGlobalObject> globalObject = mFileSystem->GetParentObject(); + MOZ_ASSERT(globalObject); + + RefPtr<File> file = File::Create(globalObject, blobImpl); + MOZ_ASSERT(file); + + ofd->SetAsFile() = file; + } else { + MOZ_ASSERT(data.type() == + FileSystemDirectoryListingResponseData:: + TFileSystemDirectoryListingResponseDirectory); + const FileSystemDirectoryListingResponseDirectory& d = + data.get_FileSystemDirectoryListingResponseDirectory(); + + nsCOMPtr<nsIFile> path; + aRv = NS_NewLocalFile(d.directoryRealPath(), true, getter_AddRefs(path)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + RefPtr<Directory> directory = + Directory::Create(mFileSystem->GetParentObject(), path, mFileSystem); + MOZ_ASSERT(directory); + + ofd->SetAsDirectory() = directory; + } + } +} + +void GetDirectoryListingTaskChild::HandlerCallback() { + mFileSystem->AssertIsOnOwningThread(); + + if (mFileSystem->IsShutdown()) { + mPromise = nullptr; + return; + } + + if (HasError()) { + mPromise->MaybeReject(mErrorValue); + mPromise = nullptr; + return; + } + + mPromise->MaybeResolve(mTargetData); + mPromise = nullptr; +} + +/** + * GetDirectoryListingTaskParent + */ + +/* static */ +already_AddRefed<GetDirectoryListingTaskParent> +GetDirectoryListingTaskParent::Create( + FileSystemBase* aFileSystem, + const FileSystemGetDirectoryListingParams& aParam, + FileSystemRequestParent* aParent, ErrorResult& aRv) { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFileSystem); + + RefPtr<GetDirectoryListingTaskParent> task = + new GetDirectoryListingTaskParent(aFileSystem, aParam, aParent); + + aRv = NS_NewLocalFile(aParam.realPath(), true, + getter_AddRefs(task->mTargetPath)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return task.forget(); +} + +GetDirectoryListingTaskParent::GetDirectoryListingTaskParent( + FileSystemBase* aFileSystem, + const FileSystemGetDirectoryListingParams& aParam, + FileSystemRequestParent* aParent) + : FileSystemTaskParentBase(aFileSystem, aParam, aParent), + mDOMPath(aParam.domPath()), + mFilters(aParam.filters()) { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFileSystem); +} + +FileSystemResponseValue GetDirectoryListingTaskParent::GetSuccessRequestResult( + ErrorResult& aRv) const { + mozilla::ipc::AssertIsOnBackgroundThread(); + + nsTArray<FileSystemDirectoryListingResponseData> inputs; + + for (unsigned i = 0; i < mTargetData.Length(); i++) { + if (mTargetData[i].mType == FileOrDirectoryPath::eFilePath) { + nsCOMPtr<nsIFile> path; + nsresult rv = + NS_NewLocalFile(mTargetData[i].mPath, true, getter_AddRefs(path)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + FileSystemDirectoryListingResponseFile fileData; + RefPtr<BlobImpl> blobImpl = new FileBlobImpl(path); + + nsAutoString filePath; + filePath.Assign(mDOMPath); + + // This is specific for unix root filesystem. + if (!mDOMPath.EqualsLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL)) { + filePath.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL); + } + + nsAutoString name; + blobImpl->GetName(name); + filePath.Append(name); + blobImpl->SetDOMPath(filePath); + + IPCBlob ipcBlob; + rv = IPCBlobUtils::Serialize(blobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + fileData.blob() = ipcBlob; + inputs.AppendElement(fileData); + } else { + MOZ_ASSERT(mTargetData[i].mType == FileOrDirectoryPath::eDirectoryPath); + FileSystemDirectoryListingResponseDirectory directoryData; + directoryData.directoryRealPath() = mTargetData[i].mPath; + inputs.AppendElement(directoryData); + } + } + + FileSystemDirectoryListingResponse response; + response.data() = std::move(inputs); + return response; +} + +nsresult GetDirectoryListingTaskParent::IOWork() { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + MOZ_ASSERT(!NS_IsMainThread(), "Only call on worker thread!"); + + if (mFileSystem->IsShutdown()) { + return NS_ERROR_FAILURE; + } + + bool exists; + nsresult rv = mTargetPath->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + if (!mFileSystem->ShouldCreateDirectory()) { + return NS_ERROR_DOM_FILE_NOT_FOUND_ERR; + } + + rv = mTargetPath->Create(nsIFile::DIRECTORY_TYPE, 0777); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Get isDirectory. + bool isDir; + rv = mTargetPath->IsDirectory(&isDir); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isDir) { + return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR; + } + + nsCOMPtr<nsIDirectoryEnumerator> entries; + rv = mTargetPath->GetDirectoryEntries(getter_AddRefs(entries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool filterOutSensitive = false; + { + HTMLSplitOnSpacesTokenizer tokenizer(mFilters, ';'); + nsAutoString token; + while (tokenizer.hasMoreTokens()) { + token = tokenizer.nextToken(); + if (token.EqualsLiteral("filter-out-sensitive")) { + filterOutSensitive = true; + } else { + MOZ_CRASH("Unrecognized filter"); + } + } + } + + for (;;) { + nsCOMPtr<nsIFile> currFile; + if (NS_WARN_IF(NS_FAILED(entries->GetNextFile(getter_AddRefs(currFile)))) || + !currFile) { + break; + } + bool isLink, isSpecial, isFile; + if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) || + NS_FAILED(currFile->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 || isSpecial) { + continue; + } + if (NS_WARN_IF(NS_FAILED(currFile->IsFile(&isFile)) || + NS_FAILED(currFile->IsDirectory(&isDir))) || + !(isFile || isDir)) { + continue; + } + + if (filterOutSensitive) { + bool isHidden; + if (NS_WARN_IF(NS_FAILED(currFile->IsHidden(&isHidden))) || isHidden) { + continue; + } + nsAutoString leafName; + if (NS_WARN_IF(NS_FAILED(currFile->GetLeafName(leafName)))) { + continue; + } + if (leafName[0] == char16_t('.')) { + continue; + } + } + + nsAutoString path; + if (NS_WARN_IF(NS_FAILED(currFile->GetPath(path)))) { + continue; + } + + FileOrDirectoryPath element; + element.mPath = path; + element.mType = isDir ? FileOrDirectoryPath::eDirectoryPath + : FileOrDirectoryPath::eFilePath; + + if (!mTargetData.AppendElement(element, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + return NS_OK; +} + +nsresult GetDirectoryListingTaskParent::GetTargetPath(nsAString& aPath) const { + return mTargetPath->GetPath(aPath); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/GetDirectoryListingTask.h b/dom/filesystem/GetDirectoryListingTask.h new file mode 100644 index 0000000000..afb5296ac5 --- /dev/null +++ b/dom/filesystem/GetDirectoryListingTask.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GetDirectoryListing_h +#define mozilla_dom_GetDirectoryListing_h + +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/FileSystemTaskBase.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BlobImpl; +class FileSystemGetDirectoryListingParams; +class OwningFileOrDirectory; + +class GetDirectoryListingTaskChild final : public FileSystemTaskChildBase { + public: + static already_AddRefed<GetDirectoryListingTaskChild> Create( + FileSystemBase* aFileSystem, Directory* aDirectory, nsIFile* aTargetPath, + const nsAString& aFilters, ErrorResult& aRv); + + virtual ~GetDirectoryListingTaskChild(); + + already_AddRefed<Promise> GetPromise(); + + private: + // If aDirectoryOnly is set, we should ensure that the target is a directory. + GetDirectoryListingTaskChild(nsIGlobalObject* aGlobalObject, + FileSystemBase* aFileSystem, + Directory* aDirectory, nsIFile* aTargetPath, + const nsAString& aFilters); + + virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath, + ErrorResult& aRv) const override; + + virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue, + ErrorResult& aRv) override; + + virtual void HandlerCallback() override; + + RefPtr<Promise> mPromise; + RefPtr<Directory> mDirectory; + nsCOMPtr<nsIFile> mTargetPath; + nsString mFilters; + + FallibleTArray<OwningFileOrDirectory> mTargetData; +}; + +class GetDirectoryListingTaskParent final : public FileSystemTaskParentBase { + public: + static already_AddRefed<GetDirectoryListingTaskParent> Create( + FileSystemBase* aFileSystem, + const FileSystemGetDirectoryListingParams& aParam, + FileSystemRequestParent* aParent, ErrorResult& aRv); + + nsresult GetTargetPath(nsAString& aPath) const override; + + private: + GetDirectoryListingTaskParent( + FileSystemBase* aFileSystem, + const FileSystemGetDirectoryListingParams& aParam, + FileSystemRequestParent* aParent); + + virtual FileSystemResponseValue GetSuccessRequestResult( + ErrorResult& aRv) const override; + + virtual nsresult IOWork() override; + + nsCOMPtr<nsIFile> mTargetPath; + nsString mDOMPath; + nsString mFilters; + + struct FileOrDirectoryPath { + nsString mPath; + + enum { eFilePath, eDirectoryPath } mType; + }; + + FallibleTArray<FileOrDirectoryPath> mTargetData; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_GetDirectoryListing_h diff --git a/dom/filesystem/GetFileOrDirectoryTask.cpp b/dom/filesystem/GetFileOrDirectoryTask.cpp new file mode 100644 index 0000000000..7f7d1c2816 --- /dev/null +++ b/dom/filesystem/GetFileOrDirectoryTask.cpp @@ -0,0 +1,270 @@ +/* -*- 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 "GetFileOrDirectoryTask.h" + +#include "js/Value.h" +#include "mozilla/dom/FileBlobImpl.h" +#include "mozilla/dom/FileSystemBase.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/PFileSystemParams.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIFile.h" +#include "nsString.h" + +namespace mozilla::dom { + +/** + * GetFileOrDirectoryTaskChild + */ + +/* static */ +already_AddRefed<GetFileOrDirectoryTaskChild> +GetFileOrDirectoryTaskChild::Create(FileSystemBase* aFileSystem, + nsIFile* aTargetPath, ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!"); + MOZ_ASSERT(aFileSystem); + + nsCOMPtr<nsIGlobalObject> globalObject = aFileSystem->GetParentObject(); + if (NS_WARN_IF(!globalObject)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<GetFileOrDirectoryTaskChild> task = + new GetFileOrDirectoryTaskChild(globalObject, aFileSystem, aTargetPath); + + // aTargetPath can be null. In this case SetError will be called. + + task->mPromise = Promise::Create(globalObject, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return task.forget(); +} + +GetFileOrDirectoryTaskChild::GetFileOrDirectoryTaskChild( + nsIGlobalObject* aGlobalObject, FileSystemBase* aFileSystem, + nsIFile* aTargetPath) + : FileSystemTaskChildBase(aGlobalObject, aFileSystem), + mTargetPath(aTargetPath) { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!"); + MOZ_ASSERT(aFileSystem); +} + +GetFileOrDirectoryTaskChild::~GetFileOrDirectoryTaskChild() { + MOZ_ASSERT(NS_IsMainThread()); +} + +already_AddRefed<Promise> GetFileOrDirectoryTaskChild::GetPromise() { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!"); + return RefPtr<Promise>(mPromise).forget(); +} + +FileSystemParams GetFileOrDirectoryTaskChild::GetRequestParams( + const nsString& aSerializedDOMPath, ErrorResult& aRv) const { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!"); + + nsAutoString path; + aRv = mTargetPath->GetPath(path); + if (NS_WARN_IF(aRv.Failed())) { + return FileSystemGetFileOrDirectoryParams(); + } + + return FileSystemGetFileOrDirectoryParams(aSerializedDOMPath, path); +} + +void GetFileOrDirectoryTaskChild::SetSuccessRequestResult( + const FileSystemResponseValue& aValue, ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!"); + switch (aValue.type()) { + case FileSystemResponseValue::TFileSystemFileResponse: { + FileSystemFileResponse r = aValue; + + RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(r.blob()); + MOZ_ASSERT(blobImpl); + + nsCOMPtr<nsIGlobalObject> globalObject = mFileSystem->GetParentObject(); + MOZ_ASSERT(globalObject); + + mResultFile = File::Create(globalObject, blobImpl); + if (NS_WARN_IF(!mResultFile)) { + aRv.Throw(NS_ERROR_FAILURE); + } + break; + } + case FileSystemResponseValue::TFileSystemDirectoryResponse: { + FileSystemDirectoryResponse r = aValue; + + nsCOMPtr<nsIFile> file; + aRv = NS_NewLocalFile(r.realPath(), true, getter_AddRefs(file)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mResultDirectory = + Directory::Create(mFileSystem->GetParentObject(), file, mFileSystem); + MOZ_ASSERT(mResultDirectory); + break; + } + default: { + MOZ_CRASH("not reached"); + break; + } + } +} + +void GetFileOrDirectoryTaskChild::HandlerCallback() { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!"); + if (mFileSystem->IsShutdown()) { + mPromise = nullptr; + return; + } + + if (HasError()) { + mPromise->MaybeReject(mErrorValue); + mPromise = nullptr; + return; + } + + if (mResultDirectory) { + mPromise->MaybeResolve(mResultDirectory); + mResultDirectory = nullptr; + mPromise = nullptr; + return; + } + + MOZ_ASSERT(mResultFile); + mPromise->MaybeResolve(mResultFile); + mResultFile = nullptr; + mPromise = nullptr; +} + +/** + * GetFileOrDirectoryTaskParent + */ + +/* static */ +already_AddRefed<GetFileOrDirectoryTaskParent> +GetFileOrDirectoryTaskParent::Create( + FileSystemBase* aFileSystem, + const FileSystemGetFileOrDirectoryParams& aParam, + FileSystemRequestParent* aParent, ErrorResult& aRv) { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFileSystem); + + RefPtr<GetFileOrDirectoryTaskParent> task = + new GetFileOrDirectoryTaskParent(aFileSystem, aParam, aParent); + + aRv = NS_NewLocalFile(aParam.realPath(), true, + getter_AddRefs(task->mTargetPath)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return task.forget(); +} + +GetFileOrDirectoryTaskParent::GetFileOrDirectoryTaskParent( + FileSystemBase* aFileSystem, + const FileSystemGetFileOrDirectoryParams& aParam, + FileSystemRequestParent* aParent) + : FileSystemTaskParentBase(aFileSystem, aParam, aParent), + mIsDirectory(false) { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFileSystem); +} + +FileSystemResponseValue GetFileOrDirectoryTaskParent::GetSuccessRequestResult( + ErrorResult& aRv) const { + mozilla::ipc::AssertIsOnBackgroundThread(); + + nsAutoString path; + aRv = mTargetPath->GetPath(path); + if (NS_WARN_IF(aRv.Failed())) { + return FileSystemDirectoryResponse(); + } + + if (mIsDirectory) { + return FileSystemDirectoryResponse(path); + } + + RefPtr<BlobImpl> blobImpl = new FileBlobImpl(mTargetPath); + + IPCBlob ipcBlob; + aRv = IPCBlobUtils::Serialize(blobImpl, ipcBlob); + if (NS_WARN_IF(aRv.Failed())) { + return FileSystemDirectoryResponse(); + } + + return FileSystemFileResponse(ipcBlob); +} + +nsresult GetFileOrDirectoryTaskParent::IOWork() { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + MOZ_ASSERT(!NS_IsMainThread(), "Only call on worker thread!"); + + if (mFileSystem->IsShutdown()) { + return NS_ERROR_FAILURE; + } + + // Whether we want to get the root directory. + bool exists; + nsresult rv = mTargetPath->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + if (!mFileSystem->ShouldCreateDirectory()) { + return NS_ERROR_DOM_FILE_NOT_FOUND_ERR; + } + + rv = mTargetPath->Create(nsIFile::DIRECTORY_TYPE, 0777); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Get isDirectory. + rv = mTargetPath->IsDirectory(&mIsDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mIsDirectory) { + return NS_OK; + } + + bool isFile; + // Get isFile + rv = mTargetPath->IsFile(&isFile); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isFile) { + // Neither directory or file. + return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR; + } + + if (!mFileSystem->IsSafeFile(mTargetPath)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + return NS_OK; +} + +nsresult GetFileOrDirectoryTaskParent::GetTargetPath(nsAString& aPath) const { + return mTargetPath->GetPath(aPath); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/GetFileOrDirectoryTask.h b/dom/filesystem/GetFileOrDirectoryTask.h new file mode 100644 index 0000000000..8d15d9804f --- /dev/null +++ b/dom/filesystem/GetFileOrDirectoryTask.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GetFileOrDirectory_h +#define mozilla_dom_GetFileOrDirectory_h + +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/FileSystemTaskBase.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BlobImpl; +class FileSystemGetFileOrDirectoryParams; + +class GetFileOrDirectoryTaskChild final : public FileSystemTaskChildBase { + public: + static already_AddRefed<GetFileOrDirectoryTaskChild> Create( + FileSystemBase* aFileSystem, nsIFile* aTargetPath, ErrorResult& aRv); + + virtual ~GetFileOrDirectoryTaskChild(); + + already_AddRefed<Promise> GetPromise(); + + protected: + virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath, + ErrorResult& aRv) const override; + + virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue, + ErrorResult& aRv) override; + virtual void HandlerCallback() override; + + private: + GetFileOrDirectoryTaskChild(nsIGlobalObject* aGlobalObject, + FileSystemBase* aFileSystem, + nsIFile* aTargetPath); + + RefPtr<Promise> mPromise; + nsCOMPtr<nsIFile> mTargetPath; + + RefPtr<File> mResultFile; + RefPtr<Directory> mResultDirectory; +}; + +class GetFileOrDirectoryTaskParent final : public FileSystemTaskParentBase { + public: + static already_AddRefed<GetFileOrDirectoryTaskParent> Create( + FileSystemBase* aFileSystem, + const FileSystemGetFileOrDirectoryParams& aParam, + FileSystemRequestParent* aParent, ErrorResult& aRv); + + nsresult GetTargetPath(nsAString& aPath) const override; + + protected: + virtual FileSystemResponseValue GetSuccessRequestResult( + ErrorResult& aRv) const override; + + virtual nsresult IOWork() override; + + private: + GetFileOrDirectoryTaskParent(FileSystemBase* aFileSystem, + const FileSystemGetFileOrDirectoryParams& aParam, + FileSystemRequestParent* aParent); + + nsCOMPtr<nsIFile> mTargetPath; + + // Whether we get a directory. + bool mIsDirectory; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_GetFileOrDirectory_h diff --git a/dom/filesystem/GetFilesHelper.cpp b/dom/filesystem/GetFilesHelper.cpp new file mode 100644 index 0000000000..5c91b4cfe5 --- /dev/null +++ b/dom/filesystem/GetFilesHelper.cpp @@ -0,0 +1,506 @@ +/* -*- 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<RefPtr<Promise>>&& aPromises, + nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks) { + if (NS_IsMainThread()) { + return; + } + + RefPtr<ReleaseRunnable> 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<RefPtr<Promise>>&& aPromises, + nsTArray<RefPtr<GetFilesCallback>>&& aCallbacks) + : Runnable("dom::ReleaseRunnable"), + mPromises(std::move(aPromises)), + mCallbacks(std::move(aCallbacks)) {} + + nsTArray<RefPtr<Promise>> mPromises; + nsTArray<RefPtr<GetFilesCallback>> mCallbacks; +}; + +} // namespace + +/////////////////////////////////////////////////////////////////////////////// +// GetFilesHelper Base class + +already_AddRefed<GetFilesHelper> GetFilesHelper::Create( + const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory, + bool aRecursiveFlag, ErrorResult& aRv) { + RefPtr<GetFilesHelper> 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> 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<nsIEventTarget> 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> 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<RefPtr<Promise>> promises = std::move(mPromises); + + for (uint32_t i = 0; i < promises.Length(); ++i) { + ResolveOrRejectPromise(promises[i]); + } + + // Let's process the pending callbacks. + nsTArray<RefPtr<GetFilesCallback>> 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<nsIFile> 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<nsIDirectoryEnumerator> entries; + nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(entries)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (;;) { + nsCOMPtr<nsIFile> 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> 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<RefPtr<File>> files; + + if (NS_SUCCEEDED(mErrorResult)) { + for (uint32_t i = 0; i < mTargetBlobImplArray.Length(); ++i) { + RefPtr<File> 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<RefPtr<BlobImpl>>& aBlobImpls) override { + if (NS_FAILED(aStatus)) { + mParent->mContentParent->SendGetFilesResponseAndForget( + mParent->mUUID, GetFilesResponseFailure(aStatus)); + return; + } + + GetFilesResponseSuccess success; + + nsTArray<IPCBlob>& 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> GetFilesHelperParent::Create( + const nsID& aUUID, const nsAString& aDirectoryPath, bool aRecursiveFlag, + ContentParent* aContentParent, ErrorResult& aRv) { + MOZ_ASSERT(aContentParent); + + RefPtr<GetFilesHelperParent> helper = + new GetFilesHelperParent(aUUID, aContentParent, aRecursiveFlag); + helper->SetDirectoryPath(aDirectoryPath); + + helper->Work(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<GetFilesHelperParentCallback> callback = + new GetFilesHelperParentCallback(helper); + helper->AddCallback(callback); + + return helper.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/GetFilesHelper.h b/dom/filesystem/GetFilesHelper.h new file mode 100644 index 0000000000..2b15e69059 --- /dev/null +++ b/dom/filesystem/GetFilesHelper.h @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GetFilesHelper_h +#define mozilla_dom_GetFilesHelper_h + +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "nsCycleCollectionTraversalCallback.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +class nsIGlobalObject; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BlobImpl; +class ContentParent; +class File; +class GetFilesHelperParent; +class OwningFileOrDirectory; +class Promise; + +class GetFilesCallback { + public: + NS_INLINE_DECL_REFCOUNTING(GetFilesCallback); + + virtual void Callback(nsresult aStatus, + const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) = 0; + + protected: + virtual ~GetFilesCallback() = default; +}; + +class GetFilesHelperBase { + protected: + explicit GetFilesHelperBase(bool aRecursiveFlag) + : mRecursiveFlag(aRecursiveFlag) {} + + virtual ~GetFilesHelperBase() = default; + + virtual bool IsCanceled() { return false; } + + nsresult ExploreDirectory(const nsAString& aDOMPath, nsIFile* aFile); + + bool mRecursiveFlag; + + // We populate this array in the I/O thread with the BlobImpl. + FallibleTArray<RefPtr<BlobImpl>> mTargetBlobImplArray; +}; + +// Retrieving the list of files can be very time/IO consuming. We use this +// helper class to do it just once. +class GetFilesHelper : public Runnable, public GetFilesHelperBase { + friend class GetFilesHelperParent; + + public: + static already_AddRefed<GetFilesHelper> Create( + const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory, + bool aRecursiveFlag, ErrorResult& aRv); + + void AddPromise(Promise* aPromise); + + void AddCallback(GetFilesCallback* aCallback); + + // CC methods + void Unlink(); + void Traverse(nsCycleCollectionTraversalCallback& cb); + + protected: + explicit GetFilesHelper(bool aRecursiveFlag); + + virtual ~GetFilesHelper(); + + void SetDirectoryPath(const nsAString& aDirectoryPath) { + mDirectoryPath = aDirectoryPath; + } + + virtual bool IsCanceled() override { + MutexAutoLock lock(mMutex); + return mCanceled; + } + + virtual void Work(ErrorResult& aRv); + + virtual void Cancel(){}; + + NS_IMETHOD + Run() override; + + void RunIO(); + + void OperationCompleted(); + + void ResolveOrRejectPromise(Promise* aPromise); + + void RunCallback(GetFilesCallback* aCallback); + + bool mListingCompleted; + nsString mDirectoryPath; + + // Error code to propagate. + nsresult mErrorResult; + + nsTArray<RefPtr<Promise>> mPromises; + nsTArray<RefPtr<GetFilesCallback>> mCallbacks; + + Mutex mMutex MOZ_UNANNOTATED; + + // This variable is protected by mutex. + bool mCanceled; +}; + +class GetFilesHelperChild final : public GetFilesHelper { + public: + explicit GetFilesHelperChild(bool aRecursiveFlag) + : GetFilesHelper(aRecursiveFlag), mPendingOperation(false) {} + + virtual void Work(ErrorResult& aRv) override; + + virtual void Cancel() override; + + bool AppendBlobImpl(BlobImpl* aBlobImpl); + + void Finished(nsresult aResult); + + private: + nsID mUUID; + bool mPendingOperation; +}; + +class GetFilesHelperParentCallback; + +class GetFilesHelperParent final : public GetFilesHelper { + friend class GetFilesHelperParentCallback; + + public: + static already_AddRefed<GetFilesHelperParent> Create( + const nsID& aUUID, const nsAString& aDirectoryPath, bool aRecursiveFlag, + ContentParent* aContentParent, ErrorResult& aRv); + + private: + GetFilesHelperParent(const nsID& aUUID, ContentParent* aContentParent, + bool aRecursiveFlag); + + ~GetFilesHelperParent(); + + RefPtr<ContentParent> mContentParent; + nsID mUUID; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_GetFilesHelper_h diff --git a/dom/filesystem/GetFilesTask.cpp b/dom/filesystem/GetFilesTask.cpp new file mode 100644 index 0000000000..280df0b0bc --- /dev/null +++ b/dom/filesystem/GetFilesTask.cpp @@ -0,0 +1,246 @@ +/* -*- 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 "GetFilesTask.h" + +#include "HTMLSplitOnSpacesTokenizer.h" +#include "js/Value.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileSystemBase.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/PFileSystemParams.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsIFile.h" +#include "nsString.h" + +namespace mozilla::dom { + +/** + * GetFilesTaskChild + */ + +/* static */ +already_AddRefed<GetFilesTaskChild> GetFilesTaskChild::Create( + FileSystemBase* aFileSystem, Directory* aDirectory, nsIFile* aTargetPath, + bool aRecursiveFlag, ErrorResult& aRv) { + MOZ_ASSERT(aFileSystem); + MOZ_ASSERT(aDirectory); + aFileSystem->AssertIsOnOwningThread(); + + nsCOMPtr<nsIGlobalObject> globalObject = aFileSystem->GetParentObject(); + if (NS_WARN_IF(!globalObject)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<GetFilesTaskChild> task = new GetFilesTaskChild( + globalObject, aFileSystem, aDirectory, aTargetPath, aRecursiveFlag); + + // aTargetPath can be null. In this case SetError will be called. + + task->mPromise = Promise::Create(globalObject, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return task.forget(); +} + +GetFilesTaskChild::GetFilesTaskChild(nsIGlobalObject* aGlobalObject, + FileSystemBase* aFileSystem, + Directory* aDirectory, + nsIFile* aTargetPath, bool aRecursiveFlag) + : FileSystemTaskChildBase(aGlobalObject, aFileSystem), + mDirectory(aDirectory), + mTargetPath(aTargetPath), + mRecursiveFlag(aRecursiveFlag) { + MOZ_ASSERT(aFileSystem); + MOZ_ASSERT(aDirectory); + aFileSystem->AssertIsOnOwningThread(); +} + +GetFilesTaskChild::~GetFilesTaskChild() { + mFileSystem->AssertIsOnOwningThread(); +} + +already_AddRefed<Promise> GetFilesTaskChild::GetPromise() { + mFileSystem->AssertIsOnOwningThread(); + return RefPtr<Promise>(mPromise).forget(); +} + +FileSystemParams GetFilesTaskChild::GetRequestParams( + const nsString& aSerializedDOMPath, ErrorResult& aRv) const { + mFileSystem->AssertIsOnOwningThread(); + + nsAutoString path; + aRv = mTargetPath->GetPath(path); + if (NS_WARN_IF(aRv.Failed())) { + return FileSystemGetFilesParams(); + } + + nsAutoString domPath; + mDirectory->GetPath(domPath, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return FileSystemGetFilesParams(); + } + + return FileSystemGetFilesParams(aSerializedDOMPath, path, domPath, + mRecursiveFlag); +} + +void GetFilesTaskChild::SetSuccessRequestResult( + const FileSystemResponseValue& aValue, ErrorResult& aRv) { + mFileSystem->AssertIsOnOwningThread(); + MOZ_ASSERT(aValue.type() == + FileSystemResponseValue::TFileSystemFilesResponse); + + FileSystemFilesResponse r = aValue; + + if (!mTargetData.SetLength(r.data().Length(), mozilla::fallible_t())) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsCOMPtr<nsIGlobalObject> globalObject = mFileSystem->GetParentObject(); + MOZ_ASSERT(globalObject); + + for (uint32_t i = 0; i < r.data().Length(); ++i) { + const FileSystemFileResponse& data = r.data()[i]; + RefPtr<BlobImpl> blobImpl = IPCBlobUtils::Deserialize(data.blob()); + MOZ_ASSERT(blobImpl); + + mTargetData[i] = File::Create(globalObject, blobImpl); + if (NS_WARN_IF(!mTargetData[i])) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } +} + +void GetFilesTaskChild::HandlerCallback() { + mFileSystem->AssertIsOnOwningThread(); + if (mFileSystem->IsShutdown()) { + mPromise = nullptr; + return; + } + + if (HasError()) { + mPromise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + mPromise = nullptr; + return; + } + + mPromise->MaybeResolve(mTargetData); + mPromise = nullptr; +} + +/** + * GetFilesTaskParent + */ + +/* static */ +already_AddRefed<GetFilesTaskParent> GetFilesTaskParent::Create( + FileSystemBase* aFileSystem, const FileSystemGetFilesParams& aParam, + FileSystemRequestParent* aParent, ErrorResult& aRv) { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFileSystem); + + RefPtr<GetFilesTaskParent> task = + new GetFilesTaskParent(aFileSystem, aParam, aParent); + + aRv = NS_NewLocalFile(aParam.realPath(), true, + getter_AddRefs(task->mTargetPath)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return task.forget(); +} + +GetFilesTaskParent::GetFilesTaskParent(FileSystemBase* aFileSystem, + const FileSystemGetFilesParams& aParam, + FileSystemRequestParent* aParent) + : FileSystemTaskParentBase(aFileSystem, aParam, aParent), + GetFilesHelperBase(aParam.recursiveFlag()), + mDirectoryDOMPath(aParam.domPath()) { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aFileSystem); +} + +FileSystemResponseValue GetFilesTaskParent::GetSuccessRequestResult( + ErrorResult& aRv) const { + mozilla::ipc::AssertIsOnBackgroundThread(); + + FallibleTArray<FileSystemFileResponse> inputs; + if (!inputs.SetLength(mTargetBlobImplArray.Length(), mozilla::fallible_t())) { + FileSystemFilesResponse response; + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return response; + } + + for (unsigned i = 0; i < mTargetBlobImplArray.Length(); i++) { + IPCBlob ipcBlob; + aRv = IPCBlobUtils::Serialize(mTargetBlobImplArray[i], ipcBlob); + if (NS_WARN_IF(aRv.Failed())) { + FileSystemFilesResponse response; + return response; + } + + inputs[i] = FileSystemFileResponse(ipcBlob); + } + + FileSystemFilesResponse response; + response.data() = std::move(inputs); + return response; +} + +nsresult GetFilesTaskParent::IOWork() { + MOZ_ASSERT(XRE_IsParentProcess(), "Only call from parent process!"); + MOZ_ASSERT(!NS_IsMainThread(), "Only call on I/O thread!"); + + if (mFileSystem->IsShutdown()) { + return NS_ERROR_FAILURE; + } + + bool exists; + nsresult rv = mTargetPath->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + return NS_OK; + } + + bool isDir; + rv = mTargetPath->IsDirectory(&isDir); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isDir) { + return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR; + } + + // Get isDirectory. + rv = ExploreDirectory(mDirectoryDOMPath, mTargetPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult GetFilesTaskParent::GetTargetPath(nsAString& aPath) const { + return mTargetPath->GetPath(aPath); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/GetFilesTask.h b/dom/filesystem/GetFilesTask.h new file mode 100644 index 0000000000..6f6f15e879 --- /dev/null +++ b/dom/filesystem/GetFilesTask.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_GetFilesTask_h +#define mozilla_dom_GetFilesTask_h + +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/FileSystemTaskBase.h" +#include "mozilla/dom/GetFilesHelper.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BlobImpl; +class FileSystemGetFilesParams; + +class GetFilesTaskChild final : public FileSystemTaskChildBase { + public: + static already_AddRefed<GetFilesTaskChild> Create(FileSystemBase* aFileSystem, + Directory* aDirectory, + nsIFile* aTargetPath, + bool aRecursiveFlag, + ErrorResult& aRv); + + virtual ~GetFilesTaskChild(); + + already_AddRefed<Promise> GetPromise(); + + private: + // If aDirectoryOnly is set, we should ensure that the target is a directory. + GetFilesTaskChild(nsIGlobalObject* aGlobalObject, FileSystemBase* aFileSystem, + Directory* aDirectory, nsIFile* aTargetPath, + bool aRecursiveFlag); + + virtual FileSystemParams GetRequestParams(const nsString& aSerializedDOMPath, + ErrorResult& aRv) const override; + + virtual void SetSuccessRequestResult(const FileSystemResponseValue& aValue, + ErrorResult& aRv) override; + + virtual void HandlerCallback() override; + + RefPtr<Promise> mPromise; + RefPtr<Directory> mDirectory; + nsCOMPtr<nsIFile> mTargetPath; + bool mRecursiveFlag; + + // We store the fullpath and the dom path of Files. + FallibleTArray<RefPtr<File>> mTargetData; +}; + +class GetFilesTaskParent final : public FileSystemTaskParentBase, + public GetFilesHelperBase { + public: + static already_AddRefed<GetFilesTaskParent> Create( + FileSystemBase* aFileSystem, const FileSystemGetFilesParams& aParam, + FileSystemRequestParent* aParent, ErrorResult& aRv); + + nsresult GetTargetPath(nsAString& aPath) const override; + + private: + GetFilesTaskParent(FileSystemBase* aFileSystem, + const FileSystemGetFilesParams& aParam, + FileSystemRequestParent* aParent); + + virtual FileSystemResponseValue GetSuccessRequestResult( + ErrorResult& aRv) const override; + + virtual nsresult IOWork() override; + + nsString mDirectoryDOMPath; + nsCOMPtr<nsIFile> mTargetPath; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_GetFilesTask_h diff --git a/dom/filesystem/OSFileSystem.cpp b/dom/filesystem/OSFileSystem.cpp new file mode 100644 index 0000000000..2322d07b93 --- /dev/null +++ b/dom/filesystem/OSFileSystem.cpp @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/OSFileSystem.h" + +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "nsIGlobalObject.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsIFile.h" + +namespace mozilla::dom { + +OSFileSystem::OSFileSystem(const nsAString& aRootDir) { + mLocalRootPath = aRootDir; +} + +already_AddRefed<FileSystemBase> OSFileSystem::Clone() { + AssertIsOnOwningThread(); + + RefPtr<OSFileSystem> fs = new OSFileSystem(mLocalRootPath); + if (mGlobal) { + fs->Init(mGlobal); + } + + return fs.forget(); +} + +void OSFileSystem::Init(nsIGlobalObject* aGlobal) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!mGlobal, "No duple Init() calls"); + MOZ_ASSERT(aGlobal); + + mGlobal = aGlobal; +} + +nsIGlobalObject* OSFileSystem::GetParentObject() const { + AssertIsOnOwningThread(); + return mGlobal; +} + +bool OSFileSystem::IsSafeFile(nsIFile* aFile) const { + // The concept of "safe files" is specific to the Device Storage API where + // files are only "safe" if they're of a type that is appropriate for the + // area of device storage that is being used. + MOZ_CRASH("Don't use OSFileSystem with the Device Storage API"); + return true; +} + +bool OSFileSystem::IsSafeDirectory(Directory* aDir) const { + // The concept of "safe directory" is specific to the Device Storage API + // where a directory is only "safe" if it belongs to the area of device + // storage that it is being used with. + MOZ_CRASH("Don't use OSFileSystem with the Device Storage API"); + return true; +} + +void OSFileSystem::Unlink() { + AssertIsOnOwningThread(); + mGlobal = nullptr; +} + +void OSFileSystem::Traverse(nsCycleCollectionTraversalCallback& cb) { + AssertIsOnOwningThread(); + + OSFileSystem* tmp = this; + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal); +} + +void OSFileSystem::SerializeDOMPath(nsAString& aOutput) const { + AssertIsOnOwningThread(); + aOutput = mLocalRootPath; +} + +/** + * OSFileSystemParent + */ + +OSFileSystemParent::OSFileSystemParent(const nsAString& aRootDir) { + mLocalRootPath = aRootDir; +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/OSFileSystem.h b/dom/filesystem/OSFileSystem.h new file mode 100644 index 0000000000..cb2ade7fc5 --- /dev/null +++ b/dom/filesystem/OSFileSystem.h @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_OSFileSystem_h +#define mozilla_dom_OSFileSystem_h + +#include "mozilla/dom/FileSystemBase.h" + +namespace mozilla::dom { + +class OSFileSystem final : public FileSystemBase { + public: + explicit OSFileSystem(const nsAString& aRootDir); + + void Init(nsIGlobalObject* aGlobal); + + // Overrides FileSystemBase + + virtual already_AddRefed<FileSystemBase> Clone() override; + + virtual bool ShouldCreateDirectory() override { + MOZ_CRASH("This should not be called."); + // Because OSFileSystem should not be used when the creation of directories + // is needed. For that we have OSFileSystemParent. + return false; + } + + virtual nsIGlobalObject* GetParentObject() const override; + + virtual bool IsSafeFile(nsIFile* aFile) const override; + + virtual bool IsSafeDirectory(Directory* aDir) const override; + + virtual void SerializeDOMPath(nsAString& aOutput) const override; + + // CC methods + virtual void Unlink() override; + virtual void Traverse(nsCycleCollectionTraversalCallback& cb) override; + + private: + virtual ~OSFileSystem() = default; + + nsCOMPtr<nsIGlobalObject> mGlobal; +}; + +class OSFileSystemParent final : public FileSystemBase { + public: + explicit OSFileSystemParent(const nsAString& aRootDir); + + // Overrides FileSystemBase + + virtual already_AddRefed<FileSystemBase> Clone() override { + MOZ_CRASH("This should not be called on the PBackground thread."); + return nullptr; + } + + virtual bool ShouldCreateDirectory() override { return false; } + + virtual nsIGlobalObject* GetParentObject() const override { + MOZ_CRASH("This should not be called on the PBackground thread."); + return nullptr; + } + + virtual void GetDirectoryName(nsIFile* aFile, nsAString& aRetval, + ErrorResult& aRv) const override { + MOZ_CRASH("This should not be called on the PBackground thread."); + } + + virtual bool IsSafeFile(nsIFile* aFile) const override { return true; } + + virtual bool IsSafeDirectory(Directory* aDir) const override { + MOZ_CRASH("This should not be called on the PBackground thread."); + return true; + } + + virtual void SerializeDOMPath(nsAString& aOutput) const override { + MOZ_CRASH("This should not be called on the PBackground thread."); + } + + // CC methods + virtual void Unlink() override { + MOZ_CRASH("This should not be called on the PBackground thread."); + } + + virtual void Traverse(nsCycleCollectionTraversalCallback& cb) override { + MOZ_CRASH("This should not be called on the PBackground thread."); + } + + private: + virtual ~OSFileSystemParent() = default; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_OSFileSystem_h diff --git a/dom/filesystem/PFileSystemParams.ipdlh b/dom/filesystem/PFileSystemParams.ipdlh new file mode 100644 index 0000000000..40b1e095f1 --- /dev/null +++ b/dom/filesystem/PFileSystemParams.ipdlh @@ -0,0 +1,48 @@ +/* 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/. */ + +namespace mozilla { +namespace dom { + +struct FileSystemGetDirectoryListingParams +{ + nsString filesystem; + nsString realPath; + nsString domPath; + + // 'filters' could be an array rather than a semicolon separated string + // (we'd then use nsTArray<nsString> internally), but that is + // wasteful. E10s requires us to pass the filters over as a string anyway, + // so avoiding using an array avoids serialization on the side passing the + // filters. Since an nsString can share its buffer when copied, + // using that instead of nsTArray<nsString> makes copying the filters + // around in any given process a bit more efficient too, since copying a + // single nsString is cheaper than copying nsTArray member data and + // each nsString that it contains. + nsString filters; +}; + +struct FileSystemGetFilesParams +{ + nsString filesystem; + nsString realPath; + nsString domPath; + bool recursiveFlag; +}; + +struct FileSystemGetFileOrDirectoryParams +{ + nsString filesystem; + nsString realPath; +}; + +union FileSystemParams +{ + FileSystemGetDirectoryListingParams; + FileSystemGetFilesParams; + FileSystemGetFileOrDirectoryParams; +}; + +} // dom namespace +} // mozilla namespace diff --git a/dom/filesystem/PFileSystemRequest.ipdl b/dom/filesystem/PFileSystemRequest.ipdl new file mode 100644 index 0000000000..3ae24e269b --- /dev/null +++ b/dom/filesystem/PFileSystemRequest.ipdl @@ -0,0 +1,77 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +include protocol PBackground; + +include IPCBlob; + +include "mozilla/dom/FileSystemTaskBase.h"; + +namespace mozilla { +namespace dom { + +struct FileSystemFileResponse +{ + IPCBlob blob; +}; + +struct FileSystemDirectoryResponse +{ + nsString realPath; +}; + +struct FileSystemDirectoryListingResponseFile +{ + IPCBlob blob; +}; + +struct FileSystemDirectoryListingResponseDirectory +{ + // This is the full real path for the directory that we are sending via IPC. + nsString directoryRealPath; +}; + +union FileSystemDirectoryListingResponseData +{ + FileSystemDirectoryListingResponseFile; + FileSystemDirectoryListingResponseDirectory; +}; + +struct FileSystemDirectoryListingResponse +{ + FileSystemDirectoryListingResponseData[] data; +}; + +struct FileSystemFilesResponse +{ + FileSystemFileResponse[] data; +}; + +struct FileSystemErrorResponse +{ + nsresult error; +}; + +union FileSystemResponseValue +{ + FileSystemDirectoryResponse; + FileSystemDirectoryListingResponse; + FileSystemFileResponse; + FileSystemFilesResponse; + FileSystemErrorResponse; +}; + +[ChildImpl="FileSystemTaskChildBase"] +protocol PFileSystemRequest +{ + manager PBackground; + +child: + async __delete__(FileSystemResponseValue response); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/filesystem/compat/CallbackRunnables.cpp b/dom/filesystem/compat/CallbackRunnables.cpp new file mode 100644 index 0000000000..cbdd2f2f56 --- /dev/null +++ b/dom/filesystem/compat/CallbackRunnables.cpp @@ -0,0 +1,280 @@ +/* -*- 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 "CallbackRunnables.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/DirectoryBinding.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileBinding.h" +#include "mozilla/dom/FileSystem.h" +#include "mozilla/dom/FileSystemDirectoryReaderBinding.h" +#include "mozilla/dom/FileSystemFileEntry.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/Unused.h" +#include "nsIGlobalObject.h" +#include "nsIFile.h" +#include "nsPIDOMWindow.h" + +#include "../GetFileOrDirectoryTask.h" + +namespace mozilla::dom { + +EntryCallbackRunnable::EntryCallbackRunnable(FileSystemEntryCallback* aCallback, + FileSystemEntry* aEntry) + : Runnable("EntryCallbackRunnable"), mCallback(aCallback), mEntry(aEntry) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aEntry); +} + +NS_IMETHODIMP +EntryCallbackRunnable::Run() { + mCallback->Call(*mEntry); + return NS_OK; +} + +ErrorCallbackRunnable::ErrorCallbackRunnable(nsIGlobalObject* aGlobalObject, + ErrorCallback* aCallback, + nsresult aError) + : Runnable("ErrorCallbackRunnable"), + mGlobal(aGlobalObject), + mCallback(aCallback), + mError(aError) { + MOZ_ASSERT(aGlobalObject); + MOZ_ASSERT(aCallback); + MOZ_ASSERT(NS_FAILED(aError)); +} + +NS_IMETHODIMP +ErrorCallbackRunnable::Run() { + RefPtr<DOMException> exception = DOMException::Create(mError); + mCallback->Call(*exception); + return NS_OK; +} + +EmptyEntriesCallbackRunnable::EmptyEntriesCallbackRunnable( + FileSystemEntriesCallback* aCallback) + : Runnable("EmptyEntriesCallbackRunnable"), mCallback(aCallback) { + MOZ_ASSERT(aCallback); +} + +NS_IMETHODIMP +EmptyEntriesCallbackRunnable::Run() { + Sequence<OwningNonNull<FileSystemEntry>> sequence; + mCallback->Call(sequence); + return NS_OK; +} + +GetEntryHelper::GetEntryHelper(FileSystemDirectoryEntry* aParentEntry, + Directory* aDirectory, + nsTArray<nsString>& aParts, + FileSystem* aFileSystem, + FileSystemEntryCallback* aSuccessCallback, + ErrorCallback* aErrorCallback, + FileSystemDirectoryEntry::GetInternalType aType) + : mParentEntry(aParentEntry), + mDirectory(aDirectory), + mParts(aParts.Clone()), + mFileSystem(aFileSystem), + mSuccessCallback(aSuccessCallback), + mErrorCallback(aErrorCallback), + mType(aType) { + MOZ_ASSERT(aParentEntry); + MOZ_ASSERT(aDirectory); + MOZ_ASSERT(!aParts.IsEmpty()); + MOZ_ASSERT(aFileSystem); + MOZ_ASSERT(aSuccessCallback || aErrorCallback); +} + +GetEntryHelper::~GetEntryHelper() = default; + +namespace { + +nsresult DOMPathToRealPath(Directory* aDirectory, const nsAString& aPath, + nsIFile** aFile) { + nsString relativePath; + relativePath = aPath; + + // Trim white spaces. + static const char kWhitespace[] = "\b\t\r\n "; + relativePath.Trim(kWhitespace); + + nsTArray<nsString> parts; + if (!FileSystemUtils::IsValidRelativeDOMPath(relativePath, parts)) { + return NS_ERROR_DOM_FILESYSTEM_INVALID_PATH_ERR; + } + + nsCOMPtr<nsIFile> file; + nsresult rv = aDirectory->GetInternalNsIFile()->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (uint32_t i = 0; i < parts.Length(); ++i) { + rv = file->AppendRelativePath(parts[i]); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + file.forget(aFile); + return NS_OK; +} + +} // namespace + +void GetEntryHelper::Run() { + MOZ_ASSERT(!mParts.IsEmpty()); + + nsCOMPtr<nsIFile> realPath; + nsresult error = + DOMPathToRealPath(mDirectory, mParts[0], getter_AddRefs(realPath)); + + ErrorResult rv; + RefPtr<FileSystemBase> fs = mDirectory->GetFileSystem(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + Error(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + RefPtr<GetFileOrDirectoryTaskChild> task = + GetFileOrDirectoryTaskChild::Create(fs, realPath, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + Error(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + task->SetError(error); + task->Start(); + + RefPtr<Promise> promise = task->GetPromise(); + + mParts.RemoveElementAt(0); + promise->AppendNativeHandler(this); +} + +void GetEntryHelper::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + + // This is not the last part of the path. + if (!mParts.IsEmpty()) { + ContinueRunning(obj); + return; + } + + CompleteOperation(obj); +} + +void GetEntryHelper::CompleteOperation(JSObject* aObj) { + MOZ_ASSERT(mParts.IsEmpty()); + + if (mType == FileSystemDirectoryEntry::eGetFile) { + RefPtr<File> file; + if (NS_FAILED(UNWRAP_OBJECT(File, aObj, file))) { + Error(NS_ERROR_DOM_TYPE_MISMATCH_ERR); + return; + } + + RefPtr<FileSystemFileEntry> entry = new FileSystemFileEntry( + mParentEntry->GetParentObject(), file, mParentEntry, mFileSystem); + mSuccessCallback->Call(*entry); + return; + } + + MOZ_ASSERT(mType == FileSystemDirectoryEntry::eGetDirectory); + + RefPtr<Directory> directory; + if (NS_FAILED(UNWRAP_OBJECT(Directory, aObj, directory))) { + Error(NS_ERROR_DOM_TYPE_MISMATCH_ERR); + return; + } + + RefPtr<FileSystemDirectoryEntry> entry = new FileSystemDirectoryEntry( + mParentEntry->GetParentObject(), directory, mParentEntry, mFileSystem); + mSuccessCallback->Call(*entry); +} + +void GetEntryHelper::ContinueRunning(JSObject* aObj) { + MOZ_ASSERT(!mParts.IsEmpty()); + + RefPtr<Directory> directory; + if (NS_FAILED(UNWRAP_OBJECT(Directory, aObj, directory))) { + Error(NS_ERROR_DOM_TYPE_MISMATCH_ERR); + return; + } + + RefPtr<FileSystemDirectoryEntry> entry = new FileSystemDirectoryEntry( + mParentEntry->GetParentObject(), directory, mParentEntry, mFileSystem); + + // Update the internal values. + mParentEntry = entry; + mDirectory = directory; + + Run(); +} + +void GetEntryHelper::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + Error(NS_ERROR_DOM_NOT_FOUND_ERR); +} + +void GetEntryHelper::Error(nsresult aError) { + MOZ_ASSERT(NS_FAILED(aError)); + + if (mErrorCallback) { + RefPtr<ErrorCallbackRunnable> runnable = new ErrorCallbackRunnable( + mParentEntry->GetParentObject(), mErrorCallback, aError); + + FileSystemUtils::DispatchRunnable(mParentEntry->GetParentObject(), + runnable.forget()); + } +} + +NS_IMPL_ISUPPORTS0(GetEntryHelper); + +/* static */ +void FileSystemEntryCallbackHelper::Call( + nsIGlobalObject* aGlobalObject, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aEntryCallback, + FileSystemEntry* aEntry) { + MOZ_ASSERT(aGlobalObject); + MOZ_ASSERT(aEntry); + + if (aEntryCallback.WasPassed()) { + RefPtr<EntryCallbackRunnable> runnable = + new EntryCallbackRunnable(&aEntryCallback.Value(), aEntry); + + FileSystemUtils::DispatchRunnable(aGlobalObject, runnable.forget()); + } +} + +/* static */ +void ErrorCallbackHelper::Call( + nsIGlobalObject* aGlobal, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + nsresult aError) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(NS_FAILED(aError)); + + if (aErrorCallback.WasPassed()) { + RefPtr<ErrorCallbackRunnable> runnable = + new ErrorCallbackRunnable(aGlobal, &aErrorCallback.Value(), aError); + + FileSystemUtils::DispatchRunnable(aGlobal, runnable.forget()); + } +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/CallbackRunnables.h b/dom/filesystem/compat/CallbackRunnables.h new file mode 100644 index 0000000000..e2e1c47913 --- /dev/null +++ b/dom/filesystem/compat/CallbackRunnables.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ErrorCallbackRunnable_h +#define mozilla_dom_ErrorCallbackRunnable_h + +#include "FileSystemDirectoryEntry.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "nsThreadUtils.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +class FileSystemEntriesCallback; + +class EntryCallbackRunnable final : public Runnable { + public: + EntryCallbackRunnable(FileSystemEntryCallback* aCallback, + FileSystemEntry* aEntry); + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override; + + private: + const RefPtr<FileSystemEntryCallback> mCallback; + const RefPtr<FileSystemEntry> mEntry; +}; + +class ErrorCallbackRunnable final : public Runnable { + public: + ErrorCallbackRunnable(nsIGlobalObject* aGlobalObject, + ErrorCallback* aCallback, nsresult aError); + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override; + + private: + nsCOMPtr<nsIGlobalObject> mGlobal; + const RefPtr<ErrorCallback> mCallback; + nsresult mError; +}; + +class EmptyEntriesCallbackRunnable final : public Runnable { + public: + explicit EmptyEntriesCallbackRunnable(FileSystemEntriesCallback* aCallback); + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override; + + private: + const RefPtr<FileSystemEntriesCallback> mCallback; +}; + +class GetEntryHelper final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + GetEntryHelper(FileSystemDirectoryEntry* aParentEntry, Directory* aDirectory, + nsTArray<nsString>& aParts, FileSystem* aFileSystem, + FileSystemEntryCallback* aSuccessCallback, + ErrorCallback* aErrorCallback, + FileSystemDirectoryEntry::GetInternalType aType); + + void Run(); + + MOZ_CAN_RUN_SCRIPT + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + private: + ~GetEntryHelper(); + + void Error(nsresult aError); + + void ContinueRunning(JSObject* aObj); + + MOZ_CAN_RUN_SCRIPT void CompleteOperation(JSObject* aObj); + + RefPtr<FileSystemDirectoryEntry> mParentEntry; + RefPtr<Directory> mDirectory; + nsTArray<nsString> mParts; + RefPtr<FileSystem> mFileSystem; + + const RefPtr<FileSystemEntryCallback> mSuccessCallback; + RefPtr<ErrorCallback> mErrorCallback; + + FileSystemDirectoryEntry::GetInternalType mType; +}; + +class FileSystemEntryCallbackHelper { + public: + static void Call( + nsIGlobalObject* aGlobalObject, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aEntryCallback, + FileSystemEntry* aEntry); +}; + +class ErrorCallbackHelper { + public: + static void Call(nsIGlobalObject* aGlobal, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + nsresult aError); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_CallbackRunnables_h diff --git a/dom/filesystem/compat/FileSystem.cpp b/dom/filesystem/compat/FileSystem.cpp new file mode 100644 index 0000000000..734b3ec600 --- /dev/null +++ b/dom/filesystem/compat/FileSystem.cpp @@ -0,0 +1,60 @@ +/* -*- 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 "FileSystem.h" +#include "FileSystemRootDirectoryEntry.h" +#include "mozilla/dom/FileSystemBinding.h" +#include "nsIDUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileSystem, mParent, mRoot) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystem) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystem) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystem) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed<FileSystem> FileSystem::Create(nsIGlobalObject* aGlobalObject) + +{ + MOZ_ASSERT(aGlobalObject); + + nsID id; + nsresult rv = nsID::GenerateUUIDInPlace(id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + NSID_TrimBracketsUTF16 name(id); + + RefPtr<FileSystem> fs = new FileSystem(aGlobalObject, name); + + return fs.forget(); +} + +FileSystem::FileSystem(nsIGlobalObject* aGlobal, const nsAString& aName) + : mParent(aGlobal), mName(aName) { + MOZ_ASSERT(aGlobal); +} + +FileSystem::~FileSystem() = default; + +JSObject* FileSystem::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FileSystem_Binding::Wrap(aCx, this, aGivenProto); +} + +void FileSystem::CreateRoot(const Sequence<RefPtr<FileSystemEntry>>& aEntries) { + MOZ_ASSERT(!mRoot); + mRoot = new FileSystemRootDirectoryEntry(mParent, aEntries, this); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/FileSystem.h b/dom/filesystem/compat/FileSystem.h new file mode 100644 index 0000000000..92cb563ef4 --- /dev/null +++ b/dom/filesystem/compat/FileSystem.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystem_h +#define mozilla_dom_FileSystem_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +class FileSystemDirectoryEntry; +class FileSystemEntry; +class OwningFileOrDirectory; + +class FileSystem final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystem) + + static already_AddRefed<FileSystem> Create(nsIGlobalObject* aGlobalObject); + + nsIGlobalObject* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetName(nsAString& aName) const { aName = mName; } + + FileSystemDirectoryEntry* Root() const { return mRoot; } + + void CreateRoot(const Sequence<RefPtr<FileSystemEntry>>& aEntries); + + private: + explicit FileSystem(nsIGlobalObject* aGlobalObject, const nsAString& aName); + ~FileSystem(); + + nsCOMPtr<nsIGlobalObject> mParent; + RefPtr<FileSystemDirectoryEntry> mRoot; + nsString mName; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystem_h diff --git a/dom/filesystem/compat/FileSystemDirectoryEntry.cpp b/dom/filesystem/compat/FileSystemDirectoryEntry.cpp new file mode 100644 index 0000000000..7358f997f0 --- /dev/null +++ b/dom/filesystem/compat/FileSystemDirectoryEntry.cpp @@ -0,0 +1,92 @@ +/* -*- 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 "FileSystemDirectoryEntry.h" +#include "CallbackRunnables.h" +#include "FileSystemDirectoryReader.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/FileSystemDirectoryEntryBinding.h" +#include "mozilla/dom/FileSystemUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemDirectoryEntry, FileSystemEntry, + mDirectory) + +NS_IMPL_ADDREF_INHERITED(FileSystemDirectoryEntry, FileSystemEntry) +NS_IMPL_RELEASE_INHERITED(FileSystemDirectoryEntry, FileSystemEntry) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemDirectoryEntry) +NS_INTERFACE_MAP_END_INHERITING(FileSystemEntry) + +FileSystemDirectoryEntry::FileSystemDirectoryEntry( + nsIGlobalObject* aGlobal, Directory* aDirectory, + FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem) + : FileSystemEntry(aGlobal, aParentEntry, aFileSystem), + mDirectory(aDirectory) { + MOZ_ASSERT(aGlobal); +} + +FileSystemDirectoryEntry::~FileSystemDirectoryEntry() = default; + +JSObject* FileSystemDirectoryEntry::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return FileSystemDirectoryEntry_Binding::Wrap(aCx, this, aGivenProto); +} + +void FileSystemDirectoryEntry::GetName(nsAString& aName, + ErrorResult& aRv) const { + MOZ_ASSERT(mDirectory); + mDirectory->GetName(aName, aRv); +} + +void FileSystemDirectoryEntry::GetFullPath(nsAString& aPath, + ErrorResult& aRv) const { + MOZ_ASSERT(mDirectory); + mDirectory->GetPath(aPath, aRv); +} + +already_AddRefed<FileSystemDirectoryReader> +FileSystemDirectoryEntry::CreateReader() { + MOZ_ASSERT(mDirectory); + + RefPtr<FileSystemDirectoryReader> reader = + new FileSystemDirectoryReader(this, Filesystem(), mDirectory); + return reader.forget(); +} + +void FileSystemDirectoryEntry::GetInternal( + const nsAString& aPath, const FileSystemFlags& aFlag, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + GetInternalType aType) { + MOZ_ASSERT(mDirectory); + + if (!aSuccessCallback.WasPassed() && !aErrorCallback.WasPassed()) { + return; + } + + if (aFlag.mCreate) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsTArray<nsString> parts; + if (!FileSystemUtils::IsValidRelativeDOMPath(aPath, parts)) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + NS_ERROR_DOM_NOT_FOUND_ERR); + return; + } + + RefPtr<GetEntryHelper> helper = new GetEntryHelper( + this, mDirectory, parts, Filesystem(), + aSuccessCallback.WasPassed() ? &aSuccessCallback.Value() : nullptr, + aErrorCallback.WasPassed() ? &aErrorCallback.Value() : nullptr, aType); + helper->Run(); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/FileSystemDirectoryEntry.h b/dom/filesystem/compat/FileSystemDirectoryEntry.h new file mode 100644 index 0000000000..1b772691e1 --- /dev/null +++ b/dom/filesystem/compat/FileSystemDirectoryEntry.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemDirectoryEntry_h +#define mozilla_dom_FileSystemDirectoryEntry_h + +#include "mozilla/dom/FileSystemEntry.h" + +namespace mozilla::dom { + +class Directory; +class FileSystemDirectoryReader; + +class FileSystemDirectoryEntry : public FileSystemEntry { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemDirectoryEntry, + FileSystemEntry) + + FileSystemDirectoryEntry(nsIGlobalObject* aGlobalObject, + Directory* aDirectory, + FileSystemDirectoryEntry* aParentEntry, + FileSystem* aFileSystem); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual bool IsDirectory() const override { return true; } + + virtual void GetName(nsAString& aName, ErrorResult& aRv) const override; + + virtual void GetFullPath(nsAString& aFullPath, + ErrorResult& aRv) const override; + + virtual already_AddRefed<FileSystemDirectoryReader> CreateReader(); + + void GetFile( + const Optional<nsAString>& aPath, const FileSystemFlags& aFlag, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) { + GetInternal(aPath.WasPassed() ? aPath.Value() : u""_ns, aFlag, + aSuccessCallback, aErrorCallback, eGetFile); + } + + void GetDirectory( + const Optional<nsAString>& aPath, const FileSystemFlags& aFlag, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) { + GetInternal(aPath.WasPassed() ? aPath.Value() : u""_ns, aFlag, + aSuccessCallback, aErrorCallback, eGetDirectory); + } + + enum GetInternalType { eGetFile, eGetDirectory }; + + virtual void GetInternal( + const nsAString& aPath, const FileSystemFlags& aFlag, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + GetInternalType aType); + + protected: + virtual ~FileSystemDirectoryEntry(); + + private: + RefPtr<Directory> mDirectory; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemDirectoryEntry_h diff --git a/dom/filesystem/compat/FileSystemDirectoryReader.cpp b/dom/filesystem/compat/FileSystemDirectoryReader.cpp new file mode 100644 index 0000000000..533faae413 --- /dev/null +++ b/dom/filesystem/compat/FileSystemDirectoryReader.cpp @@ -0,0 +1,181 @@ +/* -*- 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 "FileSystemDirectoryReader.h" +#include "CallbackRunnables.h" +#include "FileSystemFileEntry.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_GetElement +#include "mozilla/dom/FileBinding.h" +#include "mozilla/dom/FileSystem.h" +#include "mozilla/dom/FileSystemDirectoryReaderBinding.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/DirectoryBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" + +namespace mozilla::dom { + +namespace { + +class PromiseHandler final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + PromiseHandler(FileSystemDirectoryEntry* aParentEntry, + FileSystem* aFileSystem, + FileSystemEntriesCallback* aSuccessCallback, + ErrorCallback* aErrorCallback) + : mParentEntry(aParentEntry), + mFileSystem(aFileSystem), + mSuccessCallback(aSuccessCallback), + mErrorCallback(aErrorCallback) { + MOZ_ASSERT(aParentEntry); + MOZ_ASSERT(aFileSystem); + MOZ_ASSERT(aSuccessCallback); + } + + MOZ_CAN_RUN_SCRIPT + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + + uint32_t length; + if (NS_WARN_IF(!JS::GetArrayLength(aCx, obj, &length))) { + return; + } + + Sequence<OwningNonNull<FileSystemEntry>> sequence; + if (NS_WARN_IF(!sequence.SetLength(length, fallible))) { + return; + } + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted<JS::Value> value(aCx); + if (NS_WARN_IF(!JS_GetElement(aCx, obj, i, &value))) { + return; + } + + if (NS_WARN_IF(!value.isObject())) { + return; + } + + JS::Rooted<JSObject*> valueObj(aCx, &value.toObject()); + + RefPtr<File> file; + if (NS_SUCCEEDED(UNWRAP_OBJECT(File, valueObj, file))) { + RefPtr<FileSystemFileEntry> entry = new FileSystemFileEntry( + mParentEntry->GetParentObject(), file, mParentEntry, mFileSystem); + sequence[i] = entry; + continue; + } + + RefPtr<Directory> directory; + if (NS_WARN_IF( + NS_FAILED(UNWRAP_OBJECT(Directory, valueObj, directory)))) { + return; + } + + RefPtr<FileSystemDirectoryEntry> entry = + new FileSystemDirectoryEntry(mParentEntry->GetParentObject(), + directory, mParentEntry, mFileSystem); + sequence[i] = entry; + } + + mSuccessCallback->Call(sequence); + } + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + if (mErrorCallback) { + RefPtr<ErrorCallbackRunnable> runnable = new ErrorCallbackRunnable( + mParentEntry->GetParentObject(), mErrorCallback, + NS_ERROR_DOM_INVALID_STATE_ERR); + + FileSystemUtils::DispatchRunnable(mParentEntry->GetParentObject(), + runnable.forget()); + } + } + + private: + ~PromiseHandler() = default; + + RefPtr<FileSystemDirectoryEntry> mParentEntry; + RefPtr<FileSystem> mFileSystem; + const RefPtr<FileSystemEntriesCallback> mSuccessCallback; + RefPtr<ErrorCallback> mErrorCallback; +}; + +NS_IMPL_ISUPPORTS0(PromiseHandler); + +} // anonymous namespace + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileSystemDirectoryReader, mParentEntry, + mDirectory, mFileSystem) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemDirectoryReader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemDirectoryReader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemDirectoryReader) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +FileSystemDirectoryReader::FileSystemDirectoryReader( + FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem, + Directory* aDirectory) + : mParentEntry(aParentEntry), + mFileSystem(aFileSystem), + mDirectory(aDirectory), + mAlreadyRead(false) { + MOZ_ASSERT(aParentEntry); + MOZ_ASSERT(aFileSystem); +} + +FileSystemDirectoryReader::~FileSystemDirectoryReader() = default; + +JSObject* FileSystemDirectoryReader::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return FileSystemDirectoryReader_Binding::Wrap(aCx, this, aGivenProto); +} + +void FileSystemDirectoryReader::ReadEntries( + FileSystemEntriesCallback& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + ErrorResult& aRv) { + MOZ_ASSERT(mDirectory); + + if (mAlreadyRead) { + RefPtr<EmptyEntriesCallbackRunnable> runnable = + new EmptyEntriesCallbackRunnable(&aSuccessCallback); + + FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget()); + return; + } + + // This object can be used only once. + mAlreadyRead = true; + + ErrorResult rv; + RefPtr<Promise> promise = mDirectory->GetFilesAndDirectories(rv); + if (NS_WARN_IF(rv.Failed())) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + rv.StealNSResult()); + return; + } + + RefPtr<PromiseHandler> handler = new PromiseHandler( + mParentEntry, mFileSystem, &aSuccessCallback, + aErrorCallback.WasPassed() ? &aErrorCallback.Value() : nullptr); + promise->AppendNativeHandler(handler); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/FileSystemDirectoryReader.h b/dom/filesystem/compat/FileSystemDirectoryReader.h new file mode 100644 index 0000000000..90274943b4 --- /dev/null +++ b/dom/filesystem/compat/FileSystemDirectoryReader.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemDirectoryReader_h +#define mozilla_dom_FileSystemDirectoryReader_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/FileSystemDirectoryEntry.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class Directory; +class FileSystem; +class FileSystemEntriesCallback; + +class FileSystemDirectoryReader : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemDirectoryReader) + + explicit FileSystemDirectoryReader(FileSystemDirectoryEntry* aDirectoryEntry, + FileSystem* aFileSystem, + Directory* aDirectory); + + nsIGlobalObject* GetParentObject() const { + return mParentEntry->GetParentObject(); + } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual void ReadEntries( + FileSystemEntriesCallback& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + ErrorResult& aRv); + + protected: + virtual ~FileSystemDirectoryReader(); + + private: + RefPtr<FileSystemDirectoryEntry> mParentEntry; + RefPtr<FileSystem> mFileSystem; + RefPtr<Directory> mDirectory; + + bool mAlreadyRead; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FileSystemDirectoryReader_h diff --git a/dom/filesystem/compat/FileSystemEntry.cpp b/dom/filesystem/compat/FileSystemEntry.cpp new file mode 100644 index 0000000000..3a69e2537a --- /dev/null +++ b/dom/filesystem/compat/FileSystemEntry.cpp @@ -0,0 +1,80 @@ +/* -*- 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 "FileSystemEntry.h" +#include "CallbackRunnables.h" +#include "FileSystem.h" +#include "FileSystemDirectoryEntry.h" +#include "FileSystemFileEntry.h" +#include "mozilla/dom/FileSystemEntryBinding.h" +#include "mozilla/dom/UnionTypes.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FileSystemEntry, mParent, mParentEntry, + mFileSystem) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(FileSystemEntry) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FileSystemEntry) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemEntry) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed<FileSystemEntry> FileSystemEntry::Create( + nsIGlobalObject* aGlobalObject, + const OwningFileOrDirectory& aFileOrDirectory, FileSystem* aFileSystem) { + MOZ_ASSERT(aGlobalObject); + MOZ_ASSERT(aFileSystem); + + RefPtr<FileSystemEntry> entry; + if (aFileOrDirectory.IsFile()) { + entry = new FileSystemFileEntry(aGlobalObject, aFileOrDirectory.GetAsFile(), + nullptr, aFileSystem); + } else { + MOZ_ASSERT(aFileOrDirectory.IsDirectory()); + entry = new FileSystemDirectoryEntry( + aGlobalObject, aFileOrDirectory.GetAsDirectory(), nullptr, aFileSystem); + } + + return entry.forget(); +} + +FileSystemEntry::FileSystemEntry(nsIGlobalObject* aGlobal, + FileSystemEntry* aParentEntry, + FileSystem* aFileSystem) + : mParent(aGlobal), mParentEntry(aParentEntry), mFileSystem(aFileSystem) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aFileSystem); +} + +FileSystemEntry::~FileSystemEntry() = default; + +JSObject* FileSystemEntry::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FileSystemEntry_Binding::Wrap(aCx, this, aGivenProto); +} + +void FileSystemEntry::GetParent( + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) { + if (!aSuccessCallback.WasPassed() && !aErrorCallback.WasPassed()) { + return; + } + + if (mParentEntry) { + FileSystemEntryCallbackHelper::Call(GetParentObject(), aSuccessCallback, + mParentEntry); + return; + } + + FileSystemEntryCallbackHelper::Call(GetParentObject(), aSuccessCallback, + this); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/FileSystemEntry.h b/dom/filesystem/compat/FileSystemEntry.h new file mode 100644 index 0000000000..2275c600a9 --- /dev/null +++ b/dom/filesystem/compat/FileSystemEntry.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemEntry_h +#define mozilla_dom_FileSystemEntry_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/FileSystemBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIGlobalObject.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class FileSystem; +class OwningFileOrDirectory; + +class FileSystemEntry : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FileSystemEntry) + + static already_AddRefed<FileSystemEntry> Create( + nsIGlobalObject* aGlobalObject, + const OwningFileOrDirectory& aFileOrDirectory, FileSystem* aFileSystem); + + nsIGlobalObject* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual bool IsFile() const { return false; } + + virtual bool IsDirectory() const { return false; } + + virtual void GetName(nsAString& aName, ErrorResult& aRv) const = 0; + + virtual void GetFullPath(nsAString& aFullPath, ErrorResult& aRv) const = 0; + + void GetParent( + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback); + + FileSystem* Filesystem() const { return mFileSystem; } + + protected: + FileSystemEntry(nsIGlobalObject* aGlobalObject, FileSystemEntry* aParentEntry, + FileSystem* aFileSystem); + virtual ~FileSystemEntry(); + + private: + nsCOMPtr<nsIGlobalObject> mParent; + RefPtr<FileSystemEntry> mParentEntry; + RefPtr<FileSystem> mFileSystem; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FileSystemEntry_h diff --git a/dom/filesystem/compat/FileSystemFileEntry.cpp b/dom/filesystem/compat/FileSystemFileEntry.cpp new file mode 100644 index 0000000000..59216641cc --- /dev/null +++ b/dom/filesystem/compat/FileSystemFileEntry.cpp @@ -0,0 +1,94 @@ +/* -*- 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 "FileSystemFileEntry.h" +#include "CallbackRunnables.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/MultipartBlobImpl.h" +#include "mozilla/dom/FileSystemFileEntryBinding.h" + +namespace mozilla::dom { + +namespace { + +class FileCallbackRunnable final : public Runnable { + public: + FileCallbackRunnable(FileCallback* aCallback, File* aFile) + : Runnable("FileCallbackRunnable"), mCallback(aCallback), mFile(aFile) { + MOZ_ASSERT(aCallback); + MOZ_ASSERT(aFile); + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + // Here we clone the File object. + + RefPtr<File> file = File::Create(mFile->GetParentObject(), mFile->Impl()); + mCallback->Call(*file); + return NS_OK; + } + + private: + const RefPtr<FileCallback> mCallback; + RefPtr<File> mFile; +}; + +} // anonymous namespace + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemFileEntry, FileSystemEntry, mFile) + +NS_IMPL_ADDREF_INHERITED(FileSystemFileEntry, FileSystemEntry) +NS_IMPL_RELEASE_INHERITED(FileSystemFileEntry, FileSystemEntry) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemFileEntry) +NS_INTERFACE_MAP_END_INHERITING(FileSystemEntry) + +FileSystemFileEntry::FileSystemFileEntry(nsIGlobalObject* aGlobal, File* aFile, + FileSystemDirectoryEntry* aParentEntry, + FileSystem* aFileSystem) + : FileSystemEntry(aGlobal, aParentEntry, aFileSystem), mFile(aFile) { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(mFile); +} + +FileSystemFileEntry::~FileSystemFileEntry() = default; + +JSObject* FileSystemFileEntry::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FileSystemFileEntry_Binding::Wrap(aCx, this, aGivenProto); +} + +void FileSystemFileEntry::GetName(nsAString& aName, ErrorResult& aRv) const { + mFile->GetName(aName); +} + +void FileSystemFileEntry::GetFullPath(nsAString& aPath, + ErrorResult& aRv) const { + mFile->Impl()->GetDOMPath(aPath); + if (aPath.IsEmpty()) { + // We're under the root directory. webkitRelativePath + // (implemented as GetPath) is for cases when file is selected because its + // ancestor directory is selected. But that is not the case here, so need to + // manually prepend '/'. + nsAutoString name; + mFile->GetName(name); + aPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL); + aPath.Append(name); + } +} + +void FileSystemFileEntry::GetFile( + FileCallback& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) const { + RefPtr<FileCallbackRunnable> runnable = + new FileCallbackRunnable(&aSuccessCallback, mFile); + + FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget()); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/FileSystemFileEntry.h b/dom/filesystem/compat/FileSystemFileEntry.h new file mode 100644 index 0000000000..1fe244a66b --- /dev/null +++ b/dom/filesystem/compat/FileSystemFileEntry.h @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemFileEntry_h +#define mozilla_dom_FileSystemFileEntry_h + +#include "mozilla/dom/FileSystemEntry.h" + +namespace mozilla::dom { + +class File; +class FileCallback; +class FileSystemDirectoryEntry; + +class FileSystemFileEntry final : public FileSystemEntry { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemFileEntry, FileSystemEntry) + + FileSystemFileEntry(nsIGlobalObject* aGlobalObject, File* aFile, + FileSystemDirectoryEntry* aParentEntry, + FileSystem* aFileSystem); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual bool IsFile() const override { return true; } + + virtual void GetName(nsAString& aName, ErrorResult& aRv) const override; + + virtual void GetFullPath(nsAString& aFullPath, + ErrorResult& aRv) const override; + + void GetFile( + FileCallback& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback) const; + + private: + ~FileSystemFileEntry(); + + RefPtr<File> mFile; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemFileEntry_h diff --git a/dom/filesystem/compat/FileSystemRootDirectoryEntry.cpp b/dom/filesystem/compat/FileSystemRootDirectoryEntry.cpp new file mode 100644 index 0000000000..fad6243d15 --- /dev/null +++ b/dom/filesystem/compat/FileSystemRootDirectoryEntry.cpp @@ -0,0 +1,138 @@ +/* -*- 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 "FileSystemRootDirectoryEntry.h" +#include "FileSystemRootDirectoryReader.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "CallbackRunnables.h" +#include "nsReadableUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemRootDirectoryEntry, + FileSystemDirectoryEntry, mEntries) + +NS_IMPL_ADDREF_INHERITED(FileSystemRootDirectoryEntry, FileSystemDirectoryEntry) +NS_IMPL_RELEASE_INHERITED(FileSystemRootDirectoryEntry, + FileSystemDirectoryEntry) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemRootDirectoryEntry) +NS_INTERFACE_MAP_END_INHERITING(FileSystemDirectoryEntry) + +FileSystemRootDirectoryEntry::FileSystemRootDirectoryEntry( + nsIGlobalObject* aGlobal, Sequence<RefPtr<FileSystemEntry>> aEntries, + FileSystem* aFileSystem) + : FileSystemDirectoryEntry(aGlobal, nullptr, nullptr, aFileSystem), + mEntries(std::move(aEntries)) { + MOZ_ASSERT(aGlobal); +} + +FileSystemRootDirectoryEntry::~FileSystemRootDirectoryEntry() = default; + +void FileSystemRootDirectoryEntry::GetName(nsAString& aName, + ErrorResult& aRv) const { + aName.Truncate(); +} + +void FileSystemRootDirectoryEntry::GetFullPath(nsAString& aPath, + ErrorResult& aRv) const { + aPath.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL); +} + +already_AddRefed<FileSystemDirectoryReader> +FileSystemRootDirectoryEntry::CreateReader() { + RefPtr<FileSystemDirectoryReader> reader = + new FileSystemRootDirectoryReader(this, Filesystem(), mEntries); + return reader.forget(); +} + +void FileSystemRootDirectoryEntry::GetInternal( + const nsAString& aPath, const FileSystemFlags& aFlag, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + GetInternalType aType) { + if (!aSuccessCallback.WasPassed() && !aErrorCallback.WasPassed()) { + return; + } + + if (aFlag.mCreate) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsTArray<nsString> parts; + if (!FileSystemUtils::IsValidRelativeDOMPath(aPath, parts)) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + NS_ERROR_DOM_NOT_FOUND_ERR); + return; + } + + MOZ_ASSERT(!parts.IsEmpty()); + + RefPtr<FileSystemEntry> entry; + for (uint32_t i = 0; i < mEntries.Length(); ++i) { + ErrorResult rv; + nsAutoString name; + mEntries[i]->GetName(name, rv); + + if (NS_WARN_IF(rv.Failed())) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + rv.StealNSResult()); + return; + } + + if (name == parts[0]) { + entry = mEntries[i]; + break; + } + } + + // Not found. + if (!entry) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + NS_ERROR_DOM_NOT_FOUND_ERR); + return; + } + + // No subdirectory in the path. + if (parts.Length() == 1) { + if ((entry->IsFile() && aType == eGetDirectory) || + (entry->IsDirectory() && aType == eGetFile)) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + NS_ERROR_DOM_TYPE_MISMATCH_ERR); + return; + } + + if (aSuccessCallback.WasPassed()) { + RefPtr<EntryCallbackRunnable> runnable = + new EntryCallbackRunnable(&aSuccessCallback.Value(), entry); + + FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget()); + } + return; + } + + // Subdirectories, but this is a file. + if (entry->IsFile()) { + ErrorCallbackHelper::Call(GetParentObject(), aErrorCallback, + NS_ERROR_DOM_NOT_FOUND_ERR); + return; + } + + // Let's recreate a path without the first directory. + nsAutoString path; + StringJoinAppend( + path, + NS_LITERAL_STRING_FROM_CSTRING(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL), + Span{parts}.From(1)); + + auto* directoryEntry = static_cast<FileSystemDirectoryEntry*>(entry.get()); + directoryEntry->GetInternal(path, aFlag, aSuccessCallback, aErrorCallback, + aType); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/FileSystemRootDirectoryEntry.h b/dom/filesystem/compat/FileSystemRootDirectoryEntry.h new file mode 100644 index 0000000000..b6ffb976b8 --- /dev/null +++ b/dom/filesystem/compat/FileSystemRootDirectoryEntry.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemRootDirectoryEntry_h +#define mozilla_dom_FileSystemRootDirectoryEntry_h + +#include "mozilla/dom/FileSystemDirectoryEntry.h" + +namespace mozilla::dom { + +class FileSystemRootDirectoryEntry final : public FileSystemDirectoryEntry { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemRootDirectoryEntry, + FileSystemDirectoryEntry) + + FileSystemRootDirectoryEntry(nsIGlobalObject* aGlobalObject, + Sequence<RefPtr<FileSystemEntry>> aEntries, + FileSystem* aFileSystem); + + virtual void GetName(nsAString& aName, ErrorResult& aRv) const override; + + virtual void GetFullPath(nsAString& aFullPath, + ErrorResult& aRv) const override; + + virtual already_AddRefed<FileSystemDirectoryReader> CreateReader() override; + + private: + ~FileSystemRootDirectoryEntry(); + + virtual void GetInternal( + const nsAString& aPath, const FileSystemFlags& aFlag, + const Optional<OwningNonNull<FileSystemEntryCallback>>& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + GetInternalType aType) override; + + void Error(const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + nsresult aError) const; + + Sequence<RefPtr<FileSystemEntry>> mEntries; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemRootDirectoryEntry_h diff --git a/dom/filesystem/compat/FileSystemRootDirectoryReader.cpp b/dom/filesystem/compat/FileSystemRootDirectoryReader.cpp new file mode 100644 index 0000000000..5d30f0c1cb --- /dev/null +++ b/dom/filesystem/compat/FileSystemRootDirectoryReader.cpp @@ -0,0 +1,93 @@ +/* -*- 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 "FileSystemRootDirectoryReader.h" +#include "CallbackRunnables.h" +#include "nsIGlobalObject.h" +#include "mozilla/dom/FileSystemDirectoryReaderBinding.h" +#include "mozilla/dom/FileSystemUtils.h" + +namespace mozilla::dom { + +namespace { + +class EntriesCallbackRunnable final : public Runnable { + public: + EntriesCallbackRunnable(FileSystemEntriesCallback* aCallback, + const Sequence<RefPtr<FileSystemEntry>>& aEntries) + : Runnable("EntriesCallbackRunnable"), + mCallback(aCallback), + mEntries(aEntries) { + MOZ_ASSERT(aCallback); + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + Sequence<OwningNonNull<FileSystemEntry>> entries; + for (uint32_t i = 0; i < mEntries.Length(); ++i) { + if (!entries.AppendElement(mEntries[i].forget(), fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + mCallback->Call(entries); + return NS_OK; + } + + private: + const RefPtr<FileSystemEntriesCallback> mCallback; + Sequence<RefPtr<FileSystemEntry>> mEntries; +}; + +} // anonymous namespace + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FileSystemRootDirectoryReader, + FileSystemDirectoryReader, mEntries) + +NS_IMPL_ADDREF_INHERITED(FileSystemRootDirectoryReader, + FileSystemDirectoryReader) +NS_IMPL_RELEASE_INHERITED(FileSystemRootDirectoryReader, + FileSystemDirectoryReader) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FileSystemRootDirectoryReader) +NS_INTERFACE_MAP_END_INHERITING(FileSystemDirectoryReader) + +FileSystemRootDirectoryReader::FileSystemRootDirectoryReader( + FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem, + const Sequence<RefPtr<FileSystemEntry>>& aEntries) + : FileSystemDirectoryReader(aParentEntry, aFileSystem, nullptr), + mEntries(aEntries), + mAlreadyRead(false) { + MOZ_ASSERT(aParentEntry); + MOZ_ASSERT(aFileSystem); +} + +FileSystemRootDirectoryReader::~FileSystemRootDirectoryReader() = default; + +void FileSystemRootDirectoryReader::ReadEntries( + FileSystemEntriesCallback& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + ErrorResult& aRv) { + if (mAlreadyRead) { + RefPtr<EmptyEntriesCallbackRunnable> runnable = + new EmptyEntriesCallbackRunnable(&aSuccessCallback); + + aRv = + FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget()); + return; + } + + // This object can be used only once. + mAlreadyRead = true; + + RefPtr<EntriesCallbackRunnable> runnable = + new EntriesCallbackRunnable(&aSuccessCallback, mEntries); + + aRv = FileSystemUtils::DispatchRunnable(GetParentObject(), runnable.forget()); +} + +} // namespace mozilla::dom diff --git a/dom/filesystem/compat/FileSystemRootDirectoryReader.h b/dom/filesystem/compat/FileSystemRootDirectoryReader.h new file mode 100644 index 0000000000..7429de8a9c --- /dev/null +++ b/dom/filesystem/compat/FileSystemRootDirectoryReader.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileSystemRootDirectoryReader_h +#define mozilla_dom_FileSystemRootDirectoryReader_h + +#include "FileSystemDirectoryReader.h" + +namespace mozilla::dom { + +class FileSystemRootDirectoryReader final : public FileSystemDirectoryReader { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FileSystemRootDirectoryReader, + FileSystemDirectoryReader) + + explicit FileSystemRootDirectoryReader( + FileSystemDirectoryEntry* aParentEntry, FileSystem* aFileSystem, + const Sequence<RefPtr<FileSystemEntry>>& aEntries); + + virtual void ReadEntries( + FileSystemEntriesCallback& aSuccessCallback, + const Optional<OwningNonNull<ErrorCallback>>& aErrorCallback, + ErrorResult& aRv) override; + + private: + ~FileSystemRootDirectoryReader(); + + Sequence<RefPtr<FileSystemEntry>> mEntries; + bool mAlreadyRead; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FileSystemRootDirectoryReader_h diff --git a/dom/filesystem/compat/moz.build b/dom/filesystem/compat/moz.build new file mode 100644 index 0000000000..6757a73d9e --- /dev/null +++ b/dom/filesystem/compat/moz.build @@ -0,0 +1,30 @@ +# -*- 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/. + +TEST_DIRS += ["tests"] + +EXPORTS.mozilla.dom += [ + "FileSystem.h", + "FileSystemDirectoryEntry.h", + "FileSystemDirectoryReader.h", + "FileSystemEntry.h", + "FileSystemFileEntry.h", +] + +UNIFIED_SOURCES += [ + "CallbackRunnables.cpp", + "FileSystem.cpp", + "FileSystemDirectoryEntry.cpp", + "FileSystemDirectoryReader.cpp", + "FileSystemEntry.cpp", + "FileSystemFileEntry.cpp", + "FileSystemRootDirectoryEntry.cpp", + "FileSystemRootDirectoryReader.cpp", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/dom/filesystem/compat/tests/mochitest.ini b/dom/filesystem/compat/tests/mochitest.ini new file mode 100644 index 0000000000..2b6eafb550 --- /dev/null +++ b/dom/filesystem/compat/tests/mochitest.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + script_entries.js + !/dom/html/test/form_submit_server.sjs + +[test_basic.html] +[test_no_dnd.html] +[test_formSubmission.html] diff --git a/dom/filesystem/compat/tests/moz.build b/dom/filesystem/compat/tests/moz.build new file mode 100644 index 0000000000..7c990fbc62 --- /dev/null +++ b/dom/filesystem/compat/tests/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ["mochitest.ini"] diff --git a/dom/filesystem/compat/tests/script_entries.js b/dom/filesystem/compat/tests/script_entries.js new file mode 100644 index 0000000000..7f52fe6bf2 --- /dev/null +++ b/dom/filesystem/compat/tests/script_entries.js @@ -0,0 +1,47 @@ +/* eslint-env mozilla/chrome-script */ +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["File", "Directory"]); +var tmpFile, tmpDir; + +addMessageListener("entries.open", function (e) { + tmpFile = Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + tmpFile.append("file.txt"); + tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + tmpDir = Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + + tmpDir.append("dir-test"); + tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + + var file1 = tmpDir.clone(); + file1.append("foo.txt"); + file1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var dir1 = tmpDir.clone(); + dir1.append("subdir"); + dir1.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + + var file2 = dir1.clone(); + file2.append("bar..txt"); // Note the double .. + file2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var dir2 = dir1.clone(); + dir2.append("subsubdir"); + dir2.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + + File.createFromNsIFile(tmpFile).then(function (file) { + sendAsyncMessage("entries.opened", { + data: [new Directory(tmpDir.path), file], + }); + }); +}); + +addMessageListener("entries.delete", function (e) { + tmpFile.remove(true); + tmpDir.remove(true); + sendAsyncMessage("entries.deleted"); +}); diff --git a/dom/filesystem/compat/tests/test_basic.html b/dom/filesystem/compat/tests/test_basic.html new file mode 100644 index 0000000000..4ad0c37d67 --- /dev/null +++ b/dom/filesystem/compat/tests/test_basic.html @@ -0,0 +1,549 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Blink FileSystem API - subset</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<input id="entries" type="file"></input> +<script type="application/javascript"> +var fileEntry; +var directoryEntry; +var script; + +function setup_tests() { + SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true], + ["dom.filesystem.pathcheck.disabled", true], + ["dom.webkitBlink.filesystem.enabled", true]]}, next); +} + +function populate_entries() { + var url = SimpleTest.getTestFileURL("script_entries.js"); + script = SpecialPowers.loadChromeScript(url); + + function onOpened(message) { + var entries = document.getElementById("entries"); + SpecialPowers.wrap(entries).mozSetDndFilesAndDirectories(message.data); + next(); + } + + script.addMessageListener("entries.opened", onOpened); + script.sendAsyncMessage("entries.open"); +} + +function test_entries() { + var entries = document.getElementById("entries"); + ok("webkitEntries" in entries, "HTMLInputElement.webkitEntries"); + is(entries.webkitEntries.length, 2, "HTMLInputElement.webkitEntries.length == 2"); + is(entries.files.length, 1, "HTMLInputElement.files is still populated"); + + for (var i = 0; i < entries.webkitEntries.length; ++i) { + if (entries.webkitEntries[i].isFile) { + ok(!fileEntry, "We just want 1 fileEntry"); + fileEntry = entries.webkitEntries[i]; + } else { + ok(entries.webkitEntries[i].isDirectory, "If not a file, we have a directory."); + ok(!directoryEntry, "We just want 1 directoryEntry"); + directoryEntry = entries.webkitEntries[i]; + } + } + + next(); +} + +function test_fileEntry() { + ok("name" in fileEntry, "We have a name."); + ok("fullPath" in fileEntry, "We have a fullPath."); + ok("filesystem" in fileEntry, "We have a filesystem."); + + next(); +} + +function test_fileEntry_file() { + fileEntry.file(function(file) { + ok(file, "We have a file here!"); + is(file.name, fileEntry.name, "Same file name."); + next(); + }, function() { + ok(false, "Something when wrong!"); + }); +} + +function test_fileEntry_getParent() { + fileEntry.getParent(function(entry) { + is(fileEntry.fullPath, entry.fullPath, "Top level FileEntry should return itself as parent."); + next(); + }, function() { + ok(false, "This is wrong."); + }); +} + +function test_directoryEntry() { + ok("name" in directoryEntry, "We have a name."); + ok("fullPath" in directoryEntry, "We have a fullPath."); + ok("filesystem" in directoryEntry, "We have a filesystem."); + + next(); +} + +function test_directoryEntry_createReader() { + var reader = directoryEntry.createReader(); + ok(reader, "We have a DirectoryReader"); + + reader.readEntries(function(a) { + ok(Array.isArray(a), "We want an array."); + is(a.length, 2, "reader.readyEntries returns 2 elements."); + + for (var i = 0; i < 2; ++i) { + ok(a[i].name == "subdir" || a[i].name == "foo.txt", "Correct names"); + is(a[i].fullPath, directoryEntry.fullPath + "/" + a[i].name, "FullPath is correct"); + } + + // Called twice: + reader.readEntries(function(a1) { + ok(Array.isArray(a1), "We want an array."); + is(a1.length, 0, "reader.readyEntries returns 0 elements."); + next(); + }, function() { + ok(false, "Something when wrong!"); + }); + }, function() { + ok(false, "Something when wrong!"); + }); +} + +function test_directoryEntry_getParent() { + directoryEntry.getParent(function(entry) { + is(directoryEntry.fullPath, entry.fullPath, "Top level FileEntry should return itself as parent."); + next(); + }, function() { + ok(false, "This is wrong."); + }); +} + +function test_directoryEntry_getFile_securityError() { + directoryEntry.getFile("foo", { create: true }, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "SecurityError", "This must generate a SecurityError."); + next(); + }); +} + +function test_directoryEntry_getFile_typeMismatchError() { + directoryEntry.getFile("subdir", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError."); + next(); + }); +} + +function test_directoryEntry_getFile_nonValidPath() { + directoryEntry.getFile("../../", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_directoryEntry_getFile_nonExistingPath() { + directoryEntry.getFile("foo_bar.txt", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_directoryEntry_getFile_simple() { + directoryEntry.getFile("foo.txt", {}, + function(e) { + is(e.name, "foo.txt", "We have the right FileEntry."); + test_getParent(e, directoryEntry, /* nested */ false); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_directoryEntry_getFile_deep() { + directoryEntry.getFile("subdir/bar..txt", {}, + function(e) { + is(e.name, "bar..txt", "We have the right FileEntry."); + test_getParent(e, directoryEntry, /* nested */ true); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_directoryEntry_getDirectory_securityError() { + directoryEntry.getDirectory("foo", { create: true }, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "SecurityError", "This must generate a SecurityError."); + next(); + }); +} + +function test_directoryEntry_getDirectory_typeMismatchError() { + directoryEntry.getDirectory("foo.txt", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError."); + next(); + }); +} + +function test_directoryEntry_getDirectory_nonValidPath() { + directoryEntry.getDirectory("../../", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_directoryEntry_getDirectory_nonExistingPath() { + directoryEntry.getDirectory("non_existing_dir", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_directoryEntry_getDirectory_simple() { + directoryEntry.getDirectory("subdir", {}, + function(e) { + is(e.name, "subdir", "We have the right DirectoryEntry."); + test_getParent(e, directoryEntry, /* nested */ false); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_directoryEntry_getDirectory_deep() { + directoryEntry.getDirectory("subdir/subsubdir", {}, + function(e) { + is(e.name, "subsubdir", "We have the right DirectoryEntry."); + test_getParent(e, directoryEntry, /* nested */ true); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_filesystem() { + is(fileEntry.filesystem, directoryEntry.filesystem, "FileSystem object is shared."); + + var fs = fileEntry.filesystem; + ok(fs.name, "FileSystem.name exists."); + ok(fs.root, "FileSystem has a root."); + + is(fs.root.name, "", "FileSystem.root.name must be an empty string."); + is(fs.root.fullPath, "/", "FileSystem.root.fullPath must be '/'"); + + var reader = fs.root.createReader(); + reader.readEntries(function(a) { + ok(Array.isArray(a), "We want an array."); + is(a.length, 2, "reader.readyEntries returns 2 elements."); + next(); + }, function() { + ok(false, "Something when wrong!"); + }); +} + +function test_root_getFile_securityError() { + fileEntry.filesystem.root.getFile("foo", { create: true }, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "SecurityError", "This must generate a SecurityError."); + next(); + }); +} + +function test_root_getFile_typeMismatchError() { + fileEntry.filesystem.root.getFile(directoryEntry.name, {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError."); + next(); + }); +} + +function test_root_getFile_nonValidPath() { + fileEntry.filesystem.root.getFile("../../", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_root_getFile_nonExistingPath() { + fileEntry.filesystem.root.getFile("existing.txt", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_root_getFile_simple() { + fileEntry.filesystem.root.getFile(fileEntry.name, {}, + function(e) { + is(e.name, fileEntry.name, "We have the right FileEntry."); + next(); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_root_getFile_deep() { + fileEntry.filesystem.root.getFile(directoryEntry.name + "/subdir/bar..txt", {}, + function(e) { + is(e.name, "bar..txt", "We have the right FileEntry."); + next(); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_root_getDirectory_securityError() { + fileEntry.filesystem.root.getDirectory("foo", { create: true }, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "SecurityError", "This must generate a SecurityError."); + next(); + }); +} + +function test_root_getDirectory_typeMismatchError() { + fileEntry.filesystem.root.getDirectory(fileEntry.name, {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "TypeMismatchError", "This must generate a TypeMismatchError."); + next(); + }); +} + +function test_root_getDirectory_nonValidPath() { + fileEntry.filesystem.root.getDirectory("../../", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_root_getDirectory_nonExistingPath() { + fileEntry.filesystem.root.getDirectory("404", {}, + function() { + ok(false, "This should not happen."); + }, function(e) { + is(e.name, "NotFoundError", "This must generate a NotFoundError."); + next(); + }); +} + +function test_root_getDirectory_simple() { + fileEntry.filesystem.root.getDirectory(directoryEntry.name, {}, + function(e) { + is(e.name, directoryEntry.name, "We have the right DirectoryEntry."); + next(); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_root_getDirectory_deep() { + fileEntry.filesystem.root.getDirectory(directoryEntry.name + "/subdir/subsubdir", {}, + function(e) { + is(e.name, "subsubdir", "We have the right DirectoryEntry."); + next(); + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function cleanUpTestingFiles() { + script.addMessageListener("entries.deleted", function onDeleted() { + script.removeMessageListener("entries.deleted"); + script.destroy(); + next(); + }); + + script.sendAsyncMessage("entries.delete"); +} + +function test_getParent(entry, parentEntry, nested) { + entry.getParent(function(e) { + ok(e, "We have a parent Entry."); + if (!nested) { + is(e, parentEntry, "Parent entry matches"); + next(); + } else { + test_getParent(e, parentEntry, false); + } + }, function(e) { + ok(false, "This should not happen."); + }); +} + +function test_webkitRelativePath() { + fileEntry.file(function(file1) { + ok(file1, "We have a file here!"); + ok(!file1.webkitRelativePath, "webkitRelativePath is an empty string"); + + fileEntry.file(function(file2) { + ok(file2, "We have a file here!"); + ok(!file2.webkitRelativePath, "webkitRelativePath is an empty string"); + isnot(file1, file2, "The 2 files are not the same"); + + next(); + }, function() { + ok(false, "Something when wrong!"); + }); + }, function() { + ok(false, "Something when wrong!"); + }); +} + +function test_deprecatedCallbacks() { + try { + fileEntry.file({ handleEvent: _ => { ok(false, "This function should not be called!"); }}); + ok(false, "fileEntry.file() should throw with wrong arguments"); + } catch (e) { + ok(true, "fileEntry.file() should throw with wrong arguments"); + is(e.name, "TypeError", "Correct exception"); + } + + try { + fileEntry.getParent({ handleEvent: _ => { ok(false, "This function should not be called!"); }}); + ok(false, "fileEntry.getParent() should throw with wrong arguments"); + } catch (e) { + ok(true, "fileEntry.getParent() should throw with wrong arguments"); + is(e.name, "TypeError", "Correct exception"); + } + + try { + directoryEntry.getFile("file.txt", {}, { handleEvent: _ => { ok(false, "This function should not be called!"); }}); + ok(false, "directoryEntry.getFile() should throw with wrong arguments"); + } catch (e) { + ok(true, "directoryEntry.getFile() should throw with wrong arguments"); + is(e.name, "TypeError", "Correct exception"); + } + + try { + directoryEntry.getDirectory("foo", { create: true }, { handleEvent: _ => { ok(false, "This function should not be called!"); }}); + ok(false, "directoryEntry.getDirectory() should throw with wrong arguments"); + } catch (e) { + ok(true, "directoryEntry.getDirectory() should throw with wrong arguments"); + is(e.name, "TypeError", "Correct exception"); + } + + try { + directoryEntry.getParent({ handleEvent: _ => { ok(false, "This function should not be called!"); }}); + ok(false, "directoryEntry.getParent() should throw with wrong arguments"); + } catch (e) { + ok(true, "directoryEntry.getParent() should throw with wrong arguments"); + is(e.name, "TypeError", "Correct exception"); + } + + let reader = directoryEntry.createReader(); + ok(reader, "We have a DirectoryReader"); + + try { + reader.readEntries({ handleEvent: _ => { ok(false, "This function should not be called!"); }}); + ok(false, "reader.readEntries() should throw with wrong arguments"); + } catch (e) { + ok(true, "reader.readEntries() should throw with wrong arguments"); + is(e.name, "TypeError", "Correct exception"); + } + + next(); +} + +var tests = [ + setup_tests, + populate_entries, + + test_entries, + + test_fileEntry, + test_fileEntry_file, + test_fileEntry_getParent, + + test_directoryEntry, + test_directoryEntry_createReader, + test_directoryEntry_getParent, + + test_directoryEntry_getFile_securityError, + test_directoryEntry_getFile_typeMismatchError, + test_directoryEntry_getFile_nonValidPath, + test_directoryEntry_getFile_nonExistingPath, + test_directoryEntry_getFile_simple, + test_directoryEntry_getFile_deep, + + test_directoryEntry_getDirectory_securityError, + test_directoryEntry_getDirectory_typeMismatchError, + test_directoryEntry_getDirectory_nonValidPath, + test_directoryEntry_getDirectory_nonExistingPath, + test_directoryEntry_getDirectory_simple, + test_directoryEntry_getDirectory_deep, + + test_filesystem, + + test_root_getFile_securityError, + test_root_getFile_typeMismatchError, + test_root_getFile_nonValidPath, + test_root_getFile_nonExistingPath, + test_root_getFile_simple, + test_root_getFile_deep, + + test_root_getDirectory_securityError, + test_root_getDirectory_typeMismatchError, + test_root_getDirectory_nonValidPath, + test_root_getDirectory_nonExistingPath, + test_root_getDirectory_simple, + test_root_getDirectory_deep, + + test_webkitRelativePath, + + test_deprecatedCallbacks, + + cleanUpTestingFiles, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); +</script> +</body> +</html> diff --git a/dom/filesystem/compat/tests/test_formSubmission.html b/dom/filesystem/compat/tests/test_formSubmission.html new file mode 100644 index 0000000000..2447e7a071 --- /dev/null +++ b/dom/filesystem/compat/tests/test_formSubmission.html @@ -0,0 +1,271 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Directory form submission</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> +</head> +<body onload="return next();"> + +<iframe name="target_iframe" id="target_iframe"></iframe> + +<form action="../../../html/test/form_submit_server.sjs" target="target_iframe" id="form" + method="POST" enctype="multipart/form-data"> +</form> + +<script class="testbody" type="text/javascript"> +var form; +var iframe; +var input; +var script; +var xhr; + +function setup_tests() { + form = document.getElementById("form"); + + iframe = document.getElementById("target_iframe"); + iframe.onload = function() { + info("Frame loaded!"); + next(); + }; + + SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true], + ["dom.filesystem.pathcheck.disabled", true], + ["dom.webkitBlink.filesystem.enabled", true]]}, next); +} + +function populate_entries(webkitDirectory) { + var url = SimpleTest.getTestFileURL("script_entries.js"); + script = SpecialPowers.loadChromeScript(url); + + if (input) { + form.removeChild(input); + } + + input = document.createElement("input"); + input.setAttribute("id", "input"); + input.setAttribute("type", "file"); + input.setAttribute("name", "input"); + + if (webkitDirectory) { + input.setAttribute("webkitdirectory", "true"); + } + + form.appendChild(input); + + function onOpened(message) { + input.addEventListener("change", function() { + next(); + }, {once: true}); + + SpecialPowers.wrap(input).mozSetDndFilesAndDirectories([message.data[0]]); + } + + script.addMessageListener("entries.opened", onOpened); + script.sendAsyncMessage("entries.open"); +} + +function delete_entries() { + script.sendAsyncMessage("entries.delete"); + script.addMessageListener("entries.deleted", function() { + script.destroy(); + next(); + }); +} + +function setup_plain() { + info("Preparing for a plain text submission..."); + form.action = "../../../html/test/form_submit_server.sjs?plain"; + form.method = "POST"; + form.enctype = "text/plain"; + form.submit(); +} + +function test_plain() { + var content = iframe.contentDocument.documentElement.textContent; + var submission = JSON.parse(content); + info(submission); + is(submission, input.webkitEntries.map(function(v) { + return "input=" + v.name + "\r\n"; + }).join(""), "Data match"); + + next(); +} + +function setup_urlencoded() { + info("Preparing for a urlencoded submission..."); + form.action = "../../../html/test/form_submit_server.sjs?url"; + form.method = "POST"; + form.enctype = "application/x-www-form-urlencoded"; + form.submit(); +} + +function setup_urlencoded_get() { + info("Preparing for a urlencoded+GET submission..."); + form.action = "../../../html/test/form_submit_server.sjs?xxyy"; + form.method = "GET"; + form.enctype = ""; + form.submit(); +} + +function setup_urlencoded_empty() { + info("Preparing for a urlencoded+default values submission..."); + form.action = "../../../html/test/form_submit_server.sjs"; + form.method = ""; + form.enctype = ""; + form.submit(); +} + +function test_urlencoded() { + var content = iframe.contentDocument.documentElement.textContent; + var submission = JSON.parse(content); + info(submission); + is(submission, input.webkitEntries.map(function(v) { + return "input=" + v.name; + }).join("&"), "Data match"); + + next(); +} + +function setup_formData() { + info("Preparing for a fromData submission..."); + + xhr = new XMLHttpRequest(); + xhr.onload = next; + xhr.open("POST", "../../../html/test/form_submit_server.sjs"); + xhr.send(new FormData(form)); +} + +function test_multipart() { + var submission = JSON.parse(xhr.responseText); + + var array = input.webkitEntries; + is(submission.length, array.length, "Same length"); + info(submission); + + for (var i = 0; i < array.length; ++i) { + if (array[i].isDirectory) { + is(submission[i].headers["Content-Disposition"], + "form-data; name=\"input\"; filename=\"/" + array[i].name + "\"", + "Correct Content-Disposition"); + is(submission[i].headers["Content-Type"], "application/octet-stream", + "Correct Content-Type"); + is(submission[i].body, "", "Correct body"); + } else { + ok(array[i].isFile); + is(submission[i].headers["Content-Disposition"], + "form-data; name=\"input\"; filename=\"" + array[i].name + "\"", + "Correct Content-Disposition"); + is(submission[i].headers["Content-Type"], array[i].type, + "Correct Content-Type"); + is(submission[i].body, "", "Correct body"); + } + } + + next(); +} + +function getInputFiles(inputElement) { + var array = []; + for (var i = 0; i < inputElement.files.length; ++i) { + array.push(inputElement.files[i]); + } + return array; +} + +function test_webkit_plain() { + var content = iframe.contentDocument.documentElement.textContent; + var submission = JSON.parse(content); + + is(submission, getInputFiles(input).map(function(v) { + return "input=" + v.name + "\r\n"; + }).join(""), "Data match"); + + next(); +} + +function test_webkit_urlencoded() { + var content = iframe.contentDocument.documentElement.textContent; + var submission = JSON.parse(content); + is(submission, getInputFiles(input).map(function(v) { + return "input=" + v.name; + }).join("&"), "Data match"); + + next(); +} + +function test_webkit_multipart() { + var submission = JSON.parse(xhr.responseText); + var array = getInputFiles(input); + is(submission.length, array.length, "Same length"); + + for (var i = 0; i < array.length; ++i) { + ok(array[i] instanceof File); + is(submission[i].headers["Content-Disposition"], + "form-data; name=\"input\"; filename=\"" + array[i].webkitRelativePath + "\"", + "Correct Content-Disposition"); + is(submission[i].headers["Content-Type"], array[i].type, + "Correct Content-Type"); + is(submission[i].body, "", "Correct body"); + } + next(); +} + +var tests = [ + setup_tests, + + function() { populate_entries(false); }, + + setup_plain, + test_plain, + + setup_urlencoded, + test_urlencoded, + + setup_urlencoded_get, + test_urlencoded, + + setup_urlencoded_empty, + test_urlencoded, + + setup_formData, + test_multipart, + + delete_entries, + + function() { populate_entries(true); }, + + setup_plain, + test_webkit_plain, + + setup_urlencoded, + test_webkit_urlencoded, + + setup_urlencoded_get, + test_webkit_urlencoded, + + setup_urlencoded_empty, + test_webkit_urlencoded, + + setup_formData, + test_webkit_multipart, + + delete_entries, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/filesystem/compat/tests/test_no_dnd.html b/dom/filesystem/compat/tests/test_no_dnd.html new file mode 100644 index 0000000000..c49dd5d40f --- /dev/null +++ b/dom/filesystem/compat/tests/test_no_dnd.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Blink FileSystem API - no DND == no webkitEntries</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<script type="application/javascript"> +var fileEntry; +var directoryEntry; +var script; +var entries; + +function setup_tests() { + SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true], + ["dom.filesystem.pathcheck.disabled", true], + ["dom.webkitBlink.filesystem.enabled", true]]}, next); +} + +function populate_entries() { + entries = document.createElement("input"); + entries.setAttribute("type", "file"); + document.body.appendChild(entries); + + var url = SimpleTest.getTestFileURL("script_entries.js"); + script = SpecialPowers.loadChromeScript(url); + + function onOpened(message) { + for (var i = 0 ; i < message.data.length; ++i) { + if (message.data[i] instanceof File) { + SpecialPowers.wrap(entries).mozSetFileArray([message.data[i]]); + next(); + } + } + } + + script.addMessageListener("entries.opened", onOpened); + script.sendAsyncMessage("entries.open"); +} + +function test_entries() { + ok("webkitEntries" in entries, "HTMLInputElement.webkitEntries"); + is(entries.webkitEntries.length, 0, "HTMLInputElement.webkitEntries.length == 0"); + is(entries.files.length, 1, "HTMLInputElement.files is still populated"); + + next(); +} + +function cleanUpTestingFiles() { + script.addMessageListener("entries.deleted", function onDeleted() { + script.removeMessageListener("entries.deleted"); + script.destroy(); + next(); + }); + + script.sendAsyncMessage("entries.delete"); +} + +var tests = [ + setup_tests, + populate_entries, + + test_entries, + + cleanUpTestingFiles, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); +</script> +</body> +</html> diff --git a/dom/filesystem/moz.build b/dom/filesystem/moz.build new file mode 100644 index 0000000000..8287f85777 --- /dev/null +++ b/dom/filesystem/moz.build @@ -0,0 +1,50 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: File") + +DIRS += ["compat"] + +TEST_DIRS += ["tests"] + +EXPORTS.mozilla.dom += [ + "Directory.h", + "FileSystemBase.h", + "FileSystemRequestParent.h", + "FileSystemSecurity.h", + "FileSystemTaskBase.h", + "FileSystemUtils.h", + "GetFilesHelper.h", + "OSFileSystem.h", +] + +UNIFIED_SOURCES += [ + "Directory.cpp", + "FileSystemBase.cpp", + "FileSystemRequestParent.cpp", + "FileSystemSecurity.cpp", + "FileSystemTaskBase.cpp", + "FileSystemUtils.cpp", + "GetDirectoryListingTask.cpp", + "GetFileOrDirectoryTask.cpp", + "GetFilesHelper.cpp", + "GetFilesTask.cpp", + "OSFileSystem.cpp", +] + +FINAL_LIBRARY = "xul" + +IPDL_SOURCES += [ + "PFileSystemParams.ipdlh", + "PFileSystemRequest.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/dom/base", +] diff --git a/dom/filesystem/tests/filesystem_commons.js b/dom/filesystem/tests/filesystem_commons.js new file mode 100644 index 0000000000..af0ce36339 --- /dev/null +++ b/dom/filesystem/tests/filesystem_commons.js @@ -0,0 +1,180 @@ +function createPath(parentDir, dirOrFile) { + return parentDir.path + (parentDir.path == "/" ? "" : "/") + dirOrFile.name; +} + +function createRelativePath(parentDir, dirOrFile) { + let path = createPath(parentDir, dirOrFile); + is(path[0], "/", "The full path should start with '/'"); + return path.substring(1); +} + +function setup_tests(aNext) { + SimpleTest.requestLongerTimeout(2); + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.filesystem.pathcheck.disabled", true], + ["dom.webkitBlink.dirPicker.enabled", true], + ], + }, + aNext + ); +} + +function test_basic(aDirectory, aNext) { + ok(aDirectory, "Directory exists."); + ok(aDirectory instanceof Directory, "We have a directory."); + is(aDirectory.path, "/" + aDirectory.name, "directory.path must be '/'+name"); + aNext(); +} + +function test_getFilesAndDirectories(aDirectory, aRecursive, aNext) { + function checkSubDir(dir) { + return dir.getFilesAndDirectories().then(function (data) { + for (var i = 0; i < data.length; ++i) { + ok( + data[i] instanceof File || data[i] instanceof Directory, + "Just Files or Directories" + ); + if (data[i] instanceof Directory) { + isnot( + data[i].name, + "/", + "Subdirectory should be called with the leafname" + ); + isnot( + data[i].path, + "/", + "Subdirectory path should be called with the leafname" + ); + isnot( + data[i].path, + dir.path, + "Subdirectory path should contain the parent path." + ); + is( + data[i].path, + createPath(dir, data[i]), + "Subdirectory path should be called parentdir.path + '/' + leafname: " + + data[i].path + ); + } + + if (data[i] instanceof File) { + is( + data[i].webkitRelativePath, + createRelativePath(dir, data[i]), + "File.webkitRelativePath should be called: parentdir.path + '/' + file.name: " + + data[i].webkitRelativePath + ); + ok( + !data[i].webkitRelativePath.endsWith("symlink.txt"), + "We should never see a path ending with symlink.txt, our symlink sentinel." + ); + } + } + }); + } + + aDirectory + .getFilesAndDirectories() + .then( + function (data) { + ok(data.length, "We should have some data."); + var promises = []; + for (var i = 0; i < data.length; ++i) { + ok( + data[i] instanceof File || data[i] instanceof Directory, + "Just Files or Directories: " + data[i].name + ); + if (data[i] instanceof Directory) { + isnot( + data[i].name, + "/", + "Subdirectory should be called with the leafname" + ); + is( + data[i].path, + createPath(aDirectory, data[i]), + "Subdirectory path should be called parentdir.path + '/' + leafname: " + + data[i].path + ); + if (aRecursive) { + promises.push(checkSubDir(data[i])); + } + } + + if (data[i] instanceof File) { + is( + data[i].webkitRelativePath, + createRelativePath(aDirectory, data[i]), + "File.webkitRelativePath should be called file.name: " + + data[i].webkitRelativePath + ); + } + } + + return Promise.all(promises); + }, + function () { + ok(false, "Something when wrong"); + } + ) + .then(aNext); +} + +function test_getFiles(aDirectory, aRecursive, aNext) { + aDirectory + .getFiles(aRecursive) + .then( + function (data) { + for (var i = 0; i < data.length; ++i) { + ok(data[i] instanceof File, "File: " + data[i].name); + is(aDirectory.path[0], "/", "Directory path must start with '/'"); + ok( + data[i].webkitRelativePath.indexOf(aDirectory.path.substring(1)) == + 0 && + data[i].webkitRelativePath.indexOf("/" + data[i].name) + + ("/" + data[i].name).length == + data[i].webkitRelativePath.length, + "File.webkitRelativePath should be called dir.path + '/' + file.name: " + + data[i].webkitRelativePath + ); + } + }, + function () { + ok(false, "Something when wrong"); + } + ) + .then(aNext); +} + +function test_getFiles_recursiveComparison(aDirectory, aNext) { + aDirectory + .getFiles(true) + .then(function (data) { + is(data.length, 2, "Only 2 files for this test."); + ok( + data[0].name == "foo.txt" || data[0].name == "bar.txt", + "First filename matches" + ); + ok( + data[1].name == "foo.txt" || data[1].name == "bar.txt", + "Second filename matches" + ); + }) + .then(function () { + return aDirectory.getFiles(false); + }) + .then(function (data) { + is(data.length, 1, "Only 1 file for this test."); + ok( + data[0].name == "foo.txt" || data[0].name == "bar.txt", + "First filename matches" + ); + }) + .catch(function () { + ok(false, "Something when wrong"); + }) + .then(aNext); +} diff --git a/dom/filesystem/tests/mochitest.ini b/dom/filesystem/tests/mochitest.ini new file mode 100644 index 0000000000..00326a6e0d --- /dev/null +++ b/dom/filesystem/tests/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = + filesystem_commons.js + script_fileList.js + worker_basic.js + +[test_basic.html] +[test_webkitdirectory.html] +skip-if = os == "android" # Bug 1674428 +support-files = script_promptHandler.js +[test_worker_basic.html] +[test_bug1319088.html] diff --git a/dom/filesystem/tests/moz.build b/dom/filesystem/tests/moz.build new file mode 100644 index 0000000000..7c990fbc62 --- /dev/null +++ b/dom/filesystem/tests/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ["mochitest.ini"] diff --git a/dom/filesystem/tests/script_fileList.js b/dom/filesystem/tests/script_fileList.js new file mode 100644 index 0000000000..47438aa7b3 --- /dev/null +++ b/dom/filesystem/tests/script_fileList.js @@ -0,0 +1,176 @@ +/* eslint-env mozilla/chrome-script */ +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["File"]); +function createProfDFile() { + return Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); +} + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +// Creates a parametric arity directory hierarchy as a function of depth. +// Each directory contains one leaf file, and subdirectories of depth [1, depth). +// e.g. for depth 3: +// +// subdir3 +// - file.txt +// - subdir2 +// - file.txt +// - subdir1 +// - file.txt +// - subdir1 +// - file.txt +// +// Returns the parent directory of the subtree. +function createTreeFile(depth, parent) { + if (!parent) { + parent = Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + parent.append("dir-tree-test"); + parent.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + } + + var nextFile = parent.clone(); + if (depth == 0) { + nextFile.append("file.txt"); + nextFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + // It's not possible to create symlinks on windows by default or on our + // Android platforms, so we can't create the symlink file there. Our + // callers that care are aware of this and also check AppConstants. + if ( + AppConstants.platform !== "win" && + AppConstants.platform !== "android" + ) { + var linkFile = parent.clone(); + linkFile.append("symlink.txt"); + createSymLink(nextFile.path, linkFile.path); + } + } else { + nextFile.append("subdir" + depth); + nextFile.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + // Decrement the maximal depth by one for each level of nesting. + for (var i = 0; i < depth; i++) { + createTreeFile(i, nextFile); + } + } + + return parent; +} + +function createRootFile() { + var testFile = createProfDFile(); + + // Let's go back to the root of the FileSystem + while (true) { + var parent = testFile.parent; + if (!parent) { + break; + } + + testFile = parent; + } + + return testFile; +} + +var process; +function createSymLink(target, linkName) { + if (!process) { + var ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + ln.initWithPath("/bin/ln"); + + process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(ln); + } + + const args = ["-s", target, linkName]; + process.run(true, args, args.length); + Assert.equal(process.exitValue, 0); +} + +function createTestFile() { + var tmpFile = Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + tmpFile.append("dir-test"); + tmpFile.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + + var file1 = tmpFile.clone(); + file1.append("foo.txt"); + file1.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + var dir = tmpFile.clone(); + dir.append("subdir"); + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700); + + var file2 = dir.clone(); + file2.append("bar.txt"); + file2.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + // It's not possible to create symlinks on windows by default or on our + // Android platforms, so we can't create the symlink file there. Our + // callers that care are aware of this and also check AppConstants. + if (AppConstants.platform !== "win" && AppConstants.platform !== "android") { + var linkFile = dir.clone(); + linkFile.append("symlink.txt"); + createSymLink(file1.path, linkFile.path); + } + + return tmpFile; +} + +addMessageListener("dir.open", function (e) { + var testFile; + + switch (e.path) { + case "ProfD": + // Note that files in the profile directory are not guaranteed to persist- + // see bug 1284742. + testFile = createProfDFile(); + break; + + case "root": + testFile = createRootFile(); + break; + + case "test": + testFile = createTestFile(); + break; + + case "tree": + testFile = createTreeFile(3); + break; + } + + sendAsyncMessage("dir.opened", { + dir: testFile.path, + name: testFile.leafName, + }); +}); + +addMessageListener("file.open", function (e) { + var testFile = Services.dirsvc + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("prefs.js"); + + File.createFromNsIFile(testFile).then(function (file) { + sendAsyncMessage("file.opened", { file }); + }); +}); + +addMessageListener("symlink.open", function (e) { + let testDir = createTestFile(); + let testFile = testDir.clone(); + testFile.append("subdir"); + testFile.append("symlink.txt"); + + File.createFromNsIFile(testFile).then(function (file) { + sendAsyncMessage("symlink.opened", { dir: testDir.path, file }); + }); +}); diff --git a/dom/filesystem/tests/script_promptHandler.js b/dom/filesystem/tests/script_promptHandler.js new file mode 100644 index 0000000000..ed3bda7404 --- /dev/null +++ b/dom/filesystem/tests/script_promptHandler.js @@ -0,0 +1,18 @@ +/* eslint-env mozilla/chrome-script */ + +let dialogObserverTopic = "common-dialog-loaded"; + +function dialogObserver(subj, topic, data) { + subj.document.querySelector("dialog").acceptDialog(); + sendAsyncMessage("promptAccepted"); +} + +addMessageListener("init", message => { + Services.obs.addObserver(dialogObserver, dialogObserverTopic); + sendAsyncMessage("initDone"); +}); + +addMessageListener("cleanup", message => { + Services.obs.removeObserver(dialogObserver, dialogObserverTopic); + sendAsyncMessage("cleanupDone"); +}); diff --git a/dom/filesystem/tests/test_basic.html b/dom/filesystem/tests/test_basic.html new file mode 100644 index 0000000000..b4daea79c9 --- /dev/null +++ b/dom/filesystem/tests/test_basic.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Directory API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="filesystem_commons.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<script type="application/javascript"> + +var directory; +var fileList; + +function create_fileList(aPath) { + fileList = document.createElement("input"); + fileList.setAttribute("type", "file"); + document.body.appendChild(fileList); + + var url = SimpleTest.getTestFileURL("script_fileList.js"); + var script = SpecialPowers.loadChromeScript(url); + + function onOpened(message) { + SpecialPowers.wrap(fileList).mozSetDirectory(message.dir); + fileList.setAttribute("data-name", message.name); + + SpecialPowers.wrap(fileList).getFilesAndDirectories().then(function(array) { + array = SpecialPowers.unwrap(array); + is(array.length, 1, "We want just 1 directory."); + ok(array[0] instanceof Directory, "We want just 1 directory."); + + directory = array[0]; + script.destroy(); + next(); + }); + } + + script.addMessageListener("dir.opened", onOpened); + script.sendAsyncMessage("dir.open", { path: aPath }); +} + +function test_simpleFilePicker(aPath) { + var url = SimpleTest.getTestFileURL("script_fileList.js"); + var script = SpecialPowers.loadChromeScript(url); + + function onOpened(message) { + SpecialPowers.wrap(fileList).mozSetFileArray([message.file]); + + is(fileList.files.length, 1, "we want 1 element"); + ok(fileList.files[0] instanceof File, "we want 1 file"); + ok("webkitRelativePath" in fileList.files[0], "we have webkitRelativePath attribute"); + is(fileList.files[0].webkitRelativePath, "", "No webkit relative path for normal filePicker"); + + script.destroy(); + next(); + } + + script.addMessageListener("file.opened", onOpened); + script.sendAsyncMessage("file.open"); +} + +function test_duplicateGetFilesAndDirectories() { + var url = SimpleTest.getTestFileURL("script_fileList.js"); + var script = SpecialPowers.loadChromeScript(url); + + function onOpened(message) { + SpecialPowers.wrap(fileList).mozSetDirectory(message.dir); + + var p1 = SpecialPowers.wrap(fileList).getFilesAndDirectories(); + var p2 = SpecialPowers.wrap(fileList).getFilesAndDirectories(); + + isnot(p1, p2, "We create 2 different promises"); + + script.destroy(); + next(); + } + + script.addMessageListener("dir.opened", onOpened); + script.sendAsyncMessage("dir.open", { path: "test" }); +} + +var tests = [ + function() { setup_tests(next); }, + + function() { create_fileList("tree"); }, + function() { test_basic(directory, next); }, + function() { test_getFilesAndDirectories(directory, true, next); }, + function() { test_getFiles(directory, false, next); }, + function() { test_getFiles(directory, true, next); }, + + function() { create_fileList("test"); }, + function() { test_getFiles_recursiveComparison(directory, next); }, + + function() { create_fileList("root"); }, + function() { test_basic(directory, next); }, + function() { test_getFilesAndDirectories(directory, false, next); }, + function() { test_getFiles(directory, false, next); }, + + test_duplicateGetFilesAndDirectories, + test_simpleFilePicker, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); +</script> +</body> +</html> diff --git a/dom/filesystem/tests/test_bug1319088.html b/dom/filesystem/tests/test_bug1319088.html new file mode 100644 index 0000000000..98e0ad46f0 --- /dev/null +++ b/dom/filesystem/tests/test_bug1319088.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1319088</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<input id="input" type="file"></input> + +<script type="application/javascript"> + +function testSetup() { + SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true]]}, next); +} + +function populateInputFile() { + var url = SimpleTest.getTestFileURL("script_fileList.js"); + var script = SpecialPowers.loadChromeScript(url); + + function onOpened(message) { + var input = document.getElementById("input"); + SpecialPowers.wrap(input).mozSetFileArray([message.file]); + + script.destroy(); + next(); + } + + script.addMessageListener("file.opened", onOpened); + script.sendAsyncMessage("file.open"); +} + +function checkBug() { + var input = document.getElementById("input"); + is(input.files[0].webkitRelativePath, "", "No relative path!"); + + let form = document.createElement("form"); + form.appendChild(input); + + is(input.files[0].webkitRelativePath, "", "No relative path!"); + SimpleTest.finish(); +} + +var tests = [ + testSetup, + populateInputFile, + checkBug, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); +</script> +</body> +</html> diff --git a/dom/filesystem/tests/test_webkitdirectory.html b/dom/filesystem/tests/test_webkitdirectory.html new file mode 100644 index 0000000000..50080ad7ba --- /dev/null +++ b/dom/filesystem/tests/test_webkitdirectory.html @@ -0,0 +1,309 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for webkitdirectory and webkitRelativePath</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<input id="inputFileWebkitDirectory" type="file" webkitdirectory></input> +<input id="inputFileWebkitFile" type="file"></input> +<input id="inputFileDirectoryChange" type="file" webkitdirectory></input> + +<script type="application/javascript"> + +const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +let promptHandler; + +function waitForEvent(element, eventName) { + return new Promise(function(resolve) { + element.addEventListener(eventName, e => resolve(e.detail), { once: true }); + }); +} + +function waitForPromptHandled() { + return new Promise(resolve => promptHandler.addMessageListener("promptAccepted", resolve)); +} + +// Populate the given input type=file `aInputFile`'s `files` attribute by: +// - loading `script_fileList.js` in the parent process +// - telling it to generate the "test" template directory pattern which will +// create "foo.txt", "subdir/bar.txt", and if symlinks are available on the +// platform, "symlink.txt" which will be a symlink to "foo.txt". (Note that +// we explicitly expect the symlink to be filtered out if generated, and +// during the enhancement of the test we verified the file was created on +// linux by running the test before fixing the GetFilesHelper logic to filter +// the symlink out and verifying the subsequent `test_fileList` check failed.) +// - Triggering the mock file picker with the base directory of the "test" +// template directory. +// +// It's expected that `test_fileList` will be used after this step completes in +// order to validate the results. +function populateInputFile(aInputFile) { + var url = SimpleTest.getTestFileURL("script_fileList.js"); + var script = SpecialPowers.loadChromeScript(url); + + var MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder); + + async function onOpened(message) { + MockFilePicker.useDirectory(message.dir); + + let input = document.getElementById(aInputFile); + input.setAttribute("data-name", message.name); + + let promptHandled = waitForPromptHandled(); + let changeEvent = waitForEvent(input, "change"); + + input.click(); + + await promptHandled; + await changeEvent; + + MockFilePicker.cleanup(); + script.destroy(); + next(); + } + + script.addMessageListener("dir.opened", onOpened); + script.sendAsyncMessage("dir.open", { path: "test" }); +} + +function checkFile(file, fileList, dirName) { + for (var i = 0; i < fileList.length; ++i) { + ok(fileList[i] instanceof File, "We want just files."); + if (fileList[i].name == file.name) { + is(fileList[i].webkitRelativePath, dirName + file.path, "Path matches"); + return; + } + } + + ok(false, "File not found."); +} + +// Validate the contents of the given input type=file `aInputFile`'s' `files` +// property against the expected list of files `aWhat`. +function test_fileList(aInputFile, aWhat) { + var input = document.getElementById(aInputFile); + var fileList = input.files; + + if (aWhat == null) { + is(fileList, null, "We want a null fileList for " + aInputFile); + next(); + return; + } + + is(fileList.length, aWhat.length, "We want just " + aWhat.length + " elements for " + aInputFile); + for (var i = 0; i < aWhat.length; ++i) { + checkFile(aWhat[i], fileList, input.dataset.name); + } + + next(); +} + +// Verify that we can explicitly select a symlink and it will not be filtered +// out. This is really a verification that GetFileHelper's file-handling logic +// https://searchfox.org/mozilla-central/rev/065102493dfc49234120c37fc6a334a5b1d86d9e/dom/filesystem/GetFilesHelper.cpp#81-86 +// does not proactively take an action to filter out a selected symlink. +// +// This is a glass box test that is not entirely realistic for our actual system +// file pickers but does reflect what will happen in the drag-and-drop case +// for `HTMLInputElement::MozSetDndFilesAndDirectories` and this helps ensure +// that future implementation changes will behave as expected. Specifically, +// the presence of webkitdirectory will result in the file picker using +// `modeGetFolder` which will only allow selection of a directory and forbid +// file selection. +// +// This test explicitly does not validate HTMLInputElement's non-webkitdirectory +// file selection mechanism because it does not involve GetFileHelper. +async function test_individualSymlink(aInputFile) { + const input = document.getElementById(aInputFile); + + // -- Create the symlink and get a `File` instance pointing at it. + const url = SimpleTest.getTestFileURL("script_fileList.js"); + const script = SpecialPowers.loadChromeScript(url); + + let opened = new Promise(resolve => script.addMessageListener("symlink.opened", resolve)); + script.sendAsyncMessage("symlink.open", {}); + let { dir, file: symlinkFile } = await opened; + info(`symlink.open provided dir: ${dir}`) + + // -- Have the picker pick it + var MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeOpen); + + MockFilePicker.displayDirectory = dir; + let pickerShown = new Promise(resolve => { + MockFilePicker.showCallback = function() { + // This is where we are diverging from a realistic scenario in order to get + // the expected coverage. + MockFilePicker.setFiles([symlinkFile]); + resolve(); + } + }); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + let changeEvent = waitForEvent(input, "change"); + + input.click(); + + await pickerShown; + await changeEvent; + + MockFilePicker.cleanup(); + script.destroy(); + + // -- Verify that we see the symlink. + let fileList = input.files; + is(fileList.length, 1, "There should be 1 file."); + is(fileList[0].name, "symlink.txt", "The file should be the symlink."); + next(); +} + +function test_webkitdirectory_attribute() { + var a = document.createElement("input"); + a.setAttribute("type", "file"); + + ok("webkitdirectory" in a, "HTMLInputElement.webkitdirectory exists"); + + ok(!a.hasAttribute("webkitdirectory"), "No webkitdirectory DOM attribute by default"); + ok(!a.webkitdirectory, "No webkitdirectory attribute by default"); + + a.webkitdirectory = true; + + ok(a.hasAttribute("webkitdirectory"), "Webkitdirectory DOM attribute is set"); + ok(a.webkitdirectory, "Webkitdirectory attribute is set"); + + next(); +} + +function test_changeDataWhileWorking() { + var url = SimpleTest.getTestFileURL("script_fileList.js"); + var script = SpecialPowers.loadChromeScript(url); + + var MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder); + let promptHandled; + + // Let's start retrieving the root nsIFile object + new Promise(function(resolve) { + function onOpened(message) { + script.removeMessageListener("dir.opened", onOpened); + resolve(message.dir); + } + + script.addMessageListener("dir.opened", onOpened); + script.sendAsyncMessage("dir.open", { path: "root" }); + }) + + // input.click() pointing to the root dir + .then(async function(aDir) { + MockFilePicker.cleanup(); + MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder); + MockFilePicker.useDirectory(aDir); + var input = document.getElementById("inputFileDirectoryChange"); + + promptHandled = waitForPromptHandled(); + input.click(); + }) + + // Before onchange, let's take the 'test' directory + .then(function() { + return new Promise(function(resolve) { + function onOpened(message) { + script.removeMessageListener("dir.opened", onOpened); + script.destroy(); + resolve(message.dir); + } + + script.addMessageListener("dir.opened", onOpened); + script.sendAsyncMessage("dir.open", { path: "test" }); + }); + }) + + // Now let's click again and wait for onchange. + .then(async function(aDir) { + MockFilePicker.cleanup(); + MockFilePicker.init(window, "A Mock File Picker", SpecialPowers.Ci.nsIFilePicker.modeGetFolder); + MockFilePicker.useDirectory(aDir); + + let input = document.getElementById("inputFileDirectoryChange"); + let changeEvent = waitForEvent(input, "change"); + + input.click(); + + await promptHandled; + await changeEvent; + + MockFilePicker.cleanup(); + }) + .then(function() { + test_fileList("inputFileWebkitDirectory", testDirData); + }); +} + +async function test_setup() { + let promptHandlerUrl = SimpleTest.getTestFileURL("script_promptHandler.js") + promptHandler = SpecialPowers.loadChromeScript(promptHandlerUrl); + + let promptHandlerReady = new Promise(resolve => promptHandler.addMessageListener("initDone", resolve)); + promptHandler.sendAsyncMessage("init"); + await promptHandlerReady; + + SpecialPowers.pushPrefEnv({"set": [["dom.filesystem.pathcheck.disabled", true], + ["dom.webkitBlink.dirPicker.enabled", true]]}, next); +} + +async function test_cleanup() { + let promptHandlerDone = new Promise(resolve => promptHandler.addMessageListener("cleanupDone", resolve)); + promptHandler.sendAsyncMessage("cleanup"); + await promptHandlerDone; + promptHandler.destroy(); +} + +var testDirData = [ { name: "foo.txt", path: "/foo.txt" }, + { name: "bar.txt", path: "/subdir/bar.txt" }]; + +var tests = [ + test_setup, + + function() { populateInputFile("inputFileWebkitDirectory"); }, + + function() { test_fileList("inputFileWebkitDirectory", testDirData); }, + + function() { + // Symlinks are not available on Windows and so will not be created. + if (AppConstants.platform === "win" || AppConstants.platform === "android") { + info("Skipping individual symlink check on Windows and Android."); + next(); + return; + } + + test_individualSymlink("inputFileWebkitFile").catch(err => ok(false, `Problem in symlink case: ${err}`)); + }, + + test_webkitdirectory_attribute, + + test_changeDataWhileWorking, +]; + +async function next() { + if (!tests.length) { + await test_cleanup(); + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + await test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); +</script> +</body> +</html> diff --git a/dom/filesystem/tests/test_worker_basic.html b/dom/filesystem/tests/test_worker_basic.html new file mode 100644 index 0000000000..920f32719b --- /dev/null +++ b/dom/filesystem/tests/test_worker_basic.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Directory API in workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="filesystem_commons.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<script type="application/javascript"> + +var fileList; + +function create_fileList() { + fileList = document.createElement("input"); + fileList.setAttribute("type", "file"); + document.body.appendChild(fileList); + + var url = SimpleTest.getTestFileURL("script_fileList.js"); + var script = SpecialPowers.loadChromeScript(url); + + function onOpened(message) { + SpecialPowers.wrap(fileList).mozSetDirectory(message.dir); + script.destroy(); + next(); + } + + script.addMessageListener("dir.opened", onOpened); + script.sendAsyncMessage("dir.open", { path: "test" }); +} + +function test_worker() { + SpecialPowers.wrap(fileList).getFilesAndDirectories().then(function(array) { + array = SpecialPowers.unwrap(array); + var worker = new Worker("worker_basic.js"); + worker.onmessage = function(e) { + if (e.data.type == "finish") { + next(); + return; + } + + if (e.data.type == "test") { + ok(e.data.test, e.data.message); + } + }; + + worker.postMessage(array[0]); + }); +} + +var tests = [ + function() { setup_tests(next); }, + + create_fileList, + test_worker, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); +</script> +</body> +</html> diff --git a/dom/filesystem/tests/worker_basic.js b/dom/filesystem/tests/worker_basic.js new file mode 100644 index 0000000000..2771c778fd --- /dev/null +++ b/dom/filesystem/tests/worker_basic.js @@ -0,0 +1,50 @@ +/* eslint-env worker */ +importScripts("filesystem_commons.js"); + +function finish() { + postMessage({ type: "finish" }); +} + +function ok(a, msg) { + postMessage({ type: "test", test: !!a, message: msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function isnot(a, b, msg) { + ok(a != b, msg); +} + +var tests = [ + function () { + test_basic(directory, next); + }, + function () { + test_getFilesAndDirectories(directory, true, next); + }, + function () { + test_getFiles(directory, false, next); + }, + function () { + test_getFiles(directory, true, next); + }, +]; + +function next() { + if (!tests.length) { + finish(); + return; + } + + var test = tests.shift(); + test(); +} + +var directory; + +onmessage = function (e) { + directory = e.data; + next(); +}; |