Browse Source

feat: implement File System API support (#41419)

Shelley Vohr 1 year ago
parent
commit
344aba0838

+ 2 - 0
chromium_src/BUILD.gn

@@ -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",

+ 2 - 7
docs/api/session.md

@@ -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.

+ 5 - 0
docs/api/structures/filesystem-permission-request.md

@@ -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`.

+ 5 - 0
docs/api/structures/media-access-permission-request.md

@@ -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`.

+ 3 - 0
docs/api/structures/open-external-permission-request.md

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

+ 4 - 0
docs/api/structures/permission-request.md

@@ -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.

+ 4 - 0
filenames.auto.gni

@@ -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",

+ 4 - 0
filenames.gni

@@ -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",

+ 1 - 0
patches/chromium/.patches

@@ -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

+ 303 - 0
patches/chromium/refactor_expose_file_system_access_blocklist.patch

@@ -0,0 +1,303 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Shelley Vohr <[email protected]>
+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,

+ 6 - 0
shell/browser/electron_browser_context.cc

@@ -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>(

+ 2 - 0
shell/browser/electron_browser_context.h

@@ -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();

+ 799 - 0
shell/browser/file_system_access/file_system_access_permission_context.cc

@@ -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

+ 154 - 0
shell/browser/file_system_access/file_system_access_permission_context.h

@@ -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_

+ 51 - 0
shell/browser/file_system_access/file_system_access_permission_context_factory.cc

@@ -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

+ 42 - 0
shell/browser/file_system_access/file_system_access_permission_context_factory.h

@@ -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_

+ 3 - 1
shell/browser/web_contents_permission_helper.h

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

+ 14 - 0
shell/common/api/electron_api_clipboard.cc

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

+ 3 - 0
shell/common/api/electron_api_clipboard.h

@@ -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

+ 2 - 0
shell/common/gin_converters/content_converter.cc

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

+ 126 - 1
spec/chromium-spec.ts

@@ -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);

+ 26 - 0
spec/fixtures/file-system/test-writable.html

@@ -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 - 0
spec/fixtures/file-system/test.txt

@@ -0,0 +1 @@
+hello world