-
-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Detect Flatpak permission errors on drag and drop (#1004)
- Loading branch information
1 parent
a5a9447
commit 79e8db5
Showing
9 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |