mirror of
https://github.com/TurboWarp/desktop.git
synced 2024-11-25 16:36:20 +08:00
Detect Flatpak permission errors on drag and drop (#1004)
This commit is contained in:
parent
a5a9447326
commit
79e8db5b9b
@ -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."
|
||||
}
|
||||
}
|
@ -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'),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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).
|
||||
|
101
src-main/windows/file-access-window.js
Normal file
101
src-main/windows/file-access-window.js
Normal 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;
|
@ -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(`
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
14
src-preload/file-access.js
Normal file
14
src-preload/file-access.js
Normal 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);
|
||||
});
|
@ -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
|
||||
});
|
||||
}
|
||||
|
140
src-renderer/file-access/file-access.html
Normal file
140
src-renderer/file-access/file-access.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user