aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/plugins')
-rw-r--r--server/lib/plugins/hooks.ts2
-rw-r--r--server/lib/plugins/plugin-helpers.ts133
-rw-r--r--server/lib/plugins/plugin-index.ts8
-rw-r--r--server/lib/plugins/plugin-manager.ts307
-rw-r--r--server/lib/plugins/register-helpers-store.ts355
5 files changed, 625 insertions, 180 deletions
diff --git a/server/lib/plugins/hooks.ts b/server/lib/plugins/hooks.ts
index bcc8c674e..aa92f03cc 100644
--- a/server/lib/plugins/hooks.ts
+++ b/server/lib/plugins/hooks.ts
@@ -25,7 +25,7 @@ const Hooks = {
25 }, 25 },
26 26
27 runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => { 27 runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => {
28 PluginManager.Instance.runHook(hookName, params) 28 PluginManager.Instance.runHook(hookName, undefined, params)
29 .catch(err => logger.error('Fatal hook error.', { err })) 29 .catch(err => logger.error('Fatal hook error.', { err }))
30 } 30 }
31} 31}
diff --git a/server/lib/plugins/plugin-helpers.ts b/server/lib/plugins/plugin-helpers.ts
new file mode 100644
index 000000000..de82b4918
--- /dev/null
+++ b/server/lib/plugins/plugin-helpers.ts
@@ -0,0 +1,133 @@
1import { PeerTubeHelpers } from '@server/typings/plugins'
2import { sequelizeTypescript } from '@server/initializers/database'
3import { buildLogger } from '@server/helpers/logger'
4import { VideoModel } from '@server/models/video/video'
5import { WEBSERVER } from '@server/initializers/constants'
6import { ServerModel } from '@server/models/server/server'
7import { getServerActor } from '@server/models/application/application'
8import { addServerInBlocklist, removeServerFromBlocklist, addAccountInBlocklist, removeAccountFromBlocklist } from '../blocklist'
9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
10import { AccountModel } from '@server/models/account/account'
11import { VideoBlacklistCreate } from '@shared/models'
12import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
13import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
14import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
15
16function buildPluginHelpers (npmName: string): PeerTubeHelpers {
17 const logger = buildPluginLogger(npmName)
18
19 const database = buildDatabaseHelpers()
20 const videos = buildVideosHelpers()
21
22 const config = buildConfigHelpers()
23
24 const server = buildServerHelpers()
25
26 const moderation = buildModerationHelpers()
27
28 return {
29 logger,
30 database,
31 videos,
32 config,
33 moderation,
34 server
35 }
36}
37
38export {
39 buildPluginHelpers
40}
41
42// ---------------------------------------------------------------------------
43
44function buildPluginLogger (npmName: string) {
45 return buildLogger(npmName)
46}
47
48function buildDatabaseHelpers () {
49 return {
50 query: sequelizeTypescript.query.bind(sequelizeTypescript)
51 }
52}
53
54function buildServerHelpers () {
55 return {
56 getServerActor: () => getServerActor()
57 }
58}
59
60function buildVideosHelpers () {
61 return {
62 loadByUrl: (url: string) => {
63 return VideoModel.loadByUrl(url)
64 },
65
66 removeVideo: (id: number) => {
67 return sequelizeTypescript.transaction(async t => {
68 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id, t)
69
70 await video.destroy({ transaction: t })
71 })
72 }
73 }
74}
75
76function buildModerationHelpers () {
77 return {
78 blockServer: async (options: { byAccountId: number, hostToBlock: string }) => {
79 const serverToBlock = await ServerModel.loadOrCreateByHost(options.hostToBlock)
80
81 await addServerInBlocklist(options.byAccountId, serverToBlock.id)
82 },
83
84 unblockServer: async (options: { byAccountId: number, hostToUnblock: string }) => {
85 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(options.byAccountId, options.hostToUnblock)
86 if (!serverBlock) return
87
88 await removeServerFromBlocklist(serverBlock)
89 },
90
91 blockAccount: async (options: { byAccountId: number, handleToBlock: string }) => {
92 const accountToBlock = await AccountModel.loadByNameWithHost(options.handleToBlock)
93 if (!accountToBlock) return
94
95 await addAccountInBlocklist(options.byAccountId, accountToBlock.id)
96 },
97
98 unblockAccount: async (options: { byAccountId: number, handleToUnblock: string }) => {
99 const targetAccount = await AccountModel.loadByNameWithHost(options.handleToUnblock)
100 if (!targetAccount) return
101
102 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(options.byAccountId, targetAccount.id)
103 if (!accountBlock) return
104
105 await removeAccountFromBlocklist(accountBlock)
106 },
107
108 blacklistVideo: async (options: { videoIdOrUUID: number | string, createOptions: VideoBlacklistCreate }) => {
109 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID)
110 if (!video) return
111
112 await blacklistVideo(video, options.createOptions)
113 },
114
115 unblacklistVideo: async (options: { videoIdOrUUID: number | string }) => {
116 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID)
117 if (!video) return
118
119 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(video.id)
120 if (!videoBlacklist) return
121
122 await unblacklistVideo(videoBlacklist, video)
123 }
124 }
125}
126
127function buildConfigHelpers () {
128 return {
129 getWebserverUrl () {
130 return WEBSERVER.URL
131 }
132 }
133}
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts
index 25b4f3c61..170f0c7e2 100644
--- a/server/lib/plugins/plugin-index.ts
+++ b/server/lib/plugins/plugin-index.ts
@@ -27,11 +27,11 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
27 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' 27 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins'
28 28
29 try { 29 try {
30 const { body } = await doRequest({ uri, qs, json: true }) 30 const { body } = await doRequest<any>({ uri, qs, json: true })
31 31
32 logger.debug('Got result from PeerTube index.', { body }) 32 logger.debug('Got result from PeerTube index.', { body })
33 33
34 await addInstanceInformation(body) 34 addInstanceInformation(body)
35 35
36 return body as ResultList<PeerTubePluginIndex> 36 return body as ResultList<PeerTubePluginIndex>
37 } catch (err) { 37 } catch (err) {
@@ -40,7 +40,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
40 } 40 }
41} 41}
42 42
43async function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) { 43function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) {
44 for (const d of result.data) { 44 for (const d of result.data) {
45 d.installed = PluginManager.Instance.isRegistered(d.npmName) 45 d.installed = PluginManager.Instance.isRegistered(d.npmName)
46 d.name = PluginModel.normalizePluginName(d.npmName) 46 d.name = PluginModel.normalizePluginName(d.npmName)
@@ -57,7 +57,7 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu
57 57
58 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version' 58 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version'
59 59
60 const { body } = await doRequest({ uri, body: bodyRequest, json: true, method: 'POST' }) 60 const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' })
61 61
62 return body 62 return body
63} 63}
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 7ebdabd34..950acf7ad 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -9,23 +9,20 @@ import {
9 PluginTranslationPaths as PackagePluginTranslations 9 PluginTranslationPaths as PackagePluginTranslations
10} from '../../../shared/models/plugins/plugin-package-json.model' 10} from '../../../shared/models/plugins/plugin-package-json.model'
11import { createReadStream, createWriteStream } from 'fs' 11import { createReadStream, createWriteStream } from 'fs'
12import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' 12import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
13import { PluginType } from '../../../shared/models/plugins/plugin.type' 13import { PluginType } from '../../../shared/models/plugins/plugin.type'
14import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' 14import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
15import { outputFile, readJSON } from 'fs-extra' 15import { outputFile, readJSON } from 'fs-extra'
16import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' 16import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model'
17import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model'
18import { ServerHook, ServerHookName, serverHookObject } from '../../../shared/models/plugins/server-hook.model'
19import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' 17import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
20import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model' 18import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model'
21import { PluginLibrary } from '../../typings/plugins' 19import { PluginLibrary } from '../../typings/plugins'
22import { ClientHtml } from '../client-html' 20import { ClientHtml } from '../client-html'
23import { RegisterServerHookOptions } from '../../../shared/models/plugins/register-server-hook.model'
24import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
25import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
26import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model'
27import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
28import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' 21import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
22import { RegisterHelpersStore } from './register-helpers-store'
23import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
24import { MOAuthTokenUser, MUser } from '@server/typings/models'
25import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
29 26
30export interface RegisteredPlugin { 27export interface RegisteredPlugin {
31 npmName: string 28 npmName: string
@@ -44,6 +41,7 @@ export interface RegisteredPlugin {
44 css: string[] 41 css: string[]
45 42
46 // Only if this is a plugin 43 // Only if this is a plugin
44 registerHelpersStore?: RegisterHelpersStore
47 unregister?: Function 45 unregister?: Function
48} 46}
49 47
@@ -54,35 +52,18 @@ export interface HookInformationValue {
54 priority: number 52 priority: number
55} 53}
56 54
57type AlterableVideoConstant = 'language' | 'licence' | 'category'
58type VideoConstant = { [ key in number | string ]: string }
59type UpdatedVideoConstant = {
60 [ name in AlterableVideoConstant ]: {
61 [ npmName: string ]: {
62 added: { key: number | string, label: string }[],
63 deleted: { key: number | string, label: string }[]
64 }
65 }
66}
67
68type PluginLocalesTranslations = { 55type PluginLocalesTranslations = {
69 [ locale: string ]: PluginTranslation 56 [locale: string]: PluginTranslation
70} 57}
71 58
72export class PluginManager implements ServerHook { 59export class PluginManager implements ServerHook {
73 60
74 private static instance: PluginManager 61 private static instance: PluginManager
75 62
76 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} 63 private registeredPlugins: { [name: string]: RegisteredPlugin } = {}
77 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
78 private hooks: { [ name: string ]: HookInformationValue[] } = {}
79 private translations: PluginLocalesTranslations = {}
80 64
81 private updatedVideoConstants: UpdatedVideoConstant = { 65 private hooks: { [name: string]: HookInformationValue[] } = {}
82 language: {}, 66 private translations: PluginLocalesTranslations = {}
83 licence: {},
84 category: {}
85 }
86 67
87 private constructor () { 68 private constructor () {
88 } 69 }
@@ -97,7 +78,7 @@ export class PluginManager implements ServerHook {
97 return this.registeredPlugins[npmName] 78 return this.registeredPlugins[npmName]
98 } 79 }
99 80
100 getRegisteredPlugin (name: string) { 81 getRegisteredPluginByShortName (name: string) {
101 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) 82 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN)
102 const registered = this.getRegisteredPluginOrTheme(npmName) 83 const registered = this.getRegisteredPluginOrTheme(npmName)
103 84
@@ -106,7 +87,7 @@ export class PluginManager implements ServerHook {
106 return registered 87 return registered
107 } 88 }
108 89
109 getRegisteredTheme (name: string) { 90 getRegisteredThemeByShortName (name: string) {
110 const npmName = PluginModel.buildNpmName(name, PluginType.THEME) 91 const npmName = PluginModel.buildNpmName(name, PluginType.THEME)
111 const registered = this.getRegisteredPluginOrTheme(npmName) 92 const registered = this.getRegisteredPluginOrTheme(npmName)
112 93
@@ -123,17 +104,102 @@ export class PluginManager implements ServerHook {
123 return this.getRegisteredPluginsOrThemes(PluginType.THEME) 104 return this.getRegisteredPluginsOrThemes(PluginType.THEME)
124 } 105 }
125 106
107 getIdAndPassAuths () {
108 return this.getRegisteredPlugins()
109 .map(p => ({
110 npmName: p.npmName,
111 name: p.name,
112 version: p.version,
113 idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths()
114 }))
115 .filter(v => v.idAndPassAuths.length !== 0)
116 }
117
118 getExternalAuths () {
119 return this.getRegisteredPlugins()
120 .map(p => ({
121 npmName: p.npmName,
122 name: p.name,
123 version: p.version,
124 externalAuths: p.registerHelpersStore.getExternalAuths()
125 }))
126 .filter(v => v.externalAuths.length !== 0)
127 }
128
126 getRegisteredSettings (npmName: string) { 129 getRegisteredSettings (npmName: string) {
127 return this.settings[npmName] || [] 130 const result = this.getRegisteredPluginOrTheme(npmName)
131 if (!result || result.type !== PluginType.PLUGIN) return []
132
133 return result.registerHelpersStore.getSettings()
134 }
135
136 getRouter (npmName: string) {
137 const result = this.getRegisteredPluginOrTheme(npmName)
138 if (!result || result.type !== PluginType.PLUGIN) return null
139
140 return result.registerHelpersStore.getRouter()
128 } 141 }
129 142
130 getTranslations (locale: string) { 143 getTranslations (locale: string) {
131 return this.translations[locale] || {} 144 return this.translations[locale] || {}
132 } 145 }
133 146
147 async isTokenValid (token: MOAuthTokenUser, type: 'access' | 'refresh') {
148 const auth = this.getAuth(token.User.pluginAuth, token.authName)
149 if (!auth) return true
150
151 if (auth.hookTokenValidity) {
152 try {
153 const { valid } = await auth.hookTokenValidity({ token, type })
154
155 if (valid === false) {
156 logger.info('Rejecting %s token validity from auth %s of plugin %s', type, token.authName, token.User.pluginAuth)
157 }
158
159 return valid
160 } catch (err) {
161 logger.warn('Cannot run check token validity from auth %s of plugin %s.', token.authName, token.User.pluginAuth, { err })
162 return true
163 }
164 }
165
166 return true
167 }
168
169 // ###################### External events ######################
170
171 onLogout (npmName: string, authName: string, user: MUser) {
172 const auth = this.getAuth(npmName, authName)
173
174 if (auth?.onLogout) {
175 logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName)
176
177 try {
178 auth.onLogout(user)
179 } catch (err) {
180 logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err })
181 }
182 }
183 }
184
185 onSettingsChanged (name: string, settings: any) {
186 const registered = this.getRegisteredPluginByShortName(name)
187 if (!registered) {
188 logger.error('Cannot find plugin %s to call on settings changed.', name)
189 }
190
191 for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) {
192 try {
193 cb(settings)
194 } catch (err) {
195 logger.error('Cannot run on settings changed callback for %s.', registered.npmName, { err })
196 }
197 }
198 }
199
134 // ###################### Hooks ###################### 200 // ###################### Hooks ######################
135 201
136 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 202 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
137 if (!this.hooks[hookName]) return Promise.resolve(result) 203 if (!this.hooks[hookName]) return Promise.resolve(result)
138 204
139 const hookType = getHookType(hookName) 205 const hookType = getHookType(hookName)
@@ -185,7 +251,6 @@ export class PluginManager implements ServerHook {
185 } 251 }
186 252
187 delete this.registeredPlugins[plugin.npmName] 253 delete this.registeredPlugins[plugin.npmName]
188 delete this.settings[plugin.npmName]
189 254
190 this.deleteTranslations(plugin.npmName) 255 this.deleteTranslations(plugin.npmName)
191 256
@@ -197,7 +262,8 @@ export class PluginManager implements ServerHook {
197 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) 262 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
198 } 263 }
199 264
200 this.reinitVideoConstants(plugin.npmName) 265 const store = plugin.registerHelpersStore
266 store.reinitVideoConstants(plugin.npmName)
201 267
202 logger.info('Regenerating registered plugin CSS to global file.') 268 logger.info('Regenerating registered plugin CSS to global file.')
203 await this.regeneratePluginGlobalCSS() 269 await this.regeneratePluginGlobalCSS()
@@ -303,8 +369,11 @@ export class PluginManager implements ServerHook {
303 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) 369 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
304 370
305 let library: PluginLibrary 371 let library: PluginLibrary
372 let registerHelpersStore: RegisterHelpersStore
306 if (plugin.type === PluginType.PLUGIN) { 373 if (plugin.type === PluginType.PLUGIN) {
307 library = await this.registerPlugin(plugin, pluginPath, packageJSON) 374 const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
375 library = result.library
376 registerHelpersStore = result.registerStore
308 } 377 }
309 378
310 const clientScripts: { [id: string]: ClientScript } = {} 379 const clientScripts: { [id: string]: ClientScript } = {}
@@ -312,7 +381,7 @@ export class PluginManager implements ServerHook {
312 clientScripts[c.script] = c 381 clientScripts[c.script] = c
313 } 382 }
314 383
315 this.registeredPlugins[ npmName ] = { 384 this.registeredPlugins[npmName] = {
316 npmName, 385 npmName,
317 name: plugin.name, 386 name: plugin.name,
318 type: plugin.type, 387 type: plugin.type,
@@ -323,6 +392,7 @@ export class PluginManager implements ServerHook {
323 staticDirs: packageJSON.staticDirs, 392 staticDirs: packageJSON.staticDirs,
324 clientScripts, 393 clientScripts,
325 css: packageJSON.css, 394 css: packageJSON.css,
395 registerHelpersStore: registerHelpersStore || undefined,
326 unregister: library ? library.unregister : undefined 396 unregister: library ? library.unregister : undefined
327 } 397 }
328 398
@@ -341,15 +411,15 @@ export class PluginManager implements ServerHook {
341 throw new Error('Library code is not valid (miss register or unregister function)') 411 throw new Error('Library code is not valid (miss register or unregister function)')
342 } 412 }
343 413
344 const registerHelpers = this.getRegisterHelpers(npmName, plugin) 414 const { registerOptions, registerStore } = this.getRegisterHelpers(npmName, plugin)
345 library.register(registerHelpers) 415 library.register(registerOptions)
346 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) 416 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err }))
347 417
348 logger.info('Add plugin %s CSS to global file.', npmName) 418 logger.info('Add plugin %s CSS to global file.', npmName)
349 419
350 await this.addCSSToGlobalFile(pluginPath, packageJSON.css) 420 await this.addCSSToGlobalFile(pluginPath, packageJSON.css)
351 421
352 return library 422 return { library, registerStore }
353 } 423 }
354 424
355 // ###################### Translations ###################### 425 // ###################### Translations ######################
@@ -432,13 +502,23 @@ export class PluginManager implements ServerHook {
432 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) 502 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName)
433 } 503 }
434 504
505 private getAuth (npmName: string, authName: string) {
506 const plugin = this.getRegisteredPluginOrTheme(npmName)
507 if (!plugin || plugin.type !== PluginType.PLUGIN) return null
508
509 let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
510 auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
511
512 return auths.find(a => a.authName === authName)
513 }
514
435 // ###################### Private getters ###################### 515 // ###################### Private getters ######################
436 516
437 private getRegisteredPluginsOrThemes (type: PluginType) { 517 private getRegisteredPluginsOrThemes (type: PluginType) {
438 const plugins: RegisteredPlugin[] = [] 518 const plugins: RegisteredPlugin[] = []
439 519
440 for (const npmName of Object.keys(this.registeredPlugins)) { 520 for (const npmName of Object.keys(this.registeredPlugins)) {
441 const plugin = this.registeredPlugins[ npmName ] 521 const plugin = this.registeredPlugins[npmName]
442 if (plugin.type !== type) continue 522 if (plugin.type !== type) continue
443 523
444 plugins.push(plugin) 524 plugins.push(plugin)
@@ -449,149 +529,26 @@ export class PluginManager implements ServerHook {
449 529
450 // ###################### Generate register helpers ###################### 530 // ###################### Generate register helpers ######################
451 531
452 private getRegisterHelpers (npmName: string, plugin: PluginModel): RegisterServerOptions { 532 private getRegisterHelpers (
453 const registerHook = (options: RegisterServerHookOptions) => { 533 npmName: string,
454 if (serverHookObject[options.target] !== true) { 534 plugin: PluginModel
455 logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, npmName) 535 ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } {
456 return 536 const onHookAdded = (options: RegisterServerHookOptions) => {
457 }
458
459 if (!this.hooks[options.target]) this.hooks[options.target] = [] 537 if (!this.hooks[options.target]) this.hooks[options.target] = []
460 538
461 this.hooks[options.target].push({ 539 this.hooks[options.target].push({
462 npmName, 540 npmName: npmName,
463 pluginName: plugin.name, 541 pluginName: plugin.name,
464 handler: options.handler, 542 handler: options.handler,
465 priority: options.priority || 0 543 priority: options.priority || 0
466 }) 544 })
467 } 545 }
468 546
469 const registerSetting = (options: RegisterServerSettingOptions) => { 547 const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this))
470 if (!this.settings[npmName]) this.settings[npmName] = []
471
472 this.settings[npmName].push(options)
473 }
474
475 const settingsManager: PluginSettingsManager = {
476 getSetting: (name: string) => PluginModel.getSetting(plugin.name, plugin.type, name),
477
478 setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, plugin.type, name, value)
479 }
480
481 const storageManager: PluginStorageManager = {
482 getData: (key: string) => PluginModel.getData(plugin.name, plugin.type, key),
483
484 storeData: (key: string, data: any) => PluginModel.storeData(plugin.name, plugin.type, key, data)
485 }
486
487 const videoLanguageManager: PluginVideoLanguageManager = {
488 addLanguage: (key: string, label: string) => this.addConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }),
489
490 deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
491 }
492
493 const videoCategoryManager: PluginVideoCategoryManager = {
494 addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }),
495
496 deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
497 }
498
499 const videoLicenceManager: PluginVideoLicenceManager = {
500 addLicence: (key: number, label: string) => this.addConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key, label }),
501
502 deleteLicence: (key: number) => this.deleteConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key })
503 }
504
505 const peertubeHelpers = {
506 logger
507 }
508 548
509 return { 549 return {
510 registerHook, 550 registerStore: registerHelpersStore,
511 registerSetting, 551 registerOptions: registerHelpersStore.buildRegisterHelpers()
512 settingsManager,
513 storageManager,
514 videoLanguageManager,
515 videoCategoryManager,
516 videoLicenceManager,
517 peertubeHelpers
518 }
519 }
520
521 private addConstant <T extends string | number> (parameters: {
522 npmName: string,
523 type: AlterableVideoConstant,
524 obj: VideoConstant,
525 key: T,
526 label: string
527 }) {
528 const { npmName, type, obj, key, label } = parameters
529
530 if (obj[key]) {
531 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
532 return false
533 }
534
535 if (!this.updatedVideoConstants[type][npmName]) {
536 this.updatedVideoConstants[type][npmName] = {
537 added: [],
538 deleted: []
539 }
540 }
541
542 this.updatedVideoConstants[type][npmName].added.push({ key, label })
543 obj[key] = label
544
545 return true
546 }
547
548 private deleteConstant <T extends string | number> (parameters: {
549 npmName: string,
550 type: AlterableVideoConstant,
551 obj: VideoConstant,
552 key: T
553 }) {
554 const { npmName, type, obj, key } = parameters
555
556 if (!obj[key]) {
557 logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
558 return false
559 }
560
561 if (!this.updatedVideoConstants[type][npmName]) {
562 this.updatedVideoConstants[type][npmName] = {
563 added: [],
564 deleted: []
565 }
566 }
567
568 this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
569 delete obj[key]
570
571 return true
572 }
573
574 private reinitVideoConstants (npmName: string) {
575 const hash = {
576 language: VIDEO_LANGUAGES,
577 licence: VIDEO_LICENCES,
578 category: VIDEO_CATEGORIES
579 }
580 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category' ]
581
582 for (const type of types) {
583 const updatedConstants = this.updatedVideoConstants[type][npmName]
584 if (!updatedConstants) continue
585
586 for (const added of updatedConstants.added) {
587 delete hash[type][added.key]
588 }
589
590 for (const deleted of updatedConstants.deleted) {
591 hash[type][deleted.key] = deleted.label
592 }
593
594 delete this.updatedVideoConstants[type][npmName]
595 } 552 }
596 } 553 }
597 554
@@ -604,7 +561,7 @@ export class PluginManager implements ServerHook {
604 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType) 561 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType)
605 if (!packageJSONValid) { 562 if (!packageJSONValid) {
606 const formattedFields = badFields.map(f => `"${f}"`) 563 const formattedFields = badFields.map(f => `"${f}"`)
607 .join(', ') 564 .join(', ')
608 565
609 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`) 566 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`)
610 } 567 }
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts
new file mode 100644
index 000000000..e337b1cb0
--- /dev/null
+++ b/server/lib/plugins/register-helpers-store.ts
@@ -0,0 +1,355 @@
1import * as express from 'express'
2import { logger } from '@server/helpers/logger'
3import {
4 VIDEO_CATEGORIES,
5 VIDEO_LANGUAGES,
6 VIDEO_LICENCES,
7 VIDEO_PLAYLIST_PRIVACIES,
8 VIDEO_PRIVACIES
9} from '@server/initializers/constants'
10import { onExternalUserAuthenticated } from '@server/lib/auth'
11import { PluginModel } from '@server/models/server/plugin'
12import { RegisterServerOptions } from '@server/typings/plugins'
13import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
14import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
15import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model'
16import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
17import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
18import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
19import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
20import {
21 RegisterServerAuthExternalOptions,
22 RegisterServerAuthExternalResult,
23 RegisterServerAuthPassOptions,
24 RegisterServerExternalAuthenticatedResult
25} from '@shared/models/plugins/register-server-auth.model'
26import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
27import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
28import { serverHookObject } from '@shared/models/plugins/server-hook.model'
29import { buildPluginHelpers } from './plugin-helpers'
30
31type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
32type VideoConstant = { [key in number | string]: string }
33
34type UpdatedVideoConstant = {
35 [name in AlterableVideoConstant]: {
36 added: { key: number | string, label: string }[]
37 deleted: { key: number | string, label: string }[]
38 }
39}
40
41export class RegisterHelpersStore {
42 private readonly updatedVideoConstants: UpdatedVideoConstant = {
43 playlistPrivacy: { added: [], deleted: [] },
44 privacy: { added: [], deleted: [] },
45 language: { added: [], deleted: [] },
46 licence: { added: [], deleted: [] },
47 category: { added: [], deleted: [] }
48 }
49
50 private readonly settings: RegisterServerSettingOptions[] = []
51
52 private idAndPassAuths: RegisterServerAuthPassOptions[] = []
53 private externalAuths: RegisterServerAuthExternalOptions[] = []
54
55 private readonly onSettingsChangeCallbacks: ((settings: any) => void)[] = []
56
57 private readonly router: express.Router
58
59 constructor (
60 private readonly npmName: string,
61 private readonly plugin: PluginModel,
62 private readonly onHookAdded: (options: RegisterServerHookOptions) => void
63 ) {
64 this.router = express.Router()
65 }
66
67 buildRegisterHelpers (): RegisterServerOptions {
68 const registerHook = this.buildRegisterHook()
69 const registerSetting = this.buildRegisterSetting()
70
71 const getRouter = this.buildGetRouter()
72
73 const settingsManager = this.buildSettingsManager()
74 const storageManager = this.buildStorageManager()
75
76 const videoLanguageManager = this.buildVideoLanguageManager()
77
78 const videoLicenceManager = this.buildVideoLicenceManager()
79 const videoCategoryManager = this.buildVideoCategoryManager()
80
81 const videoPrivacyManager = this.buildVideoPrivacyManager()
82 const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
83
84 const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
85 const registerExternalAuth = this.buildRegisterExternalAuth()
86 const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
87 const unregisterExternalAuth = this.buildUnregisterExternalAuth()
88
89 const peertubeHelpers = buildPluginHelpers(this.npmName)
90
91 return {
92 registerHook,
93 registerSetting,
94
95 getRouter,
96
97 settingsManager,
98 storageManager,
99
100 videoLanguageManager,
101 videoCategoryManager,
102 videoLicenceManager,
103
104 videoPrivacyManager,
105 playlistPrivacyManager,
106
107 registerIdAndPassAuth,
108 registerExternalAuth,
109 unregisterIdAndPassAuth,
110 unregisterExternalAuth,
111
112 peertubeHelpers
113 }
114 }
115
116 reinitVideoConstants (npmName: string) {
117 const hash = {
118 language: VIDEO_LANGUAGES,
119 licence: VIDEO_LICENCES,
120 category: VIDEO_CATEGORIES,
121 privacy: VIDEO_PRIVACIES,
122 playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
123 }
124 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
125
126 for (const type of types) {
127 const updatedConstants = this.updatedVideoConstants[type][npmName]
128 if (!updatedConstants) continue
129
130 for (const added of updatedConstants.added) {
131 delete hash[type][added.key]
132 }
133
134 for (const deleted of updatedConstants.deleted) {
135 hash[type][deleted.key] = deleted.label
136 }
137
138 delete this.updatedVideoConstants[type][npmName]
139 }
140 }
141
142 getSettings () {
143 return this.settings
144 }
145
146 getRouter () {
147 return this.router
148 }
149
150 getIdAndPassAuths () {
151 return this.idAndPassAuths
152 }
153
154 getExternalAuths () {
155 return this.externalAuths
156 }
157
158 getOnSettingsChangedCallbacks () {
159 return this.onSettingsChangeCallbacks
160 }
161
162 private buildGetRouter () {
163 return () => this.router
164 }
165
166 private buildRegisterSetting () {
167 return (options: RegisterServerSettingOptions) => {
168 this.settings.push(options)
169 }
170 }
171
172 private buildRegisterHook () {
173 return (options: RegisterServerHookOptions) => {
174 if (serverHookObject[options.target] !== true) {
175 logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, this.npmName)
176 return
177 }
178
179 return this.onHookAdded(options)
180 }
181 }
182
183 private buildRegisterIdAndPassAuth () {
184 return (options: RegisterServerAuthPassOptions) => {
185 if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') {
186 logger.error('Cannot register auth plugin %s: authName, getWeight or login are not valid.', this.npmName, { options })
187 return
188 }
189
190 this.idAndPassAuths.push(options)
191 }
192 }
193
194 private buildRegisterExternalAuth () {
195 const self = this
196
197 return (options: RegisterServerAuthExternalOptions) => {
198 if (!options.authName || typeof options.authDisplayName !== 'function' || typeof options.onAuthRequest !== 'function') {
199 logger.error('Cannot register auth plugin %s: authName, authDisplayName or onAuthRequest are not valid.', this.npmName, { options })
200 return
201 }
202
203 this.externalAuths.push(options)
204
205 return {
206 userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void {
207 onExternalUserAuthenticated({
208 npmName: self.npmName,
209 authName: options.authName,
210 authResult: result
211 }).catch(err => {
212 logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err })
213 })
214 }
215 } as RegisterServerAuthExternalResult
216 }
217 }
218
219 private buildUnregisterExternalAuth () {
220 return (authName: string) => {
221 this.externalAuths = this.externalAuths.filter(a => a.authName !== authName)
222 }
223 }
224
225 private buildUnregisterIdAndPassAuth () {
226 return (authName: string) => {
227 this.idAndPassAuths = this.idAndPassAuths.filter(a => a.authName !== authName)
228 }
229 }
230
231 private buildSettingsManager (): PluginSettingsManager {
232 return {
233 getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name, this.settings),
234
235 getSettings: (names: string[]) => PluginModel.getSettings(this.plugin.name, this.plugin.type, names, this.settings),
236
237 setSetting: (name: string, value: string) => PluginModel.setSetting(this.plugin.name, this.plugin.type, name, value),
238
239 onSettingsChange: (cb: (settings: any) => void) => this.onSettingsChangeCallbacks.push(cb)
240 }
241 }
242
243 private buildStorageManager (): PluginStorageManager {
244 return {
245 getData: (key: string) => PluginModel.getData(this.plugin.name, this.plugin.type, key),
246
247 storeData: (key: string, data: any) => PluginModel.storeData(this.plugin.name, this.plugin.type, key, data)
248 }
249 }
250
251 private buildVideoLanguageManager (): PluginVideoLanguageManager {
252 return {
253 addLanguage: (key: string, label: string) => {
254 return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label })
255 },
256
257 deleteLanguage: (key: string) => {
258 return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
259 }
260 }
261 }
262
263 private buildVideoCategoryManager (): PluginVideoCategoryManager {
264 return {
265 addCategory: (key: number, label: string) => {
266 return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
267 },
268
269 deleteCategory: (key: number) => {
270 return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
271 }
272 }
273 }
274
275 private buildVideoPrivacyManager (): PluginVideoPrivacyManager {
276 return {
277 deletePrivacy: (key: number) => {
278 return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key })
279 }
280 }
281 }
282
283 private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager {
284 return {
285 deletePlaylistPrivacy: (key: number) => {
286 return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key })
287 }
288 }
289 }
290
291 private buildVideoLicenceManager (): PluginVideoLicenceManager {
292 return {
293 addLicence: (key: number, label: string) => {
294 return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
295 },
296
297 deleteLicence: (key: number) => {
298 return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key })
299 }
300 }
301 }
302
303 private addConstant<T extends string | number> (parameters: {
304 npmName: string
305 type: AlterableVideoConstant
306 obj: VideoConstant
307 key: T
308 label: string
309 }) {
310 const { npmName, type, obj, key, label } = parameters
311
312 if (obj[key]) {
313 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
314 return false
315 }
316
317 if (!this.updatedVideoConstants[type][npmName]) {
318 this.updatedVideoConstants[type][npmName] = {
319 added: [],
320 deleted: []
321 }
322 }
323
324 this.updatedVideoConstants[type][npmName].added.push({ key, label })
325 obj[key] = label
326
327 return true
328 }
329
330 private deleteConstant<T extends string | number> (parameters: {
331 npmName: string
332 type: AlterableVideoConstant
333 obj: VideoConstant
334 key: T
335 }) {
336 const { npmName, type, obj, key } = parameters
337
338 if (!obj[key]) {
339 logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
340 return false
341 }
342
343 if (!this.updatedVideoConstants[type][npmName]) {
344 this.updatedVideoConstants[type][npmName] = {
345 added: [],
346 deleted: []
347 }
348 }
349
350 this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
351 delete obj[key]
352
353 return true
354 }
355}