From 29666219fdd7cbad97838addc3fb73f5a2be8d41 Mon Sep 17 00:00:00 2001 From: Zack Date: Fri, 8 Jul 2022 10:42:04 -0400 Subject: [PATCH] Stop relying on require() to load addons (#1332) --- renderer/src/modules/addonmanager.js | 60 ++++++++++----------------- renderer/src/modules/pluginmanager.js | 49 +++++++++------------- renderer/src/modules/thememanager.js | 20 +++++---- 3 files changed, 53 insertions(+), 76 deletions(-) diff --git a/renderer/src/modules/addonmanager.js b/renderer/src/modules/addonmanager.js index 734c4094..4891f10c 100644 --- a/renderer/src/modules/addonmanager.js +++ b/renderer/src/modules/addonmanager.js @@ -15,7 +15,6 @@ const React = DiscordModules.React; const path = require("path"); const fs = require("fs"); -const Module = require("module").Module; const shell = require("electron").shell; const openItem = shell.openItem || shell.openPath; @@ -32,7 +31,6 @@ const stripBOM = function(fileContent) { export default class AddonManager { get name() {return "";} - get moduleExtension() {return "";} get extension() {return "";} get duplicatePattern() {return /./;} get addonFolder() {return "";} @@ -51,8 +49,6 @@ export default class AddonManager { } initialize() { - this.originalRequire = Module._extensions[this.moduleExtension]; - Module._extensions[this.moduleExtension] = this.getAddonRequire(); Settings.on(this.collection, this.category, this.id, (enabled) => { if (enabled) this.watchAddons(); else this.unwatchAddons(); @@ -63,9 +59,6 @@ export default class AddonManager { // Subclasses should overload this and modify the addon object as needed to fully load it initializeAddon() {return;} - // Subclasses should overload this and modify the fileContent as needed to require() the file - getFileModification(module, fileContent) {return fileContent;} - startAddon() {return;} stopAddon() {return;} @@ -177,43 +170,32 @@ export default class AddonManager { return out; } - getAddonRequire() { - const self = this; - // const baseFolder = this.addonFolder; - const originalRequire = this.originalRequire; - return function(module, filename) { - const possiblePath = path.resolve(self.addonFolder, path.basename(filename)); - if (!fs.existsSync(possiblePath) || filename !== fs.realpathSync(possiblePath)) return Reflect.apply(originalRequire, this, arguments); - let fileContent = fs.readFileSync(filename, "utf8"); - fileContent = stripBOM(fileContent); - const stats = fs.statSync(filename); - const meta = self.extractMeta(fileContent); - if (!meta.author) meta.author = Strings.Addons.unknownAuthor; - if (!meta.version) meta.version = "???"; - if (!meta.description) meta.description = Strings.Addons.noDescription; - meta.id = path.basename(filename).replace(self.extension, "").replace(/ /g, "-"); - meta.filename = path.basename(filename); - meta.added = stats.atimeMs; - meta.modified = stats.mtimeMs; - meta.size = stats.size; - fileContent = self.getFileModification(module, fileContent, meta); - module._compile(fileContent, filename); - }; + // Subclasses should overload this and modify the addon using the fileContent as needed to "require()"" the file + requireAddon(filename) { + let fileContent = fs.readFileSync(filename, "utf8"); + fileContent = stripBOM(fileContent); + const stats = fs.statSync(filename); + const addon = this.extractMeta(fileContent); + if (!addon.author) addon.author = Strings.Addons.unknownAuthor; + if (!addon.version) addon.version = "???"; + if (!addon.description) addon.description = Strings.Addons.noDescription; + // if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || path.basename(filename), filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix); + addon.id = addon.name; + addon.slug = path.basename(filename).replace(this.extension, "").replace(/ /g, "-"); + addon.filename = path.basename(filename); + addon.added = stats.atimeMs; + addon.modified = stats.mtimeMs; + addon.size = stats.size; + addon.fileContent = fileContent; + return addon; } // Subclasses should use the return (if not AddonError) and push to this.addonList loadAddon(filename, shouldToast = false) { if (typeof(filename) === "undefined") return; - try { - delete __non_webpack_require__.cache[__non_webpack_require__.resolve(path.resolve(this.addonFolder, filename))]; - __non_webpack_require__(path.resolve(this.addonFolder, filename)); - } - catch (error) { - Logger.stacktrace(this.name, `Could not load ${path.basename(filename)}:`, error); - return new AddonError(filename, filename, Strings.Addons.compileError, {message: error.message, stack: error.stack}, this.prefix); - } - const addon = __non_webpack_require__(path.resolve(this.addonFolder, filename)); + const addon = this.requireAddon(path.resolve(this.addonFolder, filename)); + if (addon instanceof AddonError) return addon; if (this.addonList.find(c => c.id == addon.id)) return new AddonError(addon.name, filename, Strings.Addons.alreadyExists.format({type: this.prefix, name: addon.name}), this.prefix); const error = this.initializeAddon(addon); @@ -231,7 +213,7 @@ export default class AddonManager { const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon; if (!addon) return false; if (this.state[addon.id]) isReload ? this.stopAddon(addon) : this.disableAddon(addon); - delete __non_webpack_require__.cache[__non_webpack_require__.resolve(path.resolve(this.addonFolder, addon.filename))]; + this.addonList.splice(this.addonList.indexOf(addon), 1); this.emit("unloaded", addon.id); if (shouldToast) Toasts.success(`${addon.name} was unloaded.`); diff --git a/renderer/src/modules/pluginmanager.js b/renderer/src/modules/pluginmanager.js index d6e2e2f6..a5cb34b4 100644 --- a/renderer/src/modules/pluginmanager.js +++ b/renderer/src/modules/pluginmanager.js @@ -13,18 +13,16 @@ import SettingsRenderer from "../ui/settings"; const path = require("path"); const vm = require("vm"); - -const fileModification = name => ` +const normalizeExports = name => ` if (module.exports.default) { module.exports = module.exports.default; } if (typeof(module.exports) !== "function") { - module.exports = eval("${name};") + module.exports = eval("${name}"); }`; export default new class PluginManager extends AddonManager { get name() {return "PluginManager";} - get moduleExtension() {return ".js";} get extension() {return ".plugin.js";} get duplicatePattern() {return /\.plugin\s?\([0-9]+\)\.js/;} get addonFolder() {return path.resolve(Config.dataPath, "plugins");} @@ -33,7 +31,6 @@ export default new class PluginManager extends AddonManager { constructor() { super(); - this.promises = {}; this.onSwitch = this.onSwitch.bind(this); this.observer = new MutationObserver((mutations) => { for (let i = 0, mlen = mutations.length; i < mlen; i++) { @@ -115,29 +112,23 @@ export default new class PluginManager extends AddonManager { } } - getFileModification(module, fileContent, meta) { - fileContent += fileModification(meta.exports || meta.name); - - window.global = window; - window.module = module; - window.__filename = module.filename; - window.__dirname = this.addonFolder; - const wrapped = `(${vm.compileFunction(fileContent, ["exports", "require", "module", "__filename", "__dirname"]).toString()})`; - const final = `${wrapped}(window.module.exports, window.require, window.module, window.__filename, window.__dirname)\n//# sourceURL=betterdiscord://plugins/${window.__filename}`; - - const container = document.createElement("script"); - container.innerHTML = final; - container.id = `${meta.id}-script-container`; - // container.src = `data:text/javascript;${btoa(final)}`; - document.head.append(container); - - meta.exports = module.exports; - module.exports = meta; - delete window.module; - delete window.__filename; - delete window.__dirname; - container.remove(); - return ""; + requireAddon(filename) { + const addon = super.requireAddon(filename); + try { + const module = {filename, exports: {}}; + // Test if the code is valid gracefully + vm.compileFunction(addon.fileContent, ["require", "module", "exports", "__filename", "__dirname"]); + addon.fileContent += normalizeExports(addon.exports || addon.name); + addon.fileContent += `\n//# sourceURL=betterdiscord://plugins/${addon.filename}`; + const wrappedPlugin = new Function(["require", "module", "exports", "__filename", "__dirname"], addon.fileContent); // eslint-disable-line no-new-func + wrappedPlugin(window.require, module, module.exports, module.filename, this.addonFolder); + addon.exports = module.exports; + delete addon.fileContent; + return addon; + } + catch (err) { + return new AddonError(addon.name || addon.filename, module.filename, "Plugin could not be compiled", {message: err.message, stack: err.stack}, this.prefix); + } } startAddon(id) {return this.startPlugin(id);} @@ -185,9 +176,7 @@ export default new class PluginManager extends AddonManager { } setupFunctions() { - // electronRemote.getCurrentWebContents().on("did-navigate-in-page", this.onSwitch.bind(this)); Events.on("navigate", this.onSwitch); - // ipc.on(IPCEvents.NAVIGATE, this.onSwitch); this.observer.observe(document, { childList: true, subtree: true diff --git a/renderer/src/modules/thememanager.js b/renderer/src/modules/thememanager.js index adb1e670..3e28feb1 100644 --- a/renderer/src/modules/thememanager.js +++ b/renderer/src/modules/thememanager.js @@ -1,5 +1,6 @@ import {Config} from "data"; import AddonManager from "./addonmanager"; +import AddonError from "../structs/addonerror"; import Settings from "./settingsmanager"; import DOMManager from "./dommanager"; import Strings from "./strings"; @@ -12,7 +13,6 @@ const path = require("path"); export default new class ThemeManager extends AddonManager { get name() {return "ThemeManager";} - get moduleExtension() {return ".css";} get extension() {return ".theme.css";} get duplicatePattern() {return /\.theme\s?\([0-9]+\)\.css/;} get addonFolder() {return path.resolve(Config.dataPath, "themes");} @@ -54,10 +54,16 @@ export default new class ThemeManager extends AddonManager { } /* Overrides */ - getFileModification(module, fileContent, meta) { - meta.css = fileContent; - if (meta.format == "json") meta.css = meta.css.split("\n").slice(1).join("\n"); - return `module.exports = ${JSON.stringify(meta)};`; + initializeAddon(addon) { + if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || addon.filename, addon.filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix); + } + + requireAddon(filename) { + const addon = super.requireAddon(filename); + addon.css = addon.fileContent; + delete addon.fileContent; + if (addon.format == "json") addon.css = addon.css.split("\n").slice(1).join("\n"); + return addon; } startAddon(id) {return this.addTheme(id);} @@ -66,14 +72,14 @@ export default new class ThemeManager extends AddonManager { addTheme(idOrAddon) { const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon; if (!addon) return; - DOMManager.injectTheme(addon.id + "-theme-container", addon.css); + DOMManager.injectTheme(addon.slug + "-theme-container", addon.css); Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version})); } removeTheme(idOrAddon) { const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon; if (!addon) return; - DOMManager.removeTheme(addon.id + "-theme-container"); + DOMManager.removeTheme(addon.slug + "-theme-container"); Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version})); } }; \ No newline at end of file