2014-07-04 14:43:03 -04:00
'use strict' ;
2016-03-09 14:53:32 +02:00
var fs = require ( 'fs' ) ;
var path = require ( 'path' ) ;
var async = require ( 'async' ) ;
var winston = require ( 'winston' ) ;
var semver = require ( 'semver' ) ;
var express = require ( 'express' ) ;
var nconf = require ( 'nconf' ) ;
var db = require ( './database' ) ;
var utils = require ( '../public/src/utils' ) ;
var hotswap = require ( './hotswap' ) ;
var file = require ( './file' ) ;
2016-11-23 09:50:49 -07:00
var languages = require ( './languages' ) ;
2016-03-09 14:53:32 +02:00
var app ;
var middleware ;
2013-12-02 16:19:30 -05:00
2016-10-13 11:43:39 +02:00
( function ( Plugins ) {
2014-12-26 18:54:20 -05:00
require ( './plugins/install' ) ( Plugins ) ;
require ( './plugins/load' ) ( Plugins ) ;
require ( './plugins/hooks' ) ( Plugins ) ;
2013-12-03 13:36:44 -05:00
Plugins . libraries = { } ;
Plugins . loadedHooks = { } ;
Plugins . staticDirs = { } ;
Plugins . cssFiles = [ ] ;
2014-02-25 14:13:09 -05:00
Plugins . lessFiles = [ ] ;
2014-03-02 13:28:09 -05:00
Plugins . clientScripts = [ ] ;
2016-01-18 15:17:21 -05:00
Plugins . acpScripts = [ ] ;
2014-10-03 17:29:42 -04:00
Plugins . libraryPaths = [ ] ;
2015-08-20 11:01:59 -04:00
Plugins . versionWarning = [ ] ;
2016-10-14 21:48:38 +03:00
Plugins . languageCodes = [ ] ;
2013-09-13 11:10:17 -04:00
2013-12-03 13:36:44 -05:00
Plugins . initialized = false ;
2013-07-28 01:16:21 -04:00
2016-10-13 11:43:39 +02:00
Plugins . requireLibrary = function ( pluginID , libraryPath ) {
2015-01-06 23:29:48 -05:00
Plugins . libraries [ pluginID ] = require ( libraryPath ) ;
2014-12-26 18:54:20 -05:00
Plugins . libraryPaths . push ( libraryPath ) ;
} ;
2016-10-13 11:43:39 +02:00
Plugins . init = function ( nbbApp , nbbMiddleware , callback ) {
callback = callback || function ( ) { } ;
2013-12-03 13:36:44 -05:00
if ( Plugins . initialized ) {
2015-04-27 20:26:02 -04:00
return callback ( ) ;
2013-12-03 13:36:44 -05:00
}
2016-11-17 11:51:38 -05:00
if ( nbbApp ) {
app = nbbApp ;
middleware = nbbMiddleware ;
hotswap . prepare ( nbbApp ) ;
}
2014-09-21 14:29:27 -04:00
2013-12-03 13:36:44 -05:00
if ( global . env === 'development' ) {
2014-11-26 13:28:10 -05:00
winston . verbose ( '[plugins] Initializing plugins system' ) ;
2013-12-03 13:36:44 -05:00
}
2016-10-13 11:43:39 +02:00
Plugins . reload ( function ( err ) {
2013-12-03 13:36:44 -05:00
if ( err ) {
2014-11-26 13:28:10 -05:00
winston . error ( '[plugins] NodeBB encountered a problem while loading plugins' , err . message ) ;
2015-04-27 20:26:02 -04:00
return callback ( err ) ;
2013-12-03 13:36:44 -05:00
}
2013-11-03 17:15:18 -05:00
2013-12-03 13:36:44 -05:00
if ( global . env === 'development' ) {
winston . info ( '[plugins] Plugins OK' ) ;
}
2014-07-21 13:25:25 -04:00
2013-12-03 13:36:44 -05:00
Plugins . initialized = true ;
2015-04-27 20:26:02 -04:00
callback ( ) ;
2013-12-03 13:36:44 -05:00
} ) ;
} ;
2016-10-13 11:43:39 +02:00
Plugins . reload = function ( callback ) {
2013-12-03 13:36:44 -05:00
// Resetting all local plugin data
2014-10-03 17:29:42 -04:00
Plugins . libraries = { } ;
2013-12-03 13:36:44 -05:00
Plugins . loadedHooks = { } ;
Plugins . staticDirs = { } ;
2015-08-20 11:01:59 -04:00
Plugins . versionWarning = [ ] ;
2013-12-03 13:36:44 -05:00
Plugins . cssFiles . length = 0 ;
2014-03-06 17:50:15 -05:00
Plugins . lessFiles . length = 0 ;
Plugins . clientScripts . length = 0 ;
2016-01-18 15:17:21 -05:00
Plugins . acpScripts . length = 0 ;
2014-10-03 17:29:42 -04:00
Plugins . libraryPaths . length = 0 ;
2013-12-03 13:36:44 -05:00
async . waterfall ( [
2016-10-13 11:43:39 +02:00
function ( next ) {
2016-05-16 15:23:21 -04:00
// Build language code list
2016-11-23 09:50:49 -07:00
languages . list ( function ( err , languages ) {
2016-08-16 19:46:59 +02:00
if ( err ) {
return next ( err ) ;
}
2016-11-23 09:50:49 -07:00
Plugins . languageCodes = languages . map ( function ( data ) {
return data . code ;
2016-05-16 15:23:21 -04:00
} ) ;
next ( ) ;
} ) ;
} ,
2016-11-19 14:24:37 -05:00
async . apply ( Plugins . getPluginPaths ) ,
2016-11-20 13:33:35 +03:00
function ( paths , next ) {
2016-11-19 14:24:37 -05:00
async . eachSeries ( paths , Plugins . loadPlugin , next ) ;
2013-12-03 13:36:44 -05:00
} ,
2016-10-13 11:43:39 +02:00
function ( next ) {
2015-08-20 11:01:59 -04:00
// If some plugins are incompatible, throw the warning here
2015-08-28 13:03:40 -04:00
if ( Plugins . versionWarning . length && nconf . get ( 'isPrimary' ) === 'true' ) {
2015-08-20 11:01:59 -04:00
process . stdout . write ( '\n' ) ;
winston . warn ( '[plugins/load] The following plugins may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing. In the event of an unresponsive NodeBB caused by this plugin, run `./nodebb reset -p PLUGINNAME` to disable it.' ) ;
2017-02-18 01:52:56 -07:00
for ( var x = 0 , numPlugins = Plugins . versionWarning . length ; x < numPlugins ; x += 1 ) {
2016-03-03 13:31:39 +02:00
process . stdout . write ( ' * ' . yellow + Plugins . versionWarning [ x ] + '\n' ) ;
2015-08-20 11:01:59 -04:00
}
process . stdout . write ( '\n' ) ;
}
2016-10-13 11:43:39 +02:00
Object . keys ( Plugins . loadedHooks ) . forEach ( function ( hook ) {
2013-12-03 13:36:44 -05:00
var hooks = Plugins . loadedHooks [ hook ] ;
2016-10-13 11:43:39 +02:00
hooks = hooks . sort ( function ( a , b ) {
2013-12-03 13:36:44 -05:00
return a . priority - b . priority ;
} ) ;
} ) ;
2013-11-03 17:15:18 -05:00
2013-12-03 13:36:44 -05:00
next ( ) ;
2017-02-17 19:31:21 -07:00
} ,
2013-12-03 13:36:44 -05:00
] , callback ) ;
} ;
2016-10-13 11:43:39 +02:00
Plugins . reloadRoutes = function ( callback ) {
callback = callback || function ( ) { } ;
2014-09-21 14:29:27 -04:00
var router = express . Router ( ) ;
2016-08-26 18:50:37 +03:00
2014-09-21 14:29:27 -04:00
router . hotswapId = 'plugins' ;
2016-10-13 11:43:39 +02:00
router . render = function ( ) {
2014-09-21 14:29:27 -04:00
app . render . apply ( app , arguments ) ;
} ;
2016-08-26 18:50:37 +03:00
var controllers = require ( './controllers' ) ;
2016-10-13 11:43:39 +02:00
Plugins . fireHook ( 'static:app.load' , { app : app , router : router , middleware : middleware , controllers : controllers } , function ( err ) {
2015-04-16 14:26:58 -04:00
if ( err ) {
return winston . error ( '[plugins] Encountered error while executing post-router plugins hooks: ' + err . message ) ;
}
2014-09-21 14:29:27 -04:00
hotswap . replace ( 'plugins' , router ) ;
2014-11-26 13:28:10 -05:00
winston . verbose ( '[plugins] All plugins reloaded and rerouted' ) ;
2014-09-21 14:29:27 -04:00
callback ( ) ;
} ) ;
2014-08-24 12:30:49 -04:00
} ;
2016-10-13 11:43:39 +02:00
Plugins . getTemplates = function ( callback ) {
2017-02-17 20:20:42 -07:00
var templates = { } ;
var tplName ;
2016-01-22 11:12:21 -05:00
async . waterfall ( [
async . apply ( db . getSortedSetRange , 'plugins:active' , 0 , - 1 ) ,
2016-10-13 11:43:39 +02:00
function ( plugins , next ) {
2016-01-22 11:12:21 -05:00
var pluginBasePath = path . join ( _ _dirname , '../node_modules' ) ;
2016-10-13 11:43:39 +02:00
var paths = plugins . map ( function ( plugin ) {
2016-01-22 11:12:21 -05:00
return path . join ( pluginBasePath , plugin ) ;
} ) ;
// Filter out plugins with invalid paths
2016-10-13 11:43:39 +02:00
async . filter ( paths , file . exists , function ( paths ) {
2016-01-22 11:12:21 -05:00
next ( null , paths ) ;
} ) ;
} ,
2016-10-13 11:43:39 +02:00
function ( paths , next ) {
2016-01-22 11:12:21 -05:00
async . map ( paths , Plugins . loadPluginInfo , next ) ;
2017-02-17 19:31:21 -07:00
} ,
2016-10-13 11:43:39 +02:00
] , function ( err , plugins ) {
2016-01-22 11:12:21 -05:00
if ( err ) {
return callback ( err ) ;
}
2014-03-06 12:06:19 -05:00
2016-10-13 11:43:39 +02:00
async . eachSeries ( plugins , function ( plugin , next ) {
2016-01-22 11:12:21 -05:00
if ( plugin . templates || plugin . id . startsWith ( 'nodebb-theme-' ) ) {
winston . verbose ( '[plugins] Loading templates (' + plugin . id + ')' ) ;
2016-01-14 15:12:50 -05:00
var templatesPath = path . join ( _ _dirname , '../node_modules' , plugin . id , plugin . templates || 'templates' ) ;
2016-10-13 11:43:39 +02:00
utils . walk ( templatesPath , function ( err , pluginTemplates ) {
2014-09-26 13:19:23 -04:00
if ( pluginTemplates ) {
2016-10-13 11:43:39 +02:00
pluginTemplates . forEach ( function ( pluginTemplate ) {
2016-03-09 14:53:32 +02:00
if ( pluginTemplate . endsWith ( '.tpl' ) ) {
tplName = "/" + pluginTemplate . replace ( templatesPath , '' ) . substring ( 1 ) ;
2016-01-22 11:12:21 -05:00
2016-03-09 14:53:32 +02:00
if ( templates . hasOwnProperty ( tplName ) ) {
winston . verbose ( '[plugins] ' + tplName + ' replaced by ' + plugin . id ) ;
}
2016-01-22 11:12:21 -05:00
2016-03-09 14:53:32 +02:00
templates [ tplName ] = pluginTemplate ;
} else {
winston . warn ( '[plugins] Skipping ' + pluginTemplate + ' by plugin ' + plugin . id ) ;
}
2014-09-26 13:19:23 -04:00
} ) ;
} else {
2016-08-16 19:46:59 +02:00
if ( err ) {
winston . error ( err ) ;
} else {
winston . warn ( '[plugins/' + plugin . id + '] A templates directory was defined for this plugin, but was not found.' ) ;
}
2014-09-26 13:19:23 -04:00
}
2014-03-06 12:06:19 -05:00
2015-01-07 17:23:39 -05:00
next ( false ) ;
2014-03-06 12:06:19 -05:00
} ) ;
} else {
next ( false ) ;
}
2016-10-13 11:43:39 +02:00
} , function ( err ) {
2014-03-06 12:06:19 -05:00
callback ( err , templates ) ;
} ) ;
} ) ;
} ;
2013-08-22 10:47:24 -04:00
2016-10-13 11:43:39 +02:00
Plugins . get = function ( id , callback ) {
2015-07-08 17:04:21 -04:00
var url = ( nconf . get ( 'registry' ) || 'https://packages.nodebb.org' ) + '/api/v1/plugins/' + id ;
require ( 'request' ) ( url , {
2017-02-17 19:31:21 -07:00
json : true ,
2016-10-13 11:43:39 +02:00
} , function ( err , res , body ) {
2015-08-06 12:41:16 -04:00
if ( res . statusCode === 404 || ! body . payload ) {
return callback ( err , { } ) ;
}
2016-10-13 11:43:39 +02:00
Plugins . normalise ( [ body . payload ] , function ( err , normalised ) {
normalised = normalised . filter ( function ( plugin ) {
2015-08-06 12:41:16 -04:00
return plugin . id === id ;
2015-07-08 17:27:45 -04:00
} ) ;
2015-07-08 17:04:21 -04:00
return callback ( err , ! err ? normalised [ 0 ] : undefined ) ;
} ) ;
} ) ;
} ;
2016-10-13 11:43:39 +02:00
Plugins . list = function ( matching , callback ) {
2015-08-24 13:41:25 -04:00
if ( arguments . length === 1 && typeof matching === 'function' ) {
callback = matching ;
matching = true ;
}
2016-10-27 11:57:07 +03:00
var version = require ( path . join ( nconf . get ( 'base_dir' ) , 'package.json' ) ) . version ;
var url = ( nconf . get ( 'registry' ) || 'https://packages.nodebb.org' ) + '/api/v1/plugins' + ( matching !== false ? '?version=' + version : '' ) ;
2015-03-11 18:04:27 -04:00
2015-07-08 17:04:21 -04:00
require ( 'request' ) ( url , {
2017-02-17 19:31:21 -07:00
json : true ,
2016-10-13 11:43:39 +02:00
} , function ( err , res , body ) {
2015-07-08 17:04:21 -04:00
if ( err ) {
2014-04-22 21:02:58 -04:00
winston . error ( 'Error parsing plugins : ' + err . message ) ;
2016-09-16 14:20:07 +03:00
return callback ( err ) ;
2014-04-22 21:02:58 -04:00
}
2015-07-08 17:04:21 -04:00
Plugins . normalise ( body , callback ) ;
} ) ;
} ;
2016-10-13 11:43:39 +02:00
Plugins . normalise = function ( apiReturn , callback ) {
2015-12-21 10:37:33 +02:00
var pluginMap = { } ;
2016-10-27 11:57:07 +03:00
var dependencies = require ( path . join ( nconf . get ( 'base_dir' ) , 'package.json' ) ) . dependencies ;
2015-12-21 10:32:53 +02:00
apiReturn = apiReturn || [ ] ;
2017-02-18 01:52:56 -07:00
for ( var i = 0 ; i < apiReturn . length ; i += 1 ) {
2015-07-08 17:04:21 -04:00
apiReturn [ i ] . id = apiReturn [ i ] . name ;
apiReturn [ i ] . installed = false ;
apiReturn [ i ] . active = false ;
apiReturn [ i ] . url = apiReturn [ i ] . url ? apiReturn [ i ] . url : apiReturn [ i ] . repository ? apiReturn [ i ] . repository . url : '' ;
pluginMap [ apiReturn [ i ] . name ] = apiReturn [ i ] ;
}
2016-10-13 11:43:39 +02:00
Plugins . showInstalled ( function ( err , installedPlugins ) {
2015-07-08 17:04:21 -04:00
if ( err ) {
return callback ( err ) ;
2014-04-23 21:23:03 -04:00
}
2016-10-13 11:43:39 +02:00
installedPlugins = installedPlugins . filter ( function ( plugin ) {
2015-08-13 12:33:24 -04:00
return plugin && ! plugin . system ;
} ) ;
2016-10-13 11:43:39 +02:00
async . each ( installedPlugins , function ( plugin , next ) {
2015-07-08 17:04:21 -04:00
// If it errored out because a package.json or plugin.json couldn't be read, no need to do this stuff
if ( plugin . error ) {
pluginMap [ plugin . id ] = pluginMap [ plugin . id ] || { } ;
pluginMap [ plugin . id ] . installed = true ;
pluginMap [ plugin . id ] . error = true ;
return next ( ) ;
}
pluginMap [ plugin . id ] = pluginMap [ plugin . id ] || { } ;
pluginMap [ plugin . id ] . id = pluginMap [ plugin . id ] . id || plugin . id ;
pluginMap [ plugin . id ] . name = plugin . name || pluginMap [ plugin . id ] . name ;
pluginMap [ plugin . id ] . description = plugin . description ;
pluginMap [ plugin . id ] . url = pluginMap [ plugin . id ] . url || plugin . url ;
pluginMap [ plugin . id ] . installed = true ;
pluginMap [ plugin . id ] . isTheme = ! ! plugin . id . match ( 'nodebb-theme-' ) ;
pluginMap [ plugin . id ] . error = plugin . error || false ;
pluginMap [ plugin . id ] . active = plugin . active ;
pluginMap [ plugin . id ] . version = plugin . version ;
2016-12-15 13:03:14 +03:00
pluginMap [ plugin . id ] . settingsRoute = plugin . settingsRoute ;
2015-08-25 12:22:25 -04:00
// If package.json defines a version to use, stick to that
if ( dependencies . hasOwnProperty ( plugin . id ) && semver . valid ( dependencies [ plugin . id ] ) ) {
pluginMap [ plugin . id ] . latest = dependencies [ plugin . id ] ;
} else {
pluginMap [ plugin . id ] . latest = pluginMap [ plugin . id ] . latest || plugin . version ;
}
2015-07-08 17:04:21 -04:00
pluginMap [ plugin . id ] . outdated = semver . gt ( pluginMap [ plugin . id ] . latest , pluginMap [ plugin . id ] . version ) ;
next ( ) ;
2016-10-13 11:43:39 +02:00
} , function ( err ) {
2014-04-23 21:23:03 -04:00
if ( err ) {
return callback ( err ) ;
}
2014-04-22 21:02:58 -04:00
2015-07-08 17:04:21 -04:00
var pluginArray = [ ] ;
2014-12-03 16:17:12 -05:00
2015-07-08 17:04:21 -04:00
for ( var key in pluginMap ) {
if ( pluginMap . hasOwnProperty ( key ) ) {
pluginArray . push ( pluginMap [ key ] ) ;
2014-04-22 21:02:58 -04:00
}
2015-07-08 17:04:21 -04:00
}
2014-04-22 21:02:58 -04:00
2016-10-13 11:43:39 +02:00
pluginArray . sort ( function ( a , b ) {
2017-02-18 01:21:34 -07:00
if ( a . name > b . name ) {
2015-07-08 17:04:21 -04:00
return 1 ;
2017-02-18 01:21:34 -07:00
} else if ( a . name < b . name ) {
2015-07-08 17:04:21 -04:00
return - 1 ;
} else {
return 0 ;
2014-04-23 21:23:03 -04:00
}
} ) ;
2015-07-08 17:04:21 -04:00
callback ( null , pluginArray ) ;
2014-04-23 21:23:03 -04:00
} ) ;
2014-04-22 21:02:58 -04:00
} ) ;
} ;
2016-10-13 11:43:39 +02:00
Plugins . showInstalled = function ( callback ) {
2014-04-08 21:36:18 +01:00
var npmPluginPath = path . join ( _ _dirname , '../node_modules' ) ;
2013-12-03 13:36:44 -05:00
async . waterfall ( [
2014-04-08 21:36:18 +01:00
async . apply ( fs . readdir , npmPluginPath ) ,
2016-10-13 11:43:39 +02:00
function ( dirs , next ) {
2016-10-25 21:34:47 +02:00
dirs = dirs . filter ( function ( dir ) {
2015-03-11 18:04:27 -04:00
return dir . startsWith ( 'nodebb-plugin-' ) ||
dir . startsWith ( 'nodebb-widget-' ) ||
dir . startsWith ( 'nodebb-rewards-' ) ||
2015-02-23 15:55:35 -05:00
dir . startsWith ( 'nodebb-theme-' ) ;
2016-10-25 21:34:47 +02:00
} ) . map ( function ( dir ) {
2014-04-08 21:36:18 +01:00
return path . join ( npmPluginPath , dir ) ;
} ) ;
2016-10-25 21:34:47 +02:00
async . filter ( dirs , function ( dir , callback ) {
fs . stat ( dir , function ( err , stats ) {
2015-03-11 18:04:27 -04:00
callback ( ! err && stats . isDirectory ( ) ) ;
2014-04-23 21:23:03 -04:00
} ) ;
2016-10-25 21:34:47 +02:00
} , function ( plugins ) {
2014-04-08 21:36:18 +01:00
next ( null , plugins ) ;
2013-12-03 13:36:44 -05:00
} ) ;
} ,
2014-04-08 21:36:18 +01:00
2016-10-13 11:43:39 +02:00
function ( files , next ) {
2013-12-03 13:36:44 -05:00
var plugins = [ ] ;
2016-10-13 11:43:39 +02:00
async . each ( files , function ( file , next ) {
2013-12-03 13:36:44 -05:00
async . waterfall ( [
2016-10-13 11:43:39 +02:00
function ( next ) {
2015-01-06 23:29:48 -05:00
Plugins . loadPluginInfo ( file , next ) ;
2013-12-03 13:36:44 -05:00
} ,
2016-10-13 11:43:39 +02:00
function ( pluginData , next ) {
Plugins . isActive ( pluginData . name , function ( err , active ) {
2013-12-04 17:57:51 -05:00
if ( err ) {
2014-12-08 17:50:45 -05:00
return next ( new Error ( 'no-active-state' ) ) ;
2013-12-04 17:57:51 -05:00
}
2013-07-28 01:16:21 -04:00
2015-01-06 23:29:48 -05:00
delete pluginData . hooks ;
delete pluginData . library ;
pluginData . active = active ;
pluginData . installed = true ;
pluginData . error = false ;
next ( null , pluginData ) ;
2013-12-03 13:36:44 -05:00
} ) ;
2017-02-17 19:31:21 -07:00
} ,
2016-10-13 11:43:39 +02:00
] , function ( err , pluginData ) {
2014-07-04 14:43:03 -04:00
if ( err ) {
return next ( ) ; // Silently fail
}
2013-07-28 01:16:21 -04:00
2015-03-11 18:04:27 -04:00
plugins . push ( pluginData ) ;
2013-12-03 13:36:44 -05:00
next ( ) ;
} ) ;
2016-10-13 11:43:39 +02:00
} , function ( err ) {
2016-08-16 19:46:59 +02:00
next ( err , plugins ) ;
2013-12-03 13:36:44 -05:00
} ) ;
2017-02-17 19:31:21 -07:00
} ,
2014-12-26 18:54:20 -05:00
] , callback ) ;
2014-03-06 12:06:19 -05:00
} ;
2014-07-21 13:42:33 -04:00
2013-12-03 13:36:44 -05:00
} ( exports ) ) ;