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-17 20:26:24 +02:00
import Setting from '../../models/Setting' ;
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-11-17 20:26:24 +02:00
const md5File = require ( 'md5-file/promise' ) ;
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-18 12:17:27 +02:00
public async loadPluginFromJsBundle ( baseDir : string , jsBundleString : string , pluginIdIfNotSpecified : 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 ) ;
2020-11-17 20:26:24 +02:00
return this . loadPlugin ( baseDir , r . manifestText , r . scriptText , pluginIdIfNotSpecified ) ;
}
public async loadPluginFromPackage ( baseDir : string , path : string ) : Promise < Plugin > {
baseDir = rtrimSlashes ( baseDir ) ;
const fname = filename ( path ) ;
const hash = await md5File ( path ) ;
const unpackDir = ` ${ Setting . value ( 'tempDir' ) } / ${ fname } ` ;
const manifestFilePath = ` ${ unpackDir } /manifest.json ` ;
let manifest : any = await this . loadManifestToObject ( manifestFilePath ) ;
if ( ! manifest || manifest . _package_hash !== hash ) {
await shim . fsDriver ( ) . remove ( unpackDir ) ;
await shim . fsDriver ( ) . mkdir ( unpackDir ) ;
await require ( 'tar' ) . extract ( {
strict : true ,
portable : true ,
file : path ,
cwd : unpackDir ,
} ) ;
manifest = await this . loadManifestToObject ( manifestFilePath ) ;
if ( ! manifest ) throw new Error ( ` Missing manifest file at: ${ manifestFilePath } ` ) ;
manifest . _package_hash = hash ;
await shim . fsDriver ( ) . writeFile ( manifestFilePath , JSON . stringify ( manifest ) , 'utf8' ) ;
}
return this . loadPluginFromPath ( unpackDir ) ;
}
// Loads the manifest as a simple object with no validation. Used only
// when unpacking a package.
private async loadManifestToObject ( path : string ) : Promise < any > {
try {
const manifestText = await shim . fsDriver ( ) . readFile ( path , 'utf8' ) ;
return JSON . parse ( manifestText ) ;
} catch ( error ) {
return null ;
}
2020-10-13 12:16:36 +02:00
}
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-11-17 20:26:24 +02:00
if ( path . toLowerCase ( ) . endsWith ( '.js' ) ) {
return this . loadPluginFromJsBundle ( dirname ( path ) , await fsDriver . readFile ( path ) , filename ( path ) ) ;
} else if ( path . toLowerCase ( ) . endsWith ( '.jpl' ) ) {
return this . loadPluginFromPackage ( dirname ( path ) , path ) ;
} else {
let distPath = path ;
if ( ! ( await fsDriver . exists ( ` ${ distPath } /manifest.json ` ) ) ) {
distPath = ` ${ path } /dist ` ;
}
2020-10-09 19:35:46 +02:00
2020-11-17 20:26:24 +02:00
this . logger ( ) . info ( ` PluginService: Loading plugin from ${ path } ` ) ;
2020-10-09 19:35:46 +02:00
2020-11-17 20:26:24 +02:00
const scriptText = await fsDriver . readFile ( ` ${ distPath } /index.js ` ) ;
const manifestText = await fsDriver . readFile ( ` ${ distPath } /manifest.json ` ) ;
const pluginId = makePluginId ( filename ( path ) ) ;
2020-10-09 19:35:46 +02:00
2020-11-17 20:26:24 +02:00
return this . loadPlugin ( distPath , manifestText , scriptText , pluginId ) ;
}
2020-10-13 12:16:36 +02:00
}
2020-11-17 20:26:24 +02:00
private async loadPlugin ( baseDir : string , manifestText : string , scriptText : string , pluginIdIfNotSpecified : 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 ) ;
2020-11-17 20:26:24 +02:00
const deprecationNotices = [ ] ;
2020-11-15 16:18:46 +02:00
if ( ! manifestObj . app_min_version ) {
manifestObj . app_min_version = '1.4' ;
2020-11-17 20:26:24 +02:00
deprecationNotices . push ( '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.' ) ;
}
if ( ! manifestObj . id ) {
manifestObj . id = pluginIdIfNotSpecified ;
deprecationNotices . push ( ` The manifest must contain an "id" key, which should be a globally unique ID for your plugin, such as "com.example.MyPlugin" or a UUID. It was automatically set to " ${ manifestObj . id } ", but please update your manifest.json file. ` ) ;
2020-11-15 16:18:46 +02:00
}
const manifest = manifestFromObject ( manifestObj ) ;
2020-11-17 20:26:24 +02:00
const pluginId = manifest . id ;
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 : { } ,
} ,
} ) ;
}
2020-11-17 20:26:24 +02:00
for ( const msg of deprecationNotices ) {
plugin . deprecationNotice ( '1.5' , msg ) ;
2020-11-15 16:18:46 +02:00
}
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
}