Browse Source

feat: add webUtils module with getPathForFile method (#38776)

* feat: add blinkUtils module with getPathForFile method

This is designed to replace the File.path augmentation
we currently have in place to allow apps to get the filesystem
path for a file that blink has a representation of.

File.path is non-standard and messes with certain websites, using
a method like this is effectively 0-cost and removes one of the final
deviations we have with web standards.

* add error

* refactor: update per PR feedback

* chore: update patches

* oops

* chore: update patches

* chore: update patches

* feat: add blinkUtils module with getPathForFile method

This is designed to replace the File.path augmentation
we currently have in place to allow apps to get the filesystem
path for a file that blink has a representation of.

File.path is non-standard and messes with certain websites, using
a method like this is effectively 0-cost and removes one of the final
deviations we have with web standards.

* add error

* refactor: update per PR feedback

* chore: update patches

* oops

* chore: update patches

* chore: update patches

* chore: update patches

* fix: provide isolate to WebBlob::FromV8Value

* chore: add tests

* build: fix depshash mismatch on arm64 macOS

---------

Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
Samuel Attard 1 year ago
parent
commit
d6bb9b40b0

+ 1 - 1
.circleci/config/base.yml

@@ -2165,7 +2165,7 @@ jobs:
       <<: *env-ninja-status
       <<: *env-macos-build
       <<: *env-apple-silicon
-      GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac'
+      GCLIENT_EXTRA_ARGS: '--custom-var=checkout_mac=True --custom-var=host_os=mac --custom-var=host_cpu=arm64'
     steps:
       - electron-build:
           persist: true

+ 5 - 0
docs/api/file-object.md

@@ -2,6 +2,11 @@
 
 > Use the HTML5 `File` API to work natively with files on the filesystem.
 
+> **Warning**
+> The `path` property that Electron adds to the `File` interface is deprecated
+> and **will** be removed in a future Electron release.  We recommend you
+> use `webUtils.getPathForFile` instead.
+
 The DOM's File interface provides abstraction around native files in order to
 let users work on native files directly with the HTML5 file API. Electron has
 added a `path` attribute to the `File` interface which exposes the file's real

+ 26 - 0
docs/api/web-utils.md

@@ -0,0 +1,26 @@
+# webUtils
+
+> A utility layer to interact with Web API objects (Files, Blobs, etc.)
+
+Process: [Renderer](../glossary.md#renderer-process)
+
+## Methods
+
+The `webUtils` module has the following methods:
+
+### `webUtils.getPathForFile(file)`
+
+* `file` File - A web [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object.
+
+Returns `string` - The file system path that this `File` object points to. In the case where the object passed in is not a `File` object an exception is thrown. In the case where the File object passed in was constructed in JS and is not backed by a file on disk an empty string is returned.
+
+This method superceded the previous augmentation to the `File` object with the `path` property.  An example is included below.
+
+```js
+// Before
+const oldPath = document.querySelector('input').files[0].path
+
+// After
+const { webUtils } = require('electron')
+const newPath = webUtils.getPathForFile(document.querySelector('input').files[0])
+```

+ 1 - 1
docs/tutorial/sandbox.md

@@ -46,7 +46,7 @@ scripts attached to sandboxed renderers will still have a polyfilled subset of N
 APIs available. A `require` function similar to Node's `require` module is exposed,
 but can only import a subset of Electron and Node's built-in modules:
 
-* `electron` (following renderer process modules: `contextBridge`, `crashReporter`, `ipcRenderer`, `nativeImage`, `webFrame`)
+* `electron` (following renderer process modules: `contextBridge`, `crashReporter`, `ipcRenderer`, `nativeImage`, `webFrame`, `webUtils`)
 * [`events`](https://nodejs.org/api/events.html)
 * [`timers`](https://nodejs.org/api/timers.html)
 * [`url`](https://nodejs.org/api/url.html)

+ 4 - 0
filenames.auto.gni

@@ -67,6 +67,7 @@ auto_filenames = {
     "docs/api/web-frame-main.md",
     "docs/api/web-frame.md",
     "docs/api/web-request.md",
+    "docs/api/web-utils.md",
     "docs/api/webview-tag.md",
     "docs/api/window-open.md",
     "docs/api/structures/bluetooth-device.md",
@@ -151,6 +152,7 @@ auto_filenames = {
     "lib/renderer/api/crash-reporter.ts",
     "lib/renderer/api/ipc-renderer.ts",
     "lib/renderer/api/web-frame.ts",
+    "lib/renderer/api/web-utils.ts",
     "lib/renderer/common-init.ts",
     "lib/renderer/inspector.ts",
     "lib/renderer/ipc-renderer-internal-utils.ts",
@@ -281,6 +283,7 @@ auto_filenames = {
     "lib/renderer/api/ipc-renderer.ts",
     "lib/renderer/api/module-list.ts",
     "lib/renderer/api/web-frame.ts",
+    "lib/renderer/api/web-utils.ts",
     "lib/renderer/common-init.ts",
     "lib/renderer/init.ts",
     "lib/renderer/inspector.ts",
@@ -318,6 +321,7 @@ auto_filenames = {
     "lib/renderer/api/ipc-renderer.ts",
     "lib/renderer/api/module-list.ts",
     "lib/renderer/api/web-frame.ts",
+    "lib/renderer/api/web-utils.ts",
     "lib/renderer/ipc-renderer-internal-utils.ts",
     "lib/renderer/ipc-renderer-internal.ts",
     "lib/worker/init.ts",

+ 2 - 0
filenames.gni

@@ -682,6 +682,8 @@ filenames = {
     "shell/renderer/api/electron_api_spell_check_client.cc",
     "shell/renderer/api/electron_api_spell_check_client.h",
     "shell/renderer/api/electron_api_web_frame.cc",
+    "shell/renderer/api/electron_api_web_utils.cc",
+    "shell/renderer/api/electron_api_web_utils.h",
     "shell/renderer/browser_exposed_renderer_interfaces.cc",
     "shell/renderer/browser_exposed_renderer_interfaces.h",
     "shell/renderer/content_settings_observer.cc",

+ 2 - 1
lib/renderer/api/module-list.ts

@@ -4,5 +4,6 @@ export const rendererModuleList: ElectronInternal.ModuleEntry[] = [
   { name: 'contextBridge', loader: () => require('./context-bridge') },
   { name: 'crashReporter', loader: () => require('./crash-reporter') },
   { name: 'ipcRenderer', loader: () => require('./ipc-renderer') },
-  { name: 'webFrame', loader: () => require('./web-frame') }
+  { name: 'webFrame', loader: () => require('./web-frame') },
+  { name: 'webUtils', loader: () => require('./web-utils') }
 ];

+ 3 - 0
lib/renderer/api/web-utils.ts

@@ -0,0 +1,3 @@
+const binding = process._linkedBinding('electron_renderer_web_utils');
+
+export const getPathForFile = binding.getPathForFile;

+ 4 - 0
lib/sandboxed_renderer/api/module-list.ts

@@ -18,5 +18,9 @@ export const moduleList: ElectronInternal.ModuleEntry[] = [
   {
     name: 'webFrame',
     loader: () => require('@electron/internal/renderer/api/web-frame')
+  },
+  {
+    name: 'webUtils',
+    loader: () => require('@electron/internal/renderer/api/web-utils')
   }
 ];

+ 2 - 1
patches/chromium/.patches

@@ -130,8 +130,9 @@ fix_harden_blink_scriptstate_maybefrom.patch
 chore_add_buildflag_guard_around_new_include.patch
 fix_use_delegated_generic_capturer_when_available.patch
 build_remove_ent_content_analysis_assert.patch
-fix_activate_background_material_on_windows.patch
+expose_webblob_path_to_allow_embedders_to_get_file_paths.patch
 fix_move_autopipsettingshelper_behind_branding_buildflag.patch
 revert_remove_the_allowaggressivethrottlingwithwebsocket_feature.patch
+fix_activate_background_material_on_windows.patch
 feat_allow_passing_of_objecttemplate_to_objecttemplatebuilder.patch
 chore_remove_check_is_test_on_script_injection_tracker.patch

+ 46 - 0
patches/chromium/expose_webblob_path_to_allow_embedders_to_get_file_paths.patch

@@ -0,0 +1,46 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Samuel Attard <[email protected]>
+Date: Tue, 13 Jun 2023 15:36:04 -0700
+Subject: expose WebBlob::Path to allow embedders to get file paths
+
+Used to replace the File.path augmentation Electron currently implements.  This is safer / more web-standard technique.
+
+diff --git a/third_party/blink/public/web/web_blob.h b/third_party/blink/public/web/web_blob.h
+index 384a59138db11ea38028f844dd67e328ebffbe7b..f153997c2afccef1fa1b64ee5f162c28a2d07e5d 100644
+--- a/third_party/blink/public/web/web_blob.h
++++ b/third_party/blink/public/web/web_blob.h
+@@ -67,6 +67,7 @@ class BLINK_EXPORT WebBlob {
+   void Reset();
+   void Assign(const WebBlob&);
+   WebString Uuid();
++  std::string Path();
+ 
+   bool IsNull() const { return private_.IsNull(); }
+ 
+diff --git a/third_party/blink/renderer/core/exported/web_blob.cc b/third_party/blink/renderer/core/exported/web_blob.cc
+index ce7b5e229789d606df5e74461f09e2e1db59fc95..b1bf2affa5b7f10d9b45d062a2ce0479f5a3b80a 100644
+--- a/third_party/blink/renderer/core/exported/web_blob.cc
++++ b/third_party/blink/renderer/core/exported/web_blob.cc
+@@ -40,6 +40,7 @@
+ #include "third_party/blink/renderer/core/execution_context/execution_context.h"
+ #include "third_party/blink/renderer/core/fileapi/blob.h"
+ #include "third_party/blink/renderer/core/fileapi/file_backed_blob_factory_dispatcher.h"
++#include "third_party/blink/renderer/core/fileapi/file.h"
+ #include "third_party/blink/renderer/platform/blob/blob_data.h"
+ #include "third_party/blink/renderer/platform/file_metadata.h"
+ #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
+@@ -83,6 +84,14 @@ WebString WebBlob::Uuid() {
+   return private_->Uuid();
+ }
+ 
++std::string WebBlob::Path() {
++  if (!private_.Get())
++    return "";
++  if (private_->IsFile() && private_->HasBackingFile())
++    return To<File>(private_.Get())->GetPath().Utf8();
++  return "";
++}
++
+ v8::Local<v8::Value> WebBlob::ToV8Value(v8::Isolate* isolate) {
+   if (!private_.Get())
+     return v8::Local<v8::Value>();

+ 1 - 0
shell/common/node_bindings.cc

@@ -90,6 +90,7 @@
   V(electron_common_v8_util)
 
 #define ELECTRON_RENDERER_BINDINGS(V) \
+  V(electron_renderer_web_utils)      \
   V(electron_renderer_context_bridge) \
   V(electron_renderer_crash_reporter) \
   V(electron_renderer_ipc)            \

+ 40 - 0
shell/renderer/api/electron_api_web_utils.cc

@@ -0,0 +1,40 @@
+// Copyright (c) 2023 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/renderer/api/electron_api_web_utils.h"
+
+#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/gin_helper/error_thrower.h"
+#include "shell/common/node_includes.h"
+#include "third_party/blink/public/web/web_blob.h"
+
+namespace electron::api::web_utils {
+
+std::string GetPathForFile(v8::Isolate* isolate, v8::Local<v8::Value> file) {
+  blink::WebBlob blob = blink::WebBlob::FromV8Value(isolate, file);
+  if (blob.IsNull()) {
+    gin_helper::ErrorThrower(isolate).ThrowTypeError(
+        "getPathForFile expected to receive a File object but one was not "
+        "provided");
+    return "";
+  }
+  return blob.Path();
+}
+
+}  // namespace electron::api::web_utils
+
+namespace {
+
+void Initialize(v8::Local<v8::Object> exports,
+                v8::Local<v8::Value> unused,
+                v8::Local<v8::Context> context,
+                void* priv) {
+  v8::Isolate* isolate = context->GetIsolate();
+  gin_helper::Dictionary dict(isolate, exports);
+  dict.SetMethod("getPathForFile", &electron::api::web_utils::GetPathForFile);
+}
+
+}  // namespace
+
+NODE_LINKED_BINDING_CONTEXT_AWARE(electron_renderer_web_utils, Initialize)

+ 16 - 0
shell/renderer/api/electron_api_web_utils.h

@@ -0,0 +1,16 @@
+// Copyright (c) 2023 Salesforce, Inc.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_RENDERER_API_ELECTRON_API_WEB_UTILS_H_
+#define ELECTRON_SHELL_RENDERER_API_ELECTRON_API_WEB_UTILS_H_
+
+#include "v8/include/v8.h"
+
+namespace electron::api::web_utils {
+
+std::string GetPathForFile(v8::Isolate* isolate, v8::Local<v8::Value> file);
+
+}  // namespace electron::api::web_utils
+
+#endif  // ELECTRON_SHELL_RENDERER_API_ELECTRON_API_WEB_UTILS_H_

+ 53 - 0
spec/api-web-utils-spec.ts

@@ -0,0 +1,53 @@
+import { expect } from 'chai';
+import * as path from 'node:path';
+import { BrowserWindow } from 'electron/main';
+import { defer } from './lib/spec-helpers';
+// import { once } from 'node:events';
+
+describe('webUtils module', () => {
+  const fixtures = path.resolve(__dirname, 'fixtures');
+
+  describe('getPathForFile', () => {
+    it('returns nothing for a Blob', async () => {
+      const w = new BrowserWindow({
+        show: false,
+        webPreferences: {
+          contextIsolation: false,
+          nodeIntegration: true,
+          sandbox: false
+        }
+      });
+      defer(() => w.close());
+      await w.loadFile(path.resolve(fixtures, 'pages', 'file-input.html'));
+      const pathFromWebUtils = await w.webContents.executeJavaScript('require("electron").webUtils.getPathForFile(new Blob([1, 2, 3]))');
+      expect(pathFromWebUtils).to.equal('');
+    });
+
+    it('reports the correct path for a File object', async () => {
+      const w = new BrowserWindow({
+        show: false,
+        webPreferences: {
+          contextIsolation: false,
+          nodeIntegration: true,
+          sandbox: false
+        }
+      });
+      defer(() => w.close());
+      await w.loadFile(path.resolve(fixtures, 'pages', 'file-input.html'));
+      const { debugger: debug } = w.webContents;
+      debug.attach();
+      try {
+        const { root: { nodeId } } = await debug.sendCommand('DOM.getDocument');
+        const { nodeId: inputNodeId } = await debug.sendCommand('DOM.querySelector', { nodeId, selector: 'input' });
+        await debug.sendCommand('DOM.setFileInputFiles', {
+          files: [__filename],
+          nodeId: inputNodeId
+        });
+        const pathFromWebUtils = await w.webContents.executeJavaScript('require("electron").webUtils.getPathForFile(document.querySelector("input").files[0])');
+        expect(pathFromWebUtils).to.equal(__filename);
+      } finally {
+        debug.detach();
+      }
+    });
+  });
+});

+ 5 - 0
spec/fixtures/pages/file-input.html

@@ -0,0 +1,5 @@
+<html>
+  <body>
+    <input type="file" id="file" />
+  </body>
+</html>