blob: dcc72223343c25ddc5433936f64031e1453cb468 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/file_system_access/global_file_system_access.h"
#include <utility>
#include "base/notreached.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/mojom/web_sandbox_flags.mojom-blink.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom-blink-forward.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom-blink.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom-shared.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_directory_picker_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_file_picker_accept_type.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_open_file_picker_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_save_file_picker_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/well_known_directory_or_file_system_handle.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
#include "third_party/blink/renderer/core/fileapi/file_error.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
#include "third_party/blink/renderer/modules/file_system_access/file_system_access_error.h"
#include "third_party/blink/renderer/modules/file_system_access/file_system_directory_handle.h"
#include "third_party/blink/renderer/modules/file_system_access/file_system_file_handle.h"
#include "third_party/blink/renderer/platform/bindings/enumeration_base.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/network/http_parsers.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h"
namespace blink {
namespace {
constexpr char kDefaultStartingDirectoryId[] = "";
constexpr bool IsHTTPWhitespace(UChar chr) {
return chr == ' ' || chr == '\n' || chr == '\t' || chr == '\r';
}
bool IsValidSuffixCodePoint(UChar chr) {
return IsASCIIAlphanumeric(chr) || chr == '+' || chr == '.';
}
bool IsValidIdCodePoint(UChar chr) {
return IsASCIIAlphanumeric(chr) || chr == '_' || chr == '-';
}
bool VerifyIsValidExtension(const String& extension,
ExceptionState& exception_state) {
if (!extension.StartsWith(".")) {
exception_state.ThrowTypeError("Extension '" + extension +
"' must start with '.'.");
return false;
}
if (!extension.IsAllSpecialCharacters<IsValidSuffixCodePoint>()) {
exception_state.ThrowTypeError("Extension '" + extension +
"' contains invalid characters.");
return false;
}
if (extension.EndsWith(".")) {
exception_state.ThrowTypeError("Extension '" + extension +
"' must not end with '.'.");
return false;
}
if (extension.length() > 16) {
exception_state.ThrowTypeError("Extension '" + extension +
"' cannot be longer than 16 characters.");
return false;
}
return true;
}
String VerifyIsValidId(const String& id, ExceptionState& exception_state) {
if (!id.IsAllSpecialCharacters<IsValidIdCodePoint>()) {
exception_state.ThrowTypeError("ID '" + id +
"' contains invalid characters.");
return String();
}
if (id.length() > 32) {
exception_state.ThrowTypeError("ID '" + id +
"' cannot be longer than 32 characters.");
return String();
}
return std::move(id);
}
bool AddExtension(const String& extension,
Vector<String>& extensions,
ExceptionState& exception_state) {
if (!VerifyIsValidExtension(extension, exception_state))
return false;
extensions.push_back(extension.Substring(1));
return true;
}
Vector<mojom::blink::ChooseFileSystemEntryAcceptsOptionPtr> ConvertAccepts(
const HeapVector<Member<FilePickerAcceptType>>& types,
ExceptionState& exception_state) {
Vector<mojom::blink::ChooseFileSystemEntryAcceptsOptionPtr> result;
result.ReserveInitialCapacity(types.size());
for (const auto& t : types) {
Vector<String> mimeTypes;
mimeTypes.ReserveInitialCapacity(t->accept().size());
Vector<String> extensions;
for (const auto& a : t->accept()) {
String type = a.first.StripWhiteSpace(IsHTTPWhitespace);
if (type.IsEmpty()) {
exception_state.ThrowTypeError("Invalid type: " + a.first);
return {};
}
Vector<String> parsed_type;
type.Split('/', true, parsed_type);
if (parsed_type.size() != 2) {
exception_state.ThrowTypeError("Invalid type: " + a.first);
return {};
}
if (!IsValidHTTPToken(parsed_type[0])) {
exception_state.ThrowTypeError("Invalid type: " + a.first);
return {};
}
if (!IsValidHTTPToken(parsed_type[1])) {
exception_state.ThrowTypeError("Invalid type: " + a.first);
return {};
}
mimeTypes.push_back(type);
if (a.second.IsUSVString()) {
if (!AddExtension(a.second.GetAsUSVString(), extensions,
exception_state))
return {};
} else {
for (const auto& extension : a.second.GetAsUSVStringSequence()) {
if (!AddExtension(extension, extensions, exception_state))
return {};
}
}
}
result.emplace_back(
blink::mojom::blink::ChooseFileSystemEntryAcceptsOption::New(
t->hasDescription() ? t->description() : g_empty_string,
std::move(mimeTypes), std::move(extensions)));
}
return result;
}
void VerifyIsAllowedToShowFilePicker(const LocalDOMWindow& window,
ExceptionState& exception_state) {
if (!window.IsCurrentlyDisplayedInFrame()) {
exception_state.ThrowDOMException(DOMExceptionCode::kAbortError, "");
return;
}
if (!window.GetSecurityOrigin()->CanAccessFileSystem()) {
if (window.IsSandboxed(network::mojom::blink::WebSandboxFlags::kOrigin)) {
exception_state.ThrowSecurityError(
"Sandboxed documents aren't allowed to show a file picker.");
return;
} else {
exception_state.ThrowSecurityError(
"This document isn't allowed to show a file picker.");
return;
}
}
LocalFrame* local_frame = window.GetFrame();
if (!local_frame || local_frame->IsCrossOriginToMainFrame()) {
exception_state.ThrowSecurityError(
"Cross origin sub frames aren't allowed to show a file picker.");
return;
}
if (!LocalFrame::HasTransientUserActivation(local_frame)) {
exception_state.ThrowSecurityError(
"Must be handling a user gesture to show a file picker.");
return;
}
}
mojom::blink::WellKnownDirectory ConvertWellKnownDirectory(
const String& directory,
ExceptionState& exception_state) {
if (directory == "")
return mojom::blink::WellKnownDirectory::kDefault;
else if (directory == "desktop")
return mojom::blink::WellKnownDirectory::kDirDesktop;
else if (directory == "documents")
return mojom::blink::WellKnownDirectory::kDirDocuments;
else if (directory == "downloads")
return mojom::blink::WellKnownDirectory::kDirDownloads;
else if (directory == "music")
return mojom::blink::WellKnownDirectory::kDirMusic;
else if (directory == "pictures")
return mojom::blink::WellKnownDirectory::kDirPictures;
else if (directory == "videos")
return mojom::blink::WellKnownDirectory::kDirVideos;
NOTREACHED();
return mojom::blink::WellKnownDirectory::kDefault;
}
ScriptPromise ShowFilePickerImpl(
ScriptState* script_state,
LocalDOMWindow& window,
mojom::blink::FilePickerOptionsPtr options,
mojom::blink::CommonFilePickerOptionsPtr common_options,
bool return_as_sequence) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise resolver_result = resolver->Promise();
// TODO(mek): Cache mojo::Remote<mojom::blink::FileSystemAccessManager>
// associated with an ExecutionContext, so we don't have to request a new one
// for each operation, and can avoid code duplication between here and other
// uses.
mojo::Remote<mojom::blink::FileSystemAccessManager> manager;
window.GetBrowserInterfaceBroker().GetInterface(
manager.BindNewPipeAndPassReceiver());
auto* raw_manager = manager.get();
raw_manager->ChooseEntries(
std::move(options), std::move(common_options),
WTF::Bind(
[](ScriptPromiseResolver* resolver,
mojo::Remote<mojom::blink::FileSystemAccessManager>,
bool return_as_sequence, LocalFrame* local_frame,
mojom::blink::FileSystemAccessErrorPtr file_operation_result,
Vector<mojom::blink::FileSystemAccessEntryPtr> entries) {
ExecutionContext* context = resolver->GetExecutionContext();
if (!context)
return;
if (file_operation_result->status !=
mojom::blink::FileSystemAccessStatus::kOk) {
file_system_access_error::Reject(resolver,
*file_operation_result);
return;
}
// While it would be better to not trust the renderer process,
// we're doing this here to avoid potential mojo message pipe
// ordering problems, where the frame activation state
// reconciliation messages would compete with concurrent File
// System Access messages to the browser.
// TODO(https://crbug.com/1017270): Remove this after spec change,
// or when activation moves to browser.
LocalFrame::NotifyUserActivation(
local_frame, mojom::blink::UserActivationNotificationType::
kFileSystemAccess);
if (return_as_sequence) {
HeapVector<Member<FileSystemHandle>> results;
results.ReserveInitialCapacity(entries.size());
for (auto& entry : entries) {
results.push_back(FileSystemHandle::CreateFromMojoEntry(
std::move(entry), context));
}
resolver->Resolve(results);
} else {
DCHECK_EQ(1u, entries.size());
resolver->Resolve(FileSystemHandle::CreateFromMojoEntry(
std::move(entries[0]), context));
}
},
WrapPersistent(resolver), std::move(manager), return_as_sequence,
WrapPersistent(window.GetFrame())));
return resolver_result;
}
} // namespace
// static
ScriptPromise GlobalFileSystemAccess::showOpenFilePicker(
ScriptState* script_state,
LocalDOMWindow& window,
const OpenFilePickerOptions* options,
ExceptionState& exception_state) {
UseCounter::Count(window, WebFeature::kFileSystemPickerMethod);
Vector<mojom::blink::ChooseFileSystemEntryAcceptsOptionPtr> accepts;
if (options->hasTypes())
accepts = ConvertAccepts(options->types(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
if (accepts.IsEmpty() && options->excludeAcceptAllOption()) {
exception_state.ThrowTypeError("Need at least one accepted type");
return ScriptPromise();
}
String starting_directory_id = kDefaultStartingDirectoryId;
if (options->hasId()) {
starting_directory_id = VerifyIsValidId(options->id(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
}
auto well_known_starting_directory =
mojom::blink::WellKnownDirectory::kDefault;
mojo::PendingRemote<blink::mojom::blink::FileSystemAccessTransferToken> token;
if (options->hasStartIn()) {
auto& start_in = options->startIn();
if (start_in.IsWellKnownDirectory()) {
well_known_starting_directory = ConvertWellKnownDirectory(
start_in.GetAsWellKnownDirectory(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
}
if (start_in.IsFileSystemHandle()) {
token = start_in.GetAsFileSystemHandle()->Transfer();
}
}
VerifyIsAllowedToShowFilePicker(window, exception_state);
if (exception_state.HadException())
return ScriptPromise();
auto open_file_picker_options = mojom::blink::OpenFilePickerOptions::New(
mojom::blink::AcceptsTypesInfo::New(std::move(accepts),
!options->excludeAcceptAllOption()),
options->multiple());
return ShowFilePickerImpl(
script_state, window,
mojom::blink::FilePickerOptions::NewOpenFilePickerOptions(
std::move(open_file_picker_options)),
mojom::blink::CommonFilePickerOptions::New(
std::move(starting_directory_id),
std::move(well_known_starting_directory), std::move(token)),
/*return_as_sequence=*/true);
}
// static
ScriptPromise GlobalFileSystemAccess::showSaveFilePicker(
ScriptState* script_state,
LocalDOMWindow& window,
const SaveFilePickerOptions* options,
ExceptionState& exception_state) {
UseCounter::Count(window, WebFeature::kFileSystemPickerMethod);
Vector<mojom::blink::ChooseFileSystemEntryAcceptsOptionPtr> accepts;
if (options->hasTypes())
accepts = ConvertAccepts(options->types(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
if (accepts.IsEmpty() && options->excludeAcceptAllOption()) {
exception_state.ThrowTypeError("Need at least one accepted type");
return ScriptPromise();
}
String starting_directory_id = kDefaultStartingDirectoryId;
if (options->hasId()) {
starting_directory_id = VerifyIsValidId(options->id(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
}
auto well_known_starting_directory =
mojom::blink::WellKnownDirectory::kDefault;
mojo::PendingRemote<blink::mojom::blink::FileSystemAccessTransferToken> token;
if (options->hasStartIn()) {
auto& start_in = options->startIn();
if (start_in.IsWellKnownDirectory()) {
well_known_starting_directory = ConvertWellKnownDirectory(
start_in.GetAsWellKnownDirectory(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
}
if (start_in.IsFileSystemHandle()) {
token = start_in.GetAsFileSystemHandle()->Transfer();
}
}
VerifyIsAllowedToShowFilePicker(window, exception_state);
if (exception_state.HadException())
return ScriptPromise();
auto save_file_picker_options = mojom::blink::SaveFilePickerOptions::New(
mojom::blink::AcceptsTypesInfo::New(std::move(accepts),
!options->excludeAcceptAllOption()),
(options->hasSuggestedName() && !options->suggestedName().IsNull())
? options->suggestedName()
: g_empty_string);
return ShowFilePickerImpl(
script_state, window,
mojom::blink::FilePickerOptions::NewSaveFilePickerOptions(
std::move(save_file_picker_options)),
mojom::blink::CommonFilePickerOptions::New(
std::move(starting_directory_id),
std::move(well_known_starting_directory), std::move(token)),
/*return_as_sequence=*/false);
}
// static
ScriptPromise GlobalFileSystemAccess::showDirectoryPicker(
ScriptState* script_state,
LocalDOMWindow& window,
const DirectoryPickerOptions* options,
ExceptionState& exception_state) {
UseCounter::Count(window, WebFeature::kFileSystemPickerMethod);
String starting_directory_id = kDefaultStartingDirectoryId;
if (options->hasId()) {
starting_directory_id = VerifyIsValidId(options->id(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
}
auto well_known_starting_directory =
mojom::blink::WellKnownDirectory::kDefault;
mojo::PendingRemote<blink::mojom::blink::FileSystemAccessTransferToken> token;
if (options->hasStartIn()) {
auto& start_in = options->startIn();
if (start_in.IsWellKnownDirectory()) {
well_known_starting_directory = ConvertWellKnownDirectory(
start_in.GetAsWellKnownDirectory(), exception_state);
if (exception_state.HadException())
return ScriptPromise();
}
if (start_in.IsFileSystemHandle()) {
token = start_in.GetAsFileSystemHandle()->Transfer();
}
}
VerifyIsAllowedToShowFilePicker(window, exception_state);
if (exception_state.HadException())
return ScriptPromise();
auto directory_picker_options = mojom::blink::DirectoryPickerOptions::New();
return ShowFilePickerImpl(
script_state, window,
mojom::blink::FilePickerOptions::NewDirectoryPickerOptions(
std::move(directory_picker_options)),
mojom::blink::CommonFilePickerOptions::New(
std::move(starting_directory_id),
std::move(well_known_starting_directory), std::move(token)),
/*return_as_sequence=*/false);
}
} // namespace blink