diff --git a/electron/main.ts b/electron/main.ts index 687d868..c1ef26a 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -38,6 +38,15 @@ if (!fs.existsSync(COMMANDS_DIR)) { fs.mkdirSync(COMMANDS_DIR) } +function getSafeNotePath(id: string): string { + const fullPath = path.resolve(NOTES_DIR, id) + const relative = path.relative(NOTES_DIR, fullPath) + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error('Access denied: Invalid path') + } + return fullPath +} + function writeCommandFile(name: string, content: string) { const filePath = path.join(COMMANDS_DIR, name) if (!fs.existsSync(filePath)) { @@ -305,9 +314,24 @@ function createWindow() { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, + webSecurity: true, }, }) + // Prevent new window creation + win.webContents.setWindowOpenHandler(() => { + return { action: 'deny' } + }) + + // Prevent navigation to external sites inside the app + win.webContents.on('will-navigate', (event, url) => { + const isLocalhost = url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:') + const isFile = url.startsWith('file://') + if (!isLocalhost && !isFile) { + event.preventDefault() + } + }) + win.on('close', saveWindowState) win.on('ready-to-show', () => { @@ -439,26 +463,36 @@ app.whenReady().then(() => { // Wait for settings to load before registering new note shortcut to get custom one // Registration is handled via IPC from the renderer on startup + let currentNewNoteShortcut = '' + let currentToggleShortcut = '' + let isShortcutsPaused = false + const registerNewNoteShortcut = (combo: string) => { + currentNewNoteShortcut = combo try { if (globalShortcut.isRegistered(combo)) { globalShortcut.unregister(combo) } - globalShortcut.register(combo, () => { - if (win) { - bringToActiveSpace(win) - win.webContents.send('trigger-new-note') - } - }) + if (!isShortcutsPaused && combo) { + globalShortcut.register(combo, () => { + if (win) { + bringToActiveSpace(win) + win.webContents.send('trigger-new-note') + } + }) + } } catch (e) {} } const registerToggleShortcut = (combo: string) => { + currentToggleShortcut = combo try { if (globalShortcut.isRegistered(combo)) { globalShortcut.unregister(combo) } - globalShortcut.register(combo, toggleWindow) + if (!isShortcutsPaused && combo) { + globalShortcut.register(combo, toggleWindow) + } } catch (e) {} } @@ -475,6 +509,17 @@ app.whenReady().then(() => { } catch (e) {} }) + ipcMain.on('pause-shortcuts', () => { + isShortcutsPaused = true + globalShortcut.unregisterAll() + }) + + ipcMain.on('resume-shortcuts', () => { + isShortcutsPaused = false + if (currentNewNoteShortcut) registerNewNoteShortcut(currentNewNoteShortcut) + if (currentToggleShortcut) registerToggleShortcut(currentToggleShortcut) + }) + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() @@ -586,7 +631,7 @@ ipcMain.handle('get-notes', () => { }) ipcMain.handle('save-note', (event, { id, content }) => { - const filePath = path.join(NOTES_DIR, id) + const filePath = getSafeNotePath(id) fs.mkdirSync(path.dirname(filePath), { recursive: true }) fs.writeFileSync(filePath, content, 'utf-8') return true @@ -596,7 +641,7 @@ ipcMain.handle('delete-note', (event, id) => { if (id.startsWith('commands/')) { return false } - const filePath = path.join(NOTES_DIR, id) + const filePath = getSafeNotePath(id) if (fs.existsSync(filePath)) { fs.unlinkSync(filePath) cleanEmptyFoldersRecursively(path.dirname(filePath)) @@ -605,8 +650,8 @@ ipcMain.handle('delete-note', (event, id) => { }) ipcMain.handle('rename-note', (event, { oldId, newId }) => { - const oldPath = path.join(NOTES_DIR, oldId) - const newPath = path.join(NOTES_DIR, newId) + const oldPath = getSafeNotePath(oldId) + const newPath = getSafeNotePath(newId) if (fs.existsSync(oldPath)) { fs.mkdirSync(path.dirname(newPath), { recursive: true }) fs.renameSync(oldPath, newPath) @@ -644,6 +689,18 @@ ipcMain.on('open-settings', () => { }, }) + settingsWin.webContents.setWindowOpenHandler(() => { + return { action: 'deny' } + }) + + settingsWin.webContents.on('will-navigate', (event, url) => { + const isLocalhost = url.startsWith('http://localhost:') || url.startsWith('http://127.0.0.1:') + const isFile = url.startsWith('file://') + if (!isLocalhost && !isFile) { + event.preventDefault() + } + }) + if (process.env.VITE_DEV_SERVER_URL) { settingsWin.loadURL(process.env.VITE_DEV_SERVER_URL + '#/settings') } else { @@ -733,7 +790,7 @@ ipcMain.on('set-launch-startup', (_, value: boolean) => { }) ipcMain.handle('read-note', async (_, id) => { - return fs.readFileSync(path.join(NOTES_DIR, id), 'utf-8') + return fs.readFileSync(getSafeNotePath(id), 'utf-8') }) ipcMain.handle('export-note', async (_, filename: string, content: string) => { @@ -757,7 +814,9 @@ ipcMain.handle('export-note', async (_, filename: string, content: string) => { }) ipcMain.on('open-external', (_, url) => { - shell.openExternal(url) + if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('mailto:')) { + shell.openExternal(url) + } }) ipcMain.on('open-file', (_, filePath) => { diff --git a/electron/preload.ts b/electron/preload.ts index 6d24a9f..d0cddb1 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -47,4 +47,6 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('power:resume', handler) return () => ipcRenderer.removeListener('power:resume', handler) }, + pauseShortcuts: () => ipcRenderer.send('pause-shortcuts'), + resumeShortcuts: () => ipcRenderer.send('resume-shortcuts'), }) diff --git a/src/Settings.tsx b/src/Settings.tsx index 127cc5a..2b76de9 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -12,7 +12,7 @@ export default function Settings() { }) const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { + if (e.key === 'Escape' && !e.defaultPrevented) { window.electronAPI.closeWindow() } } @@ -198,21 +198,11 @@ export default function Settings() {

Global Shortcuts

- setGlobalShortcutToggle(e.target.value)} - placeholder="e.g. CommandOrControl+Shift+C" - /> +
- setGlobalShortcutNewNote(e.target.value)} - placeholder="e.g. CommandOrControl+Shift+N" - /> +
@@ -338,3 +328,144 @@ export default function Settings() { ) } + +function ShortcutInput({ value, onChange }: { value: string; onChange: (val: string) => void }) { + const [recording, setRecording] = useState(false) + + useEffect(() => { + if (recording) { + if (window.electronAPI.pauseShortcuts) window.electronAPI.pauseShortcuts() + } else { + if (window.electronAPI.resumeShortcuts) window.electronAPI.resumeShortcuts() + } + }, [recording]) + + const renderShortcutDisplay = (shortcut: string) => { + if (!shortcut) return Click to record + const parts = shortcut.split('+') + return ( +
+ {parts.map((part, index) => { + let display = part + switch (part) { + case 'CommandOrControl': + case 'Command': + display = '⌘' + break + case 'Control': + display = '⌃' + break + case 'Shift': + display = '⇧' + break + case 'Alt': + case 'Option': + display = '⌥' + break + case 'Up': + display = '↑' + break + case 'Down': + display = '↓' + break + case 'Left': + display = '←' + break + case 'Right': + display = '→' + break + case 'Space': + display = '␣' + break + } + return ( + + + {display} + + {index < parts.length - 1 && ( + + + )} + + ) + })} +
+ ) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!recording) return + e.preventDefault() + + if (e.key === 'Escape') { + setRecording(false) + return + } + + if (e.key === 'Backspace' || e.key === 'Delete') { + onChange('') + setRecording(false) + return + } + + const modifiers = [] + if (e.metaKey || e.ctrlKey) modifiers.push('CommandOrControl') + if (e.altKey) modifiers.push('Alt') + if (e.shiftKey) modifiers.push('Shift') + + // Don't record if only a modifier is pressed + if (['Control', 'Shift', 'Alt', 'Meta'].includes(e.key)) { + return + } + + let key = e.key.toUpperCase() + if (key === ' ') key = 'Space' + // Map arrows and other special keys + if (key === 'ARROWUP') key = 'Up' + if (key === 'ARROWDOWN') key = 'Down' + if (key === 'ARROWLEFT') key = 'Left' + if (key === 'ARROWRIGHT') key = 'Right' + + const shortcut = [...modifiers, key].join('+') + onChange(shortcut) + setRecording(false) + } + + return ( + + ) +} diff --git a/src/types.d.ts b/src/types.d.ts index 7754708..d7ba06e 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -27,6 +27,8 @@ export interface ElectronAPI { safeStorageDecrypt: (val: string) => Promise onPowerSuspend: (callback: () => void) => () => void onPowerResume: (callback: () => void) => () => void + pauseShortcuts: () => void + resumeShortcuts: () => void } declare global {