aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/plugins.ts20
-rw-r--r--server/helpers/custom-validators/plugins.ts23
-rw-r--r--server/lib/plugins/plugin-manager.ts44
-rw-r--r--server/tests/fixtures/peertube-plugin-test-three/package.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test-two/languages/fr.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test-two/languages/it.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test-two/package.json6
-rw-r--r--server/tests/fixtures/peertube-plugin-test/languages/fr.json3
-rw-r--r--server/tests/fixtures/peertube-plugin-test/package.json5
-rw-r--r--server/tests/plugins/index.ts1
-rw-r--r--server/tests/plugins/translations.ts113
11 files changed, 212 insertions, 12 deletions
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts
index f5285ba3a..1caee9a29 100644
--- a/server/controllers/plugins.ts
+++ b/server/controllers/plugins.ts
@@ -1,11 +1,12 @@
1import * as express from 'express' 1import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' 2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 3import { join } from 'path'
4import { RegisteredPlugin } from '../lib/plugins/plugin-manager' 4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
5import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' 5import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes' 6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
8import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
9import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
9 10
10const sendFileOptions = { 11const sendFileOptions = {
11 maxAge: '30 days', 12 maxAge: '30 days',
@@ -18,6 +19,10 @@ pluginsRouter.get('/plugins/global.css',
18 servePluginGlobalCSS 19 servePluginGlobalCSS
19) 20)
20 21
22pluginsRouter.get('/plugins/translations/:locale.json',
23 getPluginTranslations
24)
25
21pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', 26pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
22 servePluginStaticDirectoryValidator(PluginType.PLUGIN), 27 servePluginStaticDirectoryValidator(PluginType.PLUGIN),
23 servePluginStaticDirectory 28 servePluginStaticDirectory
@@ -60,6 +65,19 @@ function servePluginGlobalCSS (req: express.Request, res: express.Response) {
60 return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions) 65 return res.sendFile(PLUGIN_GLOBAL_CSS_PATH, globalCSSOptions)
61} 66}
62 67
68function getPluginTranslations (req: express.Request, res: express.Response) {
69 const locale = req.params.locale
70
71 if (is18nLocale(locale)) {
72 const completeLocale = getCompleteLocale(locale)
73 const json = PluginManager.Instance.getTranslations(completeLocale)
74
75 return res.json(json)
76 }
77
78 return res.sendStatus(404)
79}
80
63function servePluginStaticDirectory (req: express.Request, res: express.Response) { 81function servePluginStaticDirectory (req: express.Request, res: express.Response) {
64 const plugin: RegisteredPlugin = res.locals.registeredPlugin 82 const plugin: RegisteredPlugin = res.locals.registeredPlugin
65 const staticEndpoint = req.params.staticEndpoint 83 const staticEndpoint = req.params.staticEndpoint
diff --git a/server/helpers/custom-validators/plugins.ts b/server/helpers/custom-validators/plugins.ts
index e0a6f98a7..b5e32abc2 100644
--- a/server/helpers/custom-validators/plugins.ts
+++ b/server/helpers/custom-validators/plugins.ts
@@ -44,7 +44,7 @@ function isPluginHomepage (value: string) {
44 return isUrlValid(value) 44 return isUrlValid(value)
45} 45}
46 46
47function isStaticDirectoriesValid (staticDirs: any) { 47function areStaticDirectoriesValid (staticDirs: any) {
48 if (!exists(staticDirs) || typeof staticDirs !== 'object') return false 48 if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
49 49
50 for (const key of Object.keys(staticDirs)) { 50 for (const key of Object.keys(staticDirs)) {
@@ -54,14 +54,24 @@ function isStaticDirectoriesValid (staticDirs: any) {
54 return true 54 return true
55} 55}
56 56
57function isClientScriptsValid (clientScripts: any[]) { 57function areClientScriptsValid (clientScripts: any[]) {
58 return isArray(clientScripts) && 58 return isArray(clientScripts) &&
59 clientScripts.every(c => { 59 clientScripts.every(c => {
60 return isSafePath(c.script) && isArray(c.scopes) 60 return isSafePath(c.script) && isArray(c.scopes)
61 }) 61 })
62} 62}
63 63
64function isCSSPathsValid (css: any[]) { 64function areTranslationPathsValid (translations: any) {
65 if (!exists(translations) || typeof translations !== 'object') return false
66
67 for (const key of Object.keys(translations)) {
68 if (!isSafePath(translations[key])) return false
69 }
70
71 return true
72}
73
74function areCSSPathsValid (css: any[]) {
65 return isArray(css) && css.every(c => isSafePath(c)) 75 return isArray(css) && css.every(c => isSafePath(c))
66} 76}
67 77
@@ -77,9 +87,10 @@ function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginT
77 exists(packageJSON.author) && 87 exists(packageJSON.author) &&
78 isUrlValid(packageJSON.bugs) && 88 isUrlValid(packageJSON.bugs) &&
79 (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) && 89 (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) &&
80 isStaticDirectoriesValid(packageJSON.staticDirs) && 90 areStaticDirectoriesValid(packageJSON.staticDirs) &&
81 isCSSPathsValid(packageJSON.css) && 91 areCSSPathsValid(packageJSON.css) &&
82 isClientScriptsValid(packageJSON.clientScripts) 92 areClientScriptsValid(packageJSON.clientScripts) &&
93 areTranslationPathsValid(packageJSON.translations)
83} 94}
84 95
85function isLibraryCodeValid (library: any) { 96function isLibraryCodeValid (library: any) {
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 81554a09e..c9beae268 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -3,7 +3,11 @@ import { logger } from '../../helpers/logger'
3import { basename, join } from 'path' 3import { basename, join } from 'path'
4import { CONFIG } from '../../initializers/config' 4import { CONFIG } from '../../initializers/config'
5import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins' 5import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
6import { ClientScript, PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' 6import {
7 ClientScript,
8 PluginPackageJson,
9 PluginTranslationPaths as PackagePluginTranslations
10} from '../../../shared/models/plugins/plugin-package-json.model'
7import { createReadStream, createWriteStream } from 'fs' 11import { createReadStream, createWriteStream } from 'fs'
8import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' 12import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants'
9import { PluginType } from '../../../shared/models/plugins/plugin.type' 13import { PluginType } from '../../../shared/models/plugins/plugin.type'
@@ -21,6 +25,7 @@ import { RegisterServerSettingOptions } from '../../../shared/models/plugins/reg
21import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model' 25import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
22import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model' 26import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model'
23import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model' 27import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
28import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
24 29
25export interface RegisteredPlugin { 30export interface RegisteredPlugin {
26 npmName: string 31 npmName: string
@@ -60,6 +65,10 @@ type UpdatedVideoConstant = {
60 } 65 }
61} 66}
62 67
68type PluginLocalesTranslations = {
69 [ locale: string ]: PluginTranslation
70}
71
63export class PluginManager implements ServerHook { 72export class PluginManager implements ServerHook {
64 73
65 private static instance: PluginManager 74 private static instance: PluginManager
@@ -67,6 +76,7 @@ export class PluginManager implements ServerHook {
67 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} 76 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {}
68 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {} 77 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
69 private hooks: { [ name: string ]: HookInformationValue[] } = {} 78 private hooks: { [ name: string ]: HookInformationValue[] } = {}
79 private translations: PluginLocalesTranslations = {}
70 80
71 private updatedVideoConstants: UpdatedVideoConstant = { 81 private updatedVideoConstants: UpdatedVideoConstant = {
72 language: {}, 82 language: {},
@@ -117,6 +127,10 @@ export class PluginManager implements ServerHook {
117 return this.settings[npmName] || [] 127 return this.settings[npmName] || []
118 } 128 }
119 129
130 getTranslations (locale: string) {
131 return this.translations[locale] || {}
132 }
133
120 // ###################### Hooks ###################### 134 // ###################### Hooks ######################
121 135
122 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 136 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
@@ -173,6 +187,8 @@ export class PluginManager implements ServerHook {
173 delete this.registeredPlugins[plugin.npmName] 187 delete this.registeredPlugins[plugin.npmName]
174 delete this.settings[plugin.npmName] 188 delete this.settings[plugin.npmName]
175 189
190 this.deleteTranslations(plugin.npmName)
191
176 if (plugin.type === PluginType.PLUGIN) { 192 if (plugin.type === PluginType.PLUGIN) {
177 await plugin.unregister() 193 await plugin.unregister()
178 194
@@ -312,6 +328,8 @@ export class PluginManager implements ServerHook {
312 css: packageJSON.css, 328 css: packageJSON.css,
313 unregister: library ? library.unregister : undefined 329 unregister: library ? library.unregister : undefined
314 } 330 }
331
332 await this.addTranslations(plugin, npmName, packageJSON.translations)
315 } 333 }
316 334
317 private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) { 335 private async registerPlugin (plugin: PluginModel, pluginPath: string, packageJSON: PluginPackageJson) {
@@ -337,6 +355,28 @@ export class PluginManager implements ServerHook {
337 return library 355 return library
338 } 356 }
339 357
358 // ###################### Translations ######################
359
360 private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) {
361 for (const locale of Object.keys(translationPaths)) {
362 const path = translationPaths[locale]
363 const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path))
364
365 if (!this.translations[locale]) this.translations[locale] = {}
366 this.translations[locale][npmName] = json
367
368 logger.info('Added locale %s of plugin %s.', locale, npmName)
369 }
370 }
371
372 private deleteTranslations (npmName: string) {
373 for (const locale of Object.keys(this.translations)) {
374 delete this.translations[locale][npmName]
375
376 logger.info('Deleted locale %s of plugin %s.', locale, npmName)
377 }
378 }
379
340 // ###################### CSS ###################### 380 // ###################### CSS ######################
341 381
342 private resetCSSGlobalFile () { 382 private resetCSSGlobalFile () {
@@ -455,7 +495,7 @@ export class PluginManager implements ServerHook {
455 deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key }) 495 deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
456 } 496 }
457 497
458 const videoCategoryManager: PluginVideoCategoryManager= { 498 const videoCategoryManager: PluginVideoCategoryManager = {
459 addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }), 499 addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }),
460 500
461 deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key }) 501 deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
diff --git a/server/tests/fixtures/peertube-plugin-test-three/package.json b/server/tests/fixtures/peertube-plugin-test-three/package.json
index 3f7819db3..41d4c93fe 100644
--- a/server/tests/fixtures/peertube-plugin-test-three/package.json
+++ b/server/tests/fixtures/peertube-plugin-test-three/package.json
@@ -15,5 +15,6 @@
15 "library": "./main.js", 15 "library": "./main.js",
16 "staticDirs": {}, 16 "staticDirs": {},
17 "css": [], 17 "css": [],
18 "clientScripts": [] 18 "clientScripts": [],
19 "translations": {}
19} 20}
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json b/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json
new file mode 100644
index 000000000..52d8313df
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json
@@ -0,0 +1,3 @@
1{
2 "Hello world": "Bonjour le monde"
3}
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/it.json b/server/tests/fixtures/peertube-plugin-test-two/languages/it.json
new file mode 100644
index 000000000..9e187d83b
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-two/languages/it.json
@@ -0,0 +1,3 @@
1{
2 "Hello world": "Ciao, mondo!"
3}
diff --git a/server/tests/fixtures/peertube-plugin-test-two/package.json b/server/tests/fixtures/peertube-plugin-test-two/package.json
index 52ebb5ac1..926f2d69b 100644
--- a/server/tests/fixtures/peertube-plugin-test-two/package.json
+++ b/server/tests/fixtures/peertube-plugin-test-two/package.json
@@ -15,5 +15,9 @@
15 "library": "./main.js", 15 "library": "./main.js",
16 "staticDirs": {}, 16 "staticDirs": {},
17 "css": [], 17 "css": [],
18 "clientScripts": [] 18 "clientScripts": [],
19 "translations": {
20 "fr-FR": "./languages/fr.json",
21 "it-IT": "./languages/it.json"
22 }
19} 23}
diff --git a/server/tests/fixtures/peertube-plugin-test/languages/fr.json b/server/tests/fixtures/peertube-plugin-test/languages/fr.json
new file mode 100644
index 000000000..9e52f7065
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test/languages/fr.json
@@ -0,0 +1,3 @@
1{
2 "Hi": "Coucou"
3}
diff --git a/server/tests/fixtures/peertube-plugin-test/package.json b/server/tests/fixtures/peertube-plugin-test/package.json
index 9d6fe5c90..108f21fd6 100644
--- a/server/tests/fixtures/peertube-plugin-test/package.json
+++ b/server/tests/fixtures/peertube-plugin-test/package.json
@@ -15,5 +15,8 @@
15 "library": "./main.js", 15 "library": "./main.js",
16 "staticDirs": {}, 16 "staticDirs": {},
17 "css": [], 17 "css": [],
18 "clientScripts": [] 18 "clientScripts": [],
19 "translations": {
20 "fr-FR": "./languages/fr.json"
21 }
19} 22}
diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts
index 95e358732..f41708055 100644
--- a/server/tests/plugins/index.ts
+++ b/server/tests/plugins/index.ts
@@ -1,3 +1,4 @@
1import './action-hooks' 1import './action-hooks'
2import './filter-hooks' 2import './filter-hooks'
3import './translations'
3import './video-constants' 4import './video-constants'
diff --git a/server/tests/plugins/translations.ts b/server/tests/plugins/translations.ts
new file mode 100644
index 000000000..88d91a033
--- /dev/null
+++ b/server/tests/plugins/translations.ts
@@ -0,0 +1,113 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 cleanupTests,
7 flushAndRunMultipleServers,
8 flushAndRunServer, killallServers, reRunServer,
9 ServerInfo,
10 waitUntilLog
11} from '../../../shared/extra-utils/server/servers'
12import {
13 addVideoCommentReply,
14 addVideoCommentThread,
15 deleteVideoComment,
16 getPluginTestPath,
17 getVideosList,
18 installPlugin,
19 removeVideo,
20 setAccessTokensToServers,
21 updateVideo,
22 uploadVideo,
23 viewVideo,
24 getVideosListPagination,
25 getVideo,
26 getVideoCommentThreads,
27 getVideoThreadComments,
28 getVideoWithToken,
29 setDefaultVideoChannel,
30 waitJobs,
31 doubleFollow, getVideoLanguages, getVideoLicences, getVideoCategories, uninstallPlugin, getPluginTranslations
32} from '../../../shared/extra-utils'
33import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
34import { VideoDetails } from '../../../shared/models/videos'
35import { getYoutubeVideoUrl, importVideo } from '../../../shared/extra-utils/videos/video-imports'
36
37const expect = chai.expect
38
39describe('Test plugin translations', function () {
40 let server: ServerInfo
41
42 before(async function () {
43 this.timeout(30000)
44
45 server = await flushAndRunServer(1)
46 await setAccessTokensToServers([ server ])
47
48 await installPlugin({
49 url: server.url,
50 accessToken: server.accessToken,
51 path: getPluginTestPath()
52 })
53
54 await installPlugin({
55 url: server.url,
56 accessToken: server.accessToken,
57 path: getPluginTestPath('-two')
58 })
59 })
60
61 it('Should not have translations for locale pt', async function () {
62 const res = await getPluginTranslations({ url: server.url, locale: 'pt' })
63
64 expect(res.body).to.deep.equal({})
65 })
66
67 it('Should have translations for locale fr', async function () {
68 const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' })
69
70 expect(res.body).to.deep.equal({
71 'peertube-plugin-test': {
72 'Hi': 'Coucou'
73 },
74 'peertube-plugin-test-two': {
75 'Hello world': 'Bonjour le monde'
76 }
77 })
78 })
79
80 it('Should have translations of locale it', async function () {
81 const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' })
82
83 expect(res.body).to.deep.equal({
84 'peertube-plugin-test-two': {
85 'Hello world': 'Ciao, mondo!'
86 }
87 })
88 })
89
90 it('Should remove the plugin and remove the locales', async function () {
91 await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-two' })
92
93 {
94 const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' })
95
96 expect(res.body).to.deep.equal({
97 'peertube-plugin-test': {
98 'Hi': 'Coucou'
99 }
100 })
101 }
102
103 {
104 const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' })
105
106 expect(res.body).to.deep.equal({})
107 }
108 })
109
110 after(async function () {
111 await cleanupTests([ server ])
112 })
113})