From 5e2b2e2775421cd98286d6e2f75cf38aae7a212c Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Fri, 10 Apr 2020 15:07:54 +0200
Subject: Add ability for plugins to add custom routes

---
 server/lib/plugins/plugin-manager.ts         |  51 +++---
 server/lib/plugins/register-helpers-store.ts | 235 +++++++++++++++++++++++++++
 server/lib/plugins/register-helpers.ts       | 180 --------------------
 3 files changed, 260 insertions(+), 206 deletions(-)
 create mode 100644 server/lib/plugins/register-helpers-store.ts
 delete mode 100644 server/lib/plugins/register-helpers.ts

(limited to 'server/lib')

diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 44530d203..37fb07716 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -13,15 +13,14 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
 import { PluginType } from '../../../shared/models/plugins/plugin.type'
 import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
 import { outputFile, readJSON } from 'fs-extra'
-import { ServerHook, ServerHookName, serverHookObject } from '../../../shared/models/plugins/server-hook.model'
+import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model'
 import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
 import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model'
 import { PluginLibrary } from '../../typings/plugins'
 import { ClientHtml } from '../client-html'
-import { RegisterServerHookOptions } from '../../../shared/models/plugins/register-server-hook.model'
-import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
 import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
-import { buildRegisterHelpers, reinitVideoConstants } from './register-helpers'
+import { RegisterHelpersStore } from './register-helpers-store'
+import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
 
 export interface RegisteredPlugin {
   npmName: string
@@ -59,10 +58,11 @@ export class PluginManager implements ServerHook {
   private static instance: PluginManager
 
   private registeredPlugins: { [name: string]: RegisteredPlugin } = {}
-  private settings: { [name: string]: RegisterServerSettingOptions[] } = {}
   private hooks: { [name: string]: HookInformationValue[] } = {}
   private translations: PluginLocalesTranslations = {}
 
+  private registerHelpersStore: { [npmName: string]: RegisterHelpersStore } = {}
+
   private constructor () {
   }
 
@@ -103,7 +103,17 @@ export class PluginManager implements ServerHook {
   }
 
   getRegisteredSettings (npmName: string) {
-    return this.settings[npmName] || []
+    const store = this.registerHelpersStore[npmName]
+    if (store) return store.getSettings()
+
+    return []
+  }
+
+  getRouter (npmName: string) {
+    const store = this.registerHelpersStore[npmName]
+    if (!store) return null
+
+    return store.getRouter()
   }
 
   getTranslations (locale: string) {
@@ -164,7 +174,6 @@ export class PluginManager implements ServerHook {
     }
 
     delete this.registeredPlugins[plugin.npmName]
-    delete this.settings[plugin.npmName]
 
     this.deleteTranslations(plugin.npmName)
 
@@ -176,7 +185,10 @@ export class PluginManager implements ServerHook {
         this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
       }
 
-      reinitVideoConstants(plugin.npmName)
+      const store = this.registerHelpersStore[plugin.npmName]
+      store.reinitVideoConstants(plugin.npmName)
+
+      delete this.registerHelpersStore[plugin.npmName]
 
       logger.info('Regenerating registered plugin CSS to global file.')
       await this.regeneratePluginGlobalCSS()
@@ -429,34 +441,21 @@ export class PluginManager implements ServerHook {
   // ###################### Generate register helpers ######################
 
   private getRegisterHelpers (npmName: string, plugin: PluginModel): RegisterServerOptions {
-    const registerHook = (options: RegisterServerHookOptions) => {
-      if (serverHookObject[options.target] !== true) {
-        logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, npmName)
-        return
-      }
-
+    const onHookAdded = (options: RegisterServerHookOptions) => {
       if (!this.hooks[options.target]) this.hooks[options.target] = []
 
       this.hooks[options.target].push({
-        npmName,
+        npmName: npmName,
         pluginName: plugin.name,
         handler: options.handler,
         priority: options.priority || 0
       })
     }
 
-    const registerSetting = (options: RegisterServerSettingOptions) => {
-      if (!this.settings[npmName]) this.settings[npmName] = []
-
-      this.settings[npmName].push(options)
-    }
-
-    const registerHelpers = buildRegisterHelpers(npmName, plugin)
+    const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this))
+    this.registerHelpersStore[npmName] = registerHelpersStore
 
-    return Object.assign(registerHelpers, {
-      registerHook,
-      registerSetting
-    })
+    return registerHelpersStore.buildRegisterHelpers()
   }
 
   private sanitizeAndCheckPackageJSONOrThrow (packageJSON: PluginPackageJson, pluginType: PluginType) {
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts
new file mode 100644
index 000000000..c76c0161a
--- /dev/null
+++ b/server/lib/plugins/register-helpers-store.ts
@@ -0,0 +1,235 @@
+import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
+import { PluginModel } from '@server/models/server/plugin'
+import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model'
+import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
+import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '@server/initializers/constants'
+import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
+import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
+import { RegisterServerOptions } from '@server/typings/plugins'
+import { buildPluginHelpers } from './plugin-helpers'
+import { logger } from '@server/helpers/logger'
+import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
+import { serverHookObject } from '@shared/models/plugins/server-hook.model'
+import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
+import * as express from 'express'
+
+type AlterableVideoConstant = 'language' | 'licence' | 'category'
+type VideoConstant = { [key in number | string]: string }
+
+type UpdatedVideoConstant = {
+  [name in AlterableVideoConstant]: {
+    added: { key: number | string, label: string }[]
+    deleted: { key: number | string, label: string }[]
+  }
+}
+
+export class RegisterHelpersStore {
+  private readonly updatedVideoConstants: UpdatedVideoConstant = {
+    language: { added: [], deleted: [] },
+    licence: { added: [], deleted: [] },
+    category: { added: [], deleted: [] }
+  }
+
+  private readonly settings: RegisterServerSettingOptions[] = []
+
+  private readonly router: express.Router
+
+  constructor (
+    private readonly npmName: string,
+    private readonly plugin: PluginModel,
+    private readonly onHookAdded: (options: RegisterServerHookOptions) => void
+  ) {
+    this.router = express.Router()
+  }
+
+  buildRegisterHelpers (): RegisterServerOptions {
+    const registerHook = this.buildRegisterHook()
+    const registerSetting = this.buildRegisterSetting()
+
+    const getRouter = this.buildGetRouter()
+
+    const settingsManager = this.buildSettingsManager()
+    const storageManager = this.buildStorageManager()
+
+    const videoLanguageManager = this.buildVideoLanguageManager()
+
+    const videoLicenceManager = this.buildVideoLicenceManager()
+    const videoCategoryManager = this.buildVideoCategoryManager()
+
+    const peertubeHelpers = buildPluginHelpers(this.npmName)
+
+    return {
+      registerHook,
+      registerSetting,
+
+      getRouter,
+
+      settingsManager,
+      storageManager,
+
+      videoLanguageManager,
+      videoCategoryManager,
+      videoLicenceManager,
+
+      peertubeHelpers
+    }
+  }
+
+  reinitVideoConstants (npmName: string) {
+    const hash = {
+      language: VIDEO_LANGUAGES,
+      licence: VIDEO_LICENCES,
+      category: VIDEO_CATEGORIES
+    }
+    const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category' ]
+
+    for (const type of types) {
+      const updatedConstants = this.updatedVideoConstants[type][npmName]
+      if (!updatedConstants) continue
+
+      for (const added of updatedConstants.added) {
+        delete hash[type][added.key]
+      }
+
+      for (const deleted of updatedConstants.deleted) {
+        hash[type][deleted.key] = deleted.label
+      }
+
+      delete this.updatedVideoConstants[type][npmName]
+    }
+  }
+
+  getSettings () {
+    return this.settings
+  }
+
+  getRouter () {
+    return this.router
+  }
+
+  private buildGetRouter () {
+    return () => this.router
+  }
+
+  private buildRegisterSetting () {
+    return (options: RegisterServerSettingOptions) => {
+      this.settings.push(options)
+    }
+  }
+
+  private buildRegisterHook () {
+    return (options: RegisterServerHookOptions) => {
+      if (serverHookObject[options.target] !== true) {
+        logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, this.npmName)
+        return
+      }
+
+      return this.onHookAdded(options)
+    }
+  }
+
+  private buildSettingsManager (): PluginSettingsManager {
+    return {
+      getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name),
+
+      setSetting: (name: string, value: string) => PluginModel.setSetting(this.plugin.name, this.plugin.type, name, value)
+    }
+  }
+
+  private buildStorageManager (): PluginStorageManager {
+    return {
+      getData: (key: string) => PluginModel.getData(this.plugin.name, this.plugin.type, key),
+
+      storeData: (key: string, data: any) => PluginModel.storeData(this.plugin.name, this.plugin.type, key, data)
+    }
+  }
+
+  private buildVideoLanguageManager (): PluginVideoLanguageManager {
+    return {
+      addLanguage: (key: string, label: string) => {
+        return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label })
+      },
+
+      deleteLanguage: (key: string) => {
+        return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
+      }
+    }
+  }
+
+  private buildVideoCategoryManager (): PluginVideoCategoryManager {
+    return {
+      addCategory: (key: number, label: string) => {
+        return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
+      },
+
+      deleteCategory: (key: number) => {
+        return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
+      }
+    }
+  }
+
+  private buildVideoLicenceManager (): PluginVideoLicenceManager {
+    return {
+      addLicence: (key: number, label: string) => {
+        return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
+      },
+
+      deleteLicence: (key: number) => {
+        return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key })
+      }
+    }
+  }
+
+  private addConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    obj: VideoConstant
+    key: T
+    label: string
+  }) {
+    const { npmName, type, obj, key, label } = parameters
+
+    if (obj[key]) {
+      logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
+      return false
+    }
+
+    if (!this.updatedVideoConstants[type][npmName]) {
+      this.updatedVideoConstants[type][npmName] = {
+        added: [],
+        deleted: []
+      }
+    }
+
+    this.updatedVideoConstants[type][npmName].added.push({ key, label })
+    obj[key] = label
+
+    return true
+  }
+
+  private deleteConstant<T extends string | number> (parameters: {
+    npmName: string
+    type: AlterableVideoConstant
+    obj: VideoConstant
+    key: T
+  }) {
+    const { npmName, type, obj, key } = parameters
+
+    if (!obj[key]) {
+      logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
+      return false
+    }
+
+    if (!this.updatedVideoConstants[type][npmName]) {
+      this.updatedVideoConstants[type][npmName] = {
+        added: [],
+        deleted: []
+      }
+    }
+
+    this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
+    delete obj[key]
+
+    return true
+  }
+}
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts
deleted file mode 100644
index 4c0935a05..000000000
--- a/server/lib/plugins/register-helpers.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
-import { PluginModel } from '@server/models/server/plugin'
-import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model'
-import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
-import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '@server/initializers/constants'
-import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
-import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
-import { RegisterServerOptions } from '@server/typings/plugins'
-import { buildPluginHelpers } from './plugin-helpers'
-import { logger } from '@server/helpers/logger'
-
-type AlterableVideoConstant = 'language' | 'licence' | 'category'
-type VideoConstant = { [key in number | string]: string }
-type UpdatedVideoConstant = {
-  [name in AlterableVideoConstant]: {
-    [npmName: string]: {
-      added: { key: number | string, label: string }[]
-      deleted: { key: number | string, label: string }[]
-    }
-  }
-}
-
-const updatedVideoConstants: UpdatedVideoConstant = {
-  language: {},
-  licence: {},
-  category: {}
-}
-
-function buildRegisterHelpers (npmName: string, plugin: PluginModel): Omit<RegisterServerOptions, 'registerHook' | 'registerSetting'> {
-  const settingsManager = buildSettingsManager(plugin)
-  const storageManager = buildStorageManager(plugin)
-
-  const videoLanguageManager = buildVideoLanguageManager(npmName)
-
-  const videoCategoryManager = buildVideoCategoryManager(npmName)
-  const videoLicenceManager = buildVideoLicenceManager(npmName)
-
-  const peertubeHelpers = buildPluginHelpers(npmName)
-
-  return {
-    settingsManager,
-    storageManager,
-    videoLanguageManager,
-    videoCategoryManager,
-    videoLicenceManager,
-    peertubeHelpers
-  }
-}
-
-function reinitVideoConstants (npmName: string) {
-  const hash = {
-    language: VIDEO_LANGUAGES,
-    licence: VIDEO_LICENCES,
-    category: VIDEO_CATEGORIES
-  }
-  const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category' ]
-
-  for (const type of types) {
-    const updatedConstants = updatedVideoConstants[type][npmName]
-    if (!updatedConstants) continue
-
-    for (const added of updatedConstants.added) {
-      delete hash[type][added.key]
-    }
-
-    for (const deleted of updatedConstants.deleted) {
-      hash[type][deleted.key] = deleted.label
-    }
-
-    delete updatedVideoConstants[type][npmName]
-  }
-}
-
-export {
-  buildRegisterHelpers,
-  reinitVideoConstants
-}
-
-// ---------------------------------------------------------------------------
-
-function buildSettingsManager (plugin: PluginModel): PluginSettingsManager {
-  return {
-    getSetting: (name: string) => PluginModel.getSetting(plugin.name, plugin.type, name),
-
-    setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, plugin.type, name, value)
-  }
-}
-
-function buildStorageManager (plugin: PluginModel): PluginStorageManager {
-  return {
-    getData: (key: string) => PluginModel.getData(plugin.name, plugin.type, key),
-
-    storeData: (key: string, data: any) => PluginModel.storeData(plugin.name, plugin.type, key, data)
-  }
-}
-
-function buildVideoLanguageManager (npmName: string): PluginVideoLanguageManager {
-  return {
-    addLanguage: (key: string, label: string) => addConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }),
-
-    deleteLanguage: (key: string) => deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
-  }
-}
-
-function buildVideoCategoryManager (npmName: string): PluginVideoCategoryManager {
-  return {
-    addCategory: (key: number, label: string) => {
-      return addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
-    },
-
-    deleteCategory: (key: number) => {
-      return deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
-    }
-  }
-}
-
-function buildVideoLicenceManager (npmName: string): PluginVideoLicenceManager {
-  return {
-    addLicence: (key: number, label: string) => {
-      return addConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
-    },
-
-    deleteLicence: (key: number) => {
-      return deleteConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key })
-    }
-  }
-}
-
-function addConstant<T extends string | number> (parameters: {
-  npmName: string
-  type: AlterableVideoConstant
-  obj: VideoConstant
-  key: T
-  label: string
-}) {
-  const { npmName, type, obj, key, label } = parameters
-
-  if (obj[key]) {
-    logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
-    return false
-  }
-
-  if (!updatedVideoConstants[type][npmName]) {
-    updatedVideoConstants[type][npmName] = {
-      added: [],
-      deleted: []
-    }
-  }
-
-  updatedVideoConstants[type][npmName].added.push({ key, label })
-  obj[key] = label
-
-  return true
-}
-
-function deleteConstant<T extends string | number> (parameters: {
-  npmName: string
-  type: AlterableVideoConstant
-  obj: VideoConstant
-  key: T
-}) {
-  const { npmName, type, obj, key } = parameters
-
-  if (!obj[key]) {
-    logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
-    return false
-  }
-
-  if (!updatedVideoConstants[type][npmName]) {
-    updatedVideoConstants[type][npmName] = {
-      added: [],
-      deleted: []
-    }
-  }
-
-  updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
-  delete obj[key]
-
-  return true
-}
-- 
cgit v1.2.3