feat: implement File System API support (#41419)

This commit is contained in:
Shelley Vohr 2024-04-10 22:06:47 +02:00 committed by GitHub
parent 41ba963392
commit 344aba0838
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1562 additions and 9 deletions

View file

@ -45,6 +45,7 @@
#include "shell/browser/electron_browser_main_parts.h"
#include "shell/browser/electron_download_manager_delegate.h"
#include "shell/browser/electron_permission_manager.h"
#include "shell/browser/file_system_access/file_system_access_permission_context_factory.h"
#include "shell/browser/net/resolve_proxy_helper.h"
#include "shell/browser/protocol_registry.h"
#include "shell/browser/special_storage_policy.h"
@ -533,6 +534,11 @@ ElectronBrowserContext::GetReduceAcceptLanguageControllerDelegate() {
return nullptr;
}
content::FileSystemAccessPermissionContext*
ElectronBrowserContext::GetFileSystemAccessPermissionContext() {
return FileSystemAccessPermissionContextFactory::GetForBrowserContext(this);
}
ResolveProxyHelper* ElectronBrowserContext::GetResolveProxyHelper() {
if (!resolve_proxy_helper_) {
resolve_proxy_helper_ = base::MakeRefCounted<ResolveProxyHelper>(

View file

@ -150,6 +150,8 @@ class ElectronBrowserContext : public content::BrowserContext {
content::StorageNotificationService* GetStorageNotificationService() override;
content::ReduceAcceptLanguageControllerDelegate*
GetReduceAcceptLanguageControllerDelegate() override;
content::FileSystemAccessPermissionContext*
GetFileSystemAccessPermissionContext() override;
CookieChangeNotifier* cookie_change_notifier() const {
return cookie_change_notifier_.get();

View file

@ -0,0 +1,799 @@
// Copyright (c) 2024 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/file_system_access/file_system_access_permission_context.h"
#include <string>
#include <utility>
#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/json/values_util.h"
#include "base/path_service.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h" // nogncheck
#include "chrome/browser/file_system_access/file_system_access_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/disallow_activation_reason.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "shell/browser/electron_permission_manager.h"
#include "shell/browser/web_contents_permission_helper.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/origin.h"
namespace {
using BlockType = ChromeFileSystemAccessPermissionContext::BlockType;
using HandleType = content::FileSystemAccessPermissionContext::HandleType;
using GrantType = electron::FileSystemAccessPermissionContext::GrantType;
using blink::mojom::PermissionStatus;
#if BUILDFLAG(IS_WIN)
[[nodiscard]] constexpr bool ContainsInvalidDNSCharacter(
base::FilePath::StringType hostname) {
return !base::ranges::all_of(hostname, [](base::FilePath::CharType c) {
return (c >= L'A' && c <= L'Z') || (c >= L'a' && c <= L'z') ||
(c >= L'0' && c <= L'9') || (c == L'.') || (c == L'-');
});
}
bool MaybeIsLocalUNCPath(const base::FilePath& path) {
if (!path.IsNetwork()) {
return false;
}
const std::vector<base::FilePath::StringType> components =
path.GetComponents();
// Check for server name that could represent a local system. We only
// check for a very short list, as it is impossible to cover all different
// variants on Windows.
if (components.size() >= 2 &&
(base::FilePath::CompareEqualIgnoreCase(components[1],
FILE_PATH_LITERAL("localhost")) ||
components[1] == FILE_PATH_LITERAL("127.0.0.1") ||
components[1] == FILE_PATH_LITERAL(".") ||
components[1] == FILE_PATH_LITERAL("?") ||
ContainsInvalidDNSCharacter(components[1]))) {
return true;
}
// In case we missed the server name check above, we also check for shares
// ending with '$' as they represent pre-defined shares, including the local
// drives.
for (size_t i = 2; i < components.size(); ++i) {
if (components[i].back() == L'$') {
return true;
}
}
return false;
}
#endif
// Describes a rule for blocking a directory, which can be constructed
// dynamically (based on state) or statically (from kBlockedPaths).
struct BlockPathRule {
base::FilePath path;
BlockType type;
};
bool ShouldBlockAccessToPath(const base::FilePath& path,
HandleType handle_type,
std::vector<BlockPathRule> rules) {
DCHECK(!path.empty());
DCHECK(path.IsAbsolute());
#if BUILDFLAG(IS_WIN)
// On Windows, local UNC paths are rejected, as UNC path can be written in a
// way that can bypass the blocklist.
if (base::FeatureList::IsEnabled(
features::kFileSystemAccessLocalUNCPathBlock) &&
MaybeIsLocalUNCPath(path)) {
return true;
}
#endif
// Add the hard-coded rules to the dynamic rules.
for (auto const& [key, rule_path, type] :
ChromeFileSystemAccessPermissionContext::kBlockedPaths) {
if (key == ChromeFileSystemAccessPermissionContext::kNoBasePathKey) {
rules.emplace_back(base::FilePath{rule_path}, type);
} else if (base::FilePath path; base::PathService::Get(key, &path)) {
rules.emplace_back(rule_path ? path.Append(rule_path) : path, type);
}
}
base::FilePath nearest_ancestor;
BlockType nearest_ancestor_block_type = BlockType::kDontBlockChildren;
for (const auto& block : rules) {
if (path == block.path || path.IsParent(block.path)) {
DLOG(INFO) << "Blocking access to " << path
<< " because it is a parent of " << block.path;
return true;
}
if (block.path.IsParent(path) &&
(nearest_ancestor.empty() || nearest_ancestor.IsParent(block.path))) {
nearest_ancestor = block.path;
nearest_ancestor_block_type = block.type;
}
}
// The path we're checking is not in a potentially blocked directory, or the
// nearest ancestor does not block access to its children. Grant access.
if (nearest_ancestor.empty() ||
nearest_ancestor_block_type == BlockType::kDontBlockChildren) {
return false;
}
// The path we're checking is a file, and the nearest ancestor only blocks
// access to directories. Grant access.
if (handle_type == HandleType::kFile &&
nearest_ancestor_block_type == BlockType::kBlockNestedDirectories) {
return false;
}
// The nearest ancestor blocks access to its children, so block access.
DLOG(INFO) << "Blocking access to " << path << " because it is inside "
<< nearest_ancestor;
return true;
}
} // namespace
namespace electron {
class FileSystemAccessPermissionContext::PermissionGrantImpl
: public content::FileSystemAccessPermissionGrant {
public:
PermissionGrantImpl(base::WeakPtr<FileSystemAccessPermissionContext> context,
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
GrantType type,
UserAction user_action)
: context_{std::move(context)},
origin_{origin},
handle_type_{handle_type},
type_{type},
path_{path} {}
// FileSystemAccessPermissionGrant:
PermissionStatus GetStatus() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return status_;
}
base::FilePath GetPath() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return path_;
}
void RequestPermission(
content::GlobalRenderFrameHostId frame_id,
UserActivationState user_activation_state,
base::OnceCallback<void(PermissionRequestOutcome)> callback) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Check if a permission request has already been processed previously. This
// check is done first because we don't want to reset the status of a
// permission if it has already been granted.
if (GetStatus() != PermissionStatus::ASK || !context_) {
if (GetStatus() == PermissionStatus::GRANTED) {
SetStatus(PermissionStatus::GRANTED);
}
std::move(callback).Run(PermissionRequestOutcome::kRequestAborted);
return;
}
content::RenderFrameHost* rfh = content::RenderFrameHost::FromID(frame_id);
if (!rfh) {
// Requested from a no longer valid RenderFrameHost.
std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame);
return;
}
// Don't request permission for an inactive RenderFrameHost as the
// page might not distinguish properly between user denying the permission
// and automatic rejection.
if (rfh->IsInactiveAndDisallowActivation(
content::DisallowActivationReasonId::
kFileSystemAccessPermissionRequest)) {
std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame);
return;
}
// We don't allow file system access from fenced frames.
if (rfh->IsNestedWithinFencedFrame()) {
std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame);
return;
}
if (user_activation_state == UserActivationState::kRequired &&
!rfh->HasTransientUserActivation()) {
// No permission prompts without user activation.
std::move(callback).Run(PermissionRequestOutcome::kNoUserActivation);
return;
}
if (content::WebContents::FromRenderFrameHost(rfh) == nullptr) {
std::move(callback).Run(PermissionRequestOutcome::kInvalidFrame);
return;
}
auto origin = rfh->GetLastCommittedOrigin().GetURL();
if (url::Origin::Create(origin) != origin_) {
// Third party iframes are not allowed to request more permissions.
std::move(callback).Run(PermissionRequestOutcome::kThirdPartyContext);
return;
}
auto* permission_manager =
static_cast<electron::ElectronPermissionManager*>(
context_->browser_context()->GetPermissionControllerDelegate());
if (!permission_manager) {
std::move(callback).Run(PermissionRequestOutcome::kRequestAborted);
return;
}
blink::PermissionType type = static_cast<blink::PermissionType>(
electron::WebContentsPermissionHelper::PermissionType::FILE_SYSTEM);
base::Value::Dict details;
details.Set("filePath", base::FilePathToValue(path_));
details.Set("isDirectory", handle_type_ == HandleType::kDirectory);
details.Set("fileAccessType",
type_ == GrantType::kWrite ? "writable" : "readable");
permission_manager->RequestPermissionWithDetails(
type, rfh, origin, false, std::move(details),
base::BindOnce(&PermissionGrantImpl::OnPermissionRequestResult, this,
std::move(callback)));
}
const url::Origin& origin() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return origin_;
}
HandleType handle_type() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return handle_type_;
}
GrantType type() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return type_;
}
void SetStatus(PermissionStatus new_status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto permission_changed = status_ != new_status;
status_ = new_status;
if (permission_changed) {
NotifyPermissionStatusChanged();
}
}
static void UpdateGrantPath(
std::map<base::FilePath, PermissionGrantImpl*>& grants,
const base::FilePath& old_path,
const base::FilePath& new_path) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto entry_it = base::ranges::find_if(
grants,
[&old_path](const auto& entry) { return entry.first == old_path; });
if (entry_it == grants.end()) {
// There must be an entry for an ancestor of this entry. Nothing to do
// here.
return;
}
DCHECK_EQ(entry_it->second->GetStatus(), PermissionStatus::GRANTED);
auto* const grant_impl = entry_it->second;
grant_impl->SetPath(new_path);
// Update the permission grant's key in the map of active permissions.
grants.erase(entry_it);
grants.emplace(new_path, grant_impl);
}
protected:
~PermissionGrantImpl() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (context_) {
context_->PermissionGrantDestroyed(this);
}
}
private:
void OnPermissionRequestResult(
base::OnceCallback<void(PermissionRequestOutcome)> callback,
blink::mojom::PermissionStatus status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (status == blink::mojom::PermissionStatus::GRANTED) {
SetStatus(PermissionStatus::GRANTED);
std::move(callback).Run(PermissionRequestOutcome::kUserGranted);
} else {
SetStatus(PermissionStatus::DENIED);
std::move(callback).Run(PermissionRequestOutcome::kUserDenied);
}
}
void SetPath(const base::FilePath& new_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (path_ == new_path)
return;
path_ = new_path;
NotifyPermissionStatusChanged();
}
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtr<FileSystemAccessPermissionContext> const context_;
const url::Origin origin_;
const HandleType handle_type_;
const GrantType type_;
base::FilePath path_;
// This member should only be updated via SetStatus().
PermissionStatus status_ = PermissionStatus::ASK;
};
struct FileSystemAccessPermissionContext::OriginState {
// Raw pointers, owned collectively by all the handles that reference this
// grant. When last reference goes away this state is cleared as well by
// PermissionGrantDestroyed().
std::map<base::FilePath, PermissionGrantImpl*> read_grants;
std::map<base::FilePath, PermissionGrantImpl*> write_grants;
};
FileSystemAccessPermissionContext::FileSystemAccessPermissionContext(
content::BrowserContext* browser_context)
: browser_context_(browser_context) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
FileSystemAccessPermissionContext::~FileSystemAccessPermissionContext() =
default;
scoped_refptr<content::FileSystemAccessPermissionGrant>
FileSystemAccessPermissionContext::GetReadPermissionGrant(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// operator[] might insert a new OriginState in |active_permissions_map_|,
// but that is exactly what we want.
auto& origin_state = active_permissions_map_[origin];
auto*& existing_grant = origin_state.read_grants[path];
scoped_refptr<PermissionGrantImpl> new_grant;
if (existing_grant && existing_grant->handle_type() != handle_type) {
// |path| changed from being a directory to being a file or vice versa,
// don't just re-use the existing grant but revoke the old grant before
// creating a new grant.
existing_grant->SetStatus(PermissionStatus::DENIED);
existing_grant = nullptr;
}
if (!existing_grant) {
new_grant = base::MakeRefCounted<PermissionGrantImpl>(
weak_factory_.GetWeakPtr(), origin, path, handle_type, GrantType::kRead,
user_action);
existing_grant = new_grant.get();
}
// If a parent directory is already readable this new grant should also be
// readable.
if (new_grant &&
AncestorHasActivePermission(origin, path, GrantType::kRead)) {
existing_grant->SetStatus(PermissionStatus::GRANTED);
} else {
switch (user_action) {
case UserAction::kOpen:
case UserAction::kSave:
// Open and Save dialog only grant read access for individual files.
if (handle_type == HandleType::kDirectory) {
break;
}
[[fallthrough]];
case UserAction::kDragAndDrop:
// Drag&drop grants read access for all handles.
existing_grant->SetStatus(PermissionStatus::GRANTED);
break;
case UserAction::kLoadFromStorage:
case UserAction::kNone:
break;
}
}
return existing_grant;
}
scoped_refptr<content::FileSystemAccessPermissionGrant>
FileSystemAccessPermissionContext::GetWritePermissionGrant(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// operator[] might insert a new OriginState in |active_permissions_map_|,
// but that is exactly what we want.
auto& origin_state = active_permissions_map_[origin];
auto*& existing_grant = origin_state.write_grants[path];
scoped_refptr<PermissionGrantImpl> new_grant;
if (existing_grant && existing_grant->handle_type() != handle_type) {
// |path| changed from being a directory to being a file or vice versa,
// don't just re-use the existing grant but revoke the old grant before
// creating a new grant.
existing_grant->SetStatus(PermissionStatus::DENIED);
existing_grant = nullptr;
}
if (!existing_grant) {
new_grant = base::MakeRefCounted<PermissionGrantImpl>(
weak_factory_.GetWeakPtr(), origin, path, handle_type,
GrantType::kWrite, user_action);
existing_grant = new_grant.get();
}
// If a parent directory is already writable this new grant should also be
// writable.
if (new_grant &&
AncestorHasActivePermission(origin, path, GrantType::kWrite)) {
existing_grant->SetStatus(PermissionStatus::GRANTED);
} else {
switch (user_action) {
case UserAction::kSave:
// Only automatically grant write access for save dialogs.
existing_grant->SetStatus(PermissionStatus::GRANTED);
break;
case UserAction::kOpen:
case UserAction::kDragAndDrop:
case UserAction::kLoadFromStorage:
case UserAction::kNone:
break;
}
}
return existing_grant;
}
bool FileSystemAccessPermissionContext::CanObtainReadPermission(
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return true;
}
bool FileSystemAccessPermissionContext::CanObtainWritePermission(
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return true;
}
void FileSystemAccessPermissionContext::ConfirmSensitiveEntryAccess(
const url::Origin& origin,
PathType path_type,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(SensitiveEntryResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto after_blocklist_check_callback = base::BindOnce(
&FileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist,
GetWeakPtr(), origin, path, handle_type, user_action, frame_id,
std::move(callback));
CheckPathAgainstBlocklist(path_type, path, handle_type,
std::move(after_blocklist_check_callback));
}
void FileSystemAccessPermissionContext::CheckPathAgainstBlocklist(
PathType path_type,
const base::FilePath& path,
HandleType handle_type,
base::OnceCallback<void(bool)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(https://crbug.com/1009970): Figure out what external paths should be
// blocked. We could resolve the external path to a local path, and check for
// blocked directories based on that, but that doesn't work well. Instead we
// should have a separate Chrome OS only code path to block for example the
// root of certain external file systems.
if (path_type == PathType::kExternal) {
std::move(callback).Run(/*should_block=*/false);
return;
}
std::vector<BlockPathRule> extra_rules;
extra_rules.emplace_back(browser_context_->GetPath().DirName(),
BlockType::kBlockAllChildren);
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&ShouldBlockAccessToPath, path, handle_type, extra_rules),
std::move(callback));
}
void FileSystemAccessPermissionContext::PerformAfterWriteChecks(
std::unique_ptr<content::FileSystemAccessWriteItem> item,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(AfterWriteCheckResult)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(callback).Run(AfterWriteCheckResult::kAllow);
}
void FileSystemAccessPermissionContext::DidCheckPathAgainstBlocklist(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(SensitiveEntryResult)> callback,
bool should_block) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (user_action == UserAction::kNone) {
std::move(callback).Run(should_block ? SensitiveEntryResult::kAbort
: SensitiveEntryResult::kAllowed);
return;
}
// Chromium opens a dialog here, but in Electron's case we log and abort.
if (should_block) {
LOG(INFO) << path.value()
<< " is blocked by the blocklis and cannot be accessed";
std::move(callback).Run(SensitiveEntryResult::kAbort);
return;
}
std::move(callback).Run(SensitiveEntryResult::kAllowed);
}
void FileSystemAccessPermissionContext::SetLastPickedDirectory(
const url::Origin& origin,
const std::string& id,
const base::FilePath& path,
const PathType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LOG(INFO) << "NOTIMPLEMENTED SetLastPickedDirectory: " << path.value();
}
FileSystemAccessPermissionContext::PathInfo
FileSystemAccessPermissionContext::GetLastPickedDirectory(
const url::Origin& origin,
const std::string& id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
LOG(INFO) << "NOTIMPLEMENTED GetLastPickedDirectory";
return PathInfo();
}
base::FilePath FileSystemAccessPermissionContext::GetWellKnownDirectoryPath(
blink::mojom::WellKnownDirectory directory,
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
int key = base::PATH_START;
switch (directory) {
case blink::mojom::WellKnownDirectory::kDirDesktop:
key = base::DIR_USER_DESKTOP;
break;
case blink::mojom::WellKnownDirectory::kDirDocuments:
key = chrome::DIR_USER_DOCUMENTS;
break;
case blink::mojom::WellKnownDirectory::kDirDownloads:
key = chrome::DIR_DEFAULT_DOWNLOADS;
break;
case blink::mojom::WellKnownDirectory::kDirMusic:
key = chrome::DIR_USER_MUSIC;
break;
case blink::mojom::WellKnownDirectory::kDirPictures:
key = chrome::DIR_USER_PICTURES;
break;
case blink::mojom::WellKnownDirectory::kDirVideos:
key = chrome::DIR_USER_VIDEOS;
break;
}
base::FilePath directory_path;
base::PathService::Get(key, &directory_path);
return directory_path;
}
std::u16string FileSystemAccessPermissionContext::GetPickerTitle(
const blink::mojom::FilePickerOptionsPtr& options) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(asully): Consider adding custom strings for invocations of the file
// picker, as well. Returning the empty string will fall back to the platform
// default for the given picker type.
std::u16string title;
switch (options->type_specific_options->which()) {
case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag::
kDirectoryPickerOptions:
title = l10n_util::GetStringUTF16(
options->type_specific_options->get_directory_picker_options()
->request_writable
? IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_WRITABLE_DIRECTORY_TITLE
: IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_READABLE_DIRECTORY_TITLE);
break;
case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag::
kSaveFilePickerOptions:
title = l10n_util::GetStringUTF16(
IDS_FILE_SYSTEM_ACCESS_CHOOSER_OPEN_SAVE_FILE_TITLE);
break;
case blink::mojom::TypeSpecificFilePickerOptionsUnion::Tag::
kOpenFilePickerOptions:
break;
}
return title;
}
void FileSystemAccessPermissionContext::NotifyEntryMoved(
const url::Origin& origin,
const base::FilePath& old_path,
const base::FilePath& new_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (old_path == new_path) {
return;
}
auto it = active_permissions_map_.find(origin);
if (it != active_permissions_map_.end()) {
PermissionGrantImpl::UpdateGrantPath(it->second.write_grants, old_path,
new_path);
PermissionGrantImpl::UpdateGrantPath(it->second.read_grants, old_path,
new_path);
}
}
void FileSystemAccessPermissionContext::OnFileCreatedFromShowSaveFilePicker(
const GURL& file_picker_binding_context,
const storage::FileSystemURL& url) {}
void FileSystemAccessPermissionContext::CheckPathsAgainstEnterprisePolicy(
std::vector<PathInfo> entries,
content::GlobalRenderFrameHostId frame_id,
EntriesAllowedByEnterprisePolicyCallback callback) {
std::move(callback).Run(std::move(entries));
}
void FileSystemAccessPermissionContext::RevokeGrant(
const url::Origin& origin,
const base::FilePath& file_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto origin_it = active_permissions_map_.find(origin);
if (origin_it != active_permissions_map_.end()) {
OriginState& origin_state = origin_it->second;
for (auto& grant : origin_state.read_grants) {
if (file_path.empty() || grant.first == file_path) {
grant.second->SetStatus(PermissionStatus::ASK);
}
}
for (auto& grant : origin_state.write_grants) {
if (file_path.empty() || grant.first == file_path) {
grant.second->SetStatus(PermissionStatus::ASK);
}
}
}
}
bool FileSystemAccessPermissionContext::OriginHasReadAccess(
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = active_permissions_map_.find(origin);
if (it != active_permissions_map_.end()) {
return base::ranges::any_of(it->second.read_grants, [&](const auto& grant) {
return grant.second->GetStatus() == PermissionStatus::GRANTED;
});
}
return false;
}
bool FileSystemAccessPermissionContext::OriginHasWriteAccess(
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = active_permissions_map_.find(origin);
if (it != active_permissions_map_.end()) {
return base::ranges::any_of(
it->second.write_grants, [&](const auto& grant) {
return grant.second->GetStatus() == PermissionStatus::GRANTED;
});
}
return false;
}
void FileSystemAccessPermissionContext::CleanupPermissions(
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
RevokeGrant(origin);
}
bool FileSystemAccessPermissionContext::AncestorHasActivePermission(
const url::Origin& origin,
const base::FilePath& path,
GrantType grant_type) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = active_permissions_map_.find(origin);
if (it == active_permissions_map_.end()) {
return false;
}
const auto& relevant_grants = grant_type == GrantType::kWrite
? it->second.write_grants
: it->second.read_grants;
if (relevant_grants.empty()) {
return false;
}
// Permissions are inherited from the closest ancestor.
for (base::FilePath parent = path.DirName(); parent != parent.DirName();
parent = parent.DirName()) {
auto i = relevant_grants.find(parent);
if (i != relevant_grants.end() && i->second &&
i->second->GetStatus() == PermissionStatus::GRANTED) {
return true;
}
}
return false;
}
void FileSystemAccessPermissionContext::PermissionGrantDestroyed(
PermissionGrantImpl* grant) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = active_permissions_map_.find(grant->origin());
if (it == active_permissions_map_.end()) {
return;
}
auto& grants = grant->type() == GrantType::kRead ? it->second.read_grants
: it->second.write_grants;
auto grant_it = grants.find(grant->GetPath());
// Any non-denied permission grants should have still been in our grants
// list. If this invariant is violated we would have permissions that might
// be granted but won't be visible in any UI because the permission context
// isn't tracking them anymore.
if (grant_it == grants.end()) {
DCHECK_EQ(PermissionStatus::DENIED, grant->GetStatus());
return;
}
// The grant in |grants| for this path might have been replaced with a
// different grant. Only erase if it actually matches the grant that was
// destroyed.
if (grant_it->second == grant) {
grants.erase(grant_it);
}
}
base::WeakPtr<FileSystemAccessPermissionContext>
FileSystemAccessPermissionContext::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
} // namespace electron

View file

@ -0,0 +1,154 @@
// Copyright (c) 2024 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_ELECTRON_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_H_
#define ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_ELECTRON_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_H_
#include "shell/browser/file_system_access/file_system_access_permission_context.h"
#include <memory>
#include <string>
#include <vector>
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/file_system_access_permission_context.h"
class GURL;
namespace base {
class FilePath;
} // namespace base
namespace storage {
class FileSystemURL;
} // namespace storage
namespace electron {
class FileSystemAccessPermissionContext
: public KeyedService,
public content::FileSystemAccessPermissionContext {
public:
enum class GrantType { kRead, kWrite };
explicit FileSystemAccessPermissionContext(
content::BrowserContext* browser_context);
FileSystemAccessPermissionContext(const FileSystemAccessPermissionContext&) =
delete;
FileSystemAccessPermissionContext& operator=(
const FileSystemAccessPermissionContext&) = delete;
~FileSystemAccessPermissionContext() override;
// content::FileSystemAccessPermissionContext:
scoped_refptr<content::FileSystemAccessPermissionGrant>
GetReadPermissionGrant(const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action) override;
scoped_refptr<content::FileSystemAccessPermissionGrant>
GetWritePermissionGrant(const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action) override;
void ConfirmSensitiveEntryAccess(
const url::Origin& origin,
PathType path_type,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(SensitiveEntryResult)> callback) override;
void PerformAfterWriteChecks(
std::unique_ptr<content::FileSystemAccessWriteItem> item,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(AfterWriteCheckResult)> callback) override;
bool CanObtainReadPermission(const url::Origin& origin) override;
bool CanObtainWritePermission(const url::Origin& origin) override;
void SetLastPickedDirectory(const url::Origin& origin,
const std::string& id,
const base::FilePath& path,
const PathType type) override;
PathInfo GetLastPickedDirectory(const url::Origin& origin,
const std::string& id) override;
base::FilePath GetWellKnownDirectoryPath(
blink::mojom::WellKnownDirectory directory,
const url::Origin& origin) override;
std::u16string GetPickerTitle(
const blink::mojom::FilePickerOptionsPtr& options) override;
void NotifyEntryMoved(const url::Origin& origin,
const base::FilePath& old_path,
const base::FilePath& new_path) override;
void OnFileCreatedFromShowSaveFilePicker(
const GURL& file_picker_binding_context,
const storage::FileSystemURL& url) override;
void CheckPathsAgainstEnterprisePolicy(
std::vector<PathInfo> entries,
content::GlobalRenderFrameHostId frame_id,
EntriesAllowedByEnterprisePolicyCallback callback) override;
enum class Access { kRead, kWrite, kReadWrite };
enum class RequestType { kNewPermission, kRestorePermissions };
void RevokeGrant(const url::Origin& origin,
const base::FilePath& file_path = base::FilePath());
bool OriginHasReadAccess(const url::Origin& origin);
bool OriginHasWriteAccess(const url::Origin& origin);
content::BrowserContext* browser_context() const { return browser_context_; }
protected:
SEQUENCE_CHECKER(sequence_checker_);
private:
class PermissionGrantImpl;
void PermissionGrantDestroyed(PermissionGrantImpl* grant);
void CheckPathAgainstBlocklist(PathType path_type,
const base::FilePath& path,
HandleType handle_type,
base::OnceCallback<void(bool)> callback);
void DidCheckPathAgainstBlocklist(
const url::Origin& origin,
const base::FilePath& path,
HandleType handle_type,
UserAction user_action,
content::GlobalRenderFrameHostId frame_id,
base::OnceCallback<void(SensitiveEntryResult)> callback,
bool should_block);
void CleanupPermissions(const url::Origin& origin);
bool AncestorHasActivePermission(const url::Origin& origin,
const base::FilePath& path,
GrantType grant_type) const;
base::WeakPtr<FileSystemAccessPermissionContext> GetWeakPtr();
const raw_ptr<content::BrowserContext, DanglingUntriaged> browser_context_;
struct OriginState;
std::map<url::Origin, OriginState> active_permissions_map_;
base::WeakPtrFactory<FileSystemAccessPermissionContext> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_H_

View file

@ -0,0 +1,51 @@
// Copyright (c) 2024 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/file_system_access/file_system_access_permission_context_factory.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "shell/browser/file_system_access/file_system_access_permission_context.h"
namespace electron {
// static
electron::FileSystemAccessPermissionContext*
FileSystemAccessPermissionContextFactory::GetForBrowserContext(
content::BrowserContext* context) {
return static_cast<electron::FileSystemAccessPermissionContext*>(
GetInstance()->GetServiceForBrowserContext(context, true));
}
// static
FileSystemAccessPermissionContextFactory*
FileSystemAccessPermissionContextFactory::GetInstance() {
static base::NoDestructor<FileSystemAccessPermissionContextFactory> instance;
return instance.get();
}
FileSystemAccessPermissionContextFactory::
FileSystemAccessPermissionContextFactory()
: BrowserContextKeyedServiceFactory(
"FileSystemAccessPermissionContext",
BrowserContextDependencyManager::GetInstance()) {}
FileSystemAccessPermissionContextFactory::
~FileSystemAccessPermissionContextFactory() = default;
// static
KeyedService* FileSystemAccessPermissionContextFactory::BuildServiceInstanceFor(
content::BrowserContext* context) const {
return BuildInstanceFor(context).release();
}
std::unique_ptr<KeyedService>
FileSystemAccessPermissionContextFactory::BuildInstanceFor(
content::BrowserContext* context) {
return std::make_unique<FileSystemAccessPermissionContext>(context);
}
} // namespace electron

View file

@ -0,0 +1,42 @@
// Copyright (c) 2024 Microsoft, GmbH
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_FACTORY_H_
#define ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_FACTORY_H_
#include "base/no_destructor.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
#include "shell/browser/file_system_access/file_system_access_permission_context.h"
namespace electron {
class FileSystemAccessPermissionContextFactory
: public BrowserContextKeyedServiceFactory {
public:
static FileSystemAccessPermissionContext* GetForBrowserContext(
content::BrowserContext* context);
static FileSystemAccessPermissionContextFactory* GetInstance();
static std::unique_ptr<KeyedService> BuildInstanceFor(
content::BrowserContext* context);
FileSystemAccessPermissionContextFactory(
const FileSystemAccessPermissionContextFactory&) = delete;
FileSystemAccessPermissionContextFactory& operator=(
const FileSystemAccessPermissionContextFactory&) = delete;
private:
friend class base::NoDestructor<FileSystemAccessPermissionContextFactory>;
FileSystemAccessPermissionContextFactory();
~FileSystemAccessPermissionContextFactory() override;
// BrowserContextKeyedServiceFactory:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* context) const override;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_FILE_SYSTEM_ACCESS_FILE_SYSTEM_ACCESS_PERMISSION_CONTEXT_FACTORY_H_

View file

@ -30,7 +30,9 @@ class WebContentsPermissionHelper
OPEN_EXTERNAL,
SERIAL,
HID,
USB
USB,
KEYBOARD_LOCK,
FILE_SYSTEM
};
// Asynchronous Requests

View file

@ -17,6 +17,7 @@
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkPixmap.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/gfx/codec/png_codec.h"
@ -274,6 +275,17 @@ void Clipboard::Clear(gin_helper::Arguments* args) {
ui::Clipboard::GetForCurrentThread()->Clear(GetClipboardBuffer(args));
}
// This exists for testing purposes ONLY.
void Clipboard::WriteFilesForTesting(const std::vector<base::FilePath>& files) {
std::vector<ui::FileInfo> file_infos;
for (const auto& file : files) {
file_infos.emplace_back(ui::FileInfo(ui::FileInfo(file, file.BaseName())));
}
ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
writer.WriteFilenames(ui::FileInfosToURIList(file_infos));
}
} // namespace electron::api
namespace {
@ -302,6 +314,8 @@ void Initialize(v8::Local<v8::Object> exports,
dict.SetMethod("writeFindText", &electron::api::Clipboard::WriteFindText);
dict.SetMethod("readBuffer", &electron::api::Clipboard::ReadBuffer);
dict.SetMethod("writeBuffer", &electron::api::Clipboard::WriteBuffer);
dict.SetMethod("_writeFilesForTesting",
&electron::api::Clipboard::WriteFilesForTesting);
dict.SetMethod("clear", &electron::api::Clipboard::Clear);
}

View file

@ -8,6 +8,7 @@
#include <string>
#include <vector>
#include "shell/common/gin_converters/file_path_converter.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/gfx/image/image.h"
#include "v8/include/v8.h"
@ -63,6 +64,8 @@ class Clipboard {
static void WriteBuffer(const std::string& format_string,
const v8::Local<v8::Value> buffer,
gin_helper::Arguments* args);
static void WriteFilesForTesting(const std::vector<base::FilePath>& files);
};
} // namespace electron::api

View file

@ -229,6 +229,8 @@ v8::Local<v8::Value> Converter<blink::PermissionType>::ToV8(
return StringToV8(isolate, "hid");
case PermissionType::USB:
return StringToV8(isolate, "usb");
case PermissionType::FILE_SYSTEM:
return StringToV8(isolate, "fileSystem");
default:
return StringToV8(isolate, "unknown");
}