aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-07-11 14:40:19 +0200
committerChocobozzz <chocobozzz@cpy.re>2019-07-24 10:58:16 +0200
commitdba85a1e9e9f603ba52e1ea42deaf3fdd799b1d8 (patch)
tree7695023d90b78f972abafc718346c50264587ff5
parentd00dc28dd73ad9dd419d5a5ac6ac747cefbc6e8b (diff)
downloadPeerTube-dba85a1e9e9f603ba52e1ea42deaf3fdd799b1d8.tar.gz
PeerTube-dba85a1e9e9f603ba52e1ea42deaf3fdd799b1d8.tar.zst
PeerTube-dba85a1e9e9f603ba52e1ea42deaf3fdd799b1d8.zip
WIP plugins: add plugin settings/uninstall in client
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html28
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss35
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts42
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts5
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html26
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss20
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts100
-rw-r--r--client/src/app/+admin/plugins/plugins.routes.ts2
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-api.service.ts68
-rw-r--r--client/src/app/+admin/plugins/shared/toggle-plugin-type.scss21
-rw-r--r--client/src/sass/include/_bootstrap.scss2
-rw-r--r--server/controllers/api/plugins.ts27
-rw-r--r--server/helpers/custom-validators/plugins.ts7
-rw-r--r--server/lib/plugins/plugin-manager.ts25
-rw-r--r--server/middlewares/validators/plugins.ts12
-rw-r--r--server/models/server/plugin.ts28
-rw-r--r--shared/models/plugins/peertube-plugin.model.ts3
17 files changed, 404 insertions, 47 deletions
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
index 6bb8bcd75..d4501490f 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html
@@ -7,7 +7,31 @@
7</div> 7</div>
8 8
9<div class="plugins" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"> 9<div class="plugins" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true">
10 <div class="section plugin" *ngFor="let plugin of plugins"> 10 <div class="card plugin" *ngFor="let plugin of plugins">
11 {{ plugin.name }} 11 <div class="card-body">
12 <div class="first-row">
13 <a class="plugin-name" [routerLink]="getShowRouterLink(plugin)" title="Show plugin settings">{{ plugin.name }}</a>
14
15 <span class="plugin-version">{{ plugin.version }}</span>
16 </div>
17
18 <div class="second-row">
19 <div class="description">{{ plugin.description }}</div>
20
21 <div class="buttons">
22 <a class="action-button action-button-edit grey-button" target="_blank" rel="noopener noreferrer"
23 [href]="plugin.homepage" i18n-title title="Go to the plugin homepage"
24 >
25 <my-global-icon iconName="go"></my-global-icon>
26 <span i18n class="button-label">Homepage</span>
27 </a>
28
29
30 <my-edit-button [routerLink]="getShowRouterLink(plugin)" label="Settings" i18n-label></my-edit-button>
31
32 <my-delete-button (click)="uninstall(plugin)" label="Uninstall" i18n-label></my-delete-button>
33 </div>
34 </div>
35 </div>
12 </div> 36 </div>
13</div> 37</div>
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss
index 9e98fcd34..f250404ed 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.scss
@@ -1,8 +1,37 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.toggle-plugin-type { 4.first-row {
5 margin-bottom: 10px;
6
7 .plugin-name {
8 font-size: 16px;
9 margin-right: 10px;
10 font-weight: $font-semibold;
11 }
12
13 .plugin-version {
14 opacity: 0.6;
15 }
16}
17
18.second-row {
5 display: flex; 19 display: flex;
6 justify-content: center; 20 align-items: center;
7 margin-bottom: 30px; 21 justify-content: space-between;
22
23 .description {
24 opacity: 0.8
25 }
26
27 .buttons {
28 > *:not(:last-child) {
29 margin-right: 10px;
30 }
31 }
32}
33
34.action-button {
35 @include peertube-button-link;
36 @include button-with-icon(21px, 0, -2px);
8} 37}
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
index 9745bc36b..26a9a616e 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
@@ -3,13 +3,17 @@ import { PluginType } from '@shared/models/plugins/plugin.type'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 4import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
5import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' 5import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model'
6import { Notifier } from '@app/core' 6import { ConfirmService, Notifier } from '@app/core'
7import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' 7import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
8import { ActivatedRoute, Router } from '@angular/router'
8 9
9@Component({ 10@Component({
10 selector: 'my-plugin-list-installed', 11 selector: 'my-plugin-list-installed',
11 templateUrl: './plugin-list-installed.component.html', 12 templateUrl: './plugin-list-installed.component.html',
12 styleUrls: [ './plugin-list-installed.component.scss' ] 13 styleUrls: [
14 '../shared/toggle-plugin-type.scss',
15 './plugin-list-installed.component.scss'
16 ]
13}) 17})
14export class PluginListInstalledComponent implements OnInit { 18export class PluginListInstalledComponent implements OnInit {
15 pluginTypeOptions: { label: string, value: PluginType }[] = [] 19 pluginTypeOptions: { label: string, value: PluginType }[] = []
@@ -26,12 +30,18 @@ export class PluginListInstalledComponent implements OnInit {
26 constructor ( 30 constructor (
27 private i18n: I18n, 31 private i18n: I18n,
28 private pluginService: PluginApiService, 32 private pluginService: PluginApiService,
29 private notifier: Notifier 33 private notifier: Notifier,
34 private confirmService: ConfirmService,
35 private router: Router,
36 private route: ActivatedRoute
30 ) { 37 ) {
31 this.pluginTypeOptions = this.pluginService.getPluginTypeOptions() 38 this.pluginTypeOptions = this.pluginService.getPluginTypeOptions()
32 } 39 }
33 40
34 ngOnInit () { 41 ngOnInit () {
42 const query = this.route.snapshot.queryParams
43 if (query['pluginType']) this.pluginType = parseInt(query['pluginType'], 10)
44
35 this.reloadPlugins() 45 this.reloadPlugins()
36 } 46 }
37 47
@@ -39,6 +49,8 @@ export class PluginListInstalledComponent implements OnInit {
39 this.pagination.currentPage = 1 49 this.pagination.currentPage = 1
40 this.plugins = [] 50 this.plugins = []
41 51
52 this.router.navigate([], { queryParams: { pluginType: this.pluginType }})
53
42 this.loadMorePlugins() 54 this.loadMorePlugins()
43 } 55 }
44 56
@@ -69,4 +81,28 @@ export class PluginListInstalledComponent implements OnInit {
69 81
70 return this.i18n('You don\'t have themes installed yet.') 82 return this.i18n('You don\'t have themes installed yet.')
71 } 83 }
84
85 async uninstall (plugin: PeerTubePlugin) {
86 const res = await this.confirmService.confirm(
87 this.i18n('Do you really want to uninstall {{pluginName}}?', { pluginName: plugin.name }),
88 this.i18n('Uninstall')
89 )
90 if (res === false) return
91
92 this.pluginService.uninstall(plugin.name, plugin.type)
93 .subscribe(
94 () => {
95 this.notifier.success(this.i18n('{{pluginName}} uninstalled.', { pluginName: plugin.name }))
96
97 this.plugins = this.plugins.filter(p => p.name !== plugin.name)
98 this.pagination.totalItems--
99 },
100
101 err => this.notifier.error(err.message)
102 )
103 }
104
105 getShowRouterLink (plugin: PeerTubePlugin) {
106 return [ '/admin', 'plugins', 'show', this.pluginService.nameToNpmName(plugin.name, plugin.type) ]
107 }
72} 108}
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
index db1f91f3d..787be2c8c 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
@@ -13,7 +13,10 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
13@Component({ 13@Component({
14 selector: 'my-plugin-search', 14 selector: 'my-plugin-search',
15 templateUrl: './plugin-search.component.html', 15 templateUrl: './plugin-search.component.html',
16 styleUrls: [ './plugin-search.component.scss' ] 16 styleUrls: [
17 '../shared/toggle-plugin-type.scss',
18 './plugin-search.component.scss'
19 ]
17}) 20})
18export class PluginSearchComponent implements OnInit { 21export class PluginSearchComponent implements OnInit {
19 pluginTypeOptions: { label: string, value: PluginType }[] = [] 22 pluginTypeOptions: { label: string, value: PluginType }[] = []
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
index e69de29bb..aae08b94d 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.html
@@ -0,0 +1,26 @@
1<ng-container *ngIf="plugin">
2
3 <h2>
4 <ng-container>{{ pluginTypeLabel }}</ng-container>
5 {{ plugin.name }}
6 </h2>
7
8 <form *ngIf="hasRegisteredSettings()" role="form" (ngSubmit)="formValidated()" [formGroup]="form">
9 <div class="form-group" *ngFor="let setting of registeredSettings">
10 <label [attr.for]="setting.name">{{ setting.label }}</label>
11
12 <input *ngIf="setting.type === 'input'" type="text" [id]="setting.name" [formControlName]="setting.name" />
13
14 <div *ngIf="formErrors[setting.name]" class="form-error">
15 {{ formErrors[setting.name] }}
16 </div>
17 </div>
18
19 <input type="submit" i18n value="Update plugin settings" [disabled]="!form.valid">
20 </form>
21
22 <div *ngIf="!hasRegisteredSettings()" i18n class="no-settings">
23 This {{ pluginTypeLabel }} does not have settings.
24 </div>
25
26</ng-container>
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
index 5e6774739..42fc1b634 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.scss
@@ -1,2 +1,22 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3
4h2 {
5 margin-bottom: 20px;
6}
7
8input:not([type=submit]) {
9 @include peertube-input-text(340px);
10 display: block;
11}
12
13.peertube-select-container {
14 @include peertube-select-container(340px);
15}
16
17input[type=submit], button {
18 @include peertube-button;
19 @include orange-button;
20
21 margin-top: 10px;
22}
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
index f65599532..8750bfd38 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
@@ -1,14 +1,110 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
5import { Notifier } from '@app/core'
6import { ActivatedRoute } from '@angular/router'
7import { Subscription } from 'rxjs'
8import { map, switchMap } from 'rxjs/operators'
9import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model'
10import { BuildFormArgument, BuildFormDefaultValues, FormReactive, FormValidatorService } from '@app/shared'
2 11
3@Component({ 12@Component({
4 selector: 'my-plugin-show-installed', 13 selector: 'my-plugin-show-installed',
5 templateUrl: './plugin-show-installed.component.html', 14 templateUrl: './plugin-show-installed.component.html',
6 styleUrls: [ './plugin-show-installed.component.scss' ] 15 styleUrls: [ './plugin-show-installed.component.scss' ]
7}) 16})
8export class PluginShowInstalledComponent implements OnInit { 17export class PluginShowInstalledComponent extends FormReactive implements OnInit, OnDestroy{
18 plugin: PeerTubePlugin
19 registeredSettings: RegisterSettingOptions[] = []
20 pluginTypeLabel: string
21
22 private sub: Subscription
23
24 constructor (
25 protected formValidatorService: FormValidatorService,
26 private i18n: I18n,
27 private pluginService: PluginApiService,
28 private notifier: Notifier,
29 private route: ActivatedRoute
30 ) {
31 super()
32 }
9 33
10 ngOnInit () { 34 ngOnInit () {
35 this.sub = this.route.params.subscribe(
36 routeParams => {
37 const npmName = routeParams['npmName']
38
39 this.loadPlugin(npmName)
40 }
41 )
42 }
43
44 ngOnDestroy () {
45 if (this.sub) this.sub.unsubscribe()
46 }
47
48 formValidated () {
49 const settings = this.form.value
50
51 this.pluginService.updatePluginSettings(this.plugin.name, this.plugin.type, settings)
52 .subscribe(
53 () => {
54 this.notifier.success(this.i18n('Settings updated.'))
55 },
56
57 err => this.notifier.error(err.message)
58 )
59 }
60
61 hasRegisteredSettings () {
62 return Array.isArray(this.registeredSettings) && this.registeredSettings.length !== 0
63 }
64
65 private loadPlugin (npmName: string) {
66 this.pluginService.getPlugin(npmName)
67 .pipe(switchMap(plugin => {
68 return this.pluginService.getPluginRegisteredSettings(plugin.name, plugin.type)
69 .pipe(map(data => ({ plugin, registeredSettings: data.settings })))
70 }))
71 .subscribe(
72 ({ plugin, registeredSettings }) => {
73 this.plugin = plugin
74 this.registeredSettings = registeredSettings
75
76 this.pluginTypeLabel = this.pluginService.getPluginTypeLabel(this.plugin.type)
77
78 this.buildSettingsForm()
79 },
80
81 err => this.notifier.error(err.message)
82 )
83 }
84
85 private buildSettingsForm () {
86 const defaultValues: BuildFormDefaultValues = {}
87 const buildOptions: BuildFormArgument = {}
88 const settingsValues: any = {}
89
90 for (const setting of this.registeredSettings) {
91 buildOptions[ setting.name ] = null
92 settingsValues[ setting.name ] = this.getSetting(setting.name)
93 }
94
95 this.buildForm(buildOptions)
96
97 this.form.patchValue(settingsValues)
98 }
99
100 private getSetting (name: string) {
101 const settings = this.plugin.settings
102
103 if (settings && settings[name]) return settings[name]
104
105 const registered = this.registeredSettings.find(r => r.name === name)
11 106
107 return registered.default
12 } 108 }
13 109
14} 110}
diff --git a/client/src/app/+admin/plugins/plugins.routes.ts b/client/src/app/+admin/plugins/plugins.routes.ts
index 58b5534fb..02e8fd324 100644
--- a/client/src/app/+admin/plugins/plugins.routes.ts
+++ b/client/src/app/+admin/plugins/plugins.routes.ts
@@ -40,7 +40,7 @@ export const PluginsRoutes: Routes = [
40 } 40 }
41 }, 41 },
42 { 42 {
43 path: 'show/:name', 43 path: 'show/:npmName',
44 component: PluginShowInstalledComponent, 44 component: PluginShowInstalledComponent,
45 data: { 45 data: {
46 meta: { 46 meta: {
diff --git a/client/src/app/+admin/plugins/shared/plugin-api.service.ts b/client/src/app/+admin/plugins/shared/plugin-api.service.ts
index bfc2b918f..1d33cd179 100644
--- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts
+++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts
@@ -8,6 +8,9 @@ import { PluginType } from '@shared/models/plugins/plugin.type'
8import { ComponentPagination } from '@app/shared/rest/component-pagination.model' 8import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
9import { ResultList } from '@shared/models' 9import { ResultList } from '@shared/models'
10import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' 10import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
11import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model'
12import { InstallPlugin } from '@shared/models/plugins/install-plugin.model'
13import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model'
11 14
12@Injectable() 15@Injectable()
13export class PluginApiService { 16export class PluginApiService {
@@ -23,16 +26,24 @@ export class PluginApiService {
23 getPluginTypeOptions () { 26 getPluginTypeOptions () {
24 return [ 27 return [
25 { 28 {
26 label: this.i18n('Plugin'), 29 label: this.i18n('Plugins'),
27 value: PluginType.PLUGIN 30 value: PluginType.PLUGIN
28 }, 31 },
29 { 32 {
30 label: this.i18n('Theme'), 33 label: this.i18n('Themes'),
31 value: PluginType.THEME 34 value: PluginType.THEME
32 } 35 }
33 ] 36 ]
34 } 37 }
35 38
39 getPluginTypeLabel (type: PluginType) {
40 if (type === PluginType.PLUGIN) {
41 return this.i18n('plugin')
42 }
43
44 return this.i18n('theme')
45 }
46
36 getPlugins ( 47 getPlugins (
37 type: PluginType, 48 type: PluginType,
38 componentPagination: ComponentPagination, 49 componentPagination: ComponentPagination,
@@ -47,4 +58,57 @@ export class PluginApiService {
47 return this.authHttp.get<ResultList<PeerTubePlugin>>(PluginApiService.BASE_APPLICATION_URL, { params }) 58 return this.authHttp.get<ResultList<PeerTubePlugin>>(PluginApiService.BASE_APPLICATION_URL, { params })
48 .pipe(catchError(res => this.restExtractor.handleError(res))) 59 .pipe(catchError(res => this.restExtractor.handleError(res)))
49 } 60 }
61
62 getPlugin (npmName: string) {
63 const path = PluginApiService.BASE_APPLICATION_URL + '/' + npmName
64
65 return this.authHttp.get<PeerTubePlugin>(path)
66 .pipe(catchError(res => this.restExtractor.handleError(res)))
67 }
68
69 getPluginRegisteredSettings (pluginName: string, pluginType: PluginType) {
70 const path = PluginApiService.BASE_APPLICATION_URL + '/' + this.nameToNpmName(pluginName, pluginType) + '/registered-settings'
71
72 return this.authHttp.get<{ settings: RegisterSettingOptions[] }>(path)
73 .pipe(catchError(res => this.restExtractor.handleError(res)))
74 }
75
76 updatePluginSettings (pluginName: string, pluginType: PluginType, settings: any) {
77 const path = PluginApiService.BASE_APPLICATION_URL + '/' + this.nameToNpmName(pluginName, pluginType) + '/settings'
78
79 return this.authHttp.put(path, { settings })
80 .pipe(catchError(res => this.restExtractor.handleError(res)))
81 }
82
83 uninstall (pluginName: string, pluginType: PluginType) {
84 const body: ManagePlugin = {
85 npmName: this.nameToNpmName(pluginName, pluginType)
86 }
87
88 return this.authHttp.post(PluginApiService.BASE_APPLICATION_URL + '/uninstall', body)
89 .pipe(catchError(res => this.restExtractor.handleError(res)))
90 }
91
92 install (npmName: string) {
93 const body: InstallPlugin = {
94 npmName
95 }
96
97 return this.authHttp.post(PluginApiService.BASE_APPLICATION_URL + '/install', body)
98 .pipe(catchError(res => this.restExtractor.handleError(res)))
99 }
100
101 nameToNpmName (name: string, type: PluginType) {
102 const prefix = type === PluginType.PLUGIN
103 ? 'peertube-plugin-'
104 : 'peertube-theme-'
105
106 return prefix + name
107 }
108
109 pluginTypeFromNpmName (npmName: string) {
110 return npmName.startsWith('peertube-plugin-')
111 ? PluginType.PLUGIN
112 : PluginType.THEME
113 }
50} 114}
diff --git a/client/src/app/+admin/plugins/shared/toggle-plugin-type.scss b/client/src/app/+admin/plugins/shared/toggle-plugin-type.scss
new file mode 100644
index 000000000..ea2eda28c
--- /dev/null
+++ b/client/src/app/+admin/plugins/shared/toggle-plugin-type.scss
@@ -0,0 +1,21 @@
1@import '_variables';
2@import '_mixins';
3
4.toggle-plugin-type {
5 display: flex;
6 justify-content: center;
7 margin-bottom: 30px;
8
9 p-selectButton {
10 /deep/ {
11 .ui-button-text {
12 font-size: 15px;
13 }
14
15 .ui-button.ui-state-active {
16 background-color: var(--mainColor);
17 border-color: var(--mainColor);
18 }
19 }
20 }
21}
diff --git a/client/src/sass/include/_bootstrap.scss b/client/src/sass/include/_bootstrap.scss
index 0a9c9a903..b1a23be6b 100644
--- a/client/src/sass/include/_bootstrap.scss
+++ b/client/src/sass/include/_bootstrap.scss
@@ -20,7 +20,7 @@
20//@import '~bootstrap/scss/custom-forms'; 20//@import '~bootstrap/scss/custom-forms';
21@import '~bootstrap/scss/nav'; 21@import '~bootstrap/scss/nav';
22//@import '~bootstrap/scss/navbar'; 22//@import '~bootstrap/scss/navbar';
23//@import '~bootstrap/scss/card'; 23@import '~bootstrap/scss/card';
24//@import '~bootstrap/scss/breadcrumb'; 24//@import '~bootstrap/scss/breadcrumb';
25//@import '~bootstrap/scss/pagination'; 25//@import '~bootstrap/scss/pagination';
26@import '~bootstrap/scss/badge'; 26@import '~bootstrap/scss/badge';
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 89cc67f54..f17e8cab9 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -12,7 +12,7 @@ import { pluginsSortValidator } from '../../middlewares/validators'
12import { PluginModel } from '../../models/server/plugin' 12import { PluginModel } from '../../models/server/plugin'
13import { UserRight } from '../../../shared/models/users' 13import { UserRight } from '../../../shared/models/users'
14import { 14import {
15 enabledPluginValidator, 15 existingPluginValidator,
16 installPluginValidator, 16 installPluginValidator,
17 listPluginsValidator, 17 listPluginsValidator,
18 uninstallPluginValidator, 18 uninstallPluginValidator,
@@ -35,18 +35,25 @@ pluginRouter.get('/',
35 asyncMiddleware(listPlugins) 35 asyncMiddleware(listPlugins)
36) 36)
37 37
38pluginRouter.get('/:pluginName/settings', 38pluginRouter.get('/:npmName',
39 authenticate, 39 authenticate,
40 ensureUserHasRight(UserRight.MANAGE_PLUGINS), 40 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
41 asyncMiddleware(enabledPluginValidator), 41 asyncMiddleware(existingPluginValidator),
42 asyncMiddleware(listPluginSettings) 42 getPlugin
43) 43)
44 44
45pluginRouter.put('/:pluginName/settings', 45pluginRouter.get('/:npmName/registered-settings',
46 authenticate,
47 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
48 asyncMiddleware(existingPluginValidator),
49 asyncMiddleware(getPluginRegisteredSettings)
50)
51
52pluginRouter.put('/:npmName/settings',
46 authenticate, 53 authenticate,
47 ensureUserHasRight(UserRight.MANAGE_PLUGINS), 54 ensureUserHasRight(UserRight.MANAGE_PLUGINS),
48 updatePluginSettingsValidator, 55 updatePluginSettingsValidator,
49 asyncMiddleware(enabledPluginValidator), 56 asyncMiddleware(existingPluginValidator),
50 asyncMiddleware(updatePluginSettings) 57 asyncMiddleware(updatePluginSettings)
51) 58)
52 59
@@ -85,6 +92,12 @@ async function listPlugins (req: express.Request, res: express.Response) {
85 return res.json(getFormattedObjects(resultList.data, resultList.total)) 92 return res.json(getFormattedObjects(resultList.data, resultList.total))
86} 93}
87 94
95function getPlugin (req: express.Request, res: express.Response) {
96 const plugin = res.locals.plugin
97
98 return res.json(plugin.toFormattedJSON())
99}
100
88async function installPlugin (req: express.Request, res: express.Response) { 101async function installPlugin (req: express.Request, res: express.Response) {
89 const body: InstallPlugin = req.body 102 const body: InstallPlugin = req.body
90 103
@@ -101,7 +114,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) {
101 return res.sendStatus(204) 114 return res.sendStatus(204)
102} 115}
103 116
104async function listPluginSettings (req: express.Request, res: express.Response) { 117async function getPluginRegisteredSettings (req: express.Request, res: express.Response) {
105 const plugin = res.locals.plugin 118 const plugin = res.locals.plugin
106 119
107 const settings = await PluginManager.Instance.getSettings(plugin.name) 120 const settings = await PluginManager.Instance.getSettings(plugin.name)
diff --git a/server/helpers/custom-validators/plugins.ts b/server/helpers/custom-validators/plugins.ts
index 4ab5f9ce8..064af9ead 100644
--- a/server/helpers/custom-validators/plugins.ts
+++ b/server/helpers/custom-validators/plugins.ts
@@ -41,6 +41,10 @@ function isPluginEngineValid (engine: any) {
41 return exists(engine) && exists(engine.peertube) 41 return exists(engine) && exists(engine.peertube)
42} 42}
43 43
44function isPluginHomepage (value: string) {
45 return isUrlValid(value)
46}
47
44function isStaticDirectoriesValid (staticDirs: any) { 48function isStaticDirectoriesValid (staticDirs: any) {
45 if (!exists(staticDirs) || typeof staticDirs !== 'object') return false 49 if (!exists(staticDirs) || typeof staticDirs !== 'object') return false
46 50
@@ -70,7 +74,7 @@ function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginT
70 return isNpmPluginNameValid(packageJSON.name) && 74 return isNpmPluginNameValid(packageJSON.name) &&
71 isPluginDescriptionValid(packageJSON.description) && 75 isPluginDescriptionValid(packageJSON.description) &&
72 isPluginEngineValid(packageJSON.engine) && 76 isPluginEngineValid(packageJSON.engine) &&
73 isUrlValid(packageJSON.homepage) && 77 isPluginHomepage(packageJSON.homepage) &&
74 exists(packageJSON.author) && 78 exists(packageJSON.author) &&
75 isUrlValid(packageJSON.bugs) && 79 isUrlValid(packageJSON.bugs) &&
76 (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) && 80 (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) &&
@@ -88,6 +92,7 @@ export {
88 isPluginTypeValid, 92 isPluginTypeValid,
89 isPackageJSONValid, 93 isPackageJSONValid,
90 isThemeValid, 94 isThemeValid,
95 isPluginHomepage,
91 isPluginVersionValid, 96 isPluginVersionValid,
92 isPluginNameValid, 97 isPluginNameValid,
93 isPluginDescriptionValid, 98 isPluginDescriptionValid,
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 3d8375acd..8cdeff446 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -89,6 +89,8 @@ export class PluginManager {
89 async runHook (hookName: string, param?: any) { 89 async runHook (hookName: string, param?: any) {
90 let result = param 90 let result = param
91 91
92 if (!this.hooks[hookName]) return result
93
92 const wait = hookName.startsWith('static:') 94 const wait = hookName.startsWith('static:')
93 95
94 for (const hook of this.hooks[hookName]) { 96 for (const hook of this.hooks[hookName]) {
@@ -162,8 +164,8 @@ export class PluginManager {
162 : await installNpmPlugin(toInstall, version) 164 : await installNpmPlugin(toInstall, version)
163 165
164 name = fromDisk ? basename(toInstall) : toInstall 166 name = fromDisk ? basename(toInstall) : toInstall
165 const pluginType = name.startsWith('peertube-theme-') ? PluginType.THEME : PluginType.PLUGIN 167 const pluginType = PluginModel.getTypeFromNpmName(name)
166 const pluginName = this.normalizePluginName(name) 168 const pluginName = PluginModel.normalizePluginName(name)
167 169
168 const packageJSON = this.getPackageJSON(pluginName, pluginType) 170 const packageJSON = this.getPackageJSON(pluginName, pluginType)
169 if (!isPackageJSONValid(packageJSON, pluginType)) { 171 if (!isPackageJSONValid(packageJSON, pluginType)) {
@@ -173,6 +175,7 @@ export class PluginManager {
173 [ plugin ] = await PluginModel.upsert({ 175 [ plugin ] = await PluginModel.upsert({
174 name: pluginName, 176 name: pluginName,
175 description: packageJSON.description, 177 description: packageJSON.description,
178 homepage: packageJSON.homepage,
176 type: pluginType, 179 type: pluginType,
177 version: packageJSON.version, 180 version: packageJSON.version,
178 enabled: true, 181 enabled: true,
@@ -196,10 +199,10 @@ export class PluginManager {
196 await this.registerPluginOrTheme(plugin) 199 await this.registerPluginOrTheme(plugin)
197 } 200 }
198 201
199 async uninstall (packageName: string) { 202 async uninstall (npmName: string) {
200 logger.info('Uninstalling plugin %s.', packageName) 203 logger.info('Uninstalling plugin %s.', npmName)
201 204
202 const pluginName = this.normalizePluginName(packageName) 205 const pluginName = PluginModel.normalizePluginName(npmName)
203 206
204 try { 207 try {
205 await this.unregister(pluginName) 208 await this.unregister(pluginName)
@@ -207,9 +210,9 @@ export class PluginManager {
207 logger.warn('Cannot unregister plugin %s.', pluginName, { err }) 210 logger.warn('Cannot unregister plugin %s.', pluginName, { err })
208 } 211 }
209 212
210 const plugin = await PluginModel.load(pluginName) 213 const plugin = await PluginModel.loadByNpmName(npmName)
211 if (!plugin || plugin.uninstalled === true) { 214 if (!plugin || plugin.uninstalled === true) {
212 logger.error('Cannot uninstall plugin %s: it does not exist or is already uninstalled.', packageName) 215 logger.error('Cannot uninstall plugin %s: it does not exist or is already uninstalled.', npmName)
213 return 216 return
214 } 217 }
215 218
@@ -218,9 +221,9 @@ export class PluginManager {
218 221
219 await plugin.save() 222 await plugin.save()
220 223
221 await removeNpmPlugin(packageName) 224 await removeNpmPlugin(npmName)
222 225
223 logger.info('Plugin %s uninstalled.', packageName) 226 logger.info('Plugin %s uninstalled.', npmName)
224 } 227 }
225 228
226 // ###################### Private register ###################### 229 // ###################### Private register ######################
@@ -353,10 +356,6 @@ export class PluginManager {
353 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', prefix + pluginName) 356 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', prefix + pluginName)
354 } 357 }
355 358
356 private normalizePluginName (name: string) {
357 return name.replace(/^peertube-((theme)|(plugin))-/, '')
358 }
359
360 // ###################### Private getters ###################### 359 // ###################### Private getters ######################
361 360
362 private getRegisteredPluginsOrThemes (type: PluginType) { 361 private getRegisteredPluginsOrThemes (type: PluginType) {
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index 265ac7c17..a06add6b8 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -63,7 +63,7 @@ const uninstallPluginValidator = [
63 body('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), 63 body('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
64 64
65 (req: express.Request, res: express.Response, next: express.NextFunction) => { 65 (req: express.Request, res: express.Response, next: express.NextFunction) => {
66 logger.debug('Checking managePluginValidator parameters', { parameters: req.body }) 66 logger.debug('Checking uninstallPluginValidator parameters', { parameters: req.body })
67 67
68 if (areValidationErrors(req, res)) return 68 if (areValidationErrors(req, res)) return
69 69
@@ -71,15 +71,15 @@ const uninstallPluginValidator = [
71 } 71 }
72] 72]
73 73
74const enabledPluginValidator = [ 74const existingPluginValidator = [
75 body('name').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), 75 param('npmName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'),
76 76
77 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 77 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
78 logger.debug('Checking enabledPluginValidator parameters', { parameters: req.body }) 78 logger.debug('Checking enabledPluginValidator parameters', { parameters: req.params })
79 79
80 if (areValidationErrors(req, res)) return 80 if (areValidationErrors(req, res)) return
81 81
82 const plugin = await PluginModel.load(req.body.name) 82 const plugin = await PluginModel.loadByNpmName(req.params.npmName)
83 if (!plugin) { 83 if (!plugin) {
84 return res.status(404) 84 return res.status(404)
85 .json({ error: 'Plugin not found' }) 85 .json({ error: 'Plugin not found' })
@@ -110,7 +110,7 @@ export {
110 servePluginStaticDirectoryValidator, 110 servePluginStaticDirectoryValidator,
111 updatePluginSettingsValidator, 111 updatePluginSettingsValidator,
112 uninstallPluginValidator, 112 uninstallPluginValidator,
113 enabledPluginValidator, 113 existingPluginValidator,
114 installPluginValidator, 114 installPluginValidator,
115 listPluginsValidator 115 listPluginsValidator
116} 116}
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts
index 059a442de..60abaec65 100644
--- a/server/models/server/plugin.ts
+++ b/server/models/server/plugin.ts
@@ -1,7 +1,7 @@
1import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { getSort, throwIfNotValid } from '../utils' 2import { getSort, throwIfNotValid } from '../utils'
3import { 3import {
4 isPluginDescriptionValid, 4 isPluginDescriptionValid, isPluginHomepage,
5 isPluginNameValid, 5 isPluginNameValid,
6 isPluginTypeValid, 6 isPluginTypeValid,
7 isPluginVersionValid 7 isPluginVersionValid
@@ -20,7 +20,7 @@ import { FindAndCountOptions } from 'sequelize'
20 tableName: 'plugin', 20 tableName: 'plugin',
21 indexes: [ 21 indexes: [
22 { 22 {
23 fields: [ 'name' ], 23 fields: [ 'name', 'type' ],
24 unique: true 24 unique: true
25 } 25 }
26 ] 26 ]
@@ -59,6 +59,11 @@ export class PluginModel extends Model<PluginModel> {
59 @Column 59 @Column
60 description: string 60 description: string
61 61
62 @AllowNull(false)
63 @Is('PluginHomepage', value => throwIfNotValid(value, isPluginHomepage, 'homepage'))
64 @Column
65 homepage: string
66
62 @AllowNull(true) 67 @AllowNull(true)
63 @Column(DataType.JSONB) 68 @Column(DataType.JSONB)
64 settings: any 69 settings: any
@@ -84,10 +89,14 @@ export class PluginModel extends Model<PluginModel> {
84 return PluginModel.findAll(query) 89 return PluginModel.findAll(query)
85 } 90 }
86 91
87 static load (pluginName: string) { 92 static loadByNpmName (npmName: string) {
93 const name = this.normalizePluginName(npmName)
94 const type = this.getTypeFromNpmName(npmName)
95
88 const query = { 96 const query = {
89 where: { 97 where: {
90 name: pluginName 98 name,
99 type
91 } 100 }
92 } 101 }
93 102
@@ -150,6 +159,16 @@ export class PluginModel extends Model<PluginModel> {
150 }) 159 })
151 } 160 }
152 161
162 static normalizePluginName (name: string) {
163 return name.replace(/^peertube-((theme)|(plugin))-/, '')
164 }
165
166 static getTypeFromNpmName (npmName: string) {
167 return npmName.startsWith('peertube-plugin-')
168 ? PluginType.PLUGIN
169 : PluginType.THEME
170 }
171
153 toFormattedJSON (): PeerTubePlugin { 172 toFormattedJSON (): PeerTubePlugin {
154 return { 173 return {
155 name: this.name, 174 name: this.name,
@@ -159,6 +178,7 @@ export class PluginModel extends Model<PluginModel> {
159 uninstalled: this.uninstalled, 178 uninstalled: this.uninstalled,
160 peertubeEngine: this.peertubeEngine, 179 peertubeEngine: this.peertubeEngine,
161 description: this.description, 180 description: this.description,
181 homepage: this.homepage,
162 settings: this.settings, 182 settings: this.settings,
163 createdAt: this.createdAt, 183 createdAt: this.createdAt,
164 updatedAt: this.updatedAt 184 updatedAt: this.updatedAt
diff --git a/shared/models/plugins/peertube-plugin.model.ts b/shared/models/plugins/peertube-plugin.model.ts
index 2a1dfb3a7..de3c7741b 100644
--- a/shared/models/plugins/peertube-plugin.model.ts
+++ b/shared/models/plugins/peertube-plugin.model.ts
@@ -6,7 +6,8 @@ export interface PeerTubePlugin {
6 uninstalled: boolean 6 uninstalled: boolean
7 peertubeEngine: string 7 peertubeEngine: string
8 description: string 8 description: string
9 settings: any 9 homepage: string
10 settings: { [ name: string ]: string }
10 createdAt: Date 11 createdAt: Date
11 updatedAt: Date 12 updatedAt: Date
12} 13}