/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2022 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import "./updater"; import "./ipcPlugins"; import "./settings"; import { debounce } from "@shared/debounce"; import { IpcEvents } from "@shared/IpcEvents"; import { BrowserWindow, ipcMain, nativeTheme, shell, systemPreferences, app, dialog } from "electron"; import monacoHtml from "file://monacoWin.html?minify&base64"; import { FSWatcher, mkdirSync, readFileSync, watch, writeFileSync } from "fs"; import { open, readdir, readFile, unlink } from "fs/promises"; import { join, normalize } from "path"; import { registerCspIpcHandlers } from "./csp/manager"; import { ALLOWED_PROTOCOLS, QUICK_CSS_PATH, SETTINGS_DIR, THEMES_DIR } from "./utils/constants"; import { makeLinksOpenExternally } from "./utils/externalLinks"; const RENDERER_CSS_PATH = join(__dirname, "renderer.css"); mkdirSync(THEMES_DIR, { recursive: true }); registerCspIpcHandlers(); export function ensureSafePath(basePath: string, path: string) { const normalizedBasePath = normalize(basePath + "/"); const newPath = join(basePath, path); const normalizedPath = normalize(newPath); return normalizedPath.startsWith(normalizedBasePath) ? normalizedPath : null; } function readCss() { return readFile(QUICK_CSS_PATH, "utf-8").catch(() => ""); } async function listThemes(): Promise<{ fileName: string; content: string; }[]> { try { const files = await readdir(THEMES_DIR); return await Promise.all(files.map(async fileName => ({ fileName, content: await getThemeData(fileName) }))); } catch { return []; } } function getThemeData(fileName: string) { fileName = fileName.replace(/\?v=\d+$/, ""); const safePath = ensureSafePath(THEMES_DIR, fileName); if (!safePath) return Promise.reject(`Unsafe path ${fileName}`); return readFile(safePath, "utf-8"); } ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICK_CSS_PATH)); ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { try { var { protocol } = new URL(url); } catch { throw "Malformed URL"; } if (!ALLOWED_PROTOCOLS.includes(protocol)) throw "Disallowed protocol."; shell.openExternal(url); }); ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => writeFileSync(QUICK_CSS_PATH, css) ); ipcMain.handle(IpcEvents.GET_THEMES_DIR, () => THEMES_DIR); ipcMain.handle(IpcEvents.GET_THEMES_LIST, () => listThemes()); ipcMain.handle(IpcEvents.GET_THEME_DATA, (_, fileName) => getThemeData(fileName)); ipcMain.handle(IpcEvents.DELETE_THEME, (_, fileName) => { const safePath = ensureSafePath(THEMES_DIR, fileName); if (!safePath) return Promise.reject(`Unsafe path ${fileName}`); return unlink(safePath); }); ipcMain.handle(IpcEvents.GET_THEME_SYSTEM_VALUES, () => { let accentColor = systemPreferences.getAccentColor?.() ?? ""; if (accentColor.length && accentColor[0] !== "#") { accentColor = `#${accentColor}`; } return { "os-accent-color": accentColor }; }); ipcMain.handle(IpcEvents.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR)); ipcMain.handle(IpcEvents.OPEN_SETTINGS_FOLDER, () => shell.openPath(SETTINGS_DIR)); ipcMain.handle(IpcEvents.INIT_FILE_WATCHERS, ({ sender }) => { let quickCssWatcher: FSWatcher | undefined; let rendererCssWatcher: FSWatcher | undefined; open(QUICK_CSS_PATH, "a+").then(fd => { fd.close(); quickCssWatcher = watch(QUICK_CSS_PATH, { persistent: false }, debounce(async () => { sender.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }, 50)); }).catch(() => { }); const themesWatcher = watch(THEMES_DIR, { persistent: false }, debounce(() => { sender.postMessage(IpcEvents.THEME_UPDATE, void 0); })); if (IS_DEV) { rendererCssWatcher = watch(RENDERER_CSS_PATH, { persistent: false }, async () => { sender.postMessage(IpcEvents.RENDERER_CSS_UPDATE, await readFile(RENDERER_CSS_PATH, "utf-8")); }); } sender.once("destroyed", () => { quickCssWatcher?.close(); themesWatcher.close(); rendererCssWatcher?.close(); }); }); ipcMain.on(IpcEvents.GET_MONACO_THEME, e => { e.returnValue = nativeTheme.shouldUseDarkColors ? "vs-dark" : "vs-light"; }); let monacoWin: BrowserWindow | null = null; ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { if (monacoWin && !monacoWin.isDestroyed()) { monacoWin.show(); monacoWin.focus(); return; } monacoWin = new BrowserWindow({ title: "Equicord QuickCSS Editor", autoHideMenuBar: true, darkTheme: true, webPreferences: { preload: join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: false } }); monacoWin.once("closed", () => { monacoWin = null; }); makeLinksOpenExternally(monacoWin); await monacoWin.loadURL(`data:text/html;base64,${monacoHtml}`); }); app.on("before-quit", async (event) => { if (monacoWin && !monacoWin.isDestroyed() && !monacoWin.isVisible()) { const result = await dialog.showMessageBox({ type: "question", buttons: ["Cancel", "Close Anyway"], defaultId: 0, title: "QuickCSS Editor Open", message: "QuickCSS editor is still open in the background.", detail: "Do you want to close Discord anyway? This will also close the QuickCSS editor." }); if (result.response === 1) { app.exit(); } } }); ipcMain.handle(IpcEvents.GET_RENDERER_CSS, () => readFile(RENDERER_CSS_PATH, "utf-8")); if (IS_DISCORD_DESKTOP) { ipcMain.on(IpcEvents.PRELOAD_GET_RENDERER_JS, e => { e.returnValue = readFileSync(join(__dirname, "renderer.js"), "utf-8"); }); }