Store patched functions in a weakmap, move events wrapper and prevent plugins from modifying the plugin and content prototypes and their internals object

This commit is contained in:
Samuel Elliott 2018-03-08 23:43:26 +00:00
parent e7b0acb5a0
commit bc1f93dd89
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
7 changed files with 95 additions and 49 deletions

View File

@ -15,7 +15,17 @@ import Database from './database';
export default class Content { export default class Content {
constructor(internals) { constructor(internals) {
this.__internals = internals; Object.freeze(internals);
Utils.deepfreeze(internals.info);
Object.freeze(internals.paths);
Object.freeze(internals.configs);
Object.defineProperty(this, '__internals', {
value: internals,
configurable: false,
enumerable: false,
writable: false
});
this.settings.on('setting-updated', event => this.events.emit('setting-updated', event)); this.settings.on('setting-updated', event => this.events.emit('setting-updated', event));
this.settings.on('settings-updated', event => this.events.emit('settings-updated', event)); this.settings.on('settings-updated', event => this.events.emit('settings-updated', event));
@ -170,3 +180,5 @@ export default class Content {
} }
} }
Object.freeze(Content.prototype);

View File

@ -0,0 +1,45 @@
/**
* BetterDiscord Events Wrapper Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const eventemitters = new WeakMap();
export default class EventsWrapper {
constructor(eventemitter) {
eventemitters.set(this, eventemitter);
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
this.eventSubs.push({
event,
callback
});
eventemitters.get(this).on(event, callback);
}
unsubscribe(event, callback) {
for (let index of this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return;
eventemitters.get(this).off(event, this.eventSubs[index].callback);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
eventemitters.get(this).off(event.event, event.callback);
}
this.eventSubs.splice(0, this.eventSubs.length);
}
}

View File

@ -13,3 +13,4 @@ export { default as SocketProxy } from './socketproxy';
export { default as EventHook } from './eventhook'; export { default as EventHook } from './eventhook';
export { default as Permissions } from './permissionmanager'; export { default as Permissions } from './permissionmanager';
export { default as Database } from './database'; export { default as Database } from './database';
export { default as EventsWrapper } from './eventswrapper';

View File

@ -11,27 +11,27 @@
const PermissionMap = { const PermissionMap = {
IDENTIFY: { IDENTIFY: {
HEADER: 'Access your account information', HEADER: 'Access your account information',
BODY: 'Allows :NAME: to read your account information(excluding user token)' BODY: 'Allows :NAME: to read your account information (excluding user token).'
}, },
READ_MESSAGES: { READ_MESSAGES: {
HEADER: 'Read all messages', HEADER: 'Read all messages',
BODY: 'Allows :NAME: to read all messages accessible through your Discord account' BODY: 'Allows :NAME: to read all messages accessible through your Discord account.'
}, },
SEND_MESSAGES: { SEND_MESSAGES: {
HEADER: 'Send messages', HEADER: 'Send messages',
BODY: 'Allows :NAME: to send messages on your behalf' BODY: 'Allows :NAME: to send messages on your behalf.'
}, },
DELETE_MESSAGES: { DELETE_MESSAGES: {
HEADER: 'Delete messages', HEADER: 'Delete messages',
BODY: 'Allows :NAME: to delete messages on your behalf' BODY: 'Allows :NAME: to delete messages on your behalf.'
}, },
EDIT_MESSAGES: { EDIT_MESSAGES: {
HEADER: 'Edit messages', HEADER: 'Edit messages',
BODY: 'Allows :NAME: to edit messages on your behalf' BODY: 'Allows :NAME: to edit messages on your behalf.'
}, },
JOIN_SERVERS: { JOIN_SERVERS: {
HEADER: 'Join servers for you', HEADER: 'Join servers for you',
BODY: 'Allows :NAME: to join servers on your behalf' BODY: 'Allows :NAME: to join servers on your behalf.'
} }
} }

View File

@ -14,50 +14,20 @@ import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager'; import PluginManager from './pluginmanager';
import ThemeManager from './thememanager'; import ThemeManager from './thememanager';
import Events from './events'; import Events from './events';
import EventsWrapper from './eventswrapper';
import WebpackModules from './webpackmodules'; import WebpackModules from './webpackmodules';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs'; import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenuItems, Modals, DOM } from 'ui'; import { BdMenuItems, Modals, DOM } from 'ui';
import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue'; import SettingsModal from '../ui/components/bd/modals/SettingsModal.vue';
class EventsWrapper {
constructor(eventemitter) {
this.__eventemitter = eventemitter;
}
get eventSubs() {
return this._eventSubs || (this._eventSubs = []);
}
subscribe(event, callback) {
if (this.eventSubs.find(e => e.event === event && e.callback === callback)) return;
this.eventSubs.push({
event,
callback
});
this.__eventemitter.on(event, callback);
}
unsubscribe(event, callback) {
for (let index of this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) return;
this.__eventemitter.off(event, this.eventSubs[index].callback);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
this.__eventemitter.off(event.event, event.callback);
}
this._eventSubs = [];
}
}
export default class PluginApi { export default class PluginApi {
constructor(pluginInfo) { constructor(pluginInfo) {
this.pluginInfo = pluginInfo; this.pluginInfo = pluginInfo;
this.Events = new EventsWrapper(Events); this.Events = new EventsWrapper(Events);
this._menuItems = undefined;
this._injectedStyles = undefined;
this._modalStack = undefined;
} }
get plugin() { get plugin() {
@ -292,17 +262,20 @@ export default class PluginApi {
return this.addModal(Modals.settings(settingsset, headertext, options)); return this.addModal(Modals.settings(settingsset, headertext, options));
} }
get Modals() { get Modals() {
return Object.defineProperty(Object.defineProperty({ return Object.defineProperties({
add: this.addModal.bind(this), add: this.addModal.bind(this),
close: this.closeModal.bind(this), close: this.closeModal.bind(this),
closeAll: this.closeAllModals.bind(this), closeAll: this.closeAllModals.bind(this),
closeLast: this.closeLastModal.bind(this), closeLast: this.closeLastModal.bind(this),
basic: this.basicModal.bind(this), basic: this.basicModal.bind(this),
settings: this.settingsModal.bind(this) settings: this.settingsModal.bind(this)
}, 'stack', { }, {
get: () => this.modalStack stack: {
}), 'baseComponent', { get: () => this.modalStack
get: () => this.baseModalComponent },
baseComponent: {
get: () => this.baseModalComponent
}
}); });
} }
@ -396,3 +369,8 @@ export default class PluginApi {
} }
} }
// Stop plugins from modifying the plugin API for all plugins
// Plugins can still modify their own plugin API object
Object.freeze(PluginApi);
Object.freeze(PluginApi.prototype);

View File

@ -99,6 +99,9 @@ export default class extends ContentManager {
} }
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps); const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info), Vendor, deps);
if (!(plugin.prototype instanceof Plugin))
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
const instance = new plugin({ const instance = new plugin({
configs, info, main, configs, info, main,
paths: { paths: {

View File

@ -10,10 +10,15 @@
import { ClientLogger as Logger } from './logger'; import { ClientLogger as Logger } from './logger';
const patchedFunctions = new WeakMap();
export class PatchedFunction { export class PatchedFunction {
constructor(object, methodName, replaceOriginal = true) { constructor(object, methodName, replaceOriginal = true) {
if (object[methodName].__monkeyPatch) if (patchedFunctions.has(object[methodName])) {
return object[methodName].__monkeyPatch; const patchedFunction = patchedFunctions.get(object[methodName]);
if (replaceOriginal) patchedFunction.replaceOriginal();
return patchedFunction;
}
this.object = object; this.object = object;
this.methodName = methodName; this.methodName = methodName;
@ -25,7 +30,9 @@ export class PatchedFunction {
this.replace = function(...args) { this.replace = function(...args) {
patchedFunction.call(this, arguments); patchedFunction.call(this, arguments);
}; };
this.replace.__monkeyPatch = this;
patchedFunctions.set(object[methodName], this);
patchedFunctions.set(this.replace, this);
if (replaceOriginal) if (replaceOriginal)
this.replaceOriginal(); this.replaceOriginal();