import { Model } from 'framework/model'; import { RuntimeInfo } from 'const/runtime-info'; import { SettingsStore } from 'comp/settings/settings-store'; import { Plugin, PluginStatus } from 'plugins/plugin'; import { PluginCollection } from 'plugins/plugin-collection'; import { PluginGallery } from 'plugins/plugin-gallery'; import { SignatureVerifier } from 'util/data/signature-verifier'; import { Logger } from 'util/logger'; import { noop } from 'util/fn'; const logger = new Logger('plugin-mgr'); class PluginManager extends Model { static UpdateInterval = 1000 * 60 * 60 * 24 * 7; constructor() { super({ plugins: new PluginCollection() }); } init() { const ts = logger.ts(); return SettingsStore.load('plugins').then((state) => { if (!state) { return; } this.set({ autoUpdateAppVersion: state.autoUpdateAppVersion, autoUpdateDate: state.autoUpdateDate }); if (!state || !state.plugins || !state.plugins.length) { return; } return PluginGallery.getCachedGallery().then((gallery) => { const promises = state.plugins.map((plugin) => this.loadPlugin(plugin, gallery)); return Promise.all(promises).then((loadedPlugins) => { this.plugins.push(...loadedPlugins.filter((plugin) => plugin)); logger.info(`Loaded ${this.plugins.length} plugins`, logger.ts(ts)); }); }); }); } install(url, expectedManifest, skipSignatureValidation) { this.emit('change'); return Plugin.loadFromUrl(url, expectedManifest) .then((plugin) => { return this.uninstall(plugin.id).then(() => { if (skipSignatureValidation) { plugin.skipSignatureValidation = true; } return plugin.install(true, false).then(() => { this.plugins.push(plugin); this.emit('change'); this.saveState(); }); }); }) .catch((e) => { this.emit('change'); throw e; }); } installIfNew(url, expectedManifest, skipSignatureValidation) { const plugin = this.plugins.find((p) => p.url === url); if (plugin && plugin.status !== 'invalid') { return Promise.resolve(); } return this.install(url, expectedManifest, skipSignatureValidation); } uninstall(id) { const plugin = this.plugins.get(id); if (!plugin) { return Promise.resolve(); } this.emit('change'); return plugin.uninstall().then(() => { this.plugins.remove(id); this.emit('change'); this.saveState(); }); } disable(id) { const plugin = this.plugins.get(id); if (!plugin || plugin.status !== PluginStatus.STATUS_ACTIVE) { return Promise.resolve(); } this.emit('change'); return plugin.disable().then(() => { this.emit('change'); this.saveState(); }); } activate(id) { const plugin = this.plugins.get(id); if (!plugin || plugin.status === PluginStatus.STATUS_ACTIVE) { return Promise.resolve(); } this.emit('change'); return plugin.install(true, true).then(() => { this.emit('change'); this.saveState(); }); } update(id) { const oldPlugin = this.plugins.get(id); const validStatuses = [ PluginStatus.STATUS_ACTIVE, PluginStatus.STATUS_INACTIVE, PluginStatus.STATUS_NONE, PluginStatus.STATUS_ERROR, PluginStatus.STATUS_INVALID ]; if (!oldPlugin || validStatuses.indexOf(oldPlugin.status) < 0) { return Promise.reject(); } const url = oldPlugin.url; this.emit('change'); return Plugin.loadFromUrl(url) .then((newPlugin) => { return oldPlugin .update(newPlugin) .then(() => { this.emit('change'); this.saveState(); }) .catch((e) => { this.emit('change'); throw e; }); }) .catch((e) => { this.emit('change'); throw e; }); } setAutoUpdate(id, enabled) { const plugin = this.plugins.get(id); if (!plugin || plugin.autoUpdate === enabled) { return; } plugin.setAutoUpdate(enabled); this.emit('change'); this.saveState(); } runAutoUpdate() { const queue = this.plugins.filter((p) => p.autoUpdate).map((p) => p.id); if (!queue.length) { return Promise.resolve(); } const anotherVersion = this.autoUpdateAppVersion !== RuntimeInfo.version; const wasLongAgo = !this.autoUpdateDate || Date.now() - this.autoUpdateDate > PluginManager.UpdateInterval; const autoUpdateRequired = anotherVersion || wasLongAgo; if (!autoUpdateRequired) { return; } logger.info('Auto-updating plugins', queue.join(', ')); this.set({ autoUpdateAppVersion: RuntimeInfo.version, autoUpdateDate: Date.now() }); this.saveState(); const updateNext = () => { const pluginId = queue.shift(); if (pluginId) { return this.update(pluginId).catch(noop).then(updateNext); } }; return updateNext(); } loadPlugin(desc, gallery) { const plugin = new Plugin({ manifest: desc.manifest, url: desc.url, autoUpdate: desc.autoUpdate }); let enabled = desc.enabled; if (enabled) { const galleryPlugin = gallery ? gallery.plugins.find((pl) => pl.manifest.name === desc.manifest.name) : null; const expectedPublicKeys = galleryPlugin ? [galleryPlugin.manifest.publicKey] : SignatureVerifier.getPublicKeys(); enabled = expectedPublicKeys.includes(desc.manifest.publicKey); } return plugin .install(enabled, true) .then(() => plugin) .catch(() => plugin); } saveState() { SettingsStore.save('plugins', { autoUpdateAppVersion: this.autoUpdateAppVersion, autoUpdateDate: this.autoUpdateDate, plugins: this.plugins.map((plugin) => ({ manifest: plugin.manifest, url: plugin.url, enabled: plugin.status === 'active', autoUpdate: plugin.autoUpdate })) }); } getStatus(id) { const plugin = this.plugins.get(id); return plugin ? plugin.status : ''; } getPlugin(id) { return this.plugins.get(id); } } PluginManager.defineModelProperties({ plugins: null, autoUpdateAppVersion: null, autoUpdateDate: null }); const instance = new PluginManager(); export { instance as PluginManager };