mirror of https://github.com/keeweb/keeweb.git
loading saved plugins
This commit is contained in:
parent
664e7c50ae
commit
35d74033fd
|
@ -11,6 +11,7 @@ const Updater = require('./comp/updater');
|
|||
const AuthReceiver = require('./comp/auth-receiver');
|
||||
const ExportApi = require('./comp/export-api');
|
||||
const SettingsManager = require('./comp/settings-manager');
|
||||
const PluginManager = require('./plugins/plugin-manager');
|
||||
const KdbxwebInit = require('./util/kdbxweb-init');
|
||||
const Locale = require('./util/locale');
|
||||
|
||||
|
@ -19,23 +20,24 @@ $(() => {
|
|||
return AuthReceiver.receive();
|
||||
}
|
||||
loadMixins();
|
||||
initModules();
|
||||
|
||||
const appModel = new AppModel();
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
const configParam = getConfigParam();
|
||||
if (configParam) {
|
||||
appModel.loadConfig(configParam, err => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
if (err && !appModel.settings.get('cacheConfigSettings')) {
|
||||
showSettingsLoadError();
|
||||
} else {
|
||||
showApp();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showApp();
|
||||
}
|
||||
let appModel;
|
||||
initModules().then(() => {
|
||||
appModel = new AppModel();
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
const configParam = getConfigParam();
|
||||
if (configParam) {
|
||||
appModel.loadConfig(configParam, err => {
|
||||
SettingsManager.setBySettings(appModel.settings);
|
||||
if (err && !appModel.settings.get('cacheConfigSettings')) {
|
||||
showSettingsLoadError();
|
||||
} else {
|
||||
showApp();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showApp();
|
||||
}
|
||||
});
|
||||
|
||||
function isPopup() {
|
||||
return (window.parent !== window.top) || window.opener;
|
||||
|
@ -47,11 +49,14 @@ $(() => {
|
|||
}
|
||||
|
||||
function initModules() {
|
||||
const promises = [];
|
||||
KeyHandler.init();
|
||||
IdleTracker.init();
|
||||
PopupNotifier.init();
|
||||
KdbxwebInit.init();
|
||||
promises.push(PluginManager.init());
|
||||
window.kw = ExportApi;
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
function showSettingsLoadError() {
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
const Backbone = require('backbone');
|
||||
const Plugin = require('./plugin');
|
||||
const PluginCollection = require('./plugin-collection');
|
||||
const SettingsStore = require('../comp/settings-store');
|
||||
const Logger = require('../util/logger');
|
||||
|
||||
const PluginManager = Backbone.Model.extend({
|
||||
defaults: {
|
||||
|
@ -13,14 +15,33 @@ const PluginManager = Backbone.Model.extend({
|
|||
plugins: new PluginCollection()
|
||||
},
|
||||
|
||||
logger: new Logger('plugin-mgr'),
|
||||
|
||||
init() {
|
||||
const ts = this.logger.ts();
|
||||
return Promise.resolve().then(() => {
|
||||
const state = this.loadState();
|
||||
if (!state || !state.plugins) {
|
||||
return;
|
||||
}
|
||||
const promises = state.plugins.map(plugin => this.loadPlugin(plugin));
|
||||
return Promise.all(promises).then(loadedPlugins => {
|
||||
const plugins = this.get('plugins');
|
||||
plugins.add(loadedPlugins.filter(plugin => plugin));
|
||||
this.logger.info(`Loaded ${plugins.length} plugins`, this.logger.ts(ts));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
install(url) {
|
||||
const lastInstall = { url, dt: new Date() };
|
||||
this.set({ installing: url, lastInstall: lastInstall });
|
||||
return Plugin.load(url).then(plugin =>
|
||||
return Plugin.loadFromUrl(url).then(plugin =>
|
||||
this.uninstall(plugin.id).then(() => {
|
||||
return plugin.install().then(() => {
|
||||
this.get('plugins').push(plugin);
|
||||
this.set({ installing: null });
|
||||
this.saveState();
|
||||
});
|
||||
})
|
||||
).catch(e => {
|
||||
|
@ -40,6 +61,26 @@ const PluginManager = Backbone.Model.extend({
|
|||
plugins.remove(id);
|
||||
this.set('uninstalling', null);
|
||||
});
|
||||
},
|
||||
|
||||
loadPlugin(desc) {
|
||||
const plugin = new Plugin(desc.manifest, desc.url, true);
|
||||
return plugin.install()
|
||||
.then(() => plugin)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
|
||||
saveState() {
|
||||
SettingsStore.save('plugins', {
|
||||
plugins: this.get('plugins').map(plugin => ({
|
||||
manifest: plugin.get('manifest'),
|
||||
url: plugin.get('url')
|
||||
}))
|
||||
});
|
||||
},
|
||||
|
||||
loadState() {
|
||||
return SettingsStore.load('plugins');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -5,8 +5,13 @@ const Backbone = require('backbone');
|
|||
const PluginApi = require('./plugin-api');
|
||||
const Logger = require('../util/logger');
|
||||
const SettingsManager = require('../comp/settings-manager');
|
||||
const IoCache = require('../storage/io-cache');
|
||||
|
||||
const commonLogger = new Logger('plugin');
|
||||
const io = new IoCache({
|
||||
cacheName: 'PluginFiles',
|
||||
logger: new Logger('storage-plugin-files')
|
||||
});
|
||||
|
||||
const Plugin = Backbone.Model.extend({
|
||||
idAttribute: 'name',
|
||||
|
@ -15,21 +20,25 @@ const Plugin = Backbone.Model.extend({
|
|||
name: '',
|
||||
manifest: '',
|
||||
url: '',
|
||||
status: 'inactive'
|
||||
status: 'inactive',
|
||||
installTime: null
|
||||
},
|
||||
|
||||
resources: null,
|
||||
|
||||
initialize(manifest, url) {
|
||||
initialize(manifest, url, local) {
|
||||
const name = manifest.name;
|
||||
this.set({
|
||||
name: manifest.name,
|
||||
name,
|
||||
manifest,
|
||||
url
|
||||
url,
|
||||
local
|
||||
});
|
||||
this.logger = new Logger(`plugin:${manifest.name}`);
|
||||
this.logger = new Logger(`plugin:${name}`);
|
||||
},
|
||||
|
||||
install() {
|
||||
const ts = this.logger.ts();
|
||||
this.set('status', 'installing');
|
||||
return Promise.resolve().then(() => {
|
||||
const error = this.validateManifest();
|
||||
|
@ -38,7 +47,8 @@ const Plugin = Backbone.Model.extend({
|
|||
this.set('status', 'invalid');
|
||||
throw 'Plugin validation error: ' + error;
|
||||
}
|
||||
return this.installWithManifest();
|
||||
return this.installWithManifest()
|
||||
.then(this.set('installTime', this.logger.ts() - ts));
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -76,29 +86,46 @@ const Plugin = Backbone.Model.extend({
|
|||
|
||||
installWithManifest() {
|
||||
const manifest = this.get('manifest');
|
||||
const url = this.get('url');
|
||||
this.logger.info('Loading plugin with resources', Object.keys(manifest.resources).join(', '));
|
||||
const local = this.get('local');
|
||||
this.logger.info('Loading plugin with resources', Object.keys(manifest.resources).join(', '), local ? '(local)' : '(url)');
|
||||
this.resources = {};
|
||||
const ts = this.logger.ts();
|
||||
const results = [];
|
||||
if (manifest.resources.css) {
|
||||
results.push(this.loadResource('css', url + 'plugin.css'));
|
||||
}
|
||||
if (manifest.resources.js) {
|
||||
results.push(this.loadResource('js', url + 'plugin.js'));
|
||||
}
|
||||
if (manifest.resources.loc) {
|
||||
results.push(this.loadResource('loc', url + manifest.locale.name + '.json'));
|
||||
}
|
||||
const results = Object.keys(manifest.resources)
|
||||
.map(res => this.loadResource(res));
|
||||
return Promise.all(results)
|
||||
.catch(() => { throw 'Error loading plugin resources'; })
|
||||
.then(() => this.installWithResources())
|
||||
.then(() => {
|
||||
this.logger.info('Install complete', this.logger.ts(ts));
|
||||
});
|
||||
.then(() => local ? undefined : this.saveResources())
|
||||
.then(() => { this.logger.info('Install complete', this.logger.ts(ts)); });
|
||||
},
|
||||
|
||||
loadResource(type, url) {
|
||||
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}`;
|
||||
}
|
||||
},
|
||||
|
||||
loadResource(type) {
|
||||
let res;
|
||||
if (this.get('local')) {
|
||||
res = this.loadLocalResource(type);
|
||||
} else {
|
||||
const url = this.get('url');
|
||||
res = this.loadResourceFromUrl(type, url + this.getResourcePath(type));
|
||||
}
|
||||
return res.then(data => {
|
||||
this.resources[type] = data;
|
||||
});
|
||||
},
|
||||
|
||||
loadResourceFromUrl(type, url) {
|
||||
let ts = this.logger.ts();
|
||||
const manifest = this.get('manifest');
|
||||
return httpGet(url, true).then(data => {
|
||||
|
@ -116,7 +143,7 @@ const Plugin = Backbone.Model.extend({
|
|||
.then(valid => {
|
||||
if (valid) {
|
||||
this.logger.debug('Resource signature valid', type, this.logger.ts(ts));
|
||||
this.resources[type] = data;
|
||||
return data;
|
||||
} else {
|
||||
this.logger.error('Resource signature invalid', type);
|
||||
throw `Signature invalid: ${type}`;
|
||||
|
@ -125,6 +152,19 @@ const Plugin = Backbone.Model.extend({
|
|||
});
|
||||
},
|
||||
|
||||
loadLocalResource(type) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const storageKey = this.id + '/' + this.getResourcePath(type);
|
||||
io.load(storageKey, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
installWithResources() {
|
||||
this.logger.info('Installing loaded plugin');
|
||||
const manifest = this.get('manifest');
|
||||
|
@ -144,11 +184,50 @@ const Plugin = Backbone.Model.extend({
|
|||
})
|
||||
.catch(e => {
|
||||
this.logger.info('Install error', e);
|
||||
this.uninstall();
|
||||
throw e;
|
||||
return this.uninstall().then(() => { throw e; });
|
||||
});
|
||||
},
|
||||
|
||||
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'; });
|
||||
});
|
||||
},
|
||||
|
||||
saveResource(key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const storageKey = this.id + '/' + this.getResourcePath(key);
|
||||
io.save(storageKey, value, e => {
|
||||
if (e) {
|
||||
reject(e);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteResources() {
|
||||
const resourceDeletePromises = [];
|
||||
for (const key of Object.keys(this.resources)) {
|
||||
resourceDeletePromises.push(this.deleteResource(key));
|
||||
}
|
||||
return Promise.all(resourceDeletePromises);
|
||||
},
|
||||
|
||||
deleteResource(key) {
|
||||
return new Promise(resolve => {
|
||||
const storageKey = this.id + '/' + this.getResourcePath(key);
|
||||
io.remove(storageKey, () => resolve());
|
||||
});
|
||||
},
|
||||
|
||||
applyCss(name, data) {
|
||||
return Promise.resolve().then(() => {
|
||||
const text = kdbxweb.ByteUtils.bytesToString(data);
|
||||
|
@ -240,13 +319,15 @@ const Plugin = Backbone.Model.extend({
|
|||
if (manifest.resources.loc) {
|
||||
this.removeLoc(this.get('manifest').locale);
|
||||
}
|
||||
this.set('status', 'inactive');
|
||||
this.logger.info('Uninstall complete', this.logger.ts(ts));
|
||||
return this.deleteResources().then(() => {
|
||||
this.set('status', 'inactive');
|
||||
this.logger.info('Uninstall complete', this.logger.ts(ts));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Plugin.load = function(url) {
|
||||
Plugin.loadFromUrl = function(url) {
|
||||
if (url[url.length - 1] !== '/') {
|
||||
url += '/';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
'use strict';
|
||||
|
||||
const idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
|
||||
|
||||
const IoBrowserCache = function(config) {
|
||||
this.db = null;
|
||||
this.cacheName = config.cacheName;
|
||||
this.logger = config.logger;
|
||||
};
|
||||
|
||||
IoBrowserCache.enabled = !!idb;
|
||||
|
||||
_.extend(IoBrowserCache.prototype, {
|
||||
initDb(callback) {
|
||||
if (this.db) {
|
||||
return callback && callback();
|
||||
}
|
||||
try {
|
||||
const req = idb.open(this.cacheName);
|
||||
req.onerror = e => {
|
||||
this.logger.error('Error opening indexed db', e);
|
||||
if (callback) { callback(e); }
|
||||
};
|
||||
req.onsuccess = e => {
|
||||
this.db = e.target.result;
|
||||
if (callback) { callback(); }
|
||||
};
|
||||
req.onupgradeneeded = e => {
|
||||
const db = e.target.result;
|
||||
db.createObjectStore('files');
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error opening indexed db', e);
|
||||
if (callback) { callback(e); }
|
||||
}
|
||||
},
|
||||
|
||||
save(id, data, callback) {
|
||||
this.logger.debug('Save', id);
|
||||
this.initDb(err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Saved', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error saving to cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error saving to cache', id, e);
|
||||
if (callback) { callback(e); }
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
load(id, callback) {
|
||||
this.logger.debug('Load', id);
|
||||
this.initDb(err => {
|
||||
if (err) {
|
||||
return callback && callback(err, null);
|
||||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Loaded', id, this.logger.ts(ts));
|
||||
if (callback) { callback(null, req.result); }
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error loading from cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error loading from cache', id, e);
|
||||
if (callback) { callback(e, null); }
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
remove(id, callback) {
|
||||
this.logger.debug('Remove', id);
|
||||
this.initDb(err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Removed', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error removing from cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error removing from cache', id, e);
|
||||
if (callback) { callback(e); }
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = IoBrowserCache;
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const Launcher = require('../comp/launcher');
|
||||
|
||||
const IoCache = Launcher ? require('./io-browser-cache') : require('./io-browser-cache'); // TODO: use file cache
|
||||
|
||||
module.exports = IoCache;
|
|
@ -1,112 +1,33 @@
|
|||
'use strict';
|
||||
|
||||
const StorageBase = require('./storage-base');
|
||||
|
||||
const idb = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
|
||||
const IoBrowserCache = require('./io-browser-cache');
|
||||
|
||||
const StorageCache = StorageBase.extend({
|
||||
name: 'cache',
|
||||
enabled: !!idb,
|
||||
enabled: IoBrowserCache.enabled,
|
||||
system: true,
|
||||
|
||||
db: null,
|
||||
errorOpening: null,
|
||||
io: null,
|
||||
|
||||
initDb: function(callback) {
|
||||
if (this.db) {
|
||||
return callback && callback();
|
||||
}
|
||||
try {
|
||||
const req = idb.open('FilesCache');
|
||||
req.onerror = e => {
|
||||
this.logger.error('Error opening indexed db', e);
|
||||
this.errorOpening = e;
|
||||
if (callback) { callback(e); }
|
||||
};
|
||||
req.onsuccess = e => {
|
||||
this.db = e.target.result;
|
||||
if (callback) { callback(); }
|
||||
};
|
||||
req.onupgradeneeded = e => {
|
||||
const db = e.target.result;
|
||||
db.createObjectStore('files');
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error opening indexed db', e);
|
||||
if (callback) { callback(e); }
|
||||
}
|
||||
},
|
||||
|
||||
save: function(id, opts, data, callback) {
|
||||
this.logger.debug('Save', id);
|
||||
this.initDb(err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').put(data, id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Saved', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error saving to cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error saving to cache', id, e);
|
||||
if (callback) { callback(e); }
|
||||
}
|
||||
init() {
|
||||
StorageBase.prototype.init.call(this);
|
||||
this.io = new IoBrowserCache({
|
||||
cacheName: 'FilesCache',
|
||||
logger: this.logger
|
||||
});
|
||||
},
|
||||
|
||||
load: function(id, opts, callback) {
|
||||
this.logger.debug('Load', id);
|
||||
this.initDb(err => {
|
||||
if (err) {
|
||||
return callback && callback(err, null);
|
||||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readonly').objectStore('files').get(id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Loaded', id, this.logger.ts(ts));
|
||||
if (callback) { callback(null, req.result); }
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error loading from cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error loading from cache', id, e);
|
||||
if (callback) { callback(e, null); }
|
||||
}
|
||||
});
|
||||
save(id, opts, data, callback) {
|
||||
this.io.save(id, data, callback);
|
||||
},
|
||||
|
||||
remove: function(id, opts, callback) {
|
||||
this.logger.debug('Remove', id);
|
||||
this.initDb(err => {
|
||||
if (err) {
|
||||
return callback && callback(err);
|
||||
}
|
||||
try {
|
||||
const ts = this.logger.ts();
|
||||
const req = this.db.transaction(['files'], 'readwrite').objectStore('files').delete(id);
|
||||
req.onsuccess = () => {
|
||||
this.logger.debug('Removed', id, this.logger.ts(ts));
|
||||
if (callback) { callback(); }
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.logger.error('Error removing from cache', id, req.error);
|
||||
if (callback) { callback(req.error); }
|
||||
};
|
||||
} catch (e) {
|
||||
this.logger.error('Error removing from cache', id, e);
|
||||
if (callback) { callback(e); }
|
||||
}
|
||||
});
|
||||
load(id, opts, callback) {
|
||||
this.io.load(id, callback);
|
||||
},
|
||||
|
||||
remove(id, opts, callback) {
|
||||
this.io.remove(id, callback);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue