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 f10b4eb8d..6d2155332 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 @@ -2,7 +2,7 @@ -
+
{{ getNoResultMessage() }}
@@ -28,7 +28,7 @@ - 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 7641c507b..0b54ffda3 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,41 +1,6 @@ @import '_variables'; @import '_mixins'; -.first-row { - margin-bottom: 10px; - - .plugin-name { - font-size: 16px; - margin-right: 10px; - font-weight: $font-semibold; - } - - .plugin-version { - opacity: 0.6; - } -} - -.second-row { - display: flex; - align-items: center; - justify-content: space-between; - - .description { - opacity: 0.8 - } - - .buttons { - > *:not(:last-child) { - margin-right: 10px; - } - } -} - -.action-button { - @include peertube-button-link; - @include button-with-icon(21px, 0, -2px); -} - .update-button[disabled="true"] /deep/ .action-button { cursor: default !important; } 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 67a11c3a8..9809759db 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 @@ -6,13 +6,14 @@ import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pa import { ConfirmService, Notifier } from '@app/core' import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' import { ActivatedRoute, Router } from '@angular/router' -import { compareSemVer } from '@app/shared/misc/utils' +import { compareSemVer } from '@shared/core-utils/miscs/miscs' @Component({ selector: 'my-plugin-list-installed', templateUrl: './plugin-list-installed.component.html', styleUrls: [ '../shared/toggle-plugin-type.scss', + '../shared/plugin-list.component.scss', './plugin-list-installed.component.scss' ] }) diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html index e69de29bb..7dd103979 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html @@ -0,0 +1,55 @@ +
+ +
+ + + +
+ + + Popular + + + + + + + {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" + + +
+ +
+ No results. +
+ +
+
+
+
+ {{ plugin.name }} + + {{ plugin.latestVersion }} +
+ +
+
{{ plugin.description }}
+ + +
+
+
+
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss index 5e6774739..ad6ff89da 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.scss @@ -1,2 +1,23 @@ @import '_variables'; @import '_mixins'; + +.search-bar { + display: flex; + justify-content: center; + margin: 30px 0; + + input { + @include peertube-input-text(60%); + height: 35px; + } +} + +.result-title { + font-size: 22px; + font-weight: 600; + margin-bottom: 15px; + + my-global-icon { + margin-right: 5px; + } +} 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 787be2c8c..935e11362 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 @@ -1,33 +1,133 @@ -import { Component, OnInit, ViewChild } from '@angular/core' +import { Component, OnInit } from '@angular/core' import { Notifier } from '@app/core' -import { SortMeta } from 'primeng/components/common/sortmeta' -import { ConfirmService, ServerService } from '../../../core' -import { RestPagination, RestTable, UserService } from '../../../shared' +import { ConfirmService } from '../../../core' import { I18n } from '@ngx-translate/i18n-polyfill' -import { User } from '../../../../../../shared' -import { UserBanModalComponent } from '@app/shared/moderation' -import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' import { PluginType } from '@shared/models/plugins/plugin.type' import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' +import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' +import { ActivatedRoute, Router } from '@angular/router' +import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' +import { Subject } from 'rxjs' +import { debounceTime, distinctUntilChanged } from 'rxjs/operators' @Component({ selector: 'my-plugin-search', templateUrl: './plugin-search.component.html', styleUrls: [ '../shared/toggle-plugin-type.scss', + '../shared/plugin-list.component.scss', './plugin-search.component.scss' ] }) export class PluginSearchComponent implements OnInit { pluginTypeOptions: { label: string, value: PluginType }[] = [] + pluginType: PluginType = PluginType.PLUGIN + + pagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 10 + } + sort = '-popularity' + + search = '' + isSearching = false + + plugins: PeerTubePluginIndex[] = [] + installing: { [name: string]: boolean } = {} + + private searchSubject = new Subject() constructor ( private i18n: I18n, - private pluginService: PluginApiService + private pluginService: PluginApiService, + private notifier: Notifier, + private confirmService: ConfirmService, + private router: Router, + private route: ActivatedRoute ) { this.pluginTypeOptions = this.pluginService.getPluginTypeOptions() } ngOnInit () { + const query = this.route.snapshot.queryParams + if (query['pluginType']) this.pluginType = parseInt(query['pluginType'], 10) + + this.searchSubject.asObservable() + .pipe( + debounceTime(400), + distinctUntilChanged() + ) + .subscribe(search => { + this.search = search + this.reloadPlugins() + }) + + this.reloadPlugins() + } + + onSearchChange (search: string) { + this.searchSubject.next(search) + } + + reloadPlugins () { + this.pagination.currentPage = 1 + this.plugins = [] + + this.router.navigate([], { queryParams: { pluginType: this.pluginType } }) + + this.loadMorePlugins() + } + + loadMorePlugins () { + this.isSearching = true + + this.pluginService.searchAvailablePlugins(this.pluginType, this.pagination, this.sort, this.search) + .subscribe( + res => { + this.isSearching = false + + this.plugins = this.plugins.concat(res.data) + this.pagination.totalItems = res.total + }, + + err => this.notifier.error(err.message) + ) + } + + onNearOfBottom () { + if (!hasMoreItems(this.pagination)) return + + this.pagination.currentPage += 1 + + this.loadMorePlugins() + } + + isInstalling (plugin: PeerTubePluginIndex) { + return !!this.installing[plugin.npmName] + } + + async install (plugin: PeerTubePluginIndex) { + if (this.installing[plugin.npmName]) return + + const res = await this.confirmService.confirm( + this.i18n('Please only install plugins or themes you trust, since they can execute any code on your instance.'), + this.i18n('Install {{pluginName}}?', { pluginName: plugin.name }) + ) + if (res === false) return + + this.installing[plugin.npmName] = true + + this.pluginService.install(plugin.npmName) + .subscribe( + () => { + this.installing[plugin.npmName] = false + + this.notifier.success(this.i18n('{{pluginName}} installed.', { pluginName: plugin.name })) + + plugin.installed = true + }, + + err => this.notifier.error(err.message) + ) } } 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 8750bfd38..b99281a37 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 @@ -7,7 +7,7 @@ import { ActivatedRoute } from '@angular/router' import { Subscription } from 'rxjs' import { map, switchMap } from 'rxjs/operators' import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model' -import { BuildFormArgument, BuildFormDefaultValues, FormReactive, FormValidatorService } from '@app/shared' +import { BuildFormArgument, FormReactive, FormValidatorService } from '@app/shared' @Component({ selector: 'my-plugin-show-installed', @@ -83,7 +83,6 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit } private buildSettingsForm () { - const defaultValues: BuildFormDefaultValues = {} const buildOptions: BuildFormArgument = {} const settingsValues: any = {} 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 89f190675..51f086a93 100644 --- a/client/src/app/+admin/plugins/shared/plugin-api.service.ts +++ b/client/src/app/+admin/plugins/shared/plugin-api.service.ts @@ -11,6 +11,7 @@ import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' import { ManagePlugin } from '@shared/models/plugins/manage-plugin.model' import { InstallOrUpdatePlugin } from '@shared/models/plugins/install-plugin.model' import { RegisterSettingOptions } from '@shared/models/plugins/register-setting.model' +import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' @Injectable() export class PluginApiService { @@ -45,7 +46,7 @@ export class PluginApiService { } getPlugins ( - type: PluginType, + pluginType: PluginType, componentPagination: ComponentPagination, sort: string ) { @@ -53,12 +54,30 @@ export class PluginApiService { let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) - params = params.append('type', type.toString()) + params = params.append('pluginType', pluginType.toString()) return this.authHttp.get>(PluginApiService.BASE_APPLICATION_URL, { params }) .pipe(catchError(res => this.restExtractor.handleError(res))) } + searchAvailablePlugins ( + pluginType: PluginType, + componentPagination: ComponentPagination, + sort: string, + search?: string + ) { + const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + params = params.append('pluginType', pluginType.toString()) + + if (search) params = params.append('search', search) + + return this.authHttp.get>(PluginApiService.BASE_APPLICATION_URL + '/available', { params }) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + getPlugin (npmName: string) { const path = PluginApiService.BASE_APPLICATION_URL + '/' + npmName diff --git a/client/src/app/+admin/plugins/shared/plugin-list.component.scss b/client/src/app/+admin/plugins/shared/plugin-list.component.scss new file mode 100644 index 000000000..f250404ed --- /dev/null +++ b/client/src/app/+admin/plugins/shared/plugin-list.component.scss @@ -0,0 +1,37 @@ +@import '_variables'; +@import '_mixins'; + +.first-row { + margin-bottom: 10px; + + .plugin-name { + font-size: 16px; + margin-right: 10px; + font-weight: $font-semibold; + } + + .plugin-version { + opacity: 0.6; + } +} + +.second-row { + display: flex; + align-items: center; + justify-content: space-between; + + .description { + opacity: 0.8 + } + + .buttons { + > *:not(:last-child) { + margin-right: 10px; + } + } +} + +.action-button { + @include peertube-button-link; + @include button-with-icon(21px, 0, -2px); +} diff --git a/client/src/app/shared/images/global-icon.component.ts b/client/src/app/shared/images/global-icon.component.ts index 5a3db4531..5b525dec1 100644 --- a/client/src/app/shared/images/global-icon.component.ts +++ b/client/src/app/shared/images/global-icon.component.ts @@ -45,6 +45,7 @@ const icons = { 'administration': require('../../../assets/images/menu/administration.html'), 'subscriptions': require('../../../assets/images/menu/subscriptions.html'), 'users': require('../../../assets/images/global/users.html'), + 'search': require('../../../assets/images/global/search.html'), 'refresh': require('../../../assets/images/global/refresh.html') } diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index 098496d45..85fc1c3a0 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts @@ -134,23 +134,6 @@ function scrollToTop () { window.scroll(0, 0) } -// Thanks https://stackoverflow.com/a/16187766 -function compareSemVer (a: string, b: string) { - const regExStrip0 = /(\.0+)+$/ - const segmentsA = a.replace(regExStrip0, '').split('.') - const segmentsB = b.replace(regExStrip0, '').split('.') - - const l = Math.min(segmentsA.length, segmentsB.length) - - for (let i = 0; i < l; i++) { - const diff = parseInt(segmentsA[ i ], 10) - parseInt(segmentsB[ i ], 10) - - if (diff) return diff - } - - return segmentsA.length - segmentsB.length -} - export { sortBy, durationToString, @@ -161,7 +144,6 @@ export { getAbsoluteAPIUrl, dateToHuman, immutableAssign, - compareSemVer, objectToFormData, objectLineFeedToHtml, removeElementFromArray, diff --git a/client/src/assets/images/global/search.html b/client/src/assets/images/global/search.html new file mode 100644 index 000000000..46ac5848b --- /dev/null +++ b/client/src/assets/images/global/search.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/config/default.yaml b/config/default.yaml index a1b2991cf..341de49b8 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -155,6 +155,13 @@ views: remote: max_age: -1 +plugins: + # The website PeerTube will ask for available PeerTube plugins + # This is an unmoderated plugin index, so only install plugins you trust + index: + enabled: true + url: 'https://packages.joinpeertube.org' + cache: previews: size: 500 # Max number of previews you want to cache diff --git a/config/production.yaml.example b/config/production.yaml.example index 6c2eb4416..28f078efe 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -156,6 +156,13 @@ views: remote: max_age: -1 +plugins: + # The website PeerTube will ask for available PeerTube plugins + # This is an unmoderated plugin index, so only install plugins you trust + index: + enabled: true + url: 'https://packages.joinpeertube.org' + ############################################################################### # diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index 14675fdf3..114cc49b6 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts @@ -8,12 +8,13 @@ import { setDefaultPagination, setDefaultSort } from '../../middlewares' -import { pluginsSortValidator } from '../../middlewares/validators' +import { availablePluginsSortValidator, pluginsSortValidator } from '../../middlewares/validators' import { PluginModel } from '../../models/server/plugin' import { UserRight } from '../../../shared/models/users' import { existingPluginValidator, installOrUpdatePluginValidator, + listAvailablePluginsValidator, listPluginsValidator, uninstallPluginValidator, updatePluginSettingsValidator @@ -22,9 +23,22 @@ import { PluginManager } from '../../lib/plugins/plugin-manager' import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' import { logger } from '../../helpers/logger' +import { listAvailablePluginsFromIndex } from '../../lib/plugins/plugin-index' +import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' const pluginRouter = express.Router() +pluginRouter.get('/available', + authenticate, + ensureUserHasRight(UserRight.MANAGE_PLUGINS), + listAvailablePluginsValidator, + paginationValidator, + availablePluginsSortValidator, + setDefaultSort, + setDefaultPagination, + asyncMiddleware(listAvailablePlugins) +) + pluginRouter.get('/', authenticate, ensureUserHasRight(UserRight.MANAGE_PLUGINS), @@ -88,10 +102,10 @@ export { // --------------------------------------------------------------------------- async function listPlugins (req: express.Request, res: express.Response) { - const type = req.query.type + const pluginType = req.query.pluginType const resultList = await PluginModel.listForApi({ - type, + pluginType, start: req.query.start, count: req.query.count, sort: req.query.sort @@ -160,3 +174,11 @@ async function updatePluginSettings (req: express.Request, res: express.Response return res.sendStatus(204) } + +async function listAvailablePlugins (req: express.Request, res: express.Response) { + const query: PeertubePluginIndexList = req.query + + const resultList = await listAvailablePluginsFromIndex(query) + + return res.json(resultList) +} diff --git a/server/initializers/config.ts b/server/initializers/config.ts index dfc4bea21..2c1b30021 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -134,6 +134,12 @@ const CONFIG = { } } }, + PLUGINS: { + INDEX: { + ENABLED: config.get('plugins.index.enabled'), + URL: config.get('plugins.index.url') + } + }, ADMIN: { get EMAIL () { return config.get('admin.email') } }, diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 2d487a263..06e8c070b 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -64,7 +64,9 @@ const SORTABLE_COLUMNS = { VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ], - PLUGINS: [ 'name', 'createdAt', 'updatedAt' ] + PLUGINS: [ 'name', 'createdAt', 'updatedAt' ], + + AVAILABLE_PLUGINS: [ 'npmName', 'popularity' ] } const OAUTH_LIFETIME = { @@ -165,6 +167,7 @@ const SCHEDULER_INTERVALS_MS = { removeOldJobs: 60000 * 60, // 1 hour updateVideos: 60000, // 1 minute youtubeDLUpdate: 60000 * 60 * 24, // 1 day + checkPlugins: 60000 * 60 * 24, // 1 day removeOldViews: 60000 * 60 * 24, // 1 day removeOldHistory: 60000 * 60 * 24 // 1 day } diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts new file mode 100644 index 000000000..4a8a90ec8 --- /dev/null +++ b/server/lib/plugins/plugin-index.ts @@ -0,0 +1,64 @@ +import { doRequest } from '../../helpers/requests' +import { CONFIG } from '../../initializers/config' +import { + PeertubePluginLatestVersionRequest, + PeertubePluginLatestVersionResponse +} from '../../../shared/models/plugins/peertube-plugin-latest-version.model' +import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model' +import { ResultList } from '../../../shared/models' +import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' +import { PluginModel } from '../../models/server/plugin' +import { PluginManager } from './plugin-manager' +import { logger } from '../../helpers/logger' + +const packageJSON = require('../../../../package.json') + +async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { + const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options + + const qs: PeertubePluginIndexList = { + start, + count, + sort, + pluginType, + search, + currentPeerTubeEngine: packageJSON.version + } + + const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' + + const { body } = await doRequest({ uri, qs, json: true }) + + logger.debug('Got result from PeerTube index.', { body }) + + await addInstanceInformation(body) + + return body as ResultList +} + +async function addInstanceInformation (result: ResultList) { + for (const d of result.data) { + d.installed = PluginManager.Instance.isRegistered(d.npmName) + d.name = PluginModel.normalizePluginName(d.npmName) + } + + return result +} + +async function getLatestPluginsVersion (npmNames: string[]): Promise { + const bodyRequest: PeertubePluginLatestVersionRequest = { + npmNames, + currentPeerTubeEngine: packageJSON.version + } + + const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version' + + const { body } = await doRequest({ uri, body: bodyRequest }) + + return body +} + +export { + listAvailablePluginsFromIndex, + getLatestPluginsVersion +} diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 7576b284c..9e4ec5adf 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -55,6 +55,10 @@ export class PluginManager { // ###################### Getters ###################### + isRegistered (npmName: string) { + return !!this.getRegisteredPluginOrTheme(npmName) + } + getRegisteredPluginOrTheme (npmName: string) { return this.registeredPlugins[npmName] } diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts new file mode 100644 index 000000000..9c60dbcd4 --- /dev/null +++ b/server/lib/schedulers/plugins-check-scheduler.ts @@ -0,0 +1,60 @@ +import { logger } from '../../helpers/logger' +import { AbstractScheduler } from './abstract-scheduler' +import { retryTransactionWrapper } from '../../helpers/database-utils' +import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' +import { CONFIG } from '../../initializers/config' +import { PluginModel } from '../../models/server/plugin' +import { chunk } from 'lodash' +import { getLatestPluginsVersion } from '../plugins/plugin-index' +import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' + +export class PluginsCheckScheduler extends AbstractScheduler { + + private static instance: AbstractScheduler + + protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPlugins + + private constructor () { + super() + } + + protected async internalExecute () { + return retryTransactionWrapper(this.checkLatestPluginsVersion.bind(this)) + } + + private async checkLatestPluginsVersion () { + if (CONFIG.PLUGINS.INDEX.ENABLED === false) return + + logger.info('Checkin latest plugins version.') + + const plugins = await PluginModel.listInstalled() + + // Process 10 plugins in 1 HTTP request + const chunks = chunk(plugins, 10) + for (const chunk of chunks) { + // Find plugins according to their npm name + const pluginIndex: { [npmName: string]: PluginModel} = {} + for (const plugin of chunk) { + pluginIndex[PluginModel.buildNpmName(plugin.name, plugin.type)] = plugin + } + + const npmNames = Object.keys(pluginIndex) + const results = await getLatestPluginsVersion(npmNames) + + for (const result of results) { + const plugin = pluginIndex[result.npmName] + if (!result.latestVersion) continue + + if (plugin.latestVersion !== result.latestVersion && compareSemVer(plugin.latestVersion, result.latestVersion) < 0) { + plugin.latestVersion = result.latestVersion + await plugin.save() + } + } + } + + } + + static get Instance () { + return this.instance || (this.instance = new this()) + } +} diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index 8103ec7d3..8cb3153aa 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts @@ -8,6 +8,7 @@ import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc import { PluginModel } from '../../models/server/plugin' import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' import { PluginType } from '../../../shared/models/plugins/plugin.type' +import { CONFIG } from '../../initializers/config' const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [ param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), @@ -33,7 +34,7 @@ const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [ ] const listPluginsValidator = [ - query('type') + query('pluginType') .optional() .custom(isPluginTypeValid).withMessage('Should have a valid plugin type'), query('uninstalled') @@ -119,12 +120,39 @@ const updatePluginSettingsValidator = [ } ] +const listAvailablePluginsValidator = [ + query('sort') + .optional() + .exists().withMessage('Should have a valid sort'), + query('search') + .optional() + .exists().withMessage('Should have a valid search'), + query('pluginType') + .optional() + .custom(isPluginTypeValid).withMessage('Should have a valid plugin type'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking enabledPluginValidator parameters', { parameters: req.query }) + + if (areValidationErrors(req, res)) return + + if (CONFIG.PLUGINS.INDEX.ENABLED === false) { + return res.status(400) + .json({ error: 'Plugin index is not enabled' }) + .end() + } + + return next() + } +] + // --------------------------------------------------------------------------- export { servePluginStaticDirectoryValidator, updatePluginSettingsValidator, uninstallPluginValidator, + listAvailablePluginsValidator, existingPluginValidator, installOrUpdatePluginValidator, listPluginsValidator diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 102db85cb..c75e701d6 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts @@ -22,6 +22,7 @@ const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMN const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS) +const SORTABLE_AVAILABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) @@ -43,6 +44,7 @@ const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUM const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS) const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS) +const availablePluginsSortValidator = checkSort(SORTABLE_AVAILABLE_PLUGINS_COLUMNS) // --------------------------------------------------------------------------- @@ -61,6 +63,7 @@ export { videoCommentThreadsSortValidator, videoRatesSortValidator, userSubscriptionsSortValidator, + availablePluginsSortValidator, videoChannelsSearchSortValidator, accountsBlocklistSortValidator, serversBlocklistSortValidator, diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index bd3d7a81e..ba43713f6 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts @@ -10,6 +10,7 @@ import { import { PluginType } from '../../../shared/models/plugins/plugin.type' import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model' import { FindAndCountOptions, json } from 'sequelize' +import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model' @DefaultScope(() => ({ attributes: { @@ -177,7 +178,7 @@ export class PluginModel extends Model { } static listForApi (options: { - type?: PluginType, + pluginType?: PluginType, uninstalled?: boolean, start: number, count: number, @@ -193,7 +194,7 @@ export class PluginModel extends Model { } } - if (options.type) query.where['type'] = options.type + if (options.pluginType) query.where['type'] = options.pluginType return PluginModel .findAndCountAll(query) @@ -202,8 +203,18 @@ export class PluginModel extends Model { }) } - static normalizePluginName (name: string) { - return name.replace(/^peertube-((theme)|(plugin))-/, '') + static listInstalled () { + const query = { + where: { + uninstalled: false + } + } + + return PluginModel.findAll(query) + } + + static normalizePluginName (npmName: string) { + return npmName.replace(/^peertube-((theme)|(plugin))-/, '') } static getTypeFromNpmName (npmName: string) { diff --git a/shared/core-utils/miscs/miscs.ts b/shared/core-utils/miscs/miscs.ts index c668e44c1..a3921b568 100644 --- a/shared/core-utils/miscs/miscs.ts +++ b/shared/core-utils/miscs/miscs.ts @@ -2,6 +2,24 @@ function randomInt (low: number, high: number) { return Math.floor(Math.random() * (high - low) + low) } -export { - randomInt +// Thanks https://stackoverflow.com/a/16187766 +function compareSemVer (a: string, b: string) { + const regExStrip0 = /(\.0+)+$/ + const segmentsA = a.replace(regExStrip0, '').split('.') + const segmentsB = b.replace(regExStrip0, '').split('.') + + const l = Math.min(segmentsA.length, segmentsB.length) + + for (let i = 0; i < l; i++) { + const diff = parseInt(segmentsA[ i ], 10) - parseInt(segmentsB[ i ], 10) + + if (diff) return diff + } + + return segmentsA.length - segmentsB.length +} + +export { + randomInt, + compareSemVer } diff --git a/shared/models/plugins/peertube-plugin-list.model.ts b/shared/models/plugins/peertube-plugin-index-list.model.ts similarity index 79% rename from shared/models/plugins/peertube-plugin-list.model.ts rename to shared/models/plugins/peertube-plugin-index-list.model.ts index 5f0ecce68..817bac31e 100644 --- a/shared/models/plugins/peertube-plugin-list.model.ts +++ b/shared/models/plugins/peertube-plugin-index-list.model.ts @@ -1,6 +1,6 @@ import { PluginType } from './plugin.type' -export interface PeertubePluginList { +export interface PeertubePluginIndexList { start: number count: number sort: string diff --git a/shared/models/plugins/peertube-plugin-index.model.ts b/shared/models/plugins/peertube-plugin-index.model.ts index 2957a338d..e91c8b4dc 100644 --- a/shared/models/plugins/peertube-plugin-index.model.ts +++ b/shared/models/plugins/peertube-plugin-index.model.ts @@ -8,4 +8,7 @@ export interface PeerTubePluginIndex { popularity: number latestVersion: string + + name?: string + installed?: boolean } diff --git a/shared/models/plugins/peertube-plugin-latest-version.model.ts b/shared/models/plugins/peertube-plugin-latest-version.model.ts index 36dd3af54..dec4618fa 100644 --- a/shared/models/plugins/peertube-plugin-latest-version.model.ts +++ b/shared/models/plugins/peertube-plugin-latest-version.model.ts @@ -1,5 +1,10 @@ -export interface PeertubePluginLatestVersion { +export interface PeertubePluginLatestVersionRequest { currentPeerTubeEngine?: string, npmNames: string[] } + +export type PeertubePluginLatestVersionResponse = { + npmName: string + latestVersion: string | null +}[]