Detect Flatpak permission errors on drag and drop (#1004)

This commit is contained in:
GarboMuffin 2024-05-27 22:42:51 -05:00 committed by GitHub
parent a5a9447326
commit 79e8db5b9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 314 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'">
<style>
body {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: black;
background-color: white;
accent-color: #ff4c4c;
}
main {
padding: 1rem;
box-sizing: border-box;
width: 100%;
min-height: 100vh;
border: 20px solid #ff4c4c;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
h1, p, ul {
margin: 0;
}
.file-path, .command {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
}
.command::before {
content: '$ ';
}
</style>
</head>
<body>
<main>
<script>
// This file only does anything on Linux, so we don't need to worry about Windows paths.
const FLATPAK_APP_ID = 'org.turbowarp.TurboWarp';
// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
const escapeForShellDoubleQuotes = (string) => string
.replace(/\\/g, '\\\\')
.replace(/\$/g, '\\$')
.replace(/`/g, '\\`')
.replace(/!/g, '\\!');
const makeNode = () => ({
leaf: false,
children: Object.create(null)
});
const root = makeNode();
const addPathToGraph = (path) => {
const parts = path.split('/');
let node = root;
// Paths always start with / and the last part is the filename, so ignore first and last item.
for (let i = 1; i < parts.length - 1; i++) {
const name = parts[i];
if (!Object.prototype.hasOwnProperty.call(node.children, name)) {
node.children[name] = makeNode();
}
node = node.children[name];
}
node.leaf = true;
};
const getLeafDirectories = () => {
const recurse = (path, node) => {
if (node.leaf) {
// Ignore children.
return [path];
}
const result = [];
for (const childName of Object.keys(node.children)) {
const childPath = `${path}${childName}/`;
const childLeaves = recurse(childPath, node.children[childName]);
for (const leaf of childLeaves) {
result.push(leaf);
}
}
return result;
};
return recurse('/', root);
};
const addPath = (path) => {
const pathElement = document.createElement('li');
pathElement.className = 'file-path';
pathElement.textContent = path;
fileListElement.appendChild(pathElement);
addPathToGraph(path);
const overrides = getLeafDirectories().map(i => {
// --filesystem=/ isn't valid, need to use --filesystem=host
const value = i === '/' ? 'host' : i;
return `--filesystem="${escapeForShellDoubleQuotes(value)}"`;
});
const command = `flatpak override ${FLATPAK_APP_ID} --user ${overrides.join(' ')}`;
commandElement.textContent = command;
};
FileAccessPreload.onNewPath(addPath);
const {locale, strings, APP_NAME, initialPaths} = FileAccessPreload.init();
document.documentElement.lang = locale;
</script>
<p class="introduction"></p>
<script>
document.querySelector('.introduction').textContent = strings['file-access.flatpak'].replace('{APP_NAME}', APP_NAME);
</script>
<ul class="file-list"></ul>
<p class="how-to-fix"></p>
<script>
document.querySelector('.how-to-fix').textContent = strings['file-access.how-to-fix'];
</script>
<p class="command"></p>
<script>
const fileListElement = document.querySelector('.file-list');
const commandElement = document.querySelector('.command');
for (const path of initialPaths) {
addPath(path);
}
</script>
</main>
</body>
</html>