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

@ -33,6 +33,8 @@ static_library("chrome") {
"//chrome/browser/devtools/visual_logging.h",
"//chrome/browser/extensions/global_shortcut_listener.cc",
"//chrome/browser/extensions/global_shortcut_listener.h",
"//chrome/browser/file_system_access/file_system_access_features.cc",
"//chrome/browser/file_system_access/file_system_access_features.h",
"//chrome/browser/icon_loader.cc",
"//chrome/browser/icon_loader.h",
"//chrome/browser/icon_manager.cc",

View file

@ -818,15 +818,10 @@ win.webContents.session.setCertificateVerifyProc((request, callback) => {
* `top-level-storage-access` - Allow top-level sites to request third-party cookie access on behalf of embedded content originating from another site in the same related website set using the [Storage Access API](https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API).
* `window-management` - Request access to enumerate screens using the [`getScreenDetails`](https://developer.chrome.com/en/articles/multi-screen-window-placement/) API.
* `unknown` - An unrecognized permission request.
* `fileSystem` - Request access to read, write, and file management capabilities using the [File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API).
* `callback` Function
* `permissionGranted` boolean - Allow or deny the permission.
* `details` Object - Some properties are only available on certain permission types.
* `externalURL` string (optional) - The url of the `openExternal` request.
* `securityOrigin` string (optional) - The security origin of the `media` request.
* `mediaTypes` string[] (optional) - The types of media access being requested, elements can be `video`
or `audio`
* `requestingUrl` string - The last URL the requesting frame loaded
* `isMainFrame` boolean - Whether the frame making the request is the main frame
* `details` [PermissionRequest](structures/permission-request.md) | [FilesystemPermissionRequest](structures/filesystem-permission-request.md) | [MediaAccessPermissionRequest](structures/media-access-permission-request.md) | [OpenExternalPermissionRequest](structures/open-external-permission-request.md) - Additional information about the permission being requested.
Sets the handler which can be used to respond to permission requests for the `session`.
Calling `callback(true)` will allow the permission and `callback(false)` will reject it.

View file

@ -0,0 +1,5 @@
# FilesystemPermissionRequest Object extends `PermissionRequest`
* `filePath` string (optional) - The path of the `fileSystem` request.
* `isDirectory` boolean (optional) - Whether the `fileSystem` request is a directory.
* `fileAccessType` string (optional) - The access type of the `fileSystem` request. Can be `writable` or `readable`.

View file

@ -0,0 +1,5 @@
# MediaAccessPermissionRequest Object extends `PermissionRequest`
* `securityOrigin` string (optional) - The security origin of the request.
* `mediaTypes` string[] (optional) - The types of media access being requested - elements can be `video`
or `audio`.

View file

@ -0,0 +1,3 @@
# OpenExternalPermissionRequest Object extends `PermissionRequest`
* `externalURL` string (optional) - The url of the `openExternal` request.

View file

@ -0,0 +1,4 @@
# PermissionRequest Object
* `requestingUrl` string - The last URL the requesting frame loaded.
* `isMainFrame` boolean - Whether the frame making the request is the main frame.

View file

@ -89,6 +89,7 @@ auto_filenames = {
"docs/api/structures/extension.md",
"docs/api/structures/file-filter.md",
"docs/api/structures/file-path-with-headers.md",
"docs/api/structures/filesystem-permission-request.md",
"docs/api/structures/gpu-feature-status.md",
"docs/api/structures/hid-device.md",
"docs/api/structures/input-event.md",
@ -99,6 +100,7 @@ auto_filenames = {
"docs/api/structures/jump-list-item.md",
"docs/api/structures/keyboard-event.md",
"docs/api/structures/keyboard-input-event.md",
"docs/api/structures/media-access-permission-request.md",
"docs/api/structures/memory-info.md",
"docs/api/structures/memory-usage-details.md",
"docs/api/structures/mime-typed-buffer.md",
@ -106,7 +108,9 @@ auto_filenames = {
"docs/api/structures/mouse-wheel-input-event.md",
"docs/api/structures/notification-action.md",
"docs/api/structures/notification-response.md",
"docs/api/structures/open-external-permission-request.md",
"docs/api/structures/payment-discount.md",
"docs/api/structures/permission-request.md",
"docs/api/structures/point.md",
"docs/api/structures/post-body.md",
"docs/api/structures/printer-info.md",

View file

@ -380,6 +380,10 @@ filenames = {
"shell/browser/file_select_helper.cc",
"shell/browser/file_select_helper.h",
"shell/browser/file_select_helper_mac.mm",
"shell/browser/file_system_access/file_system_access_permission_context.cc",
"shell/browser/file_system_access/file_system_access_permission_context.h",
"shell/browser/file_system_access/file_system_access_permission_context_factory.cc",
"shell/browser/file_system_access/file_system_access_permission_context_factory.h",
"shell/browser/font_defaults.cc",
"shell/browser/font_defaults.h",
"shell/browser/hid/electron_hid_delegate.cc",

View file

@ -129,3 +129,4 @@ build_run_reclient_cfg_generator_after_chrome.patch
fix_suppress_clang_-wimplicit-const-int-float-conversion_in.patch
fix_getcursorscreenpoint_wrongly_returns_0_0.patch
fix_add_support_for_skipping_first_2_no-op_refreshes_in_thumb_cap.patch
refactor_expose_file_system_access_blocklist.patch

View file

@ -0,0 +1,303 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Shelley Vohr <shelley.vohr@gmail.com>
Date: Wed, 27 Mar 2024 10:47:48 +0100
Subject: refactor: expose file system access blocklist
This CL exposes the file system access blocklist publicly so that we can leverage
it in Electron and prevent drift from Chrome's blocklist. We should look for a way
to upstream this change to Chrome.
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
index 9c644d678d6d811ae5679594c0574fc0d8607f62..792cd62da17239ca6933930880af23754e4ab3d3 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
@@ -38,7 +38,6 @@
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
#include "chrome/browser/ui/file_system_access_dialogs.h"
-#include "chrome/common/chrome_paths.h"
#include "chrome/common/pdf_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
@@ -222,121 +221,6 @@ bool MaybeIsLocalUNCPath(const base::FilePath& path) {
}
#endif
-// Sentinel used to indicate that no PathService key is specified for a path in
-// the struct below.
-constexpr const int kNoBasePathKey = -1;
-
-enum BlockType {
- kBlockAllChildren,
- kBlockNestedDirectories,
- kDontBlockChildren
-};
-
-const struct {
- // base::BasePathKey value (or one of the platform specific extensions to it)
- // for a path that should be blocked. Specify kNoBasePathKey if |path| should
- // be used instead.
- int base_path_key;
-
- // Explicit path to block instead of using |base_path_key|. Set to nullptr to
- // use |base_path_key| on its own. If both |base_path_key| and |path| are set,
- // |path| is treated relative to the path |base_path_key| resolves to.
- const base::FilePath::CharType* path;
-
- // If this is set to kDontBlockChildren, only the given path and its parents
- // are blocked. If this is set to kBlockAllChildren, all children of the given
- // path are blocked as well. Finally if this is set to kBlockNestedDirectories
- // access is allowed to individual files in the directory, but nested
- // directories are still blocked.
- // The BlockType of the nearest ancestor of a path to check is what ultimately
- // determines if a path is blocked or not. If a blocked path is a descendent
- // of another blocked path, then it may override the child-blocking policy of
- // its ancestor. For example, if /home blocks all children, but
- // /home/downloads does not, then /home/downloads/file.ext will *not* be
- // blocked.
- BlockType type;
-} kBlockedPaths[] = {
- // Don't allow users to share their entire home directory, entire desktop or
- // entire documents folder, but do allow sharing anything inside those
- // directories not otherwise blocked.
- {base::DIR_HOME, nullptr, kDontBlockChildren},
- {base::DIR_USER_DESKTOP, nullptr, kDontBlockChildren},
- {chrome::DIR_USER_DOCUMENTS, nullptr, kDontBlockChildren},
- // Similar restrictions for the downloads directory.
- {chrome::DIR_DEFAULT_DOWNLOADS, nullptr, kDontBlockChildren},
- {chrome::DIR_DEFAULT_DOWNLOADS_SAFE, nullptr, kDontBlockChildren},
- // The Chrome installation itself should not be modified by the web.
- {base::DIR_EXE, nullptr, kBlockAllChildren},
-#if !BUILDFLAG(IS_FUCHSIA)
- {base::DIR_MODULE, nullptr, kBlockAllChildren},
-#endif
- {base::DIR_ASSETS, nullptr, kBlockAllChildren},
- // And neither should the configuration of at least the currently running
- // Chrome instance (note that this does not take --user-data-dir command
- // line overrides into account).
- {chrome::DIR_USER_DATA, nullptr, kBlockAllChildren},
- // ~/.ssh is pretty sensitive on all platforms, so block access to that.
- {base::DIR_HOME, FILE_PATH_LITERAL(".ssh"), kBlockAllChildren},
- // And limit access to ~/.gnupg as well.
- {base::DIR_HOME, FILE_PATH_LITERAL(".gnupg"), kBlockAllChildren},
-#if BUILDFLAG(IS_WIN)
- // Some Windows specific directories to block, basically all apps, the
- // operating system itself, as well as configuration data for apps.
- {base::DIR_PROGRAM_FILES, nullptr, kBlockAllChildren},
- {base::DIR_PROGRAM_FILESX86, nullptr, kBlockAllChildren},
- {base::DIR_PROGRAM_FILES6432, nullptr, kBlockAllChildren},
- {base::DIR_WINDOWS, nullptr, kBlockAllChildren},
- {base::DIR_ROAMING_APP_DATA, nullptr, kBlockAllChildren},
- {base::DIR_LOCAL_APP_DATA, nullptr, kBlockAllChildren},
- {base::DIR_COMMON_APP_DATA, nullptr, kBlockAllChildren},
- // Opening a file from an MTP device, such as a smartphone or a camera, is
- // implemented by Windows as opening a file in the temporary internet files
- // directory. To support that, allow opening files in that directory, but
- // not whole directories.
- {base::DIR_IE_INTERNET_CACHE, nullptr, kBlockNestedDirectories},
-#endif
-#if BUILDFLAG(IS_MAC)
- // Similar Mac specific blocks.
- {base::DIR_APP_DATA, nullptr, kBlockAllChildren},
- {base::DIR_HOME, FILE_PATH_LITERAL("Library"), kBlockAllChildren},
- // Allow access to other cloud files, such as Google Drive.
- {base::DIR_HOME, FILE_PATH_LITERAL("Library/CloudStorage"),
- kDontBlockChildren},
- // Allow the site to interact with data from its corresponding natively
- // installed (sandboxed) application. It would be nice to limit a site to
- // access only _its_ corresponding natively installed application,
- // but unfortunately there's no straightforward way to do that. See
- // https://crbug.com/984641#c22.
- {base::DIR_HOME, FILE_PATH_LITERAL("Library/Containers"),
- kDontBlockChildren},
- // Allow access to iCloud files...
- {base::DIR_HOME, FILE_PATH_LITERAL("Library/Mobile Documents"),
- kDontBlockChildren},
- // ... which may also appear at this directory.
- {base::DIR_HOME,
- FILE_PATH_LITERAL("Library/Mobile Documents/com~apple~CloudDocs"),
- kDontBlockChildren},
-#endif
-#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
- // On Linux also block access to devices via /dev.
- {kNoBasePathKey, FILE_PATH_LITERAL("/dev"), kBlockAllChildren},
- // And security sensitive data in /proc and /sys.
- {kNoBasePathKey, FILE_PATH_LITERAL("/proc"), kBlockAllChildren},
- {kNoBasePathKey, FILE_PATH_LITERAL("/sys"), kBlockAllChildren},
- // And system files in /boot and /etc.
- {kNoBasePathKey, FILE_PATH_LITERAL("/boot"), kBlockAllChildren},
- {kNoBasePathKey, FILE_PATH_LITERAL("/etc"), kBlockAllChildren},
- // And block all of ~/.config, matching the similar restrictions on mac
- // and windows.
- {base::DIR_HOME, FILE_PATH_LITERAL(".config"), kBlockAllChildren},
- // Block ~/.dbus as well, just in case, although there probably isn't much a
- // website can do with access to that directory and its contents.
- {base::DIR_HOME, FILE_PATH_LITERAL(".dbus"), kBlockAllChildren},
-#endif
- // TODO(https://crbug.com/984641): Refine this list, for example add
- // XDG_CONFIG_HOME when it is not set ~/.config?
-};
-
// Describes a rule for blocking a directory, which can be constructed
// dynamically (based on state) or statically (from kBlockedPaths).
struct BlockPathRule {
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
index 8bc8257b603a88e56f77dcf7d72aa9dad45880db..484f98c68b0dc860a6482e923df2379133c57749 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
@@ -17,12 +17,13 @@
#include "base/time/default_clock.h"
#include "chrome/browser/file_system_access/file_system_access_features.h"
#include "chrome/browser/file_system_access/file_system_access_permission_request_manager.h"
-#include "components/enterprise/buildflags/buildflags.h"
+#include "chrome/common/chrome_paths.h"
#include "components/permissions/features.h"
#include "components/permissions/object_permission_context_base.h"
#include "content/public/browser/file_system_access_permission_context.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom-forward.h"
+
#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/permissions/one_time_permissions_tracker.h"
#include "chrome/browser/permissions/one_time_permissions_tracker_observer.h"
@@ -30,7 +31,8 @@
#include "chrome/browser/web_applications/web_app_install_manager_observer.h"
#endif
-#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)
+#if 0
+#include "components/enterprise/buildflags/buildflags.h"
#include "chrome/browser/enterprise/connectors/analysis/content_analysis_delegate.h"
#include "components/enterprise/common/files_scan_data.h"
#endif
@@ -331,6 +333,121 @@ class ChromeFileSystemAccessPermissionContext
// chrome://settings/content/filesystem UI.
static constexpr char kPermissionPathKey[] = "path";
+ // Sentinel used to indicate that no PathService key is specified for a path in
+ // the struct below.
+ static constexpr int kNoBasePathKey = -1;
+
+ enum BlockType {
+ kBlockAllChildren,
+ kBlockNestedDirectories,
+ kDontBlockChildren
+ };
+
+ static constexpr struct {
+ // base::BasePathKey value (or one of the platform specific extensions to it)
+ // for a path that should be blocked. Specify kNoBasePathKey if |path| should
+ // be used instead.
+ int base_path_key;
+
+ // Explicit path to block instead of using |base_path_key|. Set to nullptr to
+ // use |base_path_key| on its own. If both |base_path_key| and |path| are set,
+ // |path| is treated relative to the path |base_path_key| resolves to.
+ const base::FilePath::CharType* path;
+
+ // If this is set to kDontBlockChildren, only the given path and its parents
+ // are blocked. If this is set to kBlockAllChildren, all children of the given
+ // path are blocked as well. Finally if this is set to kBlockNestedDirectories
+ // access is allowed to individual files in the directory, but nested
+ // directories are still blocked.
+ // The BlockType of the nearest ancestor of a path to check is what ultimately
+ // determines if a path is blocked or not. If a blocked path is a descendent
+ // of another blocked path, then it may override the child-blocking policy of
+ // its ancestor. For example, if /home blocks all children, but
+ // /home/downloads does not, then /home/downloads/file.ext will *not* be
+ // blocked.
+ BlockType type;
+ } kBlockedPaths[] = {
+ // Don't allow users to share their entire home directory, entire desktop or
+ // entire documents folder, but do allow sharing anything inside those
+ // directories not otherwise blocked.
+ {base::DIR_HOME, nullptr, kDontBlockChildren},
+ {base::DIR_USER_DESKTOP, nullptr, kDontBlockChildren},
+ {chrome::DIR_USER_DOCUMENTS, nullptr, kDontBlockChildren},
+ // Similar restrictions for the downloads directory.
+ {chrome::DIR_DEFAULT_DOWNLOADS, nullptr, kDontBlockChildren},
+ {chrome::DIR_DEFAULT_DOWNLOADS_SAFE, nullptr, kDontBlockChildren},
+ // The Chrome installation itself should not be modified by the web.
+ {base::DIR_EXE, nullptr, kBlockAllChildren},
+ #if !BUILDFLAG(IS_FUCHSIA)
+ {base::DIR_MODULE, nullptr, kBlockAllChildren},
+ #endif
+ {base::DIR_ASSETS, nullptr, kBlockAllChildren},
+ // And neither should the configuration of at least the currently running
+ // Chrome instance (note that this does not take --user-data-dir command
+ // line overrides into account).
+ {chrome::DIR_USER_DATA, nullptr, kBlockAllChildren},
+ // ~/.ssh is pretty sensitive on all platforms, so block access to that.
+ {base::DIR_HOME, FILE_PATH_LITERAL(".ssh"), kBlockAllChildren},
+ // And limit access to ~/.gnupg as well.
+ {base::DIR_HOME, FILE_PATH_LITERAL(".gnupg"), kBlockAllChildren},
+ #if BUILDFLAG(IS_WIN)
+ // Some Windows specific directories to block, basically all apps, the
+ // operating system itself, as well as configuration data for apps.
+ {base::DIR_PROGRAM_FILES, nullptr, kBlockAllChildren},
+ {base::DIR_PROGRAM_FILESX86, nullptr, kBlockAllChildren},
+ {base::DIR_PROGRAM_FILES6432, nullptr, kBlockAllChildren},
+ {base::DIR_WINDOWS, nullptr, kBlockAllChildren},
+ {base::DIR_ROAMING_APP_DATA, nullptr, kBlockAllChildren},
+ {base::DIR_LOCAL_APP_DATA, nullptr, kBlockAllChildren},
+ {base::DIR_COMMON_APP_DATA, nullptr, kBlockAllChildren},
+ // Opening a file from an MTP device, such as a smartphone or a camera, is
+ // implemented by Windows as opening a file in the temporary internet files
+ // directory. To support that, allow opening files in that directory, but
+ // not whole directories.
+ {base::DIR_IE_INTERNET_CACHE, nullptr, kBlockNestedDirectories},
+ #endif
+ #if BUILDFLAG(IS_MAC)
+ // Similar Mac specific blocks.
+ {base::DIR_APP_DATA, nullptr, kBlockAllChildren},
+ {base::DIR_HOME, FILE_PATH_LITERAL("Library"), kBlockAllChildren},
+ // Allow access to other cloud files, such as Google Drive.
+ {base::DIR_HOME, FILE_PATH_LITERAL("Library/CloudStorage"),
+ kDontBlockChildren},
+ // Allow the site to interact with data from its corresponding natively
+ // installed (sandboxed) application. It would be nice to limit a site to
+ // access only _its_ corresponding natively installed application,
+ // but unfortunately there's no straightforward way to do that. See
+ // https://crbug.com/984641#c22.
+ {base::DIR_HOME, FILE_PATH_LITERAL("Library/Containers"),
+ kDontBlockChildren},
+ // Allow access to iCloud files...
+ {base::DIR_HOME, FILE_PATH_LITERAL("Library/Mobile Documents"),
+ kDontBlockChildren},
+ // ... which may also appear at this directory.
+ {base::DIR_HOME,
+ FILE_PATH_LITERAL("Library/Mobile Documents/com~apple~CloudDocs"),
+ kDontBlockChildren},
+ #endif
+ #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+ // On Linux also block access to devices via /dev.
+ {kNoBasePathKey, FILE_PATH_LITERAL("/dev"), kBlockAllChildren},
+ // And security sensitive data in /proc and /sys.
+ {kNoBasePathKey, FILE_PATH_LITERAL("/proc"), kBlockAllChildren},
+ {kNoBasePathKey, FILE_PATH_LITERAL("/sys"), kBlockAllChildren},
+ // And system files in /boot and /etc.
+ {kNoBasePathKey, FILE_PATH_LITERAL("/boot"), kBlockAllChildren},
+ {kNoBasePathKey, FILE_PATH_LITERAL("/etc"), kBlockAllChildren},
+ // And block all of ~/.config, matching the similar restrictions on mac
+ // and windows.
+ {base::DIR_HOME, FILE_PATH_LITERAL(".config"), kBlockAllChildren},
+ // Block ~/.dbus as well, just in case, although there probably isn't much a
+ // website can do with access to that directory and its contents.
+ {base::DIR_HOME, FILE_PATH_LITERAL(".dbus"), kBlockAllChildren},
+ #endif
+ // TODO(https://crbug.com/984641): Refine this list, for example add
+ // XDG_CONFIG_HOME when it is not set ~/.config?
+ };
+
protected:
SEQUENCE_CHECKER(sequence_checker_);
@@ -350,7 +467,7 @@ class ChromeFileSystemAccessPermissionContext
void PermissionGrantDestroyed(PermissionGrantImpl* grant);
-#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)
+#if 0
void OnContentAnalysisComplete(
std::vector<PathInfo> entries,
EntriesAllowedByEnterprisePolicyCallback callback,

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");
}

View file

@ -1,5 +1,6 @@
import { expect } from 'chai';
import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents, dialog, MessageBoxOptions } from 'electron/main';
import { clipboard } from 'electron/common';
import { closeAllWindows } from './lib/window-helpers';
import * as https from 'node:https';
import * as http from 'node:http';
@ -13,6 +14,7 @@ import { PipeTransport } from './pipe-transport';
import * as ws from 'ws';
import { setTimeout } from 'node:timers/promises';
import { AddressInfo } from 'node:net';
import { MediaAccessPermissionRequest } from 'electron';
const features = process._linkedBinding('electron_common_features');
@ -846,6 +848,129 @@ describe('chromium features', () => {
});
});
describe('File System API,', () => {
afterEach(closeAllWindows);
afterEach(() => {
session.defaultSession.setPermissionRequestHandler(null);
});
it('allows access by default to reading an OPFS file', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
partition: 'file-system-spec',
contextIsolation: false
}
});
await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
const result = await w.webContents.executeJavaScript(`
new Promise(async (resolve, reject) => {
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('test', { create: true });
const { name, size } = await fileHandle.getFile();
resolve({ name, size });
}
)`, true);
expect(result).to.deep.equal({ name: 'test', size: 0 });
});
it('fileHandle.queryPermission by default has permission to read and write to OPFS files', async () => {
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
partition: 'file-system-spec',
contextIsolation: false
}
});
await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
const status = await w.webContents.executeJavaScript(`
new Promise(async (resolve, reject) => {
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('test', { create: true });
const status = await fileHandle.queryPermission({ mode: 'readwrite' });
resolve(status);
}
)`, true);
expect(status).to.equal('granted');
});
it('fileHandle.requestPermission automatically grants permission to read and write to OPFS files', async () => {
const w = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
partition: 'file-system-spec',
contextIsolation: false
}
});
await w.loadURL(`file://${fixturesPath}/pages/blank.html`);
const status = await w.webContents.executeJavaScript(`
new Promise(async (resolve, reject) => {
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('test', { create: true });
const status = await fileHandle.requestPermission({ mode: 'readwrite' });
resolve(status);
}
)`, true);
expect(status).to.equal('granted');
});
it('requests permission when trying to create a writable file handle', (done) => {
const writablePath = path.join(fixturesPath, 'file-system', 'test-writable.html');
const testFile = path.join(fixturesPath, 'file-system', 'test.txt');
const w = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
sandbox: false
}
});
w.webContents.session.setPermissionRequestHandler((wc, permission, callback, details) => {
expect(permission).to.equal('fileSystem');
const { href } = url.pathToFileURL(writablePath);
expect(details).to.deep.equal({
fileAccessType: 'writable',
isDirectory: false,
isMainFrame: true,
filePath: testFile,
requestingUrl: href
});
callback(true);
});
ipcMain.once('did-create-file-handle', async () => {
const result = await w.webContents.executeJavaScript(`
new Promise((resolve, reject) => {
try {
const writable = fileHandle.createWritable();
resolve(true);
} catch {
resolve(false);
}
})
`, true);
expect(result).to.be.true();
done();
});
w.loadFile(writablePath);
w.webContents.once('did-finish-load', () => {
// @ts-expect-error Undocumented testing method.
clipboard._writeFilesForTesting([testFile]);
w.webContents.paste();
});
});
});
describe('web workers', () => {
let appProcess: ChildProcess.ChildProcessWithoutNullStreams | undefined;
@ -1663,7 +1788,7 @@ describe('chromium features', () => {
it('provides a securityOrigin to the request handler', async () => {
session.defaultSession.setPermissionRequestHandler(
(wc, permission, callback, details) => {
if (details.securityOrigin !== undefined) {
if ((details as MediaAccessPermissionRequest).securityOrigin !== undefined) {
callback(true);
} else {
callback(false);

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<script>
const { ipcRenderer } = require('electron')
let fileHandle = null;
let sent = false;
window.document.onpaste = async (event) => {
const fileItem = event.clipboardData.items[0];
fileHandle = await fileItem.getAsFileSystemHandle();
if (!sent) {
ipcRenderer.send('did-create-file-handle');
sent = true;
}
};
</script>
</body>
</html>

1
spec/fixtures/file-system/test.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world