479 lines
14 KiB
JavaScript
479 lines
14 KiB
JavaScript
import { bdConfig, bdplugins, bdthemes, settingsCookie } from "../0globals";
|
|
import pluginModule from "./pluginModule";
|
|
import themeModule from "./themeModule";
|
|
import Utils from "./utils";
|
|
import dataStore from "./dataStore";
|
|
import {
|
|
encryptSettingsCache,
|
|
decryptSettingsCache,
|
|
processFile,
|
|
} from "./pluginCertifier";
|
|
import * as electron from "electron";
|
|
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
const Module = require("module").Module;
|
|
Module.globalPaths.push(
|
|
path.resolve(
|
|
electron.ipcRenderer.sendSync("LIGHTCORD_GET_APP_PATH"),
|
|
"node_modules"
|
|
)
|
|
);
|
|
class MetaError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "MetaError";
|
|
}
|
|
}
|
|
const originalJSRequire = Module._extensions[".js"];
|
|
const originalCSSRequire = Module._extensions[".css"]
|
|
? Module._extensions[".css"]
|
|
: () => {
|
|
return null;
|
|
};
|
|
const splitRegex = /[^\S\r\n]*?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
|
|
const escapedAtRegex = /^\\@/;
|
|
|
|
export let addonCache = {};
|
|
|
|
let hasPatched = false;
|
|
export default new (class ContentManager {
|
|
constructor() {
|
|
this.timeCache = {};
|
|
this.watchers = {};
|
|
}
|
|
|
|
patchExtensions() {
|
|
if (hasPatched) return;
|
|
hasPatched = true;
|
|
Module._extensions[".js"] = this.getContentRequire("plugin");
|
|
Module._extensions[".css"] = this.getContentRequire("theme");
|
|
}
|
|
|
|
get pluginsFolder() {
|
|
return (
|
|
this._pluginsFolder ||
|
|
(this._pluginsFolder = fs.realpathSync(
|
|
path.resolve(bdConfig.dataPath + "plugins/")
|
|
))
|
|
);
|
|
}
|
|
get themesFolder() {
|
|
return (
|
|
this._themesFolder ||
|
|
(this._themesFolder = fs.realpathSync(
|
|
path.resolve(bdConfig.dataPath + "themes/")
|
|
))
|
|
);
|
|
}
|
|
|
|
loadAddonCertifierCache() {
|
|
if (
|
|
typeof dataStore.getSettingGroup("PluginCertifierHashes") !== "string"
|
|
) {
|
|
dataStore.setSettingGroup(
|
|
"PluginCertifierHashes",
|
|
encryptSettingsCache("{}")
|
|
);
|
|
} else {
|
|
try {
|
|
addonCache = JSON.parse(
|
|
decryptSettingsCache(
|
|
dataStore.getSettingGroup("PluginCertifierHashes")
|
|
)
|
|
);
|
|
} catch (e) {
|
|
dataStore.setSettingGroup(
|
|
"PluginCertifierHashes",
|
|
encryptSettingsCache("{}")
|
|
);
|
|
addonCache = {};
|
|
}
|
|
}
|
|
Object.keys(addonCache).forEach((key) => {
|
|
let value = addonCache[key];
|
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
return delete addonCache[key];
|
|
|
|
let props = [
|
|
{
|
|
key: "timestamp",
|
|
type: "number",
|
|
},
|
|
{
|
|
key: "result",
|
|
type: "object",
|
|
},
|
|
{
|
|
key: "hash",
|
|
type: "string",
|
|
},
|
|
];
|
|
for (let prop of props) {
|
|
if (!(prop.key in value) || typeof value[prop.key] !== prop.type) {
|
|
delete addonCache[key];
|
|
return;
|
|
}
|
|
}
|
|
if (value.hash !== key) {
|
|
delete addonCache[key];
|
|
return;
|
|
}
|
|
if (value.result.suspect) {
|
|
// refetch from remote to be sure you're up to date.
|
|
delete addonCache[key];
|
|
return;
|
|
}
|
|
});
|
|
this.saveAddonCache();
|
|
}
|
|
|
|
saveAddonCache() {
|
|
dataStore.setSettingGroup(
|
|
"PluginCertifierHashes",
|
|
encryptSettingsCache(JSON.stringify(addonCache))
|
|
);
|
|
}
|
|
|
|
resetAddonCache() {
|
|
Object.keys(addonCache).forEach((key) => {
|
|
delete addonCache[key];
|
|
});
|
|
console.log(addonCache);
|
|
this.saveAddonCache();
|
|
}
|
|
|
|
watchContent(contentType) {
|
|
if (this.watchers[contentType]) return;
|
|
const isPlugin = contentType === "plugin";
|
|
const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;
|
|
const fileEnding = isPlugin ? ".plugin.js" : ".theme.css";
|
|
this.watchers[contentType] = fs.watch(
|
|
baseFolder,
|
|
{ persistent: false },
|
|
async (eventType, filename) => {
|
|
if (!eventType || !filename || !filename.endsWith(fileEnding)) return;
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
try {
|
|
fs.statSync(path.resolve(baseFolder, filename));
|
|
} catch (err) {
|
|
if (err.code !== "ENOENT") return;
|
|
delete this.timeCache[filename];
|
|
if (isPlugin) return pluginModule.unloadPlugin(filename);
|
|
return themeModule.unloadTheme(filename);
|
|
}
|
|
if (!fs.statSync(path.resolve(baseFolder, filename)).isFile()) return;
|
|
const stats = fs.statSync(path.resolve(baseFolder, filename));
|
|
if (!stats || !stats.mtime || !stats.mtime.getTime()) return;
|
|
if (typeof stats.mtime.getTime() !== "number") return;
|
|
if (this.timeCache[filename] == stats.mtime.getTime()) return;
|
|
this.timeCache[filename] = stats.mtime.getTime();
|
|
if (eventType == "rename") {
|
|
if (isPlugin) await pluginModule.loadPlugin(filename);
|
|
else await themeModule.loadTheme(filename);
|
|
}
|
|
if (eventType == "change") {
|
|
if (isPlugin) await pluginModule.reloadPlugin(filename);
|
|
else await themeModule.reloadTheme(filename);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
unwatchContent(contentType) {
|
|
if (!this.watchers[contentType]) return;
|
|
this.watchers[contentType].close();
|
|
delete this.watchers[contentType];
|
|
}
|
|
|
|
extractMeta(content) {
|
|
const firstLine = content.split("\n")[0];
|
|
const hasOldMeta = firstLine.includes("//META");
|
|
if (hasOldMeta) return this.parseOldMeta(content);
|
|
const hasNewMeta = firstLine.includes("/**");
|
|
if (hasNewMeta) return this.parseNewMeta(content);
|
|
throw new MetaError("META was not found.");
|
|
}
|
|
|
|
parseOldMeta(content) {
|
|
const meta = content.split("\n")[0];
|
|
const rawMeta = meta.substring(
|
|
meta.lastIndexOf("//META") + 6,
|
|
meta.lastIndexOf("*//")
|
|
);
|
|
if (meta.indexOf("META") < 0) throw new MetaError("META was not found.");
|
|
const parsed = Utils.testJSON(rawMeta);
|
|
if (!parsed) throw new MetaError("META could not be parsed.");
|
|
if (!parsed.name) throw new MetaError("META missing name data.");
|
|
parsed.format = "json";
|
|
return parsed;
|
|
}
|
|
|
|
parseNewMeta(content) {
|
|
const block = content.split("/**", 2)[1].split("*/", 1)[0];
|
|
const out = {};
|
|
let field = "";
|
|
let accum = "";
|
|
for (const line of block.split(splitRegex)) {
|
|
if (line.length === 0) continue;
|
|
if (line.charAt(0) === "@" && line.charAt(1) !== " ") {
|
|
out[field] = accum;
|
|
const l = line.indexOf(" ");
|
|
field = line.substr(1, l - 1);
|
|
accum = line.substr(l + 1);
|
|
} else {
|
|
accum += " " + line.replace("\\n", "\n").replace(escapedAtRegex, "@");
|
|
}
|
|
}
|
|
out[field] = accum.trim();
|
|
delete out[""];
|
|
out.format = "jsdoc";
|
|
return out;
|
|
}
|
|
|
|
getContentRequire(type) {
|
|
const isPlugin = type === "plugin";
|
|
const self = this;
|
|
const originalRequire = isPlugin ? originalJSRequire : originalCSSRequire;
|
|
return function (module, filename) {
|
|
const baseFolder = isPlugin ? self.pluginsFolder : self.themesFolder;
|
|
const possiblePath = path.resolve(baseFolder, path.basename(filename));
|
|
if (
|
|
!fs.existsSync(possiblePath) ||
|
|
filename !== fs.realpathSync(possiblePath)
|
|
)
|
|
return Reflect.apply(originalRequire, this, arguments);
|
|
let content = fs.readFileSync(filename, "utf8");
|
|
content = Utils.stripBOM(content);
|
|
|
|
const stats = fs.statSync(filename);
|
|
const meta = self.extractMeta(content);
|
|
meta.filename = path.basename(filename);
|
|
meta.added = stats.atimeMs;
|
|
meta.modified = stats.mtimeMs;
|
|
meta.size = stats.size;
|
|
if (!isPlugin) {
|
|
meta.css = content;
|
|
if (meta.format == "json")
|
|
meta.css = meta.css.split("\n").slice(1).join("\n");
|
|
content = `module.exports = ${JSON.stringify(meta)};`;
|
|
}
|
|
if (isPlugin) {
|
|
module._compile(content, module.filename);
|
|
const didExport = !Utils.isEmpty(module.exports);
|
|
if (didExport) {
|
|
meta.type = module.exports;
|
|
module.exports = meta;
|
|
content = "";
|
|
} else {
|
|
Utils.warn(
|
|
"Module Not Exported",
|
|
`${meta.name}, please start setting module.exports`
|
|
);
|
|
content += `\nmodule.exports = ${JSON.stringify(
|
|
meta
|
|
)};\nmodule.exports.type = ${meta.exports || meta.name};`;
|
|
}
|
|
}
|
|
module._compile(content, filename);
|
|
};
|
|
}
|
|
|
|
makePlaceholderPlugin(data) {
|
|
return {
|
|
plugin: {
|
|
start: () => {},
|
|
getName: () => {
|
|
return data.name || data.filename;
|
|
},
|
|
getAuthor: () => {
|
|
return "???";
|
|
},
|
|
getDescription: () => {
|
|
return data.message
|
|
? data.message
|
|
: "This plugin was unable to be loaded. Check the author's page for updates.";
|
|
},
|
|
getVersion: () => {
|
|
return "???";
|
|
},
|
|
},
|
|
name: data.name || data.filename,
|
|
filename: data.filename,
|
|
source: data.source ? data.source : "",
|
|
website: data.website ? data.website : "",
|
|
};
|
|
}
|
|
|
|
async loadContent(filename, type) {
|
|
if (typeof filename === "undefined" || typeof type === "undefined") return;
|
|
const isPlugin = type === "plugin";
|
|
const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;
|
|
|
|
if (settingsCookie["fork-ps-6"]) {
|
|
let result = await new Promise((resolve) => {
|
|
processFile(
|
|
path.resolve(baseFolder, filename),
|
|
(result) => {
|
|
console.log(result);
|
|
resolve(result);
|
|
},
|
|
(hash) => {
|
|
resolve({
|
|
suspect: false,
|
|
hash: hash,
|
|
filename: filename,
|
|
name: filename,
|
|
});
|
|
},
|
|
true
|
|
);
|
|
});
|
|
if (result) {
|
|
addonCache[result.hash] = {
|
|
timestamp: Date.now(),
|
|
hash: result.hash,
|
|
result: result,
|
|
};
|
|
this.saveAddonCache();
|
|
if (result.suspect) {
|
|
return {
|
|
name: filename,
|
|
file: filename,
|
|
message: "This plugin might be dangerous (" + result.harm + ").",
|
|
error: new Error(
|
|
"This plugin might be dangerous (" + result.harm + ")."
|
|
),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
__non_webpack_require__(path.resolve(baseFolder, filename));
|
|
} catch (error) {
|
|
return {
|
|
name: filename,
|
|
file: filename,
|
|
message: "Could not be compiled.",
|
|
error: { message: error.message, stack: error.stack },
|
|
};
|
|
}
|
|
const content = __non_webpack_require__(path.resolve(baseFolder, filename));
|
|
if (!content.name)
|
|
return {
|
|
name: filename,
|
|
file: filename,
|
|
message: "Cannot escape the ID.",
|
|
error: new Error("Cannot read property 'replace' of undefined"),
|
|
};
|
|
content.id = Utils.escapeID(content.name);
|
|
//if(!id)return {name: filename, file: filename, message: "Invalid ID", error: new Error("Please fix the name of "+filename+". BetterDiscord can't escape an ID.")}
|
|
if (isPlugin) {
|
|
if (!content.type) return;
|
|
try {
|
|
content.plugin = new content.type();
|
|
delete bdplugins[content.plugin.getName()];
|
|
bdplugins[content.plugin.getName()] = content;
|
|
} catch (error) {
|
|
return {
|
|
name: filename,
|
|
file: filename,
|
|
message: "Could not be constructed.",
|
|
error: { message: error.message, stack: error.stack },
|
|
};
|
|
}
|
|
} else {
|
|
delete bdthemes[content.name];
|
|
bdthemes[content.name] = content;
|
|
}
|
|
}
|
|
|
|
unloadContent(filename, type) {
|
|
if (typeof filename === "undefined" || typeof type === "undefined") return;
|
|
const isPlugin = type === "plugin";
|
|
const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;
|
|
try {
|
|
delete __non_webpack_require__.cache[
|
|
__non_webpack_require__.resolve(path.resolve(baseFolder, filename))
|
|
];
|
|
} catch (err) {
|
|
return {
|
|
name: filename,
|
|
file: filename,
|
|
message: "Could not be unloaded.",
|
|
error: { message: err.message, stack: err.stack },
|
|
};
|
|
}
|
|
}
|
|
|
|
isLoaded(filename, type) {
|
|
const isPlugin = type === "plugin";
|
|
const baseFolder = isPlugin ? this.pluginsFolder : this.themesFolder;
|
|
try {
|
|
__non_webpack_require__.cache[
|
|
__non_webpack_require__.resolve(path.resolve(baseFolder, filename))
|
|
];
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async reloadContent(filename, type) {
|
|
const cantUnload = this.unloadContent(filename, type);
|
|
if (cantUnload) return cantUnload;
|
|
return await this.loadContent(filename, type);
|
|
}
|
|
|
|
loadNewContent(type) {
|
|
const isPlugin = type === "plugin";
|
|
const fileEnding = isPlugin ? ".plugin.js" : ".theme.css";
|
|
const basedir = isPlugin ? this.pluginsFolder : this.themesFolder;
|
|
const files = fs.readdirSync(basedir);
|
|
const contentList = Object.values(isPlugin ? bdplugins : bdthemes);
|
|
const removed = contentList
|
|
.filter((t) => !files.includes(t.filename))
|
|
.map((c) => (isPlugin ? c.plugin.getName() : c.name));
|
|
const added = files.filter(
|
|
(f) =>
|
|
!contentList.find((t) => t.filename == f) &&
|
|
f.endsWith(fileEnding) &&
|
|
fs.statSync(path.resolve(basedir, f)).isFile()
|
|
);
|
|
return { added, removed };
|
|
}
|
|
|
|
async loadAllContent(type) {
|
|
this.patchExtensions();
|
|
const isPlugin = type === "plugin";
|
|
const fileEnding = isPlugin ? ".plugin.js" : ".theme.css";
|
|
const basedir = isPlugin ? this.pluginsFolder : this.themesFolder;
|
|
const errors = [];
|
|
const files = fs.readdirSync(basedir);
|
|
|
|
for (const filename of files) {
|
|
if (
|
|
!fs.statSync(path.resolve(basedir, filename)).isFile() ||
|
|
!filename.endsWith(fileEnding)
|
|
)
|
|
continue;
|
|
const error = await this.loadContent(filename, type);
|
|
if (error) errors.push(error);
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
loadPlugins() {
|
|
return this.loadAllContent("plugin");
|
|
}
|
|
loadThemes() {
|
|
return this.loadAllContent("theme");
|
|
}
|
|
})();
|
|
|
|
/**
|
|
* Don't expose contentManager - could be dangerous for now
|
|
*/
|