diff --git a/src/main/index.ts b/src/main/index.ts index 8479570e1..99ae3424b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,7 +4,6 @@ import electron from "electron"; import { CONFIG_PATHS } from "src/util.mjs"; import type { RepluggedWebContents } from "../types"; import { getSetting } from "./ipc/settings"; - const electronPath = require.resolve("electron"); const discordPath = join(dirname(require.main!.filename), "..", "app.orig.asar"); const discordPackage = require(join(discordPath, "package.json")); @@ -171,10 +170,16 @@ electron.app.once("ready", () => { filePath = join(CONFIG_PATHS.quickcss, reqUrl.pathname); break; case "theme": - filePath = join(CONFIG_PATHS.themes, reqUrl.pathname); + filePath = join( + reqUrl.pathname.includes(".asar") ? CONFIG_PATHS.temp_themes : CONFIG_PATHS.themes, + reqUrl.pathname.replace(".asar", ""), + ); break; case "plugin": - filePath = join(CONFIG_PATHS.plugins, reqUrl.pathname); + filePath = join( + reqUrl.pathname.includes(".asar") ? CONFIG_PATHS.temp_plugins : CONFIG_PATHS.plugins, + reqUrl.pathname.replace(".asar", ""), + ); break; } cb({ path: filePath }); diff --git a/src/main/ipc/plugins.ts b/src/main/ipc/plugins.ts index 67b9bc884..9b824de99 100644 --- a/src/main/ipc/plugins.ts +++ b/src/main/ipc/plugins.ts @@ -4,23 +4,31 @@ IPC events: - REPLUGGED_UNINSTALL_PLUGIN: returns whether a plugin by the provided name was successfully uninstalled */ -import { readFile, readdir, readlink, rm, stat } from "fs/promises"; +import { readFile, readdir, readlink, stat } from "fs/promises"; import { extname, join, sep } from "path"; import { ipcMain, shell } from "electron"; import { RepluggedIpcChannels, type RepluggedPlugin } from "../../types"; import { plugin } from "../../types/addon"; -import type { Dirent, Stats } from "fs"; -import { CONFIG_PATHS } from "src/util.mjs"; +import { type Dirent, type Stats, rmdirSync, unlinkSync } from "fs"; +import { CONFIG_PATHS, extractAddon } from "src/util.mjs"; const PLUGINS_DIR = CONFIG_PATHS.plugins; +const TEMP_PLUGINS_DIR = CONFIG_PATHS.temp_plugins; export const isFileAPlugin = (f: Dirent | Stats, name: string): boolean => { return f.isDirectory() || (f.isFile() && extname(name) === ".asar"); }; async function getPlugin(pluginName: string): Promise { - const manifestPath = join(PLUGINS_DIR, pluginName, "manifest.json"); - if (!manifestPath.startsWith(`${PLUGINS_DIR}${sep}`)) { + const isAsar = pluginName.includes(".asar"); + const pluginPath = join(PLUGINS_DIR, pluginName); + const realPluginPath = isAsar + ? join(TEMP_PLUGINS_DIR, pluginName.replace(/\.asar$/, "")) + : pluginPath; // Remove ".asar" from the directory name + if (isAsar) await extractAddon(pluginPath, realPluginPath); + + const manifestPath = join(realPluginPath, "manifest.json"); + if (!manifestPath.startsWith(`${realPluginPath}${sep}`)) { // Ensure file changes are restricted to the base path throw new Error("Invalid plugin name"); } @@ -40,7 +48,7 @@ async function getPlugin(pluginName: string): Promise { const cssPath = data.manifest.renderer?.replace(/\.js$/, ".css"); const hasCSS = cssPath && - (await stat(join(PLUGINS_DIR, pluginName, cssPath)) + (await stat(join(realPluginPath, cssPath)) .then(() => true) .catch(() => false)); @@ -90,17 +98,28 @@ ipcMain.handle(RepluggedIpcChannels.LIST_PLUGINS, async (): Promise { +ipcMain.handle(RepluggedIpcChannels.UNINSTALL_PLUGIN, (_, pluginName: string) => { + const isAsar = pluginName.includes(".asar"); const pluginPath = join(PLUGINS_DIR, pluginName); - if (!pluginPath.startsWith(`${PLUGINS_DIR}${sep}`)) { + const realPluginPath = isAsar + ? join(TEMP_PLUGINS_DIR, pluginName.replace(".asar", "")) + : pluginPath; // Remove ".asar" from the directory name + + if (!realPluginPath.startsWith(`${isAsar ? TEMP_PLUGINS_DIR : PLUGINS_DIR}${sep}`)) { // Ensure file changes are restricted to the base path throw new Error("Invalid plugin name"); } - await rm(pluginPath, { - recursive: true, - force: true, - }); + if (isAsar) { + unlinkSync(pluginPath); + rmdirSync(realPluginPath, { recursive: true }); + } else rmdirSync(pluginPath, { recursive: true }); }); ipcMain.on(RepluggedIpcChannels.OPEN_PLUGINS_FOLDER, () => shell.openPath(PLUGINS_DIR)); + +ipcMain.on(RepluggedIpcChannels.CLEAR_TEMP_THEME, () => { + try { + rmdirSync(TEMP_PLUGINS_DIR, { recursive: true }); + } catch {} +}); diff --git a/src/main/ipc/themes.ts b/src/main/ipc/themes.ts index 740bed3f3..795271cf5 100644 --- a/src/main/ipc/themes.ts +++ b/src/main/ipc/themes.ts @@ -3,28 +3,33 @@ IPC events: - REPLUGGED_LIST_THEMES: returns an array of all valid themes available - REPLUGGED_UNINSTALL_THEME: uninstalls a theme by name */ - -import { readFile, readdir, readlink, rm, stat } from "fs/promises"; +import { type Dirent, type Stats, rmdirSync, unlinkSync } from "fs"; +import { readFile, readdir, readlink, stat } from "fs/promises"; import { extname, join, sep } from "path"; import { ipcMain, shell } from "electron"; import { RepluggedIpcChannels, type RepluggedTheme } from "../../types"; import { theme } from "../../types/addon"; -import { CONFIG_PATHS } from "src/util.mjs"; -import type { Dirent, Stats } from "fs"; +import { CONFIG_PATHS, extractAddon } from "src/util.mjs"; const THEMES_DIR = CONFIG_PATHS.themes; +const TEMP_THEMES_DIR = CONFIG_PATHS.temp_themes; export const isFileATheme = (f: Dirent | Stats, name: string): boolean => { return f.isDirectory() || (f.isFile() && extname(name) === ".asar"); }; async function getTheme(path: string): Promise { - const manifestPath = join(THEMES_DIR, path, "manifest.json"); - if (!manifestPath.startsWith(`${THEMES_DIR}${sep}`)) { + const isAsar = path.includes(".asar"); + const themePath = join(THEMES_DIR, path); + const realThemePath = isAsar ? join(TEMP_THEMES_DIR, path.replace(/\.asar$/, "")) : themePath; // Remove ".asar" from the directory name + if (isAsar) await extractAddon(themePath, realThemePath); + + const manifestPath = join(realThemePath, "manifest.json"); + + if (!manifestPath.startsWith(`${realThemePath}${sep}`)) { // Ensure file changes are restricted to the base path throw new Error("Invalid theme name"); } - const manifest: unknown = JSON.parse( await readFile(manifestPath, { encoding: "utf-8", @@ -77,17 +82,26 @@ ipcMain.handle(RepluggedIpcChannels.LIST_THEMES, async (): Promise { +ipcMain.handle(RepluggedIpcChannels.UNINSTALL_THEME, (_, themeName: string) => { + const isAsar = themeName.includes(".asar"); const themePath = join(THEMES_DIR, themeName); - if (!themePath.startsWith(`${THEMES_DIR}${sep}`)) { - // Ensure file changes are restricted to the base path + const realThemePath = isAsar + ? join(TEMP_THEMES_DIR, themeName.replace(/\.asar$/, "")) + : themePath; // Remove ".asar" from the directory name + + if (!realThemePath.startsWith(`${isAsar ? TEMP_THEMES_DIR : THEMES_DIR}${sep}`)) { throw new Error("Invalid theme name"); } - - await rm(themePath, { - recursive: true, - force: true, - }); + if (isAsar) { + unlinkSync(themePath); + rmdirSync(realThemePath, { recursive: true }); + } else rmdirSync(themePath, { recursive: true }); }); ipcMain.on(RepluggedIpcChannels.OPEN_THEMES_FOLDER, () => shell.openPath(THEMES_DIR)); + +ipcMain.on(RepluggedIpcChannels.CLEAR_TEMP_THEME, () => { + try { + rmdirSync(TEMP_THEMES_DIR, { recursive: true }); + } catch {} +}); diff --git a/src/types/index.ts b/src/types/index.ts index 04f5e4185..9d85c2437 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -28,6 +28,8 @@ export enum RepluggedIpcChannels { INSTALL_ADDON = "REPLUGGED_INSTALL_ADDON", OPEN_PLUGINS_FOLDER = "REPLUGGED_OPEN_PLUGINS_FOLDER", OPEN_THEMES_FOLDER = "REPLUGGED_OPEN_THEMES_FOLDER", + CLEAR_TEMP_PLUGIN = "REPLUGGED_CLEAR_PLUGIN_TEMP", + CLEAR_TEMP_THEME = "REPLUGGED_CLEAR_THEME_TEMP", OPEN_SETTINGS_FOLDER = "REPLUGGED_OPEN_SETTINGS_FOLDER", OPEN_QUICKCSS_FOLDER = "REPLUGGED_OPEN_QUICKCSS_FOLDER", GET_REPLUGGED_VERSION = "REPLUGGED_GET_REPLUGGED_VERSION", diff --git a/src/util.mts b/src/util.mts index 99a1f8a85..c0ca1d1d2 100644 --- a/src/util.mts +++ b/src/util.mts @@ -1,6 +1,8 @@ import esbuild from "esbuild"; import { execSync } from "child_process"; -import { chownSync, existsSync, mkdirSync, statSync, writeFileSync } from "fs"; +import { extractAll } from "@electron/asar"; +import { tmpdir } from "os"; +import { chownSync, existsSync, mkdirSync, mkdtempSync, statSync, writeFileSync } from "fs"; import path, { join } from "path"; import chalk from "chalk"; @@ -55,15 +57,27 @@ const CONFIG_FOLDER_NAMES = [ "settings", "quickcss", "react-devtools", + "temp_themes", + "temp_plugins", ] as const; export const CONFIG_PATHS = Object.fromEntries( CONFIG_FOLDER_NAMES.map((name) => { - const path = join(CONFIG_PATH, name); - if (!existsSync(path)) { - mkdirSync(path); + switch (name) { + case "temp_themes": { + return [name, mkdtempSync(join(tmpdir(), "replugged-theme-"))]; + } + case "temp_plugins": { + return [name, mkdtempSync(join(tmpdir(), "replugged-plugin-"))]; + } + default: { + const path = join(CONFIG_PATH, name); + if (!existsSync(path)) { + mkdirSync(path); + } + return [name, path]; + } } - return [name, path]; }), ) as Record<(typeof CONFIG_FOLDER_NAMES)[number], string>; @@ -71,7 +85,12 @@ const { uid: REAL_UID, gid: REAL_GID } = statSync(join(CONFIG_PATH, "..")); const shouldChown = process.platform === "linux"; if (shouldChown) { chownSync(CONFIG_PATH, REAL_UID, REAL_GID); - CONFIG_FOLDER_NAMES.forEach((folder) => chownSync(join(CONFIG_PATH, folder), REAL_UID, REAL_GID)); + CONFIG_FOLDER_NAMES.forEach( + (folder) => + folder !== "temp_themes" && + folder !== "temp_plugins" && + chownSync(join(CONFIG_PATH, folder), REAL_UID, REAL_GID), + ); } const QUICK_CSS_FILE = join(CONFIG_PATHS.quickcss, "main.css"); @@ -145,3 +164,14 @@ export const logBuildPlugin: esbuild.Plugin = { }); }, }; + +export const extractAddon = async (srcPath: string, destPath: string): Promise => { + return new Promise((res) => { + // Ensure the destination directory exists + mkdirSync(destPath, { recursive: true }); + + // Extract the contents of the asar archive directly into the destination directory + extractAll(srcPath, destPath); + res(); + }); +};