2020-11-05 18:58:23 +02:00
import Plugin from './Plugin' ;
import manifestFromObject from './utils/manifestFromObject' ;
import Global from './api/Global' ;
import BasePluginRunner from './BasePluginRunner' ;
2020-10-09 19:35:46 +02:00
import BaseService from '../BaseService' ;
2020-11-05 18:58:23 +02:00
import shim from '../../shim' ;
2020-11-14 00:03:10 +02:00
import { rtrimSlashes } from '../../path-utils' ;
2020-11-15 16:18:46 +02:00
const compareVersions = require ( 'compare-versions' ) ;
2020-11-05 18:58:23 +02:00
const { filename , dirname } = require ( '../../path-utils' ) ;
2020-10-16 23:55:48 +02:00
const uslug = require ( 'uslug' ) ;
2020-10-09 19:35:46 +02:00
interface Plugins {
2020-11-12 21:29:22 +02:00
[ key : string ] : Plugin ;
2020-10-09 19:35:46 +02:00
}
2020-11-12 21:13:28 +02:00
function makePluginId ( source : string ) : string {
2020-10-09 19:35:46 +02:00
// https://www.npmjs.com/package/slug#options
2020-10-16 23:55:48 +02:00
return uslug ( source ) . substr ( 0 , 32 ) ;
2020-10-09 19:35:46 +02:00
}
export default class PluginService extends BaseService {
2020-11-12 21:13:28 +02:00
private static instance_ : PluginService = null ;
2020-10-09 19:35:46 +02:00
2020-11-12 21:13:28 +02:00
public static instance ( ) : PluginService {
2020-10-09 19:35:46 +02:00
if ( ! this . instance_ ) {
this . instance_ = new PluginService ( ) ;
}
return this . instance_ ;
}
2020-11-15 16:18:46 +02:00
private appVersion_ : string ;
2020-11-12 21:13:28 +02:00
private store_ : any = null ;
private platformImplementation_ : any = null ;
private plugins_ : Plugins = { } ;
private runner_ : BasePluginRunner = null ;
2020-10-09 19:35:46 +02:00
2020-11-15 16:18:46 +02:00
initialize ( appVersion : string , platformImplementation : any , runner : BasePluginRunner , store : any ) {
this . appVersion_ = appVersion ;
2020-10-09 19:35:46 +02:00
this . store_ = store ;
this . runner_ = runner ;
this . platformImplementation_ = platformImplementation ;
}
2020-11-12 21:13:28 +02:00
public get plugins ( ) : Plugins {
2020-10-09 19:35:46 +02:00
return this . plugins_ ;
}
2020-11-12 21:13:28 +02:00
public pluginById ( id : string ) : Plugin {
2020-10-09 19:35:46 +02:00
if ( ! this . plugins_ [ id ] ) throw new Error ( ` Plugin not found: ${ id } ` ) ;
return this . plugins_ [ id ] ;
}
2020-11-15 16:18:46 +02:00
// public allPluginIds(): string[] {
// return Object.keys(this.plugins_);
// }
2020-11-13 19:09:28 +02:00
2020-11-12 21:13:28 +02:00
private async parsePluginJsBundle ( jsBundleString : string ) {
2020-10-13 12:16:36 +02:00
const scriptText = jsBundleString ;
const lines = scriptText . split ( '\n' ) ;
2020-11-12 21:13:28 +02:00
const manifestText : string [ ] = [ ] ;
2020-10-13 12:16:36 +02:00
const StateStarted = 1 ;
const StateInManifest = 2 ;
2020-11-12 21:13:28 +02:00
let state : number = StateStarted ;
2020-10-13 12:16:36 +02:00
for ( let line of lines ) {
line = line . trim ( ) ;
if ( state !== StateInManifest ) {
if ( line === '/* joplin-manifest:' ) {
state = StateInManifest ;
}
continue ;
}
if ( state === StateInManifest ) {
if ( line . indexOf ( '*/' ) === 0 ) {
break ;
} else {
manifestText . push ( line ) ;
}
}
}
if ( ! manifestText . length ) throw new Error ( 'Could not find manifest' ) ;
return {
scriptText : scriptText ,
manifestText : manifestText.join ( '\n' ) ,
} ;
}
2020-11-12 21:13:28 +02:00
public async loadPluginFromString ( pluginId : string , baseDir : string , jsBundleString : string ) : Promise < Plugin > {
2020-11-14 00:03:10 +02:00
baseDir = rtrimSlashes ( baseDir ) ;
2020-10-13 12:16:36 +02:00
const r = await this . parsePluginJsBundle ( jsBundleString ) ;
return this . loadPlugin ( pluginId , baseDir , r . manifestText , r . scriptText ) ;
}
2020-11-12 21:13:28 +02:00
private async loadPluginFromPath ( path : string ) : Promise < Plugin > {
2020-11-14 00:03:10 +02:00
path = rtrimSlashes ( path ) ;
2020-10-09 19:35:46 +02:00
const fsDriver = shim . fsDriver ( ) ;
2020-10-13 12:16:36 +02:00
if ( path . toLowerCase ( ) . endsWith ( '.js' ) ) return this . loadPluginFromString ( filename ( path ) , dirname ( path ) , await fsDriver . readFile ( path ) ) ;
2020-10-09 19:35:46 +02:00
let distPath = path ;
if ( ! ( await fsDriver . exists ( ` ${ distPath } /manifest.json ` ) ) ) {
distPath = ` ${ path } /dist ` ;
}
this . logger ( ) . info ( ` PluginService: Loading plugin from ${ path } ` ) ;
2020-10-13 12:16:36 +02:00
const scriptText = await fsDriver . readFile ( ` ${ distPath } /index.js ` ) ;
const manifestText = await fsDriver . readFile ( ` ${ distPath } /manifest.json ` ) ;
2020-10-09 19:35:46 +02:00
const pluginId = makePluginId ( filename ( path ) ) ;
2020-10-13 12:16:36 +02:00
return this . loadPlugin ( pluginId , distPath , manifestText , scriptText ) ;
}
2020-11-12 21:13:28 +02:00
private async loadPlugin ( pluginId : string , baseDir : string , manifestText : string , scriptText : string ) : Promise < Plugin > {
2020-11-14 00:03:10 +02:00
baseDir = rtrimSlashes ( baseDir ) ;
2020-11-15 16:18:46 +02:00
const manifestObj = JSON . parse ( manifestText ) ;
let showAppMinVersionNotice = false ;
if ( ! manifestObj . app_min_version ) {
manifestObj . app_min_version = '1.4' ;
showAppMinVersionNotice = true ;
}
const manifest = manifestFromObject ( manifestObj ) ;
2020-10-13 12:16:36 +02:00
2020-10-09 19:35:46 +02:00
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if ( this . plugins_ [ pluginId ] ) throw new Error ( ` There is already a plugin with this ID: ${ pluginId } ` ) ;
2020-11-12 21:13:28 +02:00
const plugin = new Plugin ( pluginId , baseDir , manifest , scriptText , this . logger ( ) , ( action : any ) = > this . store_ . dispatch ( action ) ) ;
2020-10-09 19:35:46 +02:00
2020-11-15 16:18:46 +02:00
if ( compareVersions ( this . appVersion_ , manifest . app_min_version ) < 0 ) {
this . logger ( ) . info ( ` PluginService: Plugin " ${ pluginId } " was disabled because it requires a newer version of Joplin. ` , manifest ) ;
plugin . enabled = false ;
} else {
this . store_ . dispatch ( {
type : 'PLUGIN_ADD' ,
plugin : {
id : pluginId ,
views : { } ,
contentScripts : { } ,
} ,
} ) ;
}
if ( showAppMinVersionNotice ) {
plugin . deprecationNotice ( '1.5' , 'The manifest must contain an "app_min_version" key, which should be the minimum version of the app you support. It was automatically set to "1.4", but please update your manifest.json file.' ) ;
}
2020-10-09 19:35:46 +02:00
return plugin ;
}
2020-11-12 21:13:28 +02:00
public async loadAndRunPlugins ( pluginDirOrPaths : string | string [ ] ) {
2020-10-09 19:35:46 +02:00
let pluginPaths = [ ] ;
if ( Array . isArray ( pluginDirOrPaths ) ) {
pluginPaths = pluginDirOrPaths ;
} else {
pluginPaths = ( await shim . fsDriver ( ) . readDirStats ( pluginDirOrPaths ) )
2020-11-12 21:13:28 +02:00
. filter ( ( stat : any ) = > ( stat . isDirectory ( ) || stat . path . toLowerCase ( ) . endsWith ( '.js' ) ) )
. map ( ( stat : any ) = > ` ${ pluginDirOrPaths } / ${ stat . path } ` ) ;
2020-10-09 19:35:46 +02:00
}
for ( const pluginPath of pluginPaths ) {
if ( pluginPath . indexOf ( '_' ) === 0 ) {
this . logger ( ) . info ( ` PluginService: Plugin name starts with "_" and has not been loaded: ${ pluginPath } ` ) ;
continue ;
}
try {
2020-10-13 12:16:36 +02:00
const plugin = await this . loadPluginFromPath ( pluginPath ) ;
2020-10-09 19:35:46 +02:00
await this . runPlugin ( plugin ) ;
} catch ( error ) {
this . logger ( ) . error ( ` PluginService: Could not load plugin: ${ pluginPath } ` , error ) ;
}
}
}
2020-11-12 21:13:28 +02:00
public async runPlugin ( plugin : Plugin ) {
2020-10-09 19:35:46 +02:00
this . plugins_ [ plugin . id ] = plugin ;
const pluginApi = new Global ( this . logger ( ) , this . platformImplementation_ , plugin , this . store_ ) ;
return this . runner_ . run ( plugin , pluginApi ) ;
}
2020-11-13 19:09:28 +02:00
// public async handleDisabledPlugins() {
// const enabledPlugins = this.allPluginIds();
// const v = await this.kvStore_.value<string>('pluginService.lastEnabledPlugins');
// const lastEnabledPlugins = v ? JSON.parse(v) : [];
// const disabledPlugins = [];
// for (const id of lastEnabledPlugins) {
// if (!enabledPlugins.includes(id)) disabledPlugins.push(id);
// }
// await this.kvStore_.setValue('pluginService.lastEnabledPlugins', JSON.stringify(enabledPlugins));
// }
2020-10-09 19:35:46 +02:00
}