2017-02-18 23:46:59 +01:00
|
|
|
const kdbxweb = require('kdbxweb');
|
|
|
|
const Backbone = require('backbone');
|
2017-02-18 23:57:00 +01:00
|
|
|
const PluginApi = require('./plugin-api');
|
2017-04-27 12:02:41 +02:00
|
|
|
const ThemeVars = require('./theme-vars');
|
2017-02-18 23:46:59 +01:00
|
|
|
const Logger = require('../util/logger');
|
2017-02-19 10:29:18 +01:00
|
|
|
const SettingsManager = require('../comp/settings-manager');
|
2017-02-19 18:51:52 +01:00
|
|
|
const IoCache = require('../storage/io-cache');
|
2017-02-19 20:35:06 +01:00
|
|
|
const AppSettingsModel = require('../models/app-settings-model');
|
2017-02-21 22:05:18 +01:00
|
|
|
const BaseLocale = require('../locales/base.json');
|
2017-05-14 00:24:06 +02:00
|
|
|
const SignatureVerifier = require('../util/signature-verifier');
|
2017-05-23 20:03:29 +02:00
|
|
|
const SemVer = require('../util/semver');
|
|
|
|
const RuntimeInfo = require('../comp/runtime-info');
|
2017-02-18 23:46:59 +01:00
|
|
|
|
|
|
|
const commonLogger = new Logger('plugin');
|
2017-02-19 18:51:52 +01:00
|
|
|
const io = new IoCache({
|
|
|
|
cacheName: 'PluginFiles',
|
|
|
|
logger: new Logger('storage-plugin-files')
|
|
|
|
});
|
2017-02-18 23:46:59 +01:00
|
|
|
|
2017-04-26 22:06:07 +02:00
|
|
|
const PluginStatus = {
|
|
|
|
STATUS_NONE: '',
|
2017-04-08 17:34:27 +02:00
|
|
|
STATUS_ACTIVE: 'active',
|
|
|
|
STATUS_INACTIVE: 'inactive',
|
|
|
|
STATUS_INSTALLING: 'installing',
|
2017-04-26 22:06:07 +02:00
|
|
|
STATUS_ACTIVATING: 'activating',
|
2017-04-08 17:34:27 +02:00
|
|
|
STATUS_UNINSTALLING: 'uninstalling',
|
2017-04-08 23:35:26 +02:00
|
|
|
STATUS_UPDATING: 'updating',
|
2017-04-08 17:34:27 +02:00
|
|
|
STATUS_INVALID: 'invalid',
|
2017-04-26 22:07:48 +02:00
|
|
|
STATUS_ERROR: 'error'
|
2017-04-26 22:06:07 +02:00
|
|
|
};
|
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
const Plugin = Backbone.Model.extend(
|
|
|
|
_.extend({}, PluginStatus, {
|
|
|
|
idAttribute: 'name',
|
|
|
|
|
|
|
|
defaults: {
|
|
|
|
name: '',
|
|
|
|
manifest: '',
|
|
|
|
url: '',
|
|
|
|
status: '',
|
|
|
|
autoUpdate: false,
|
|
|
|
installTime: null,
|
|
|
|
installError: null,
|
|
|
|
updateCheckDate: null,
|
|
|
|
updateError: null,
|
|
|
|
skipSignatureValidation: false
|
|
|
|
},
|
|
|
|
|
|
|
|
resources: {},
|
|
|
|
module: null,
|
|
|
|
|
|
|
|
initialize(options) {
|
|
|
|
const name = options.manifest.name;
|
|
|
|
this.set({ name });
|
|
|
|
this.logger = new Logger(`plugin:${name}`);
|
|
|
|
},
|
|
|
|
|
|
|
|
install(activate, local) {
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
this.set('status', this.STATUS_INSTALLING);
|
|
|
|
return Promise.resolve().then(() => {
|
|
|
|
const error = this.validateManifest();
|
|
|
|
if (error) {
|
|
|
|
this.logger.error('Manifest validation error', error);
|
|
|
|
this.set('status', this.STATUS_INVALID);
|
|
|
|
throw 'Plugin validation error: ' + error;
|
|
|
|
}
|
|
|
|
this.set('status', this.STATUS_INACTIVE);
|
|
|
|
if (!activate) {
|
|
|
|
this.logger.info('Loaded inactive plugin');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return this.installWithManifest(local)
|
|
|
|
.then(() => this.set('installTime', this.logger.ts() - ts))
|
|
|
|
.catch(err => {
|
|
|
|
this.logger.error('Error installing plugin', err);
|
|
|
|
this.set({
|
|
|
|
status: this.STATUS_ERROR,
|
|
|
|
installError: err,
|
|
|
|
installTime: this.logger.ts() - ts,
|
|
|
|
updateError: null
|
|
|
|
});
|
|
|
|
throw err;
|
2017-04-08 17:34:27 +02:00
|
|
|
});
|
2019-08-16 23:05:39 +02:00
|
|
|
});
|
|
|
|
},
|
2017-02-18 23:46:59 +01:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
validateManifest() {
|
|
|
|
const manifest = this.get('manifest');
|
|
|
|
if (!manifest.name) {
|
|
|
|
return 'No plugin name';
|
2017-05-23 20:03:29 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (!manifest.description) {
|
|
|
|
return 'No plugin description';
|
2017-05-23 20:03:29 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (!/^\d+\.\d+\.\d+$/.test(manifest.version || '')) {
|
|
|
|
return 'Invalid plugin version';
|
2017-05-23 20:03:29 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (manifest.manifestVersion !== '0.1.0') {
|
|
|
|
return 'Invalid manifest version ' + manifest.manifestVersion;
|
2017-05-23 20:03:29 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (!manifest.author || !manifest.author.email || !manifest.author.name || !manifest.author.url) {
|
|
|
|
return 'Invalid plugin author';
|
|
|
|
}
|
|
|
|
if (!manifest.url) {
|
|
|
|
return 'No plugin url';
|
|
|
|
}
|
|
|
|
if (!manifest.publicKey) {
|
|
|
|
return 'No plugin public key';
|
|
|
|
}
|
|
|
|
if (!this.get('skipSignatureValidation') && manifest.publicKey !== SignatureVerifier.getPublicKey()) {
|
|
|
|
return 'Public key mismatch';
|
|
|
|
}
|
|
|
|
if (!manifest.resources || !Object.keys(manifest.resources).length) {
|
|
|
|
return 'No plugin resources';
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
manifest.resources.loc &&
|
|
|
|
(!manifest.locale || !manifest.locale.title || !/^[a-z]{2}(-[A-Z]{2})?$/.test(manifest.locale.name))
|
|
|
|
) {
|
|
|
|
return 'Bad plugin locale';
|
|
|
|
}
|
|
|
|
if (manifest.desktop && !RuntimeInfo.launcher) {
|
|
|
|
return 'Desktop plugin';
|
|
|
|
}
|
|
|
|
if (manifest.versionMin) {
|
|
|
|
if (!/^\d+\.\d+\.\d+$/.test(manifest.versionMin)) {
|
|
|
|
return 'Invalid versionMin';
|
2017-02-19 18:51:52 +01:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (SemVer.compareVersions(manifest.versionMin, RuntimeInfo.version) > 0) {
|
|
|
|
return `Required min app version is ${manifest.versionMin}, actual ${RuntimeInfo.version}`;
|
2017-02-19 18:51:52 +01:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
}
|
|
|
|
if (manifest.versionMax) {
|
|
|
|
if (!/^\d+\.\d+\.\d+$/.test(manifest.versionMax)) {
|
|
|
|
return 'Invalid versionMin';
|
|
|
|
}
|
|
|
|
if (SemVer.compareVersions(manifest.versionMax, RuntimeInfo.version) < 0) {
|
|
|
|
return `Required max app version is ${manifest.versionMax}, actual ${RuntimeInfo.version}`;
|
2017-04-27 12:02:41 +02:00
|
|
|
}
|
2017-02-21 22:05:18 +01:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
},
|
2017-02-18 23:46:59 +01:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
validateUpdatedManifest(newManifest) {
|
|
|
|
const manifest = this.get('manifest');
|
|
|
|
if (manifest.name !== newManifest.name) {
|
|
|
|
return 'Plugin name mismatch';
|
2017-05-20 13:11:31 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (manifest.publicKey !== newManifest.publicKey) {
|
|
|
|
return 'Public key mismatch';
|
2017-04-27 12:02:41 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
installWithManifest(local) {
|
|
|
|
const manifest = this.get('manifest');
|
|
|
|
this.logger.info(
|
|
|
|
'Loading plugin with resources',
|
|
|
|
Object.keys(manifest.resources).join(', '),
|
|
|
|
local ? '(local)' : '(url)'
|
|
|
|
);
|
|
|
|
this.resources = {};
|
2017-02-18 23:46:59 +01:00
|
|
|
const ts = this.logger.ts();
|
2019-08-16 23:05:39 +02:00
|
|
|
const results = Object.keys(manifest.resources).map(res => this.loadResource(res, local));
|
|
|
|
return Promise.all(results)
|
|
|
|
.catch(() => {
|
|
|
|
throw 'Error loading plugin resources';
|
|
|
|
})
|
|
|
|
.then(() => this.installWithResources())
|
|
|
|
.then(() => (local ? undefined : this.saveResources()))
|
|
|
|
.then(() => {
|
|
|
|
this.logger.info('Install complete', this.logger.ts(ts));
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
getResourcePath(res) {
|
|
|
|
switch (res) {
|
|
|
|
case 'css':
|
|
|
|
return 'plugin.css';
|
|
|
|
case 'js':
|
|
|
|
return 'plugin.js';
|
|
|
|
case 'loc':
|
|
|
|
return this.get('manifest').locale.name + '.json';
|
|
|
|
default:
|
|
|
|
throw `Unknown resource ${res}`;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
getStorageResourcePath(res) {
|
|
|
|
return this.id + '_' + this.getResourcePath(res);
|
|
|
|
},
|
|
|
|
|
|
|
|
loadResource(type, local) {
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
let res;
|
|
|
|
if (local) {
|
|
|
|
res = new Promise((resolve, reject) => {
|
|
|
|
const storageKey = this.getStorageResourcePath(type);
|
|
|
|
io.load(storageKey, (err, data) => (err ? reject(err) : resolve(data)));
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
const url = this.get('url');
|
|
|
|
res = httpGet(url + this.getResourcePath(type), true);
|
|
|
|
}
|
|
|
|
return res.then(data => {
|
|
|
|
this.logger.debug('Resource data loaded', type, this.logger.ts(ts));
|
|
|
|
return this.verifyResource(data, type).then(data => {
|
|
|
|
this.resources[type] = data;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
verifyResource(data, type) {
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
const manifest = this.get('manifest');
|
|
|
|
const signature = manifest.resources[type];
|
|
|
|
return SignatureVerifier.verify(data, signature, manifest.publicKey)
|
|
|
|
.then(valid => {
|
|
|
|
if (valid) {
|
|
|
|
this.logger.debug('Resource signature validated', type, this.logger.ts(ts));
|
|
|
|
return data;
|
2017-02-18 23:46:59 +01:00
|
|
|
} else {
|
2019-08-16 23:05:39 +02:00
|
|
|
this.logger.error('Resource signature invalid', type);
|
|
|
|
throw `Signature invalid: ${type}`;
|
2017-02-18 23:46:59 +01:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
this.logger.error('Error validating resource signature', type);
|
|
|
|
throw `Error validating resource signature for ${type}`;
|
|
|
|
});
|
|
|
|
},
|
2017-02-18 23:46:59 +01:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
installWithResources() {
|
|
|
|
this.logger.info('Installing plugin resources');
|
|
|
|
const manifest = this.get('manifest');
|
|
|
|
const promises = [];
|
|
|
|
if (this.resources.css) {
|
|
|
|
promises.push(this.applyCss(manifest.name, this.resources.css, manifest.theme));
|
|
|
|
}
|
|
|
|
if (this.resources.js) {
|
|
|
|
promises.push(this.applyJs(manifest.name, this.resources.js));
|
|
|
|
}
|
|
|
|
if (this.resources.loc) {
|
|
|
|
promises.push(this.applyLoc(manifest.locale, this.resources.loc));
|
|
|
|
}
|
|
|
|
return Promise.all(promises)
|
|
|
|
.then(() => {
|
|
|
|
this.set('status', this.STATUS_ACTIVE);
|
|
|
|
})
|
|
|
|
.catch(e => {
|
|
|
|
this.logger.info('Install error', e);
|
|
|
|
this.set('status', this.STATUS_ERROR);
|
|
|
|
return this.disable().then(() => {
|
|
|
|
throw e;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2017-02-18 23:46:59 +01:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
saveResources() {
|
|
|
|
const resourceSavePromises = [];
|
|
|
|
for (const key of Object.keys(this.resources)) {
|
|
|
|
resourceSavePromises.push(this.saveResource(key, this.resources[key]));
|
|
|
|
}
|
|
|
|
return Promise.all(resourceSavePromises).catch(e => {
|
|
|
|
this.logger.debug('Error saving plugin resources', e);
|
|
|
|
return this.uninstall().then(() => {
|
|
|
|
throw 'Error saving plugin resources';
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2017-02-18 23:46:59 +01:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
saveResource(key, value) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const storageKey = this.getStorageResourcePath(key);
|
|
|
|
io.save(storageKey, value, e => {
|
|
|
|
if (e) {
|
|
|
|
reject(e);
|
|
|
|
} else {
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2017-02-21 22:05:18 +01:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
deleteResources() {
|
|
|
|
const resourceDeletePromises = [];
|
|
|
|
for (const key of Object.keys(this.resources)) {
|
|
|
|
resourceDeletePromises.push(this.deleteResource(key));
|
|
|
|
}
|
|
|
|
return Promise.all(resourceDeletePromises);
|
|
|
|
},
|
2017-02-21 22:05:18 +01:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
deleteResource(key) {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
const storageKey = this.getStorageResourcePath(key);
|
|
|
|
io.remove(storageKey, () => resolve());
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
applyCss(name, data, theme) {
|
|
|
|
return Promise.resolve().then(() => {
|
|
|
|
const text = kdbxweb.ByteUtils.bytesToString(data);
|
|
|
|
const id = 'plugin-css-' + name;
|
|
|
|
this.createElementInHead('style', id, 'text/css', text);
|
|
|
|
if (theme) {
|
|
|
|
const locKey = this.getThemeLocaleKey(theme.name);
|
|
|
|
SettingsManager.allThemes[theme.name] = locKey;
|
|
|
|
BaseLocale[locKey] = theme.title;
|
|
|
|
for (const styleSheet of Array.from(document.styleSheets)) {
|
|
|
|
if (styleSheet.ownerNode.id === id) {
|
|
|
|
this.processThemeStyleSheet(styleSheet, theme);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.logger.debug('Plugin style installed');
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
processThemeStyleSheet(styleSheet, theme) {
|
|
|
|
const themeSelector = '.th-' + theme.name;
|
|
|
|
const badSelectors = [];
|
|
|
|
for (const rule of Array.from(styleSheet.cssRules)) {
|
|
|
|
if (rule.selectorText && rule.selectorText.lastIndexOf(themeSelector, 0) !== 0) {
|
|
|
|
badSelectors.push(rule.selectorText);
|
|
|
|
}
|
|
|
|
if (rule.selectorText === themeSelector) {
|
|
|
|
this.addThemeVariables(rule);
|
2017-05-19 22:05:35 +02:00
|
|
|
}
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
if (badSelectors.length) {
|
|
|
|
this.logger.error('Themes must not add rules outside theme namespace. Bad selectors:', badSelectors);
|
|
|
|
throw 'Invalid theme';
|
2017-04-09 09:32:05 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
addThemeVariables(rule) {
|
|
|
|
ThemeVars.apply(rule.style);
|
|
|
|
},
|
|
|
|
|
|
|
|
applyJs(name, data) {
|
|
|
|
return Promise.resolve().then(() => {
|
|
|
|
let text = kdbxweb.ByteUtils.bytesToString(data);
|
|
|
|
this.module = { exports: {} };
|
|
|
|
const id = 'plugin-' + Date.now().toString() + Math.random().toString();
|
|
|
|
global[id] = {
|
|
|
|
require: PluginApi.require,
|
|
|
|
module: this.module
|
|
|
|
};
|
|
|
|
text = `(function(require, module){${text}})(window["${id}"].require,window["${id}"].module);`;
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
this.createElementInHead('script', 'plugin-js-' + name, 'text/javascript', text);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
setTimeout(() => {
|
|
|
|
delete global[id];
|
|
|
|
if (this.module.exports.uninstall) {
|
|
|
|
this.logger.debug('Plugin script installed', this.logger.ts(ts));
|
|
|
|
this.loadPluginSettings();
|
|
|
|
resolve();
|
|
|
|
} else {
|
|
|
|
reject('Plugin script installation failed');
|
|
|
|
}
|
|
|
|
}, 0);
|
|
|
|
});
|
2017-04-26 22:06:07 +02:00
|
|
|
});
|
2019-08-16 23:05:39 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
createElementInHead(tagName, id, type, text) {
|
|
|
|
let el = document.getElementById(id);
|
|
|
|
if (el) {
|
|
|
|
el.parentNode.removeChild(el);
|
2017-02-18 23:46:59 +01:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
el = document.createElement(tagName);
|
|
|
|
el.appendChild(document.createTextNode(text));
|
|
|
|
el.setAttribute('id', id);
|
|
|
|
el.setAttribute('type', type);
|
|
|
|
document.head.appendChild(el);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeElement(id) {
|
|
|
|
const el = document.getElementById(id);
|
|
|
|
if (el) {
|
|
|
|
el.parentNode.removeChild(el);
|
2017-02-18 23:46:59 +01:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
applyLoc(locale, data) {
|
|
|
|
return Promise.resolve().then(() => {
|
|
|
|
const text = kdbxweb.ByteUtils.bytesToString(data);
|
|
|
|
const localeData = JSON.parse(text);
|
|
|
|
SettingsManager.allLocales[locale.name] = locale.title;
|
|
|
|
SettingsManager.customLocales[locale.name] = localeData;
|
|
|
|
this.logger.debug('Plugin locale installed');
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
removeLoc(locale) {
|
|
|
|
delete SettingsManager.allLocales[locale.name];
|
|
|
|
delete SettingsManager.customLocales[locale.name];
|
|
|
|
if (SettingsManager.activeLocale === locale.name) {
|
|
|
|
AppSettingsModel.instance.set('locale', 'en');
|
2017-02-21 22:05:18 +01:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
},
|
2017-04-08 23:35:26 +02:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
getThemeLocaleKey(name) {
|
|
|
|
return `setGenThemeCustom_${name}`;
|
|
|
|
},
|
|
|
|
|
|
|
|
removeTheme(theme) {
|
|
|
|
delete SettingsManager.allThemes[theme.name];
|
|
|
|
if (AppSettingsModel.instance.get('theme') === theme.name) {
|
|
|
|
AppSettingsModel.instance.set('theme', 'fb');
|
2017-04-08 23:35:26 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
delete BaseLocale[this.getThemeLocaleKey(theme.name)];
|
|
|
|
},
|
|
|
|
|
|
|
|
loadPluginSettings() {
|
|
|
|
if (!this.module || !this.module.exports || !this.module.exports.setSettings) {
|
|
|
|
return;
|
2017-04-08 23:35:26 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
const ts = this.logger.ts();
|
|
|
|
const settingPrefix = this.getSettingPrefix();
|
|
|
|
let settings = null;
|
|
|
|
for (const key of Object.keys(AppSettingsModel.instance.attributes)) {
|
|
|
|
if (key.lastIndexOf(settingPrefix, 0) === 0) {
|
|
|
|
if (!settings) {
|
|
|
|
settings = {};
|
2017-04-08 23:35:26 +02:00
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
settings[key.replace(settingPrefix, '')] = AppSettingsModel.instance.attributes[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (settings) {
|
|
|
|
this.setSettings(settings);
|
|
|
|
}
|
|
|
|
this.logger.debug('Plugin settings loaded', this.logger.ts(ts));
|
|
|
|
},
|
|
|
|
|
|
|
|
uninstallPluginCode() {
|
|
|
|
if (
|
|
|
|
this.get('manifest').resources.js &&
|
|
|
|
this.module &&
|
|
|
|
this.module.exports &&
|
|
|
|
this.module.exports.uninstall
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
this.module.exports.uninstall();
|
|
|
|
} catch (e) {
|
|
|
|
this.logger.error('Plugin uninstall method returned an error', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2017-05-19 22:05:35 +02:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
uninstall() {
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
return this.disable().then(() => {
|
|
|
|
return this.deleteResources().then(() => {
|
|
|
|
this.set('status', '');
|
|
|
|
this.logger.info('Uninstall complete', this.logger.ts(ts));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2017-05-20 11:27:28 +02:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
disable() {
|
|
|
|
const manifest = this.get('manifest');
|
|
|
|
this.logger.info('Disabling plugin with resources', Object.keys(manifest.resources).join(', '));
|
|
|
|
this.set('status', this.STATUS_UNINSTALLING);
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
return Promise.resolve().then(() => {
|
|
|
|
if (manifest.resources.css) {
|
|
|
|
this.removeElement('plugin-css-' + this.get('name'));
|
|
|
|
}
|
|
|
|
if (manifest.resources.js) {
|
|
|
|
this.uninstallPluginCode();
|
|
|
|
this.removeElement('plugin-js-' + this.get('name'));
|
|
|
|
}
|
|
|
|
if (manifest.resources.loc) {
|
|
|
|
this.removeLoc(this.get('manifest').locale);
|
|
|
|
}
|
|
|
|
if (manifest.theme) {
|
|
|
|
this.removeTheme(manifest.theme);
|
|
|
|
}
|
|
|
|
this.set('status', this.STATUS_INACTIVE);
|
|
|
|
this.logger.info('Disable complete', this.logger.ts(ts));
|
|
|
|
});
|
|
|
|
},
|
2017-05-19 22:05:35 +02:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
update(newPlugin) {
|
|
|
|
const ts = this.logger.ts();
|
|
|
|
const prevStatus = this.get('status');
|
|
|
|
this.set('status', this.STATUS_UPDATING);
|
|
|
|
return Promise.resolve().then(() => {
|
|
|
|
const manifest = this.get('manifest');
|
|
|
|
const newManifest = newPlugin.get('manifest');
|
|
|
|
if (manifest.version === newManifest.version) {
|
|
|
|
this.set({ status: prevStatus, updateCheckDate: Date.now(), updateError: null });
|
|
|
|
this.logger.info(`v${manifest.version} is the latest plugin version`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.logger.info(`Updating plugin from v${manifest.version} to v${newManifest.version}`);
|
|
|
|
const error = newPlugin.validateManifest() || this.validateUpdatedManifest(newManifest);
|
|
|
|
if (error) {
|
|
|
|
this.logger.error('Manifest validation error', error);
|
|
|
|
this.set({ status: prevStatus, updateCheckDate: Date.now(), updateError: error });
|
|
|
|
throw 'Plugin validation error: ' + error;
|
|
|
|
}
|
|
|
|
this.uninstallPluginCode();
|
|
|
|
return newPlugin
|
|
|
|
.installWithManifest(false)
|
|
|
|
.then(() => {
|
|
|
|
this.module = newPlugin.module;
|
|
|
|
this.resources = newPlugin.resources;
|
|
|
|
this.set({
|
|
|
|
status: this.STATUS_ACTIVE,
|
|
|
|
manifest: newManifest,
|
|
|
|
installTime: this.logger.ts() - ts,
|
|
|
|
installError: null,
|
|
|
|
updateCheckDate: Date.now(),
|
|
|
|
updateError: null
|
|
|
|
});
|
|
|
|
this.logger.info('Update complete', this.logger.ts(ts));
|
|
|
|
})
|
|
|
|
.catch(err => {
|
|
|
|
this.logger.error('Error updating plugin', err);
|
|
|
|
if (prevStatus === this.STATUS_ACTIVE) {
|
|
|
|
this.logger.info('Activating previous version');
|
|
|
|
return this.installWithResources().then(() => {
|
|
|
|
this.set({ updateCheckDate: Date.now(), updateError: err });
|
|
|
|
throw err;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.set({ status: prevStatus, updateCheckDate: Date.now(), updateError: err });
|
|
|
|
throw err;
|
2017-05-19 22:05:35 +02:00
|
|
|
}
|
|
|
|
});
|
2019-08-16 23:05:39 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
setAutoUpdate(enabled) {
|
|
|
|
this.set('autoUpdate', !!enabled);
|
|
|
|
},
|
|
|
|
|
|
|
|
getSettingPrefix() {
|
|
|
|
return `plugin:${this.id}:`;
|
|
|
|
},
|
|
|
|
|
|
|
|
getSettings() {
|
|
|
|
if (
|
|
|
|
this.get('status') === PluginStatus.STATUS_ACTIVE &&
|
|
|
|
this.module &&
|
|
|
|
this.module.exports &&
|
|
|
|
this.module.exports.getSettings
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
const settings = this.module.exports.getSettings();
|
|
|
|
const settingsPrefix = this.getSettingPrefix();
|
|
|
|
if (settings instanceof Array) {
|
|
|
|
return settings.map(setting => {
|
|
|
|
setting = _.clone(setting);
|
|
|
|
const value = AppSettingsModel.instance.get(settingsPrefix + setting.name);
|
|
|
|
if (value !== undefined) {
|
|
|
|
setting.value = value;
|
|
|
|
}
|
|
|
|
return setting;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.logger.error('getSettings: expected Array, got ', typeof settings);
|
|
|
|
} catch (e) {
|
|
|
|
this.logger.error('getSettings error', e);
|
2017-05-19 22:05:35 +02:00
|
|
|
}
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
},
|
2017-05-19 22:05:35 +02:00
|
|
|
|
2019-08-16 23:05:39 +02:00
|
|
|
setSettings(settings) {
|
|
|
|
for (const key of Object.keys(settings)) {
|
|
|
|
const value = settings[key];
|
|
|
|
AppSettingsModel.instance.set(this.getSettingPrefix() + key, value);
|
|
|
|
}
|
|
|
|
if (this.module.exports.setSettings) {
|
|
|
|
try {
|
|
|
|
this.module.exports.setSettings(settings);
|
|
|
|
} catch (e) {
|
|
|
|
this.logger.error('setSettings error', e);
|
|
|
|
}
|
2017-05-19 22:05:35 +02:00
|
|
|
}
|
|
|
|
}
|
2019-08-16 23:05:39 +02:00
|
|
|
})
|
|
|
|
);
|
2017-04-26 22:06:07 +02:00
|
|
|
|
|
|
|
_.extend(Plugin, PluginStatus);
|
2017-02-18 23:46:59 +01:00
|
|
|
|
2017-05-13 22:36:07 +02:00
|
|
|
Plugin.loadFromUrl = function(url, expectedManifest) {
|
2017-02-18 23:46:59 +01:00
|
|
|
if (url[url.length - 1] !== '/') {
|
|
|
|
url += '/';
|
|
|
|
}
|
|
|
|
commonLogger.info('Installing plugin from url', url);
|
|
|
|
const manifestUrl = url + 'manifest.json';
|
|
|
|
return httpGet(manifestUrl)
|
|
|
|
.catch(e => {
|
|
|
|
commonLogger.error('Error loading plugin manifest', e);
|
|
|
|
throw 'Error loading plugin manifest';
|
|
|
|
})
|
|
|
|
.then(manifest => {
|
|
|
|
try {
|
|
|
|
manifest = JSON.parse(manifest);
|
|
|
|
} catch (e) {
|
|
|
|
commonLogger.error('Failed to parse manifest', manifest);
|
|
|
|
throw 'Failed to parse manifest';
|
|
|
|
}
|
|
|
|
commonLogger.debug('Loaded manifest', manifest);
|
2017-05-13 22:36:07 +02:00
|
|
|
if (expectedManifest) {
|
|
|
|
if (expectedManifest.name !== manifest.name) {
|
|
|
|
throw 'Bad plugin name';
|
|
|
|
}
|
|
|
|
if (expectedManifest.privateKey !== manifest.privateKey) {
|
|
|
|
throw 'Bad plugin private key';
|
|
|
|
}
|
|
|
|
}
|
2017-04-08 23:35:26 +02:00
|
|
|
return new Plugin({
|
2019-08-16 23:05:39 +02:00
|
|
|
manifest,
|
|
|
|
url
|
2017-04-08 23:35:26 +02:00
|
|
|
});
|
2017-02-18 23:46:59 +01:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
function httpGet(url, binary) {
|
2017-04-10 20:51:31 +02:00
|
|
|
url += '?ts=' + Date.now();
|
|
|
|
commonLogger.debug('GET', url);
|
2017-02-18 23:46:59 +01:00
|
|
|
const ts = commonLogger.ts();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
xhr.addEventListener('load', () => {
|
|
|
|
if (xhr.status === 200) {
|
|
|
|
commonLogger.debug('GET OK', url, commonLogger.ts(ts));
|
|
|
|
resolve(xhr.response);
|
|
|
|
} else {
|
|
|
|
commonLogger.debug('GET error', url, xhr.status);
|
|
|
|
reject(xhr.status ? `HTTP status ${xhr.status}` : 'network error');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
xhr.addEventListener('error', () => {
|
|
|
|
commonLogger.debug('GET error', url, xhr.status);
|
|
|
|
reject(xhr.status ? `HTTP status ${xhr.status}` : 'network error');
|
|
|
|
});
|
|
|
|
xhr.addEventListener('abort', () => {
|
|
|
|
commonLogger.debug('GET aborted', url);
|
|
|
|
reject('Network request timeout');
|
|
|
|
});
|
|
|
|
xhr.addEventListener('timeout', () => {
|
|
|
|
commonLogger.debug('GET timeout', url);
|
|
|
|
reject('Network request timeout');
|
|
|
|
});
|
|
|
|
if (binary) {
|
|
|
|
xhr.responseType = binary ? 'arraybuffer' : 'text';
|
|
|
|
}
|
|
|
|
xhr.open('GET', url);
|
|
|
|
xhr.send();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Plugin;
|