Stop relying on require() to load addons (#1332)
This commit is contained in:
parent
57ff15ee68
commit
29666219fd
|
@ -15,7 +15,6 @@ const React = DiscordModules.React;
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const Module = require("module").Module;
|
|
||||||
const shell = require("electron").shell;
|
const shell = require("electron").shell;
|
||||||
const openItem = shell.openItem || shell.openPath;
|
const openItem = shell.openItem || shell.openPath;
|
||||||
|
|
||||||
|
@ -32,7 +31,6 @@ const stripBOM = function(fileContent) {
|
||||||
export default class AddonManager {
|
export default class AddonManager {
|
||||||
|
|
||||||
get name() {return "";}
|
get name() {return "";}
|
||||||
get moduleExtension() {return "";}
|
|
||||||
get extension() {return "";}
|
get extension() {return "";}
|
||||||
get duplicatePattern() {return /./;}
|
get duplicatePattern() {return /./;}
|
||||||
get addonFolder() {return "";}
|
get addonFolder() {return "";}
|
||||||
|
@ -51,8 +49,6 @@ export default class AddonManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.originalRequire = Module._extensions[this.moduleExtension];
|
|
||||||
Module._extensions[this.moduleExtension] = this.getAddonRequire();
|
|
||||||
Settings.on(this.collection, this.category, this.id, (enabled) => {
|
Settings.on(this.collection, this.category, this.id, (enabled) => {
|
||||||
if (enabled) this.watchAddons();
|
if (enabled) this.watchAddons();
|
||||||
else this.unwatchAddons();
|
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
|
// Subclasses should overload this and modify the addon object as needed to fully load it
|
||||||
initializeAddon() {return;}
|
initializeAddon() {return;}
|
||||||
|
|
||||||
// Subclasses should overload this and modify the fileContent as needed to require() the file
|
|
||||||
getFileModification(module, fileContent) {return fileContent;}
|
|
||||||
|
|
||||||
startAddon() {return;}
|
startAddon() {return;}
|
||||||
stopAddon() {return;}
|
stopAddon() {return;}
|
||||||
|
|
||||||
|
@ -177,43 +170,32 @@ export default class AddonManager {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddonRequire() {
|
// Subclasses should overload this and modify the addon using the fileContent as needed to "require()"" the file
|
||||||
const self = this;
|
requireAddon(filename) {
|
||||||
// const baseFolder = this.addonFolder;
|
let fileContent = fs.readFileSync(filename, "utf8");
|
||||||
const originalRequire = this.originalRequire;
|
fileContent = stripBOM(fileContent);
|
||||||
return function(module, filename) {
|
const stats = fs.statSync(filename);
|
||||||
const possiblePath = path.resolve(self.addonFolder, path.basename(filename));
|
const addon = this.extractMeta(fileContent);
|
||||||
if (!fs.existsSync(possiblePath) || filename !== fs.realpathSync(possiblePath)) return Reflect.apply(originalRequire, this, arguments);
|
if (!addon.author) addon.author = Strings.Addons.unknownAuthor;
|
||||||
let fileContent = fs.readFileSync(filename, "utf8");
|
if (!addon.version) addon.version = "???";
|
||||||
fileContent = stripBOM(fileContent);
|
if (!addon.description) addon.description = Strings.Addons.noDescription;
|
||||||
const stats = fs.statSync(filename);
|
// 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);
|
||||||
const meta = self.extractMeta(fileContent);
|
addon.id = addon.name;
|
||||||
if (!meta.author) meta.author = Strings.Addons.unknownAuthor;
|
addon.slug = path.basename(filename).replace(this.extension, "").replace(/ /g, "-");
|
||||||
if (!meta.version) meta.version = "???";
|
addon.filename = path.basename(filename);
|
||||||
if (!meta.description) meta.description = Strings.Addons.noDescription;
|
addon.added = stats.atimeMs;
|
||||||
meta.id = path.basename(filename).replace(self.extension, "").replace(/ /g, "-");
|
addon.modified = stats.mtimeMs;
|
||||||
meta.filename = path.basename(filename);
|
addon.size = stats.size;
|
||||||
meta.added = stats.atimeMs;
|
addon.fileContent = fileContent;
|
||||||
meta.modified = stats.mtimeMs;
|
return addon;
|
||||||
meta.size = stats.size;
|
|
||||||
fileContent = self.getFileModification(module, fileContent, meta);
|
|
||||||
module._compile(fileContent, filename);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subclasses should use the return (if not AddonError) and push to this.addonList
|
// Subclasses should use the return (if not AddonError) and push to this.addonList
|
||||||
loadAddon(filename, shouldToast = false) {
|
loadAddon(filename, shouldToast = false) {
|
||||||
if (typeof(filename) === "undefined") return;
|
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);
|
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);
|
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;
|
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
|
||||||
if (!addon) return false;
|
if (!addon) return false;
|
||||||
if (this.state[addon.id]) isReload ? this.stopAddon(addon) : this.disableAddon(addon);
|
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.addonList.splice(this.addonList.indexOf(addon), 1);
|
||||||
this.emit("unloaded", addon.id);
|
this.emit("unloaded", addon.id);
|
||||||
if (shouldToast) Toasts.success(`${addon.name} was unloaded.`);
|
if (shouldToast) Toasts.success(`${addon.name} was unloaded.`);
|
||||||
|
|
|
@ -13,18 +13,16 @@ import SettingsRenderer from "../ui/settings";
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const vm = require("vm");
|
const vm = require("vm");
|
||||||
|
|
||||||
|
const normalizeExports = name => `
|
||||||
const fileModification = name => `
|
|
||||||
if (module.exports.default) {
|
if (module.exports.default) {
|
||||||
module.exports = module.exports.default;
|
module.exports = module.exports.default;
|
||||||
}
|
}
|
||||||
if (typeof(module.exports) !== "function") {
|
if (typeof(module.exports) !== "function") {
|
||||||
module.exports = eval("${name};")
|
module.exports = eval("${name}");
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
export default new class PluginManager extends AddonManager {
|
export default new class PluginManager extends AddonManager {
|
||||||
get name() {return "PluginManager";}
|
get name() {return "PluginManager";}
|
||||||
get moduleExtension() {return ".js";}
|
|
||||||
get extension() {return ".plugin.js";}
|
get extension() {return ".plugin.js";}
|
||||||
get duplicatePattern() {return /\.plugin\s?\([0-9]+\)\.js/;}
|
get duplicatePattern() {return /\.plugin\s?\([0-9]+\)\.js/;}
|
||||||
get addonFolder() {return path.resolve(Config.dataPath, "plugins");}
|
get addonFolder() {return path.resolve(Config.dataPath, "plugins");}
|
||||||
|
@ -33,7 +31,6 @@ export default new class PluginManager extends AddonManager {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.promises = {};
|
|
||||||
this.onSwitch = this.onSwitch.bind(this);
|
this.onSwitch = this.onSwitch.bind(this);
|
||||||
this.observer = new MutationObserver((mutations) => {
|
this.observer = new MutationObserver((mutations) => {
|
||||||
for (let i = 0, mlen = mutations.length; i < mlen; i++) {
|
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) {
|
requireAddon(filename) {
|
||||||
fileContent += fileModification(meta.exports || meta.name);
|
const addon = super.requireAddon(filename);
|
||||||
|
try {
|
||||||
window.global = window;
|
const module = {filename, exports: {}};
|
||||||
window.module = module;
|
// Test if the code is valid gracefully
|
||||||
window.__filename = module.filename;
|
vm.compileFunction(addon.fileContent, ["require", "module", "exports", "__filename", "__dirname"]);
|
||||||
window.__dirname = this.addonFolder;
|
addon.fileContent += normalizeExports(addon.exports || addon.name);
|
||||||
const wrapped = `(${vm.compileFunction(fileContent, ["exports", "require", "module", "__filename", "__dirname"]).toString()})`;
|
addon.fileContent += `\n//# sourceURL=betterdiscord://plugins/${addon.filename}`;
|
||||||
const final = `${wrapped}(window.module.exports, window.require, window.module, window.__filename, window.__dirname)\n//# sourceURL=betterdiscord://plugins/${window.__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);
|
||||||
const container = document.createElement("script");
|
addon.exports = module.exports;
|
||||||
container.innerHTML = final;
|
delete addon.fileContent;
|
||||||
container.id = `${meta.id}-script-container`;
|
return addon;
|
||||||
// container.src = `data:text/javascript;${btoa(final)}`;
|
}
|
||||||
document.head.append(container);
|
catch (err) {
|
||||||
|
return new AddonError(addon.name || addon.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);}
|
startAddon(id) {return this.startPlugin(id);}
|
||||||
|
@ -185,9 +176,7 @@ export default new class PluginManager extends AddonManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFunctions() {
|
setupFunctions() {
|
||||||
// electronRemote.getCurrentWebContents().on("did-navigate-in-page", this.onSwitch.bind(this));
|
|
||||||
Events.on("navigate", this.onSwitch);
|
Events.on("navigate", this.onSwitch);
|
||||||
// ipc.on(IPCEvents.NAVIGATE, this.onSwitch);
|
|
||||||
this.observer.observe(document, {
|
this.observer.observe(document, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true
|
subtree: true
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {Config} from "data";
|
import {Config} from "data";
|
||||||
import AddonManager from "./addonmanager";
|
import AddonManager from "./addonmanager";
|
||||||
|
import AddonError from "../structs/addonerror";
|
||||||
import Settings from "./settingsmanager";
|
import Settings from "./settingsmanager";
|
||||||
import DOMManager from "./dommanager";
|
import DOMManager from "./dommanager";
|
||||||
import Strings from "./strings";
|
import Strings from "./strings";
|
||||||
|
@ -12,7 +13,6 @@ const path = require("path");
|
||||||
|
|
||||||
export default new class ThemeManager extends AddonManager {
|
export default new class ThemeManager extends AddonManager {
|
||||||
get name() {return "ThemeManager";}
|
get name() {return "ThemeManager";}
|
||||||
get moduleExtension() {return ".css";}
|
|
||||||
get extension() {return ".theme.css";}
|
get extension() {return ".theme.css";}
|
||||||
get duplicatePattern() {return /\.theme\s?\([0-9]+\)\.css/;}
|
get duplicatePattern() {return /\.theme\s?\([0-9]+\)\.css/;}
|
||||||
get addonFolder() {return path.resolve(Config.dataPath, "themes");}
|
get addonFolder() {return path.resolve(Config.dataPath, "themes");}
|
||||||
|
@ -54,10 +54,16 @@ export default new class ThemeManager extends AddonManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overrides */
|
/* Overrides */
|
||||||
getFileModification(module, fileContent, meta) {
|
initializeAddon(addon) {
|
||||||
meta.css = fileContent;
|
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);
|
||||||
if (meta.format == "json") meta.css = meta.css.split("\n").slice(1).join("\n");
|
}
|
||||||
return `module.exports = ${JSON.stringify(meta)};`;
|
|
||||||
|
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);}
|
startAddon(id) {return this.addTheme(id);}
|
||||||
|
@ -66,14 +72,14 @@ export default new class ThemeManager extends AddonManager {
|
||||||
addTheme(idOrAddon) {
|
addTheme(idOrAddon) {
|
||||||
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
||||||
if (!addon) return;
|
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}));
|
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTheme(idOrAddon) {
|
removeTheme(idOrAddon) {
|
||||||
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
const addon = typeof(idOrAddon) == "string" ? this.addonList.find(p => p.id == idOrAddon) : idOrAddon;
|
||||||
if (!addon) return;
|
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}));
|
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
|
||||||
}
|
}
|
||||||
};
|
};
|
Loading…
Reference in New Issue