diff --git a/src-main/l10n/en.json b/src-main/l10n/en.json index ca5a2a0e..f167aaa3 100644 --- a/src-main/l10n/en.json +++ b/src-main/l10n/en.json @@ -458,5 +458,17 @@ "rich-presence.untitled": { "string": "Untitled Project", "developer_comment": "Appears in Discord rich presence when the current project doesn't have a name." + }, + "file-access.window-title": { + "string": "Can't Access Files", + "developer_comment": "Title of window that appears when user tries to drag & drop a file that the app can't access because of a security sandbox." + }, + "file-access.flatpak": { + "string": "The Flatpak sandbox may be preventing {APP_NAME} from accessing files that you tried to drag and drop:", + "developer_comment": "Part of window that appears when user tries to drag & drop a file that the app can't access. Followed by list of paths. Leave 'Flatpak' in English." + }, + "file-access.how-to-fix": { + "string": "Use the in-app file picker instead, or run the following command in a terminal and restart the app to grant access to the files:", + "developer_comment": "Part of window that appears when user tries to drag & drop a file that the app can't access." } } \ No newline at end of file diff --git a/src-main/protocols.js b/src-main/protocols.js index d907fc64..95866613 100644 --- a/src-main/protocols.js +++ b/src-main/protocols.js @@ -52,6 +52,9 @@ const FILE_SCHEMES = { }, 'tw-security-prompt': { root: path.resolve(__dirname, '../src-renderer/security-prompt'), + }, + 'tw-file-access': { + root: path.resolve(__dirname, '../src-renderer/file-access'), } }; diff --git a/src-main/windows/editor.js b/src-main/windows/editor.js index 47176ad9..80e0804a 100644 --- a/src-main/windows/editor.js +++ b/src-main/windows/editor.js @@ -15,6 +15,7 @@ const prompts = require('../prompts'); const settings = require('../settings'); const privilegedFetch = require('../fetch'); const RichPresence = require('../rich-presence.js'); +const FileAccessWindow = require('./file-access-window.js'); const TYPE_FILE = 'file'; const TYPE_URL = 'url'; @@ -508,6 +509,10 @@ class EditorWindow extends ProjectRunningWindow { }; }); + ipc.handle('check-drag-and-drop-path', (event, filePath) => { + FileAccessWindow.check(filePath); + }); + /** * Refers to the full screen button in the editor, not the OS-level fullscreen through * F11/Alt+Enter (Windows, Linux) or buttons provided by the OS (macOS). diff --git a/src-main/windows/file-access-window.js b/src-main/windows/file-access-window.js new file mode 100644 index 00000000..45f9ca0c --- /dev/null +++ b/src-main/windows/file-access-window.js @@ -0,0 +1,101 @@ +const fsPromises = require('fs/promises'); +const pathUtil = require('path'); +const {getPlatform} = require('../platform'); +const AbstractWindow = require('./abstract'); +const {translate, getLocale, getStrings} = require('../l10n'); +const {APP_NAME} = require('../brand'); + +/** + * @param {string} path + * @returns {Promise} Promise that resolves to true if access seems to be missing. + */ +const missingFileAccess = async (path) => { + // Sanity check. + if (!pathUtil.isAbsolute(path)) { + return false; + } + + try { + await fsPromises.stat(path); + } catch (e) { + if (e.code === 'ENOENT') { + return true; + } + } + + // We were able to access the file, or the stat failed for a reason other than it not existing. + // Asking for more permission won't fix this. + return false; +}; + +class FileAccessWindow extends AbstractWindow { + constructor () { + super(); + + /** @type {string[]} */ + this.paths = []; + + /** @type {boolean} */ + this.ready = false; + + const ipc = this.window.webContents.ipc; + + ipc.on('init', (e) => { + this.ready = true; + + e.returnValue = { + locale: getLocale(), + strings: getStrings(), + APP_NAME, + initialPaths: this.paths, + }; + }); + + this.window.setTitle(`${translate('file-access.window-title')} - ${APP_NAME}`); + this.window.setMinimizable(false); + this.window.setMaximizable(false); + this.loadURL('tw-file-access://./file-access.html'); + } + + getDimensions () { + return { + width: 600, + height: 300 + }; + } + + getPreload () { + return 'file-access'; + } + + isPopup () { + return true; + } + + /** + * @param {string} path + */ + addPath (path) { + if (!this.paths.includes(path)) { + this.paths.push(path); + if (this.ready) { + this.window.webContents.postMessage('new-path', path); + } + } + } + + /** + * @param {string} path + */ + static async check (path) { + // This window only does anything in the Flatpak build for Linux + // https://github.com/electron/electron/issues/30650 + if (getPlatform() === 'linux-flatpak' && await missingFileAccess(path)) { + const window = AbstractWindow.singleton(FileAccessWindow); + window.addPath(path); + window.show(); + } + } +} + +module.exports = FileAccessWindow; diff --git a/src-main/windows/packager.js b/src-main/windows/packager.js index 9d0fcf12..e5aba656 100644 --- a/src-main/windows/packager.js +++ b/src-main/windows/packager.js @@ -2,6 +2,7 @@ const AbstractWindow = require('./abstract'); const {PACKAGER_NAME} = require('../brand'); const PackagerPreviewWindow = require('./packager-preview'); const prompts = require('../prompts'); +const FileAccessWindow = require('./file-access-window'); class PackagerWindow extends AbstractWindow { constructor (editorWindow) { @@ -40,6 +41,10 @@ class PackagerWindow extends AbstractWindow { event.returnValue = prompts.confirm(this.window, message); }); + ipc.handle('check-drag-and-drop-path', (event, path) => { + FileAccessWindow.check(path); + }); + this.window.webContents.on('did-finish-load', () => { // We can't do this from the preload script this.window.webContents.executeJavaScript(` diff --git a/src-preload/editor.js b/src-preload/editor.js index affd4d82..8098ece9 100644 --- a/src-preload/editor.js +++ b/src-preload/editor.js @@ -70,3 +70,20 @@ contextBridge.exposeInMainWorld('PromptsPreload', { alert: (message) => ipcRenderer.sendSync('alert', message), confirm: (message) => ipcRenderer.sendSync('confirm', message), }); + +// In some Linux environments, people may try to drag & drop files that we don't have access to. +// Remove when https://github.com/electron/electron/issues/30650 is fixed. +if (navigator.userAgent.includes('Linux')) { + document.addEventListener('drop', (e) => { + if (e.isTrusted) { + for (const file of e.dataTransfer.files) { + // Using webUtils is safe as we don't have a legacy build for Linux + const {webUtils} = require('electron'); + const path = webUtils.getPathForFile(file); + ipcRenderer.invoke('check-drag-and-drop-path', path); + } + } + }, { + capture: true + }); +} diff --git a/src-preload/file-access.js b/src-preload/file-access.js new file mode 100644 index 00000000..da8bbe36 --- /dev/null +++ b/src-preload/file-access.js @@ -0,0 +1,14 @@ +const {ipcRenderer, contextBridge} = require('electron'); + +let newPathCallback = () => {}; + +contextBridge.exposeInMainWorld('FileAccessPreload', { + init: () => ipcRenderer.sendSync('init'), + onNewPath: (callback) => { + newPathCallback = callback; + } +}); + +ipcRenderer.on('new-path', (event, path) => { + newPathCallback(path); +}); diff --git a/src-preload/packager.js b/src-preload/packager.js index 4a966657..cd3e3466 100644 --- a/src-preload/packager.js +++ b/src-preload/packager.js @@ -32,3 +32,20 @@ if (ipcRenderer.sendSync('is-mas')) { document.head.appendChild(style); }); } + +// In some Linux environments, people may try to drag & drop files that we don't have access to. +// Remove when https://github.com/electron/electron/issues/30650 is fixed. +if (navigator.userAgent.includes('Linux')) { + document.addEventListener('drop', (e) => { + if (e.isTrusted) { + for (const file of e.dataTransfer.files) { + // Using webUtils is safe as we don't have a legacy build for Linux + const {webUtils} = require('electron'); + const path = webUtils.getPathForFile(file); + ipcRenderer.invoke('check-drag-and-drop-path', path); + } + } + }, { + capture: true + }); +} diff --git a/src-renderer/file-access/file-access.html b/src-renderer/file-access/file-access.html new file mode 100644 index 00000000..e6092aff --- /dev/null +++ b/src-renderer/file-access/file-access.html @@ -0,0 +1,140 @@ + + + + + + + + +
+ + +

+ + + + +

+ + +

+ + +
+ +