electron icon indicating copy to clipboard operation
electron copied to clipboard

[Feature Request]: Add an event when opening file open dialog from <input type="file" />

Open SomaticIT opened this issue 3 years ago • 10 comments

Preflight Checklist

Problem Description

We are building a secure browser using electron for kiosk devices.

We would like to customize the behavior of file inputs (<input type="file" />). Indeed, we do not want public users to access the filesystem when using a public kiosk device.

Proposed Solution

A good way to customize this behavior would be to trigger a new event (eg: select-file) when users click on a file input. This event would accept a callback with the selected file.

It allows us to create a custom secured UI for file selection.

Exemple:

webContents.on("select-file", (details, callback) => {
  // details.accept
  // details.multiple
  // details.directory
  // ...

  showCustomUI(details)
    .then((selectedFilePath) => callback(selectedFilePath));
});

Alternatives Considered

We tried using a preload script that scans and overrides file inputs but we can't set the file input value manually...

Edit: We were able to set file inputs using debugger (https://github.com/electron/electron/issues/749#issuecomment-1026721073). However, there is a lot of special cases where websites create custom UI for file inputs. I think the only stable and reliable way is to implement a new event.

Additional Information

We are using <webview> so this event should be handled correctly in this environment.

SomaticIT avatar Jan 29 '22 18:01 SomaticIT

@SomaticIT I have implemented this using preload script, though I haven't used webview there.

when user is clicking on a button or in a input field I am triggering an event named chooseFile and in my preload script I am sending this event to main using ipc. In my case I am selecting Images only.

preload.js

const { ipcRenderer, contextBridge } = require("electron");


var allApi = {
  requestLocalImage: (eventName: String, cb: Function) => {
  ipcRenderer.send(eventName);
    ipcRenderer.on("choosenfile", (e, n) => {
      var ext=n.bn.split(".")[1];
      var data = "data:image/"+ext+";base64," + n.bs4;      
      cb({data,ext})
    });
  }
};
contextBridge.exposeInMainWorld("electron", allApi);

main.js

ipcMain.on("chooseFile", (event, arg) => {
  const file = dialog.showOpenDialog({
    properties: ["openFile"],
    filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg"] }],
  });
  file.then(({ canceled, filePaths, bookmarks }) => {
    const bs4 = readFileSync(filePaths[0]).toString("base64");
    event.reply("choosenfile", {bs4,bn:basename(filePaths[0])});
  });
});

script.js

function selectImage() {
    window.electron.requestLocalImage("chooseFile", (url) => {
      console.log(url.data);
    });
  }

and to save it in server I am sending this encoded format to server and there I am reconstructing it and saving it in a folder.

server.js

var fs=require( 'fs');

fs.writeFile('imagename'+.req.body.extName, req.body.base64Image, {encoding: 'base64'}, function(err) {
    console.log('File created');
});

I am very new to electron and this method might be wrong but it worked for me.

detronetdip avatar Jan 30 '22 18:01 detronetdip

Your method is OK for an owned-application.

The problem is that we are displaying external websites in our application (secured web browser). We have no way to adapt the code to call the exposed method. We have to override some behaviors to make websites works as we expect.

In our case, we are able to create a script that detect file inputs and prevent their default behavior to show a custom UI. However we cannot set the value back to these fields. So the website is not aware of the selected file.

SomaticIT avatar Jan 30 '22 21:01 SomaticIT

However we cannot set the value back to these fields. So the website is not aware of the selected file.

I guess that would be https://github.com/electron/electron/issues/749 ?

Prinzhorn avatar Jan 31 '22 09:01 Prinzhorn

@Prinzhorn: Yes, this would help!

However, it does not resolve all issues. Indeed, the above solution works only when file inputs exist on the page when it's initially rendered. If file inputs are dynamically injected into the page, it will become very difficult to detect them without a severe performance impact (timer that periodically scans the page).

Another case is a virtual file input (created in javascript but never added in the DOM). This case is totally impossible to detect and handle using the above solution.

The solution proposed in this PR would allow to handle all cases and all situations without trying to hack the DOM... It would also provide a way to resolve some cases of #749.

SomaticIT avatar Jan 31 '22 17:01 SomaticIT

I investigated the feasibility of this PR and found something interesting in chromium source:

The behavior of file input's file chooser is determined by the ChromeClient which could be overridden:

If I'm correct, this means that we can customize this behavior to add a custom event without impacting nor hacking chromium.

I'm not expert of electron sources so I have some questions:

  • Is there a custom implementation of ChromeClient in electron?
  • If yes, where is it implemented?
  • If no, how do you override behaviors determined by ChromeClient (eg. StartDragging)?

If community is OK for this feature and if some maintainers agree to help me, I can try to implement this feature. What do you think?

SomaticIT avatar Feb 01 '22 12:02 SomaticIT

Hello contributors,

I would like to progress on this feature. Is there someone that could help me?

SomaticIT avatar Mar 04 '22 15:03 SomaticIT

Anyone can help me?

SomaticIT avatar Apr 06 '22 08:04 SomaticIT

We are very interested in this feature too! Thank you.

jaime-rivas avatar Apr 13 '22 12:04 jaime-rivas

@Prinzhorn: Yes, this would help!

However, it does not resolve all issues. Indeed, the above solution works only when file inputs exist on the page when it's initially rendered. If file inputs are dynamically injected into the page, it will become very difficult to detect them without a severe performance impact (timer that periodically scans the page).

Another case is a virtual file input (created in javascript but never added in the DOM). This case is totally impossible to detect and handle using the above solution.

The solution proposed in this PR would allow to handle all cases and all situations without trying to hack the DOM... It would also provide a way to resolve some cases of #749.

If the file input is dynamically injected into the page, you can use preventDefault to prevent the file input from being clicked in advance

const preventFun = (e) => {
    console.log('preventFun', e.target, e.target.id)
    if (e.target.id === 'file_input') {
      e.preventDefault()
      setTimeout(() => {
        document.removeEventListener('click', preventFun)
      }, 100)
    }
  }
  document.addEventListener('click', preventFun)

 // #file_btn is the button that dynamically generates the file input when clicked
  document.querySelector('#file_btn').click()

you will then see the file input on the page and you will be able to use CDP

async function setFileInput(wc, selector, files) {
  try {
    wc.debugger.attach("1.1");

    const { root } = await wc.debugger.sendCommand("DOM.getDocument", {});
    const { nodeId } = await wc.debugger.sendCommand("DOM.querySelector", { nodeId: root.nodeId, selector });

    await wc.debugger.sendCommand("DOM.setFileInputFiles", { nodeId, files });
  }
  finally {
    wc.debugger.detach();
  }
}

// then somewhere in your code:
await setFileInput(win.webContents, "#file_input", ["/tmp/test.txt"]);

udbmnm avatar Feb 09 '23 09:02 udbmnm

One suggestion. Not a specific event like select-file but maybe a more generic one that covers all dialogs? So alert and confirm could be caught as well.

Not to mention with #31917 alert and confirm have been somewhat broken for years.

Example interface:

declare namespace Electron {
  interface App {
    on(
      event: "dialog",
      listener: (
        event: Event,
        webContents: WebContents,
        details: file_details | confirm_details | alert_details,
        callback: (result: string[] | boolean | void) => void
      ) => void
    ): this;
  }
}

type file_details = {
  type: "file";
  accept: string[];
  name: string | undefined;
  multiple: boolean;
};

type confirm_details = {
  type: "confirm";
  message: string | undefined;
};

type alert_details = {
  type: "alert";
  message: string | undefined;
};

LqdBcnAtWork avatar Aug 06 '24 16:08 LqdBcnAtWork

any update?

Veiintc avatar Aug 08 '25 03:08 Veiintc