Add search index tests
[oweals/peertube.git] / server / controllers / api / config.ts
1 import { Hooks } from '@server/lib/plugins/hooks'
2 import * as express from 'express'
3 import { remove, writeJSON } from 'fs-extra'
4 import { snakeCase } from 'lodash'
5 import validator from 'validator'
6 import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig, UserRight } from '../../../shared'
7 import { About } from '../../../shared/models/server/about.model'
8 import { CustomConfig } from '../../../shared/models/server/custom-config.model'
9 import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
10 import { objectConverter } from '../../helpers/core-utils'
11 import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
12 import { getServerCommit } from '../../helpers/utils'
13 import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config'
14 import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
15 import { ClientHtml } from '../../lib/client-html'
16 import { PluginManager } from '../../lib/plugins/plugin-manager'
17 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
18 import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
19 import { customConfigUpdateValidator } from '../../middlewares/validators/config'
20
21 const configRouter = express.Router()
22
23 const auditLogger = auditLoggerFactory('config')
24
25 configRouter.get('/about', getAbout)
26 configRouter.get('/',
27   asyncMiddleware(getConfig)
28 )
29
30 configRouter.get('/custom',
31   authenticate,
32   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
33   getCustomConfig
34 )
35 configRouter.put('/custom',
36   authenticate,
37   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
38   customConfigUpdateValidator,
39   asyncMiddleware(updateCustomConfig)
40 )
41 configRouter.delete('/custom',
42   authenticate,
43   ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
44   asyncMiddleware(deleteCustomConfig)
45 )
46
47 let serverCommit: string
48
49 async function getConfig (req: express.Request, res: express.Response) {
50   const { allowed } = await Hooks.wrapPromiseFun(
51     isSignupAllowed,
52     {
53       ip: req.ip
54     },
55     'filter:api.user.signup.allowed.result'
56   )
57
58   const allowedForCurrentIP = isSignupAllowedForCurrentIP(req.ip)
59   const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
60
61   if (serverCommit === undefined) serverCommit = await getServerCommit()
62
63   const json: ServerConfig = {
64     instance: {
65       name: CONFIG.INSTANCE.NAME,
66       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
67       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
68       isNSFW: CONFIG.INSTANCE.IS_NSFW,
69       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
70       customizations: {
71         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
72         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
73       }
74     },
75     search: {
76       remoteUri: {
77         users: CONFIG.SEARCH.REMOTE_URI.USERS,
78         anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
79       },
80       searchIndex: {
81         enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
82         url: CONFIG.SEARCH.SEARCH_INDEX.URL,
83         disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
84         isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
85       }
86     },
87     plugin: {
88       registered: getRegisteredPlugins(),
89       registeredExternalAuths: getExternalAuthsPlugins(),
90       registeredIdAndPassAuths: getIdAndPassAuthPlugins()
91     },
92     theme: {
93       registered: getRegisteredThemes(),
94       default: defaultTheme
95     },
96     email: {
97       enabled: isEmailEnabled()
98     },
99     contactForm: {
100       enabled: CONFIG.CONTACT_FORM.ENABLED
101     },
102     serverVersion: PEERTUBE_VERSION,
103     serverCommit,
104     signup: {
105       allowed,
106       allowedForCurrentIP,
107       requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
108     },
109     transcoding: {
110       hls: {
111         enabled: CONFIG.TRANSCODING.HLS.ENABLED
112       },
113       webtorrent: {
114         enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
115       },
116       enabledResolutions: getEnabledResolutions()
117     },
118     import: {
119       videos: {
120         http: {
121           enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
122         },
123         torrent: {
124           enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
125         }
126       }
127     },
128     autoBlacklist: {
129       videos: {
130         ofUsers: {
131           enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
132         }
133       }
134     },
135     avatar: {
136       file: {
137         size: {
138           max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
139         },
140         extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
141       }
142     },
143     video: {
144       image: {
145         extensions: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME,
146         size: {
147           max: CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max
148         }
149       },
150       file: {
151         extensions: CONSTRAINTS_FIELDS.VIDEOS.EXTNAME
152       }
153     },
154     videoCaption: {
155       file: {
156         size: {
157           max: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max
158         },
159         extensions: CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.EXTNAME
160       }
161     },
162     user: {
163       videoQuota: CONFIG.USER.VIDEO_QUOTA,
164       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
165     },
166     trending: {
167       videos: {
168         intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
169       }
170     },
171     tracker: {
172       enabled: CONFIG.TRACKER.ENABLED
173     },
174
175     followings: {
176       instance: {
177         autoFollowIndex: {
178           indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
179         }
180       }
181     },
182
183     broadcastMessage: {
184       enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
185       message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
186       level: CONFIG.BROADCAST_MESSAGE.LEVEL,
187       dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
188     }
189   }
190
191   return res.json(json)
192 }
193
194 function getAbout (req: express.Request, res: express.Response) {
195   const about: About = {
196     instance: {
197       name: CONFIG.INSTANCE.NAME,
198       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
199       description: CONFIG.INSTANCE.DESCRIPTION,
200       terms: CONFIG.INSTANCE.TERMS,
201       codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
202
203       hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
204
205       creationReason: CONFIG.INSTANCE.CREATION_REASON,
206       moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
207       administrator: CONFIG.INSTANCE.ADMINISTRATOR,
208       maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
209       businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
210
211       languages: CONFIG.INSTANCE.LANGUAGES,
212       categories: CONFIG.INSTANCE.CATEGORIES
213     }
214   }
215
216   return res.json(about).end()
217 }
218
219 function getCustomConfig (req: express.Request, res: express.Response) {
220   const data = customConfig()
221
222   return res.json(data).end()
223 }
224
225 async function deleteCustomConfig (req: express.Request, res: express.Response) {
226   await remove(CONFIG.CUSTOM_FILE)
227
228   auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig()))
229
230   reloadConfig()
231   ClientHtml.invalidCache()
232
233   const data = customConfig()
234
235   return res.json(data).end()
236 }
237
238 async function updateCustomConfig (req: express.Request, res: express.Response) {
239   const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
240
241   // camelCase to snake_case key + Force number conversion
242   const toUpdateJSON = convertCustomConfigBody(req.body)
243
244   await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
245
246   reloadConfig()
247   ClientHtml.invalidCache()
248
249   const data = customConfig()
250
251   auditLogger.update(
252     getAuditIdFromRes(res),
253     new CustomConfigAuditView(data),
254     oldCustomConfigAuditKeys
255   )
256
257   return res.json(data).end()
258 }
259
260 function getRegisteredThemes () {
261   return PluginManager.Instance.getRegisteredThemes()
262                       .map(t => ({
263                         name: t.name,
264                         version: t.version,
265                         description: t.description,
266                         css: t.css,
267                         clientScripts: t.clientScripts
268                       }))
269 }
270
271 function getEnabledResolutions () {
272   return Object.keys(CONFIG.TRANSCODING.RESOLUTIONS)
273                .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
274                .map(r => parseInt(r, 10))
275 }
276
277 function getRegisteredPlugins () {
278   return PluginManager.Instance.getRegisteredPlugins()
279                       .map(p => ({
280                         name: p.name,
281                         version: p.version,
282                         description: p.description,
283                         clientScripts: p.clientScripts
284                       }))
285 }
286
287 function getIdAndPassAuthPlugins () {
288   const result: RegisteredIdAndPassAuthConfig[] = []
289
290   for (const p of PluginManager.Instance.getIdAndPassAuths()) {
291     for (const auth of p.idAndPassAuths) {
292       result.push({
293         npmName: p.npmName,
294         name: p.name,
295         version: p.version,
296         authName: auth.authName,
297         weight: auth.getWeight()
298       })
299     }
300   }
301
302   return result
303 }
304
305 function getExternalAuthsPlugins () {
306   const result: RegisteredExternalAuthConfig[] = []
307
308   for (const p of PluginManager.Instance.getExternalAuths()) {
309     for (const auth of p.externalAuths) {
310       result.push({
311         npmName: p.npmName,
312         name: p.name,
313         version: p.version,
314         authName: auth.authName,
315         authDisplayName: auth.authDisplayName()
316       })
317     }
318   }
319
320   return result
321 }
322
323 // ---------------------------------------------------------------------------
324
325 export {
326   configRouter,
327   getEnabledResolutions,
328   getRegisteredPlugins,
329   getRegisteredThemes
330 }
331
332 // ---------------------------------------------------------------------------
333
334 function customConfig (): CustomConfig {
335   return {
336     instance: {
337       name: CONFIG.INSTANCE.NAME,
338       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
339       description: CONFIG.INSTANCE.DESCRIPTION,
340       terms: CONFIG.INSTANCE.TERMS,
341       codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
342
343       creationReason: CONFIG.INSTANCE.CREATION_REASON,
344       moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
345       administrator: CONFIG.INSTANCE.ADMINISTRATOR,
346       maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
347       businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
348       hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
349
350       languages: CONFIG.INSTANCE.LANGUAGES,
351       categories: CONFIG.INSTANCE.CATEGORIES,
352
353       isNSFW: CONFIG.INSTANCE.IS_NSFW,
354       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
355       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
356       customizations: {
357         css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
358         javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
359       }
360     },
361     theme: {
362       default: CONFIG.THEME.DEFAULT
363     },
364     services: {
365       twitter: {
366         username: CONFIG.SERVICES.TWITTER.USERNAME,
367         whitelisted: CONFIG.SERVICES.TWITTER.WHITELISTED
368       }
369     },
370     cache: {
371       previews: {
372         size: CONFIG.CACHE.PREVIEWS.SIZE
373       },
374       captions: {
375         size: CONFIG.CACHE.VIDEO_CAPTIONS.SIZE
376       }
377     },
378     signup: {
379       enabled: CONFIG.SIGNUP.ENABLED,
380       limit: CONFIG.SIGNUP.LIMIT,
381       requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
382     },
383     admin: {
384       email: CONFIG.ADMIN.EMAIL
385     },
386     contactForm: {
387       enabled: CONFIG.CONTACT_FORM.ENABLED
388     },
389     user: {
390       videoQuota: CONFIG.USER.VIDEO_QUOTA,
391       videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
392     },
393     transcoding: {
394       enabled: CONFIG.TRANSCODING.ENABLED,
395       allowAdditionalExtensions: CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS,
396       allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
397       threads: CONFIG.TRANSCODING.THREADS,
398       resolutions: {
399         '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
400         '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
401         '360p': CONFIG.TRANSCODING.RESOLUTIONS['360p'],
402         '480p': CONFIG.TRANSCODING.RESOLUTIONS['480p'],
403         '720p': CONFIG.TRANSCODING.RESOLUTIONS['720p'],
404         '1080p': CONFIG.TRANSCODING.RESOLUTIONS['1080p'],
405         '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
406       },
407       webtorrent: {
408         enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
409       },
410       hls: {
411         enabled: CONFIG.TRANSCODING.HLS.ENABLED
412       }
413     },
414     import: {
415       videos: {
416         http: {
417           enabled: CONFIG.IMPORT.VIDEOS.HTTP.ENABLED
418         },
419         torrent: {
420           enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
421         }
422       }
423     },
424     autoBlacklist: {
425       videos: {
426         ofUsers: {
427           enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
428         }
429       }
430     },
431     followers: {
432       instance: {
433         enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
434         manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
435       }
436     },
437     followings: {
438       instance: {
439         autoFollowBack: {
440           enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
441         },
442
443         autoFollowIndex: {
444           enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
445           indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
446         }
447       }
448     },
449     broadcastMessage: {
450       enabled: CONFIG.BROADCAST_MESSAGE.ENABLED,
451       message: CONFIG.BROADCAST_MESSAGE.MESSAGE,
452       level: CONFIG.BROADCAST_MESSAGE.LEVEL,
453       dismissable: CONFIG.BROADCAST_MESSAGE.DISMISSABLE
454     },
455     search: {
456       remoteUri: {
457         users: CONFIG.SEARCH.REMOTE_URI.USERS,
458         anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
459       },
460       searchIndex: {
461         enabled: CONFIG.SEARCH.SEARCH_INDEX.ENABLED,
462         url: CONFIG.SEARCH.SEARCH_INDEX.URL,
463         disableLocalSearch: CONFIG.SEARCH.SEARCH_INDEX.DISABLE_LOCAL_SEARCH,
464         isDefaultSearch: CONFIG.SEARCH.SEARCH_INDEX.IS_DEFAULT_SEARCH
465       }
466     }
467   }
468 }
469
470 function convertCustomConfigBody (body: CustomConfig) {
471   function keyConverter (k: string) {
472     // Transcoding resolutions exception
473     if (/^\d{3,4}p$/.exec(k)) return k
474     if (k === '0p') return k
475
476     return snakeCase(k)
477   }
478
479   function valueConverter (v: any) {
480     if (validator.isNumeric(v + '')) return parseInt('' + v, 10)
481
482     return v
483   }
484
485   return objectConverter(body, keyConverter, valueConverter)
486 }