Stop using require for addons

This commit is contained in:
Zack Rauen 2022-06-29 00:00:42 -04:00
parent dd5c6556fd
commit 1368667144
3 changed files with 44 additions and 65 deletions

View File

@ -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();
@ -64,7 +60,7 @@ export default class AddonManager {
initializeAddon() {return;}
// Subclasses should overload this and modify the fileContent as needed to require() the file
getFileModification(module, fileContent) {return fileContent;}
finalizeRequire(module, fileContent, meta) {return meta;}
startAddon() {return;}
stopAddon() {return;}
@ -177,44 +173,33 @@ 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 = meta.name;
meta.slug = 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);
};
requireAddon(filename) {
const module = {filename, exports: {}};
let fileContent = fs.readFileSync(filename, "utf8");
fileContent = stripBOM(fileContent);
const stats = fs.statSync(filename);
const meta = this.extractMeta(fileContent);
if (!meta.author) meta.author = Strings.Addons.unknownAuthor;
if (!meta.version) meta.version = "???";
if (!meta.description) meta.description = Strings.Addons.noDescription;
// if (!meta.name || !meta.author || !meta.description || !meta.version) return new AddonError(meta.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);
meta.id = meta.name;
meta.slug = path.basename(filename).replace(this.extension, "").replace(/ /g, "-");
meta.filename = path.basename(filename);
meta.added = stats.atimeMs;
meta.modified = stats.mtimeMs;
meta.size = stats.size;
const error = this.finalizeRequire(module, fileContent, meta);
if (error) return error;
return module.exports;
}
// 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);
@ -232,7 +217,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.`);

View File

@ -14,17 +14,16 @@ 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");}
@ -115,29 +114,20 @@ 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.slug}-script-container`;
// container.src = `data:text/javascript;${btoa(final)}`;
document.head.append(container);
finalizeRequire(module, fileContent, meta) {
fileContent += normalizeExports(meta.exports || meta.name);
fileContent += `\n//# sourceURL=betterdiscord://plugins/${module.filename}`;
try {
// Test if the code is valid gracefully
vm.compileFunction(fileContent, ["require", "module", "exports", "__filename", "__dirname"]);
const wrappedPlugin = new Function(["require", "module", "exports", "__filename", "__dirname"], fileContent); // eslint-disable-line no-new-func
wrappedPlugin(window.require, module, module.exports, module.filename, this.addonFolder);
}
catch (err) {
return new AddonError(meta.name || path.basename(module.filename), module.filename, "Plugin could not be compiled", {message: err.message, stack: err.stack}, this.prefix);
}
meta.exports = module.exports;
module.exports = meta;
delete window.module;
delete window.__filename;
delete window.__dirname;
container.remove();
return "";
}
startAddon(id) {return this.startPlugin(id);}

View File

@ -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,14 @@ export default new class ThemeManager extends AddonManager {
}
/* Overrides */
getFileModification(module, fileContent, 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);
}
finalizeRequire(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)};`;
module.exports = meta;
}
startAddon(id) {return this.addTheme(id);}