From d4a8e7a65f97bb3257facc13e1ae8ffdbad61ddb Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 28 Jun 2021 17:30:59 +0200 Subject: [PATCH] Support short uuid for GET video/playlist --- .../video-block-list.component.ts | 2 +- .../my-video-imports.component.ts | 6 +- .../remote-interaction.component.ts | 4 +- .../shared/abstract-lazy-load.resolver.ts | 2 +- .../shared/playlist-lazy-load.resolver.ts | 2 +- .../shared/video-lazy-load.resolver.ts | 2 +- .../video-go-live.component.ts | 6 +- .../video-upload.component.ts | 12 +- .../+video-edit/video-update.component.html | 2 +- .../+video-edit/video-update.component.ts | 8 +- .../comment/video-comment.component.html | 4 +- .../comment/video-comments.component.ts | 2 +- .../+video-watch/video-watch.component.ts | 26 +- .../abuse-list-table.component.ts | 4 +- .../shared-main/angular/link.component.ts | 2 +- .../users/user-notification.model.ts | 3 +- .../shared/shared-main/video/video.model.ts | 15 +- .../video-share.component.ts | 7 +- .../video-thumbnail.component.ts | 4 +- .../video-comment.model.ts | 4 +- .../video-comment.service.ts | 7 +- .../video-miniature.component.ts | 4 +- ...eo-playlist-element-miniature.component.ts | 2 +- .../video-playlist-miniature.component.ts | 2 +- .../video-playlist.model.ts | 8 + package.json | 2 +- server/controllers/api/users/token.ts | 4 +- server/controllers/api/video-playlist.ts | 4 +- server/controllers/api/videos/live.ts | 7 +- server/controllers/api/videos/upload.ts | 2 + server/helpers/custom-validators/misc.ts | 84 ++-- server/helpers/image-utils.ts | 4 +- server/helpers/uuid.ts | 32 ++ .../migrations/0080-video-channels.ts | 4 +- .../migrations/0345-video-playlists.ts | 4 +- .../migrations/0560-user-feed-token.ts | 4 +- .../shared/object-to-model-attributes.ts | 4 +- server/lib/client-html.ts | 9 +- server/lib/local-actor.ts | 4 +- server/lib/user.ts | 4 +- server/middlewares/validators/abuse.ts | 3 +- server/middlewares/validators/feeds.ts | 8 +- server/middlewares/validators/index.ts | 2 +- server/middlewares/validators/oembed.ts | 4 +- server/middlewares/validators/redundancy.ts | 23 +- server/middlewares/validators/shared/utils.ts | 19 +- server/middlewares/validators/users.ts | 6 +- .../validators/videos/video-blacklist.ts | 14 +- .../validators/videos/video-captions.ts | 18 +- .../validators/videos/video-comments.ts | 31 +- .../validators/videos/video-live.ts | 14 +- .../videos/video-ownership-changes.ts | 10 +- .../validators/videos/video-playlists.ts | 41 +- .../validators/videos/video-rates.ts | 7 +- .../validators/videos/video-shares.ts | 10 +- .../validators/videos/video-watch.ts | 9 +- .../middlewares/validators/videos/videos.ts | 17 +- .../video/formatter/video-format-utils.ts | 3 + server/models/video/video-caption.ts | 4 +- server/models/video/video-playlist.ts | 6 +- server/tests/api/activitypub/client.ts | 98 ++++- server/tests/api/check-params/abuses.ts | 2 +- server/tests/api/check-params/live.ts | 40 +- server/tests/api/check-params/redundancy.ts | 38 +- server/tests/api/check-params/users.ts | 12 +- .../tests/api/check-params/video-blacklist.ts | 12 +- .../tests/api/check-params/video-captions.ts | 38 +- .../tests/api/check-params/video-comments.ts | 31 +- .../tests/api/check-params/video-playlists.ts | 64 +-- server/tests/api/check-params/videos.ts | 52 +-- .../notifications/moderation-notifications.ts | 30 +- .../api/notifications/user-notifications.ts | 10 +- server/tests/api/server/handle-down.ts | 2 +- server/tests/api/videos/video-playlists.ts | 71 ++- server/tests/api/videos/video-privacy.ts | 411 +++++++++--------- server/tests/cli/prune-storage.ts | 30 +- server/tests/client.ts | 129 +++--- server/tools/peertube-repl.ts | 5 - shared/extra-utils/server/servers.ts | 1 + shared/extra-utils/videos/videos.ts | 6 +- shared/models/common/index.ts | 1 + .../models/{ => common}/result-list.model.ts | 0 shared/models/index.ts | 14 +- .../moderation/abuse/abuse-create.model.ts | 2 +- shared/models/tokens/index.ts | 1 + .../{ => tokens}/oauth-client-local.model.ts | 0 shared/models/videos/index.ts | 1 + shared/models/videos/playlist/index.ts | 1 + .../video-playlist-create-result.model.ts | 5 + .../videos/playlist/video-playlist.model.ts | 2 + .../videos/video-create-result.model.ts | 5 + shared/models/videos/video.model.ts | 2 + support/doc/api/openapi.yaml | 17 +- yarn.lock | 8 + 94 files changed, 1029 insertions(+), 673 deletions(-) create mode 100644 server/helpers/uuid.ts create mode 100644 shared/models/common/index.ts rename shared/models/{ => common}/result-list.model.ts (100%) create mode 100644 shared/models/tokens/index.ts rename shared/models/{ => tokens}/oauth-client-local.model.ts (100%) create mode 100644 shared/models/videos/playlist/video-playlist-create-result.model.ts create mode 100644 shared/models/videos/video-create-result.model.ts diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index 63143d0f9..08500ef5c 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts @@ -122,7 +122,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit { } getVideoUrl (videoBlock: VideoBlacklist) { - return Video.buildClientUrl(videoBlock.video.uuid) + return Video.buildWatchUrl(videoBlock.video) } toHtml (text: string) { diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts index bb9d70524..68254526a 100644 --- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts @@ -1,7 +1,7 @@ import { SortMeta } from 'primeng/api' import { Component, OnInit } from '@angular/core' import { Notifier, RestPagination, RestTable } from '@app/core' -import { VideoImportService } from '@app/shared/shared-main' +import { Video, VideoImportService } from '@app/shared/shared-main' import { VideoImport, VideoImportState } from '@shared/models' @Component({ @@ -55,11 +55,11 @@ export class MyVideoImportsComponent extends RestTable implements OnInit { } getVideoUrl (video: { uuid: string }) { - return '/w/' + video.uuid + return Video.buildWatchUrl(video) } getEditVideoUrl (video: { uuid: string }) { - return '/videos/update/' + video.uuid + return Video.buildUpdateUrl(video) } protected reloadData () { diff --git a/client/src/app/+remote-interaction/remote-interaction.component.ts b/client/src/app/+remote-interaction/remote-interaction.component.ts index 6ddf5b58d..293f7edad 100644 --- a/client/src/app/+remote-interaction/remote-interaction.component.ts +++ b/client/src/app/+remote-interaction/remote-interaction.component.ts @@ -1,7 +1,7 @@ import { forkJoin } from 'rxjs' import { Component, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { VideoChannel } from '@app/shared/shared-main' +import { Video, VideoChannel } from '@app/shared/shared-main' import { SearchService } from '@app/shared/shared-search' @Component({ @@ -39,7 +39,7 @@ export class RemoteInteractionComponent implements OnInit { if (videoResult.data.length !== 0) { const video = videoResult.data[0] - redirectUrl = '/w/' + video.uuid + redirectUrl = Video.buildWatchUrl(video) } else if (channelResult.data.length !== 0) { const channel = new VideoChannel(channelResult.data[0]) diff --git a/client/src/app/+search/shared/abstract-lazy-load.resolver.ts b/client/src/app/+search/shared/abstract-lazy-load.resolver.ts index 31240f451..b18a5b64d 100644 --- a/client/src/app/+search/shared/abstract-lazy-load.resolver.ts +++ b/client/src/app/+search/shared/abstract-lazy-load.resolver.ts @@ -1,7 +1,7 @@ import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' -import { ResultList } from '@shared/models/result-list.model' +import { ResultList } from '@shared/models' export abstract class AbstractLazyLoadResolver implements Resolve { protected router: Router diff --git a/client/src/app/+search/shared/playlist-lazy-load.resolver.ts b/client/src/app/+search/shared/playlist-lazy-load.resolver.ts index 14ae798df..3310e9627 100644 --- a/client/src/app/+search/shared/playlist-lazy-load.resolver.ts +++ b/client/src/app/+search/shared/playlist-lazy-load.resolver.ts @@ -19,6 +19,6 @@ export class PlaylistLazyLoadResolver extends AbstractLazyLoadResolver { } protected buildUrl (video: Video) { - return '/w/' + video.uuid + return Video.buildWatchUrl(video) } } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts index 4c39b276a..01c9fcb16 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts @@ -1,11 +1,11 @@ import { forkJoin } from 'rxjs' -import { AfterViewChecked, AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core' +import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core' import { Router } from '@angular/router' import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' import { scrollToTop } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' -import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' +import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' import { LiveVideoService } from '@app/shared/shared-video-live' import { LoadingBarService } from '@ngx-loading-bar/core' import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, PeerTubeProblemDocument, ServerErrorCode, VideoPrivacy } from '@shared/models' @@ -127,7 +127,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView () => { this.notifier.success($localize`Live published.`) - this.router.navigate(['/w', video.uuid]) + this.router.navigateByUrl(Video.buildWatchUrl(video)) }, err => { diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index f383662a1..ec027f257 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts @@ -1,16 +1,16 @@ +import { UploadState, UploadxOptions, UploadxService } from 'ngx-uploadx' +import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' import { Router } from '@angular/router' -import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx' -import { UploaderXFormData } from './uploaderx-form-data' import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' -import { scrollToTop, genericUploadErrorHandler } from '@app/helpers' +import { genericUploadErrorHandler, scrollToTop } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' -import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' +import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' import { VideoPrivacy } from '@shared/models' +import { UploaderXFormData } from './uploaderx-form-data' import { VideoSend } from './video-send' -import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' @Component({ selector: 'my-video-upload', @@ -243,7 +243,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy this.isUploadingVideo = false this.notifier.success($localize`Video published.`) - this.router.navigate([ '/w', video.uuid ]) + this.router.navigateByUrl(Video.buildWatchUrl(video)) }, err => { diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html index 9629081e3..33e3ddd14 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.html +++ b/client/src/app/+videos/+video-edit/video-update.component.html @@ -1,7 +1,7 @@
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts index 574669a23..1534eee82 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.ts +++ b/client/src/app/+videos/+video-edit/video-update.component.ts @@ -5,7 +5,7 @@ import { Component, HostListener, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { Notifier } from '@app/core' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' +import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' import { LiveVideoService } from '@app/shared/shared-video-live' import { LoadingBarService } from '@ngx-loading-bar/core' import { LiveVideo, LiveVideoUpdate, VideoPrivacy } from '@shared/models' @@ -156,7 +156,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { this.isUpdatingVideo = false this.loadingBar.useRef().complete() this.notifier.success($localize`Video updated.`) - this.router.navigate([ '/w', this.video.uuid ]) + this.router.navigateByUrl(Video.buildWatchUrl(this.video)) }, err => { @@ -175,4 +175,8 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { pluginData: this.video.pluginData }) } + + getVideoUrl () { + return Video.buildWatchUrl(this.videoDetails) + } } diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/comment/video-comment.component.html index 06548edc8..d8b944b35 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comment.component.html +++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.html @@ -20,7 +20,7 @@
- + {{ comment.createdAt | myFromNow }} @@ -45,7 +45,7 @@ diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts index 210236b61..2c39e63fb 100644 --- a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts @@ -247,7 +247,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { this.componentPagination.totalItems = null this.totalNotDeletedComments = null - this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid) + this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video) this.loadMoreThreads() } } diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index a444dc51f..12b0baebe 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts @@ -312,7 +312,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { getVideoUrl () { if (!this.video.url) { - return this.video.originInstanceUrl + VideoDetails.buildClientUrl(this.video.uuid) + return this.video.originInstanceUrl + VideoDetails.buildWatchUrl(this.video) } return this.video.url } @@ -415,7 +415,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private loadVideo (videoId: string) { // Video did not change - if (this.video && this.video.uuid === videoId) return + if ( + this.video && + (this.video.uuid === videoId || this.video.shortUUID === videoId) + ) return if (this.player) this.player.pause() @@ -489,7 +492,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private loadPlaylist (playlistId: string) { // Playlist did not change - if (this.playlist && this.playlist.uuid === playlistId) return + if ( + this.playlist && + (this.playlist.uuid === playlistId || this.playlist.shortUUID === playlistId) + ) return this.playlistService.getVideoPlaylist(playlistId) .pipe( @@ -772,13 +778,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private flushPlayer () { // Remove player if it exists - if (this.player) { - try { - this.player.dispose() - this.player = undefined - } catch (err) { - console.error('Cannot dispose player.', err) - } + if (!this.player) return + + try { + this.player.dispose() + this.player = undefined + } catch (err) { + console.error('Cannot dispose player.', err) } } diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 07b9dddba..67aa0e399 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts @@ -116,11 +116,11 @@ export class AbuseListTableComponent extends RestTable implements OnInit { } getVideoUrl (abuse: AdminAbuse) { - return Video.buildClientUrl(abuse.video.uuid) + return Video.buildWatchUrl(abuse.video) } getCommentUrl (abuse: AdminAbuse) { - return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId + return Video.buildWatchUrl(abuse.comment.video) + ';threadId=' + abuse.comment.threadId } getAccountUrl (abuse: ProcessedAbuse) { diff --git a/client/src/app/shared/shared-main/angular/link.component.ts b/client/src/app/shared/shared-main/angular/link.component.ts index 76d1201b9..597a16871 100644 --- a/client/src/app/shared/shared-main/angular/link.component.ts +++ b/client/src/app/shared/shared-main/angular/link.component.ts @@ -6,7 +6,7 @@ import { Component, Input, ViewEncapsulation } from '@angular/core' templateUrl: './link.component.html' }) export class LinkComponent { - @Input() internalLink?: any[] + @Input() internalLink?: string | any[] @Input() href?: string @Input() target?: string diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index c80bc13b0..4c15eb981 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts @@ -12,6 +12,7 @@ import { UserRight, VideoInfo } from '@shared/models' +import { Video } from '../video' export class UserNotification implements UserNotificationServer { id: number @@ -238,7 +239,7 @@ export class UserNotification implements UserNotificationServer { } private buildVideoUrl (video: { uuid: string }) { - return '/w/' + video.uuid + return Video.buildWatchUrl(video) } private buildAccountUrl (account: { name: string, host: string }) { diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index ab8ed9051..f0a4a3f37 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts @@ -26,12 +26,18 @@ export class Video implements VideoServerModel { licence: VideoConstant language: VideoConstant privacy: VideoConstant + description: string + duration: number durationLabel: string + id: number uuid: string + shortUUID: string + isLocal: boolean + name: string serverHost: string thumbnailPath: string @@ -85,8 +91,12 @@ export class Video implements VideoServerModel { pluginData?: any - static buildClientUrl (videoUUID: string) { - return '/w/' + videoUUID + static buildWatchUrl (video: Partial>) { + return '/w/' + (video.shortUUID || video.uuid) + } + + static buildUpdateUrl (video: Pick) { + return '/videos/update/' + video.uuid } constructor (hash: VideoServerModel, translations = {}) { @@ -109,6 +119,7 @@ export class Video implements VideoServerModel { this.id = hash.id this.uuid = hash.uuid + this.shortUUID = hash.shortUUID this.isLocal = hash.isLocal this.name = hash.name diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts index 2a73e6166..a41ff248b 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.ts +++ b/client/src/app/shared/shared-share-modal/video-share.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, Input, ViewChild } from '@angular/core' -import { VideoDetails } from '@app/shared/shared-main' +import { Video, VideoDetails } from '@app/shared/shared-main' import { VideoPlaylist } from '@app/shared/shared-video-playlist' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { VideoCaption } from '@shared/models' @@ -98,14 +98,15 @@ export class VideoShareComponent { getVideoUrl () { let baseUrl = this.customizations.originUrl ? this.video.originInstanceUrl : window.location.origin - baseUrl += '/w/' + this.video.uuid + baseUrl += Video.buildWatchUrl(this.video) + const options = this.getVideoOptions(baseUrl) return buildVideoLink(options) } getPlaylistUrl () { - const base = window.location.origin + '/w/p/' + this.playlist.uuid + const base = window.location.origin + VideoPlaylist.buildWatchUrl(this.playlist) if (!this.includeVideoInPlaylist) return base diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts index d5583c29f..ad5d30db2 100644 --- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts +++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts @@ -12,7 +12,7 @@ export class VideoThumbnailComponent { @Input() video: Video @Input() nsfw = false - @Input() videoRouterLink: any[] + @Input() videoRouterLink: string | any[] @Input() queryParams: { [ p: string ]: any } @Input() videoHref: string @Input() videoTarget: string @@ -57,7 +57,7 @@ export class VideoThumbnailComponent { getVideoRouterLink () { if (this.videoRouterLink) return this.videoRouterLink - return [ '/w', this.video.uuid ] + return Video.buildWatchUrl(this.video) } onWatchLaterClick (event: Event) { diff --git a/client/src/app/shared/shared-video-comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts index 94d6c5fa8..ba0f57e8f 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.model.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts @@ -1,5 +1,5 @@ import { getAbsoluteAPIUrl } from '@app/helpers' -import { Account, Actor } from '@app/shared/shared-main' +import { Account, Actor, Video } from '@app/shared/shared-main' import { Account as AccountInterface, VideoComment as VideoCommentServerModel, VideoCommentAdmin as VideoCommentAdminServerModel } from '@shared/models' export class VideoComment implements VideoCommentServerModel { @@ -85,7 +85,7 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel { id: hash.video.id, uuid: hash.video.uuid, name: hash.video.name, - localUrl: '/w/' + hash.video.uuid + localUrl: Video.buildWatchUrl(hash.video) } this.localUrl = this.video.localUrl + ';threadId=' + this.threadId diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts index c5aeb3c12..4f1452116 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.service.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts @@ -9,6 +9,7 @@ import { FeedFormat, ResultList, ThreadsResultList, + Video, VideoComment as VideoCommentServerModel, VideoCommentAdmin, VideoCommentCreate, @@ -127,7 +128,7 @@ export class VideoCommentService { ) } - getVideoCommentsFeeds (videoUUID?: string) { + getVideoCommentsFeeds (video: Pick) { const feeds = [ { format: FeedFormat.RSS, @@ -146,9 +147,9 @@ export class VideoCommentService { } ] - if (videoUUID !== undefined) { + if (video !== undefined) { for (const feed of feeds) { - feed.url += '?videoId=' + videoUUID + feed.url += '?videoId=' + video.uuid } } diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index fe161c977..67e0de6a2 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts @@ -85,7 +85,7 @@ export class VideoMiniatureComponent implements OnInit { playlistElementId?: number } - videoRouterLink: any[] = [] + videoRouterLink: string | any[] = [] videoHref: string videoTarget: string @@ -120,7 +120,7 @@ export class VideoMiniatureComponent implements OnInit { buildVideoLink () { if (this.videoLinkType === 'internal' || !this.video.url) { - this.videoRouterLink = [ '/w', this.video.uuid ] + this.videoRouterLink = Video.buildWatchUrl(this.video) return } diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts index 57eab4dfd..d99170e4e 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts @@ -66,7 +66,7 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit { buildRouterLink () { if (!this.playlist) return null - return [ '/w/p', this.playlist.uuid ] + return VideoPlaylist.buildWatchUrl(this.playlist) } buildRouterQuery () { diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts index 8de5092a9..c80ea2e6b 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts @@ -39,7 +39,7 @@ export class VideoPlaylistMiniatureComponent implements OnInit { } if (this.linkType === 'internal' || !this.playlist.url) { - this.routerLink = [ '/w/p', this.playlist.uuid ] + this.routerLink = VideoPlaylist.buildWatchUrl(this.playlist) return } diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.model.ts b/client/src/app/shared/shared-video-playlist/video-playlist.model.ts index d67f372f4..d96b70922 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.model.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.model.ts @@ -13,6 +13,8 @@ import { export class VideoPlaylist implements ServerVideoPlaylist { id: number uuid: string + shortUUID: string + isLocal: boolean url: string @@ -41,11 +43,17 @@ export class VideoPlaylist implements ServerVideoPlaylist { videoChannelBy?: string + static buildWatchUrl (playlist: Pick) { + return '/w/p/' + (playlist.uuid || playlist.shortUUID) + } + constructor (hash: ServerVideoPlaylist, translations: {}) { const absoluteAPIUrl = getAbsoluteAPIUrl() this.id = hash.id this.uuid = hash.uuid + this.shortUUID = hash.shortUUID + this.url = hash.url this.isLocal = hash.isLocal diff --git a/package.json b/package.json index 9fa1e4a0f..dcecd3940 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "sanitize-html": "2.x", "sequelize": "6.6.2", "sequelize-typescript": "^2.0.0-beta.1", + "short-uuid": "^4.2.0", "sitemap": "^7.0.0", "socket.io": "^4.0.1", "sql-formatter": "^4.0.0-beta.0", @@ -138,7 +139,6 @@ "tsconfig-paths": "^3.9.0", "tslib": "^2.0.0", "useragent": "^2.3.0", - "uuid": "^8.1.0", "validator": "^13.0.0", "webfinger.js": "^2.6.6", "webtorrent": "^1.0.0", diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts index e636f44f6..b405ddbf4 100644 --- a/server/controllers/api/users/token.ts +++ b/server/controllers/api/users/token.ts @@ -1,7 +1,7 @@ import * as express from 'express' import * as RateLimit from 'express-rate-limit' -import { v4 as uuidv4 } from 'uuid' import { logger } from '@server/helpers/logger' +import { buildUUID } from '@server/helpers/uuid' import { CONFIG } from '@server/initializers/config' import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' import { handleOAuthToken } from '@server/lib/auth/oauth' @@ -107,7 +107,7 @@ function getScopedTokens (req: express.Request, res: express.Response) { async function renewScopedTokens (req: express.Request, res: express.Response) { const user = res.locals.oauth.token.user - user.feedToken = uuidv4() + user.feedToken = buildUUID() await user.save() return res.json({ diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index 5c4aa50ac..87a6f6bbe 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts @@ -1,6 +1,8 @@ import * as express from 'express' import { join } from 'path' +import { uuidToShort } from '@server/helpers/uuid' import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' +import { Hooks } from '@server/lib/plugins/hooks' import { getServerActor } from '@server/models/application/application' import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' @@ -43,7 +45,6 @@ import { import { AccountModel } from '../../models/account/account' import { VideoPlaylistModel } from '../../models/video/video-playlist' import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' -import { Hooks } from '@server/lib/plugins/hooks' const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) @@ -199,6 +200,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { return res.json({ videoPlaylist: { id: videoPlaylistCreated.id, + shortUUID: uuidToShort(videoPlaylistCreated.uuid), uuid: videoPlaylistCreated.uuid } }) diff --git a/server/controllers/api/videos/live.ts b/server/controllers/api/videos/live.ts index 61fa979c4..d8c51c2d4 100644 --- a/server/controllers/api/videos/live.ts +++ b/server/controllers/api/videos/live.ts @@ -1,6 +1,6 @@ import * as express from 'express' -import { v4 as uuidv4 } from 'uuid' import { createReqFiles } from '@server/helpers/express-utils' +import { buildUUID, uuidToShort } from '@server/helpers/uuid' import { CONFIG } from '@server/initializers/config' import { ASSETS_PATH, MIMETYPES } from '@server/initializers/constants' import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' @@ -11,12 +11,12 @@ import { videoLiveAddValidator, videoLiveGetValidator, videoLiveUpdateValidator import { VideoLiveModel } from '@server/models/video/video-live' import { MVideoDetails, MVideoFullLight } from '@server/types/models' import { LiveVideoCreate, LiveVideoUpdate, VideoState } from '../../../../shared' +import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail' import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares' import { VideoModel } from '../../../models/video/video' -import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' const liveRouter = express.Router() @@ -94,7 +94,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) { const videoLive = new VideoLiveModel() videoLive.saveReplay = videoInfo.saveReplay || false videoLive.permanentLive = videoInfo.permanentLive || false - videoLive.streamKey = uuidv4() + videoLive.streamKey = buildUUID() const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({ video, @@ -138,6 +138,7 @@ async function addLiveVideo (req: express.Request, res: express.Response) { return res.json({ video: { id: videoCreated.id, + shortUUID: uuidToShort(videoCreated.uuid), uuid: videoCreated.uuid } }) diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index e767492bc..bcd21ac99 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts @@ -2,6 +2,7 @@ import * as express from 'express' import { move } from 'fs-extra' import { getLowercaseExtension } from '@server/helpers/core-utils' import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload' +import { uuidToShort } from '@server/helpers/uuid' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video' @@ -218,6 +219,7 @@ async function addVideo (options: { return res.json({ video: { id: videoCreated.id, + shortUUID: uuidToShort(videoCreated.uuid), uuid: videoCreated.uuid } }) diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 229e9f03c..528bfcfb8 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts @@ -2,6 +2,7 @@ import 'multer' import { UploadFilesForCheck } from 'express' import { sep } from 'path' import validator from 'validator' +import { isShortUUID, shortToUUID } from '../uuid' function exists (value: any) { return value !== undefined && value !== null @@ -50,42 +51,7 @@ function isIntOrNull (value: any) { return value === null || validator.isInt('' + value) } -function toIntOrNull (value: string) { - const v = toValueOrNull(value) - - if (v === null || v === undefined) return v - if (typeof v === 'number') return v - - return validator.toInt('' + v) -} - -function toBooleanOrNull (value: any) { - const v = toValueOrNull(value) - - if (v === null || v === undefined) return v - if (typeof v === 'boolean') return v - - return validator.toBoolean('' + v) -} - -function toValueOrNull (value: string) { - if (value === 'null') return null - - return value -} - -function toArray (value: any) { - if (value && isArray(value) === false) return [ value ] - - return value -} - -function toIntArray (value: any) { - if (!value) return [] - if (isArray(value) === false) return [ validator.toInt(value) ] - - return value.map(v => validator.toInt(v)) -} +// --------------------------------------------------------------------------- function isFileFieldValid ( files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], @@ -160,6 +126,51 @@ function isFileValid ( // --------------------------------------------------------------------------- +function toCompleteUUID (value: string) { + if (isShortUUID(value)) return shortToUUID(value) + + return value +} + +function toIntOrNull (value: string) { + const v = toValueOrNull(value) + + if (v === null || v === undefined) return v + if (typeof v === 'number') return v + + return validator.toInt('' + v) +} + +function toBooleanOrNull (value: any) { + const v = toValueOrNull(value) + + if (v === null || v === undefined) return v + if (typeof v === 'boolean') return v + + return validator.toBoolean('' + v) +} + +function toValueOrNull (value: string) { + if (value === 'null') return null + + return value +} + +function toArray (value: any) { + if (value && isArray(value) === false) return [ value ] + + return value +} + +function toIntArray (value: any) { + if (!value) return [] + if (isArray(value) === false) return [ validator.toInt(value) ] + + return value.map(v => validator.toInt(v)) +} + +// --------------------------------------------------------------------------- + export { exists, isArrayOf, @@ -169,6 +180,7 @@ export { isIdValid, isSafePath, isUUIDValid, + toCompleteUUID, isIdOrUUIDValid, isDateValid, toValueOrNull, diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index 122fb009d..c76ed545b 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts @@ -1,12 +1,12 @@ import { copy, readFile, remove, rename } from 'fs-extra' import * as Jimp from 'jimp' -import { v4 as uuidv4 } from 'uuid' import { getLowercaseExtension } from './core-utils' import { convertWebPToJPG, processGIF } from './ffmpeg-utils' import { logger } from './logger' +import { buildUUID } from './uuid' function generateImageFilename (extension = '.jpg') { - return uuidv4() + extension + return buildUUID() + extension } async function processImage ( diff --git a/server/helpers/uuid.ts b/server/helpers/uuid.ts new file mode 100644 index 000000000..3eb06c773 --- /dev/null +++ b/server/helpers/uuid.ts @@ -0,0 +1,32 @@ +import * as short from 'short-uuid' + +const translator = short() + +function buildUUID () { + return short.uuid() +} + +function uuidToShort (uuid: string) { + if (!uuid) return uuid + + return translator.fromUUID(uuid) +} + +function shortToUUID (shortUUID: string) { + if (!shortUUID) return shortUUID + + return translator.toUUID(shortUUID) +} + +function isShortUUID (value: string) { + if (!value) return false + + return value.length === translator.maxLength +} + +export { + buildUUID, + uuidToShort, + shortToUUID, + isShortUUID +} diff --git a/server/initializers/migrations/0080-video-channels.ts b/server/initializers/migrations/0080-video-channels.ts index 883224cb0..0e6952350 100644 --- a/server/initializers/migrations/0080-video-channels.ts +++ b/server/initializers/migrations/0080-video-channels.ts @@ -1,5 +1,5 @@ +import { buildUUID } from '@server/helpers/uuid' import * as Sequelize from 'sequelize' -import { v4 as uuidv4 } from 'uuid' async function up (utils: { transaction: Sequelize.Transaction @@ -23,7 +23,7 @@ async function up (utils: { { const authors = await utils.db.Author.findAll() for (const author of authors) { - author.uuid = uuidv4() + author.uuid = buildUUID() await author.save() } } diff --git a/server/initializers/migrations/0345-video-playlists.ts b/server/initializers/migrations/0345-video-playlists.ts index 89a14a6ee..8dd631dff 100644 --- a/server/initializers/migrations/0345-video-playlists.ts +++ b/server/initializers/migrations/0345-video-playlists.ts @@ -1,6 +1,6 @@ import * as Sequelize from 'sequelize' +import { buildUUID } from '@server/helpers/uuid' import { VideoPlaylistPrivacy, VideoPlaylistType } from '../../../shared/models/videos' -import { v4 as uuidv4 } from 'uuid' import { WEBSERVER } from '../constants' async function up (utils: { @@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS "videoPlaylistElement" const usernames = userResult.map(r => r.username) for (const username of usernames) { - const uuid = uuidv4() + const uuid = buildUUID() const baseUrl = WEBSERVER.URL + '/video-playlists/' + uuid const query = ` diff --git a/server/initializers/migrations/0560-user-feed-token.ts b/server/initializers/migrations/0560-user-feed-token.ts index 7c61def17..042301352 100644 --- a/server/initializers/migrations/0560-user-feed-token.ts +++ b/server/initializers/migrations/0560-user-feed-token.ts @@ -1,5 +1,5 @@ import * as Sequelize from 'sequelize' -import { v4 as uuidv4 } from 'uuid' +import { buildUUID } from '@server/helpers/uuid' async function up (utils: { transaction: Sequelize.Transaction @@ -26,7 +26,7 @@ async function up (utils: { const users = await utils.sequelize.query(query, options) for (const user of users) { - const queryUpdate = `UPDATE "user" SET "feedToken" = '${uuidv4()}' WHERE id = ${user.id}` + const queryUpdate = `UPDATE "user" SET "feedToken" = '${buildUUID()}' WHERE id = ${user.id}` await utils.sequelize.query(queryUpdate) } } diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts index f53b98448..1612b3ad0 100644 --- a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts @@ -1,6 +1,6 @@ -import { v4 as uuidv4 } from 'uuid' import { getLowercaseExtension } from '@server/helpers/core-utils' import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' +import { buildUUID } from '@server/helpers/uuid' import { MIMETYPES } from '@server/initializers/constants' import { ActorModel } from '@server/models/actor/actor' import { FilteredModelAttributes } from '@server/types' @@ -51,7 +51,7 @@ function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImage if (!extension) return undefined return { - name: uuidv4() + extension, + name: buildUUID() + extension, fileUrl: icon.url, height: icon.height, width: icon.width, diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 0191b55ef..72194416d 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts @@ -27,6 +27,7 @@ import { VideoChannelModel } from '../models/video/video-channel' import { VideoPlaylistModel } from '../models/video/video-playlist' import { MAccountActor, MChannelActor } from '../types/models' import { ServerConfigManager } from './server-config-manager' +import { toCompleteUUID } from '@server/helpers/custom-validators/misc' type Tags = { ogType: string @@ -78,7 +79,9 @@ class ClientHtml { return customHtml } - static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) { + static async getWatchHTMLPage (videoIdArg: string, req: express.Request, res: express.Response) { + const videoId = toCompleteUUID(videoIdArg) + // Let Angular application handle errors if (!validator.isInt(videoId) && !validator.isUUID(videoId, 4)) { res.status(HttpStatusCode.NOT_FOUND_404) @@ -136,7 +139,9 @@ class ClientHtml { return customHtml } - static async getWatchPlaylistHTMLPage (videoPlaylistId: string, req: express.Request, res: express.Response) { + static async getWatchPlaylistHTMLPage (videoPlaylistIdArg: string, req: express.Request, res: express.Response) { + const videoPlaylistId = toCompleteUUID(videoPlaylistIdArg) + // Let Angular application handle errors if (!validator.isInt(videoPlaylistId) && !validator.isUUID(videoPlaylistId, 4)) { res.status(HttpStatusCode.NOT_FOUND_404) diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index 2d2bd43a1..77667f6b0 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts @@ -2,8 +2,8 @@ import 'multer' import { queue } from 'async' import * as LRUCache from 'lru-cache' import { join } from 'path' -import { v4 as uuidv4 } from 'uuid' import { getLowercaseExtension } from '@server/helpers/core-utils' +import { buildUUID } from '@server/helpers/uuid' import { ActorModel } from '@server/models/actor/actor' import { ActivityPubActorType, ActorImageType } from '@shared/models' import { retryTransactionWrapper } from '../helpers/database-utils' @@ -44,7 +44,7 @@ async function updateLocalActorImageFile ( const extension = getLowercaseExtension(imagePhysicalFile.filename) - const imageName = uuidv4() + extension + const imageName = buildUUID() + extension const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) await processImage(imagePhysicalFile.path, destination, imageSize) diff --git a/server/lib/user.ts b/server/lib/user.ts index 8820e8243..936403692 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -1,5 +1,5 @@ import { Transaction } from 'sequelize/types' -import { v4 as uuidv4 } from 'uuid' +import { buildUUID } from '@server/helpers/uuid' import { UserModel } from '@server/models/user/user' import { MActorDefault } from '@server/types/models/actor' import { ActivityPubActorType } from '../../shared/models/activitypub' @@ -210,7 +210,7 @@ async function buildChannelAttributes (user: MUser, transaction?: Transaction, c // Conflict, generate uuid instead const actor = await ActorModel.loadLocalByName(channelName, transaction) - if (actor) channelName = uuidv4() + if (actor) channelName = buildUUID() const videoChannelDisplayName = `Main ${user.username} channel` diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts index 56c97747c..c048bc6af 100644 --- a/server/middlewares/validators/abuse.ts +++ b/server/middlewares/validators/abuse.ts @@ -12,7 +12,7 @@ import { isAbuseTimestampValid, isAbuseVideoIsValid } from '@server/helpers/custom-validators/abuses' -import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc' +import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from '@server/helpers/custom-validators/misc' import { logger } from '@server/helpers/logger' import { AbuseMessageModel } from '@server/models/abuse/abuse-message' import { AbuseCreate, UserRight } from '@shared/models' @@ -27,6 +27,7 @@ const abuseReportValidator = [ body('video.id') .optional() + .customSanitizer(toCompleteUUID) .custom(isIdOrUUIDValid) .withMessage('Should have a valid videoId'), body('video.startAt') diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts index 51e6d6fff..51b8fdd19 100644 --- a/server/middlewares/validators/feeds.ts +++ b/server/middlewares/validators/feeds.ts @@ -1,8 +1,9 @@ import * as express from 'express' import { param, query } from 'express-validator' + import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' -import { exists, isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc' +import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc' import { logger } from '../../helpers/logger' import { areValidationErrors, @@ -98,7 +99,10 @@ const videoSubscriptionFeedsValidator = [ ] const videoCommentsFeedsValidator = [ - query('videoId').optional().custom(isIdOrUUIDValid), + query('videoId') + .customSanitizer(toCompleteUUID) + .optional() + .custom(isIdOrUUIDValid), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking feeds parameters', { parameters: req.query }) diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 24faeea3e..94a3c2dea 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts @@ -11,7 +11,7 @@ export * from './sort' export * from './users' export * from './user-subscriptions' export * from './videos' -export * from './webfinger' export * from './search' export * from './server' export * from './user-history' +export * from './webfinger' diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts index e1015d7fd..0a82e6932 100644 --- a/server/middlewares/validators/oembed.ts +++ b/server/middlewares/validators/oembed.ts @@ -6,7 +6,7 @@ import { VideoPlaylistModel } from '@server/models/video/video-playlist' import { VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { isTestInstance } from '../../helpers/core-utils' -import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' +import { isIdOrUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc' import { logger } from '../../helpers/logger' import { WEBSERVER } from '../../initializers/constants' import { areValidationErrors } from './shared' @@ -79,7 +79,7 @@ const oembedValidator = [ }) } - const elementId = matches[1] + const elementId = toCompleteUUID(matches[1]) if (isIdOrUUIDValid(elementId) === false) { return res.fail({ message: 'Invalid video or playlist id.' }) } diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index da24f4c9b..116c8c611 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts @@ -2,15 +2,24 @@ import * as express from 'express' import { body, param, query } from 'express-validator' import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies' import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' -import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' +import { + exists, + isBooleanValid, + isIdOrUUIDValid, + isIdValid, + toBooleanOrNull, + toCompleteUUID, + toIntOrNull +} from '../../helpers/custom-validators/misc' import { isHostValid } from '../../helpers/custom-validators/servers' import { logger } from '../../helpers/logger' import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' import { ServerModel } from '../../models/server/server' -import { areValidationErrors, doesVideoExist } from './shared' +import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared' const videoFileRedundancyGetValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), + isValidVideoIdParam('videoId'), + param('resolution') .customSanitizer(toIntOrNull) .custom(exists).withMessage('Should have a valid resolution'), @@ -56,9 +65,8 @@ const videoFileRedundancyGetValidator = [ ] const videoPlaylistRedundancyGetValidator = [ - param('videoId') - .custom(isIdOrUUIDValid) - .not().isEmpty().withMessage('Should have a valid video id'), + isValidVideoIdParam('videoId'), + param('streamingPlaylistType') .customSanitizer(toIntOrNull) .custom(exists).withMessage('Should have a valid streaming playlist type'), @@ -135,7 +143,8 @@ const listVideoRedundanciesValidator = [ const addVideoRedundancyValidator = [ body('videoId') - .custom(isIdValid) + .customSanitizer(toCompleteUUID) + .custom(isIdOrUUIDValid) .withMessage('Should have a valid video id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { diff --git a/server/middlewares/validators/shared/utils.ts b/server/middlewares/validators/shared/utils.ts index d3e4870a9..4f08560af 100644 --- a/server/middlewares/validators/shared/utils.ts +++ b/server/middlewares/validators/shared/utils.ts @@ -1,5 +1,6 @@ import * as express from 'express' -import { query, validationResult } from 'express-validator' +import { param, query, validationResult } from 'express-validator' +import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' import { logger } from '../../../helpers/logger' function areValidationErrors (req: express.Request, res: express.Response) { @@ -41,10 +42,24 @@ function createSortableColumns (sortableColumns: string[]) { return sortableColumns.concat(sortableColumnDesc) } +function isValidVideoIdParam (paramName: string) { + return param(paramName) + .customSanitizer(toCompleteUUID) + .custom(isIdOrUUIDValid).withMessage('Should have a valid video id') +} + +function isValidPlaylistIdParam (paramName: string) { + return param(paramName) + .customSanitizer(toCompleteUUID) + .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id') +} + // --------------------------------------------------------------------------- export { areValidationErrors, checkSort, - createSortableColumns + createSortableColumns, + isValidVideoIdParam, + isValidPlaylistIdParam } diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 218633b8d..698d7d814 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -7,7 +7,7 @@ import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-code import { UserRole } from '../../../shared/models/users' import { UserRegister } from '../../../shared/models/users/user-register.model' import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' -import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' +import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' import { isThemeNameValid } from '../../helpers/custom-validators/plugins' import { isNoInstanceConfigWarningModal, @@ -35,7 +35,7 @@ import { Redis } from '../../lib/redis' import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' import { ActorModel } from '../../models/actor/actor' import { UserModel } from '../../models/user/user' -import { areValidationErrors, doesVideoExist } from './shared' +import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared' const usersListValidator = [ query('blocked') @@ -302,7 +302,7 @@ const usersGetValidator = [ ] const usersVideoRatingValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), + isValidVideoIdParam('videoId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts index 7374ba774..21141d84d 100644 --- a/server/middlewares/validators/videos/video-blacklist.ts +++ b/server/middlewares/validators/videos/video-blacklist.ts @@ -1,13 +1,13 @@ import * as express from 'express' -import { body, param, query } from 'express-validator' +import { body, query } from 'express-validator' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' -import { isBooleanValid, isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' +import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist' import { logger } from '../../../helpers/logger' -import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist } from '../shared' +import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist, isValidVideoIdParam } from '../shared' const videosBlacklistRemoveValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + isValidVideoIdParam('videoId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) @@ -21,7 +21,8 @@ const videosBlacklistRemoveValidator = [ ] const videosBlacklistAddValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + isValidVideoIdParam('videoId'), + body('unfederate') .optional() .customSanitizer(toBooleanOrNull) @@ -49,7 +50,8 @@ const videosBlacklistAddValidator = [ ] const videosBlacklistUpdateValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + isValidVideoIdParam('videoId'), + body('reason') .optional() .custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'), diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts index 2295e049a..2946f3e15 100644 --- a/server/middlewares/validators/videos/video-captions.ts +++ b/server/middlewares/validators/videos/video-captions.ts @@ -1,16 +1,18 @@ import * as express from 'express' import { body, param } from 'express-validator' import { UserRight } from '../../../../shared' -import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' import { cleanUpReqFiles } from '../../../helpers/express-utils' import { logger } from '../../../helpers/logger' import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants' -import { areValidationErrors, checkUserCanManageVideo, doesVideoCaptionExist, doesVideoExist } from '../shared' +import { areValidationErrors, checkUserCanManageVideo, doesVideoCaptionExist, doesVideoExist, isValidVideoIdParam } from '../shared' const addVideoCaptionValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), - param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), + isValidVideoIdParam('videoId'), + + param('captionLanguage') + .custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), + body('captionfile') .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile')) .withMessage( @@ -34,8 +36,10 @@ const addVideoCaptionValidator = [ ] const deleteVideoCaptionValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), - param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), + isValidVideoIdParam('videoId'), + + param('captionLanguage') + .custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params }) @@ -53,7 +57,7 @@ const deleteVideoCaptionValidator = [ ] const listVideoCaptionsValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), + isValidVideoIdParam('videoId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking listVideoCaptions parameters', { parameters: req.params }) diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 1451ab988..885506ebe 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts @@ -3,13 +3,13 @@ import { body, param, query } from 'express-validator' import { MUserAccountUrl } from '@server/types/models' import { UserRight } from '../../../../shared' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' -import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' +import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' import { logger } from '../../../helpers/logger' import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation' import { Hooks } from '../../../lib/plugins/hooks' import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video' -import { areValidationErrors, doesVideoCommentExist, doesVideoCommentThreadExist, doesVideoExist } from '../shared' +import { areValidationErrors, doesVideoCommentExist, doesVideoCommentThreadExist, doesVideoExist, isValidVideoIdParam } from '../shared' const listVideoCommentsValidator = [ query('isLocal') @@ -40,7 +40,7 @@ const listVideoCommentsValidator = [ ] const listVideoCommentThreadsValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + isValidVideoIdParam('videoId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params }) @@ -53,8 +53,10 @@ const listVideoCommentThreadsValidator = [ ] const listVideoThreadCommentsValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), - param('threadId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'), + isValidVideoIdParam('videoId'), + + param('threadId') + .custom(isIdValid).not().isEmpty().withMessage('Should have a valid threadId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params }) @@ -68,8 +70,10 @@ const listVideoThreadCommentsValidator = [ ] const addVideoCommentThreadValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), - body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), + isValidVideoIdParam('videoId'), + + body('text') + .custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body }) @@ -84,8 +88,10 @@ const addVideoCommentThreadValidator = [ ] const addVideoCommentReplyValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + isValidVideoIdParam('videoId'), + param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), + body('text').custom(isValidVideoCommentText).not().isEmpty().withMessage('Should have a valid comment text'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { @@ -102,8 +108,10 @@ const addVideoCommentReplyValidator = [ ] const videoCommentGetValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), - param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), + isValidVideoIdParam('videoId'), + + param('commentId') + .custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) @@ -117,7 +125,8 @@ const videoCommentGetValidator = [ ] const removeVideoCommentValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + isValidVideoIdParam('videoId'), + param('commentId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid commentId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index b058ff5c1..7cfb935e3 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts @@ -1,5 +1,5 @@ import * as express from 'express' -import { body, param } from 'express-validator' +import { body } from 'express-validator' import { CONSTRAINTS_FIELDS } from '@server/initializers/constants' import { isLocalLiveVideoAccepted } from '@server/lib/moderation' import { Hooks } from '@server/lib/plugins/hooks' @@ -7,16 +7,22 @@ import { VideoModel } from '@server/models/video/video' import { VideoLiveModel } from '@server/models/video/video-live' import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' import { ServerErrorCode, UserRight, VideoState } from '@shared/models' -import { isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' +import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc' import { isVideoNameValid } from '../../../helpers/custom-validators/videos' import { cleanUpReqFiles } from '../../../helpers/express-utils' import { logger } from '../../../helpers/logger' import { CONFIG } from '../../../initializers/config' -import { areValidationErrors, checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../shared' +import { + areValidationErrors, + checkUserCanManageVideo, + doesVideoChannelOfAccountExist, + doesVideoExist, + isValidVideoIdParam +} from '../shared' import { getCommonVideoEditAttributes } from './videos' const videoLiveGetValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + isValidVideoIdParam('videoId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoLiveGetValidator parameters', { parameters: req.params, user: res.locals.oauth.token.User.username }) diff --git a/server/middlewares/validators/videos/video-ownership-changes.ts b/server/middlewares/validators/videos/video-ownership-changes.ts index 120b0469c..54ac46c99 100644 --- a/server/middlewares/validators/videos/video-ownership-changes.ts +++ b/server/middlewares/validators/videos/video-ownership-changes.ts @@ -1,6 +1,6 @@ import * as express from 'express' import { param } from 'express-validator' -import { isIdOrUUIDValid } from '@server/helpers/custom-validators/misc' +import { isIdValid } from '@server/helpers/custom-validators/misc' import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership' import { logger } from '@server/helpers/logger' import { isAbleToUploadVideo } from '@server/lib/user' @@ -13,11 +13,12 @@ import { checkUserCanManageVideo, doesChangeVideoOwnershipExist, doesVideoChannelOfAccountExist, - doesVideoExist + doesVideoExist, + isValidVideoIdParam } from '../shared' const videosChangeOwnershipValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + isValidVideoIdParam('videoId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking changeOwnership parameters', { parameters: req.params }) @@ -40,7 +41,8 @@ const videosChangeOwnershipValidator = [ ] const videosTerminateChangeOwnershipValidator = [ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('id') + .custom(isIdValid).withMessage('Should have a valid id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking changeOwnership parameters', { parameters: req.params }) diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index 0d2e6e90c..5ee7ee0ce 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts @@ -11,6 +11,7 @@ import { isIdOrUUIDValid, isIdValid, isUUIDValid, + toCompleteUUID, toIntArray, toIntOrNull, toValueOrNull @@ -29,7 +30,14 @@ import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' import { MVideoPlaylist } from '../../../types/models/video/video-playlist' import { authenticatePromiseIfNeeded } from '../../auth' -import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoPlaylistFetchType } from '../shared' +import { + areValidationErrors, + doesVideoChannelIdExist, + doesVideoExist, + doesVideoPlaylistExist, + isValidPlaylistIdParam, + VideoPlaylistFetchType +} from '../shared' const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ body('displayName') @@ -43,10 +51,13 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ const body: VideoPlaylistCreate = req.body if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) - if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) { + if ( + !body.videoChannelId && + (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED) + ) { cleanUpReqFiles(req) - return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' }) + return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' }) } return next() @@ -54,8 +65,7 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ ]) const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ - param('playlistId') - .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), + isValidPlaylistIdParam('playlistId'), body('displayName') .optional() @@ -101,8 +111,7 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([ ]) const videoPlaylistsDeleteValidator = [ - param('playlistId') - .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), + isValidPlaylistIdParam('playlistId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params }) @@ -126,8 +135,7 @@ const videoPlaylistsDeleteValidator = [ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => { return [ - param('playlistId') - .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), + isValidPlaylistIdParam('playlistId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params }) @@ -184,9 +192,10 @@ const videoPlaylistsSearchValidator = [ ] const videoPlaylistsAddVideoValidator = [ - param('playlistId') - .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), + isValidPlaylistIdParam('playlistId'), + body('videoId') + .customSanitizer(toCompleteUUID) .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'), body('startTimestamp') .optional() @@ -214,9 +223,9 @@ const videoPlaylistsAddVideoValidator = [ ] const videoPlaylistsUpdateOrRemoveVideoValidator = [ - param('playlistId') - .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), + isValidPlaylistIdParam('playlistId'), param('playlistElementId') + .customSanitizer(toCompleteUUID) .custom(isIdValid).withMessage('Should have an element id/uuid'), body('startTimestamp') .optional() @@ -251,8 +260,7 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [ ] const videoPlaylistElementAPGetValidator = [ - param('playlistId') - .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), + isValidPlaylistIdParam('playlistId'), param('playlistElementId') .custom(isIdValid).withMessage('Should have an playlist element id'), @@ -287,8 +295,7 @@ const videoPlaylistElementAPGetValidator = [ ] const videoPlaylistsReorderVideosValidator = [ - param('playlistId') - .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), + isValidPlaylistIdParam('playlistId'), body('startPosition') .isInt({ min: 1 }).withMessage('Should have a valid start position'), body('insertAfterPosition') diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts index 4a802e75e..5d5dfb222 100644 --- a/server/middlewares/validators/videos/video-rates.ts +++ b/server/middlewares/validators/videos/video-rates.ts @@ -3,15 +3,16 @@ import { body, param, query } from 'express-validator' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' import { VideoRateType } from '../../../../shared/models/videos' import { isAccountNameValid } from '../../../helpers/custom-validators/accounts' -import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' +import { isIdValid } from '../../../helpers/custom-validators/misc' import { isRatingValid } from '../../../helpers/custom-validators/video-rates' import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' import { logger } from '../../../helpers/logger' import { AccountVideoRateModel } from '../../../models/account/account-video-rate' -import { areValidationErrors, doesVideoExist } from '../shared' +import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' const videoUpdateRateValidator = [ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + isValidVideoIdParam('id'), + body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts index cc2f66e94..7e54b6fc0 100644 --- a/server/middlewares/validators/videos/video-shares.ts +++ b/server/middlewares/validators/videos/video-shares.ts @@ -1,14 +1,16 @@ import * as express from 'express' import { param } from 'express-validator' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' -import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' +import { isIdValid } from '../../../helpers/custom-validators/misc' import { logger } from '../../../helpers/logger' import { VideoShareModel } from '../../../models/video/video-share' -import { areValidationErrors, doesVideoExist } from '../shared' +import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' const videosShareValidator = [ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), - param('actorId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'), + isValidVideoIdParam('id'), + + param('actorId') + .custom(isIdValid).not().isEmpty().withMessage('Should have a valid actor id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoShare parameters', { parameters: req.params }) diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts index ef8b89ece..43306f7cd 100644 --- a/server/middlewares/validators/videos/video-watch.ts +++ b/server/middlewares/validators/videos/video-watch.ts @@ -1,12 +1,13 @@ import * as express from 'express' -import { body, param } from 'express-validator' +import { body } from 'express-validator' import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' -import { isIdOrUUIDValid, toIntOrNull } from '../../../helpers/custom-validators/misc' +import { toIntOrNull } from '../../../helpers/custom-validators/misc' import { logger } from '../../../helpers/logger' -import { areValidationErrors, doesVideoExist } from '../shared' +import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' const videoWatchingValidator = [ - param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + isValidVideoIdParam('videoId'), + body('currentTime') .customSanitizer(toIntOrNull) .isInt().withMessage('Should have correct current time'), diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 8201e80c3..49e10e2b5 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts @@ -12,7 +12,6 @@ import { isBooleanValid, isDateValid, isFileFieldValid, - isIdOrUUIDValid, isIdValid, isUUIDValid, toArray, @@ -53,7 +52,8 @@ import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist, - doesVideoFileOfVideoExist + doesVideoFileOfVideoExist, + isValidVideoIdParam } from '../shared' const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([ @@ -195,7 +195,8 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([ ]) const videosUpdateValidator = getCommonVideoEditAttributes().concat([ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + isValidVideoIdParam('id'), + body('name') .optional() .trim() @@ -258,7 +259,7 @@ const videosCustomGetValidator = ( authenticateInQuery = false ) => { return [ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + isValidVideoIdParam('id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videosGet parameters', { parameters: req.params }) @@ -309,8 +310,10 @@ const videosGetValidator = videosCustomGetValidator('all') const videosDownloadValidator = videosCustomGetValidator('all', true) const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), - param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'), + isValidVideoIdParam('id'), + + param('videoFileId') + .custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params }) @@ -323,7 +326,7 @@ const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ ]) const videosRemoveValidator = [ - param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + isValidVideoIdParam('id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videosRemove parameters', { parameters: req.params }) diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index 8880c0450..672c671b8 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts @@ -1,3 +1,4 @@ +import { uuidToShort } from '@server/helpers/uuid' import { generateMagnetUri } from '@server/helpers/webtorrent' import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths' import { VideoFile } from '@shared/models/videos/video-file.model' @@ -47,6 +48,8 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor const videoObject: Video = { id: video.id, uuid: video.uuid, + shortUUID: uuidToShort(video.uuid), + name: video.name, category: { id: video.category, diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index 5ec944b9e..d24be56c3 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts @@ -15,7 +15,7 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { v4 as uuidv4 } from 'uuid' +import { buildUUID } from '@server/helpers/uuid' import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' import { AttributesOnly } from '@shared/core-utils' import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' @@ -182,7 +182,7 @@ export class VideoCaptionModel extends Model v.privacy.id === VideoPrivacy.PRIVATE) - privateVideoId = privateVideo.id - privateVideoUUID = privateVideo.uuid - - const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL) - internalVideoId = internalVideo.id - internalVideoUUID = internalVideo.uuid - }) - - it('Should not be able to watch the private/internal video with non authenticated user', async function () { - await getVideo(servers[0].url, privateVideoUUID, HttpStatusCode.UNAUTHORIZED_401) - await getVideo(servers[0].url, internalVideoUUID, HttpStatusCode.UNAUTHORIZED_401) - }) - - it('Should not be able to watch the private video with another user', async function () { - this.timeout(10000) - - const user = { - username: 'hello', - password: 'super password' - } - await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password }) - - anotherUserToken = await userLogin(servers[0], user) - await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, HttpStatusCode.FORBIDDEN_403) - }) - - it('Should be able to watch the internal video with another user', async function () { - await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, HttpStatusCode.OK_200) - }) - - it('Should be able to watch the private video with the correct user', async function () { - await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, HttpStatusCode.OK_200) - }) - - it('Should upload an unlisted video on server 2', async function () { - this.timeout(60000) - - const attributes = { - name: 'unlisted video', - privacy: VideoPrivacy.UNLISTED - } - await uploadVideo(servers[1].url, servers[1].accessToken, attributes) - - // Server 2 has transcoding enabled - await waitJobs(servers) - }) - - it('Should not have this unlisted video listed on server 1 and 2', async function () { - for (const server of servers) { - const res = await getVideosList(server.url) + it('Should not have these private and internal videos on server 2', async function () { + const res = await getVideosList(servers[1].url) expect(res.body.total).to.equal(0) expect(res.body.data).to.have.lengthOf(0) - } - }) + }) - it('Should list my (unlisted) videos', async function () { - const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 1) + it('Should not list the private and internal videos for an unauthenticated user on server 1', async function () { + const res = await getVideosList(servers[0].url) - expect(res.body.total).to.equal(1) - expect(res.body.data).to.have.lengthOf(1) + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + }) - unlistedVideoUUID = res.body.data[0].uuid - }) + it('Should not list the private video and list the internal video for an authenticated user on server 1', async function () { + const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken) - it('Should be able to get this unlisted video', async function () { - for (const server of servers) { - const res = await getVideo(server.url, unlistedVideoUUID) + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) - expect(res.body.name).to.equal('unlisted video') - } - }) + expect(res.body.data[0].privacy.id).to.equal(VideoPrivacy.INTERNAL) + }) - it('Should upload a non-federating unlisted video to server 1', async function () { - this.timeout(30000) + it('Should list my (private and internal) videos', async function () { + const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 10) - const attributes = { - name: 'unlisted video', - privacy: VideoPrivacy.UNLISTED - } - await uploadVideo(servers[0].url, servers[0].accessToken, attributes) - - await waitJobs(servers) - }) - - it('Should list my new unlisted video', async function () { - const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 3) - - expect(res.body.total).to.equal(3) - expect(res.body.data).to.have.lengthOf(3) - - nonFederatedUnlistedVideoUUID = res.body.data[0].uuid - }) - - it('Should be able to get non-federated unlisted video from origin', async function () { - const res = await getVideo(servers[0].url, nonFederatedUnlistedVideoUUID) - - expect(res.body.name).to.equal('unlisted video') - }) - - it('Should not be able to get non-federated unlisted video from federated server', async function () { - await getVideo(servers[1].url, nonFederatedUnlistedVideoUUID, HttpStatusCode.NOT_FOUND_404) - }) - - it('Should update the private and internal videos to public on server 1', async function () { - this.timeout(10000) - - now = Date.now() - - { - const attribute = { - name: 'private video becomes public', - privacy: VideoPrivacy.PUBLIC - } - - await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute) - } - - { - const attribute = { - name: 'internal video becomes public', - privacy: VideoPrivacy.PUBLIC - } - await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, attribute) - } - - await waitJobs(servers) - }) - - it('Should have this new public video listed on server 1 and 2', async function () { - for (const server of servers) { - const res = await getVideosList(server.url) expect(res.body.total).to.equal(2) expect(res.body.data).to.have.lengthOf(2) const videos: Video[] = res.body.data - const privateVideo = videos.find(v => v.name === 'private video becomes public') - const internalVideo = videos.find(v => v.name === 'internal video becomes public') - expect(privateVideo).to.not.be.undefined - expect(internalVideo).to.not.be.undefined + const privateVideo = videos.find(v => v.privacy.id === VideoPrivacy.PRIVATE) + privateVideoId = privateVideo.id + privateVideoUUID = privateVideo.uuid - expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now) - // We don't change the publish date of internal videos - expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now) + const internalVideo = videos.find(v => v.privacy.id === VideoPrivacy.INTERNAL) + internalVideoId = internalVideo.id + internalVideoUUID = internalVideo.uuid + }) - expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) - expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) - } + it('Should not be able to watch the private/internal video with non authenticated user', async function () { + await getVideo(servers[0].url, privateVideoUUID, HttpStatusCode.UNAUTHORIZED_401) + await getVideo(servers[0].url, internalVideoUUID, HttpStatusCode.UNAUTHORIZED_401) + }) + + it('Should not be able to watch the private video with another user', async function () { + this.timeout(10000) + + const user = { + username: 'hello', + password: 'super password' + } + await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password }) + + anotherUserToken = await userLogin(servers[0], user) + await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, HttpStatusCode.FORBIDDEN_403) + }) + + it('Should be able to watch the internal video with another user', async function () { + await getVideoWithToken(servers[0].url, anotherUserToken, internalVideoUUID, HttpStatusCode.OK_200) + }) + + it('Should be able to watch the private video with the correct user', async function () { + await getVideoWithToken(servers[0].url, servers[0].accessToken, privateVideoUUID, HttpStatusCode.OK_200) + }) }) - it('Should set these videos as private and internal', async function () { - this.timeout(10000) + describe('Unlisted videos', function () { - await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE }) - await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL }) + it('Should upload an unlisted video on server 2', async function () { + this.timeout(60000) - await waitJobs(servers) + const attributes = { + name: 'unlisted video', + privacy: VideoPrivacy.UNLISTED + } + await uploadVideo(servers[1].url, servers[1].accessToken, attributes) - for (const server of servers) { - const res = await getVideosList(server.url) + // Server 2 has transcoding enabled + await waitJobs(servers) + }) - expect(res.body.total).to.equal(0) - expect(res.body.data).to.have.lengthOf(0) - } + it('Should not have this unlisted video listed on server 1 and 2', async function () { + for (const server of servers) { + const res = await getVideosList(server.url) - { - const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) - const videos = res.body.data + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + } + }) + + it('Should list my (unlisted) videos', async function () { + const res = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 1) + + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) + + unlistedVideo = res.body.data[0] + }) + + it('Should not be able to get this unlisted video using its id', async function () { + await getVideo(servers[1].url, unlistedVideo.id, 404) + }) + + it('Should be able to get this unlisted video using its uuid/shortUUID', async function () { + for (const server of servers) { + for (const id of [ unlistedVideo.uuid, unlistedVideo.shortUUID ]) { + const res = await getVideo(server.url, id) + + expect(res.body.name).to.equal('unlisted video') + } + } + }) + + it('Should upload a non-federating unlisted video to server 1', async function () { + this.timeout(30000) + + const attributes = { + name: 'unlisted video', + privacy: VideoPrivacy.UNLISTED + } + await uploadVideo(servers[0].url, servers[0].accessToken, attributes) + + await waitJobs(servers) + }) + + it('Should list my new unlisted video', async function () { + const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 3) expect(res.body.total).to.equal(3) - expect(videos).to.have.lengthOf(3) + expect(res.body.data).to.have.lengthOf(3) - const privateVideo = videos.find(v => v.name === 'private video becomes public') - const internalVideo = videos.find(v => v.name === 'internal video becomes public') + nonFederatedUnlistedVideoUUID = res.body.data[0].uuid + }) - expect(privateVideo).to.not.be.undefined - expect(internalVideo).to.not.be.undefined + it('Should be able to get non-federated unlisted video from origin', async function () { + const res = await getVideo(servers[0].url, nonFederatedUnlistedVideoUUID) - expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL) - expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE) - } + expect(res.body.name).to.equal('unlisted video') + }) + + it('Should not be able to get non-federated unlisted video from federated server', async function () { + await getVideo(servers[1].url, nonFederatedUnlistedVideoUUID, HttpStatusCode.NOT_FOUND_404) + }) + }) + + describe('Privacy update', function () { + + it('Should update the private and internal videos to public on server 1', async function () { + this.timeout(10000) + + now = Date.now() + + { + const attribute = { + name: 'private video becomes public', + privacy: VideoPrivacy.PUBLIC + } + + await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute) + } + + { + const attribute = { + name: 'internal video becomes public', + privacy: VideoPrivacy.PUBLIC + } + await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, attribute) + } + + await waitJobs(servers) + }) + + it('Should have this new public video listed on server 1 and 2', async function () { + for (const server of servers) { + const res = await getVideosList(server.url) + expect(res.body.total).to.equal(2) + expect(res.body.data).to.have.lengthOf(2) + + const videos: Video[] = res.body.data + const privateVideo = videos.find(v => v.name === 'private video becomes public') + const internalVideo = videos.find(v => v.name === 'internal video becomes public') + + expect(privateVideo).to.not.be.undefined + expect(internalVideo).to.not.be.undefined + + expect(new Date(privateVideo.publishedAt).getTime()).to.be.at.least(now) + // We don't change the publish date of internal videos + expect(new Date(internalVideo.publishedAt).getTime()).to.be.below(now) + + expect(privateVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) + expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PUBLIC) + } + }) + + it('Should set these videos as private and internal', async function () { + this.timeout(10000) + + await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, { privacy: VideoPrivacy.PRIVATE }) + await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, { privacy: VideoPrivacy.INTERNAL }) + + await waitJobs(servers) + + for (const server of servers) { + const res = await getVideosList(server.url) + + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + } + + { + const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5) + const videos = res.body.data + + expect(res.body.total).to.equal(3) + expect(videos).to.have.lengthOf(3) + + const privateVideo = videos.find(v => v.name === 'private video becomes public') + const internalVideo = videos.find(v => v.name === 'internal video becomes public') + + expect(privateVideo).to.not.be.undefined + expect(internalVideo).to.not.be.undefined + + expect(privateVideo.privacy.id).to.equal(VideoPrivacy.INTERNAL) + expect(internalVideo.privacy.id).to.equal(VideoPrivacy.PRIVATE) + } + }) }) after(async function () { diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts index 5c9e023e1..a0af09de8 100644 --- a/server/tests/cli/prune-storage.ts +++ b/server/tests/cli/prune-storage.ts @@ -2,7 +2,10 @@ import 'mocha' import * as chai from 'chai' -import { waitJobs } from '../../../shared/extra-utils/server/jobs' +import { createFile, readdir } from 'fs-extra' +import { join } from 'path' +import { buildUUID } from '@server/helpers/uuid' +import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' import { buildServerDirectory, cleanupTests, @@ -21,11 +24,8 @@ import { uploadVideo, wait } from '../../../shared/extra-utils' +import { waitJobs } from '../../../shared/extra-utils/server/jobs' import { Account, VideoPlaylistPrivacy } from '../../../shared/models' -import { createFile, readdir } from 'fs-extra' -import { v4 as uuidv4 } from 'uuid' -import { join } from 'path' -import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' const expect = chai.expect @@ -131,8 +131,8 @@ describe('Test prune storage scripts', function () { { const base = buildServerDirectory(servers[0], 'videos') - const n1 = uuidv4() + '.mp4' - const n2 = uuidv4() + '.webm' + const n1 = buildUUID() + '.mp4' + const n2 = buildUUID() + '.webm' await createFile(join(base, n1)) await createFile(join(base, n2)) @@ -143,8 +143,8 @@ describe('Test prune storage scripts', function () { { const base = buildServerDirectory(servers[0], 'torrents') - const n1 = uuidv4() + '-240.torrent' - const n2 = uuidv4() + '-480.torrent' + const n1 = buildUUID() + '-240.torrent' + const n2 = buildUUID() + '-480.torrent' await createFile(join(base, n1)) await createFile(join(base, n2)) @@ -155,8 +155,8 @@ describe('Test prune storage scripts', function () { { const base = buildServerDirectory(servers[0], 'thumbnails') - const n1 = uuidv4() + '.jpg' - const n2 = uuidv4() + '.jpg' + const n1 = buildUUID() + '.jpg' + const n2 = buildUUID() + '.jpg' await createFile(join(base, n1)) await createFile(join(base, n2)) @@ -167,8 +167,8 @@ describe('Test prune storage scripts', function () { { const base = buildServerDirectory(servers[0], 'previews') - const n1 = uuidv4() + '.jpg' - const n2 = uuidv4() + '.jpg' + const n1 = buildUUID() + '.jpg' + const n2 = buildUUID() + '.jpg' await createFile(join(base, n1)) await createFile(join(base, n2)) @@ -179,8 +179,8 @@ describe('Test prune storage scripts', function () { { const base = buildServerDirectory(servers[0], 'avatars') - const n1 = uuidv4() + '.png' - const n2 = uuidv4() + '.jpg' + const n1 = buildUUID() + '.png' + const n2 = buildUUID() + '.jpg' await createFile(join(base, n1)) await createFile(join(base, n2)) diff --git a/server/tests/client.ts b/server/tests/client.ts index 253a95624..7c4fb4e46 100644 --- a/server/tests/client.ts +++ b/server/tests/client.ts @@ -3,9 +3,8 @@ import 'mocha' import * as chai from 'chai' import { omit } from 'lodash' -import * as request from 'supertest' import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' -import { Account, CustomConfig, HTMLServerConfig, ServerConfig, VideoPlaylistPrivacy } from '@shared/models' +import { Account, CustomConfig, HTMLServerConfig, ServerConfig, VideoPlaylistCreateResult, VideoPlaylistPrivacy } from '@shared/models' import { addVideoInPlaylist, cleanupTests, @@ -50,13 +49,16 @@ describe('Test a client controllers', function () { const playlistName = 'super playlist name' const playlistDescription = 'super playlist description' - let playlistUUID: string + let playlist: VideoPlaylistCreateResult const channelDescription = 'my super channel description' const watchVideoBasePaths = [ '/videos/watch/', '/w/' ] const watchPlaylistBasePaths = [ '/videos/watch/playlist/', '/w/p/' ] + let videoIds: (string | number)[] = [] + let playlistIds: (string | number)[] = [] + before(async function () { this.timeout(120000) @@ -79,7 +81,9 @@ describe('Test a client controllers', function () { const videos = resVideosRequest.body.data expect(videos.length).to.equal(1) - servers[0].video = videos[0] + const video = videos[0] + servers[0].video = video + videoIds = [ video.id, video.uuid, video.shortUUID ] // Playlist @@ -91,16 +95,14 @@ describe('Test a client controllers', function () { } const resVideoPlaylistRequest = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs }) - - const playlist = resVideoPlaylistRequest.body.videoPlaylist - const playlistId = playlist.id - playlistUUID = playlist.uuid + playlist = resVideoPlaylistRequest.body.videoPlaylist + playlistIds = [ playlist.id, playlist.shortUUID, playlist.uuid ] await addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, - playlistId, - elementAttrs: { videoId: servers[0].video.id } + playlistId: playlist.shortUUID, + elementAttrs: { videoId: video.id } }) // Account @@ -117,36 +119,43 @@ describe('Test a client controllers', function () { it('Should have valid oEmbed discovery tags for videos', async function () { for (const basePath of watchVideoBasePaths) { - const path = basePath + servers[0].video.uuid - const res = await request(servers[0].url) - .get(path) - .set('Accept', 'text/html') - .expect(HttpStatusCode.OK_200) + for (const id of videoIds) { + const res = await makeGetRequest({ + url: servers[0].url, + path: basePath + id, + accept: 'text/html', + statusCodeExpected: HttpStatusCode.OK_200 + }) - const port = servers[0].port + const port = servers[0].port - const expectedLink = '` + const expectedLink = '` - expect(res.text).to.contain(expectedLink) + expect(res.text).to.contain(expectedLink) + } } }) it('Should have valid oEmbed discovery tags for a playlist', async function () { for (const basePath of watchPlaylistBasePaths) { - const res = await request(servers[0].url) - .get(basePath + playlistUUID) - .set('Accept', 'text/html') - .expect(HttpStatusCode.OK_200) + for (const id of playlistIds) { + const res = await makeGetRequest({ + url: servers[0].url, + path: basePath + id, + accept: 'text/html', + statusCodeExpected: HttpStatusCode.OK_200 + }) - const port = servers[0].port + const port = servers[0].port - const expectedLink = '` + const expectedLink = '` - expect(res.text).to.contain(expectedLink) + expect(res.text).to.contain(expectedLink) + } } }) }) @@ -190,7 +199,7 @@ describe('Test a client controllers', function () { expect(text).to.contain(``) expect(text).to.contain(``) expect(text).to.contain('') - expect(text).to.contain(``) + expect(text).to.contain(``) } it('Should have valid Open Graph tags on the account page', async function () { @@ -206,15 +215,19 @@ describe('Test a client controllers', function () { }) it('Should have valid Open Graph tags on the watch page', async function () { - await watchVideoPageTest('/videos/watch/' + servers[0].video.id) - await watchVideoPageTest('/videos/watch/' + servers[0].video.uuid) - await watchVideoPageTest('/w/' + servers[0].video.uuid) - await watchVideoPageTest('/w/' + servers[0].video.id) + for (const path of watchVideoBasePaths) { + for (const id of videoIds) { + await watchVideoPageTest(path + id) + } + } }) it('Should have valid Open Graph tags on the watch playlist page', async function () { - await watchPlaylistPageTest('/videos/watch/playlist/' + playlistUUID) - await watchPlaylistPageTest('/w/p/' + playlistUUID) + for (const path of watchPlaylistBasePaths) { + for (const id of playlistIds) { + await watchPlaylistPageTest(path + id) + } + } }) }) @@ -263,15 +276,19 @@ describe('Test a client controllers', function () { } it('Should have valid twitter card on the watch video page', async function () { - await watchVideoPageTest('/videos/watch/' + servers[0].video.id) - await watchVideoPageTest('/videos/watch/' + servers[0].video.uuid) - await watchVideoPageTest('/w/' + servers[0].video.uuid) - await watchVideoPageTest('/w/' + servers[0].video.id) + for (const path of watchVideoBasePaths) { + for (const id of videoIds) { + await watchVideoPageTest(path + id) + } + } }) it('Should have valid twitter card on the watch playlist page', async function () { - await watchPlaylistPageTest('/videos/watch/playlist/' + playlistUUID) - await watchPlaylistPageTest('/w/p/' + playlistUUID) + for (const path of watchPlaylistBasePaths) { + for (const id of playlistIds) { + await watchPlaylistPageTest(path + id) + } + } }) it('Should have valid twitter card on the account page', async function () { @@ -333,15 +350,19 @@ describe('Test a client controllers', function () { } it('Should have valid twitter card on the watch video page', async function () { - await watchVideoPageTest('/videos/watch/' + servers[0].video.id) - await watchVideoPageTest('/videos/watch/' + servers[0].video.uuid) - await watchVideoPageTest('/w/' + servers[0].video.uuid) - await watchVideoPageTest('/w/' + servers[0].video.id) + for (const path of watchVideoBasePaths) { + for (const id of videoIds) { + await watchVideoPageTest(path + id) + } + } }) it('Should have valid twitter card on the watch playlist page', async function () { - await watchPlaylistPageTest('/videos/watch/playlist/' + playlistUUID) - await watchPlaylistPageTest('/w/p/' + playlistUUID) + for (const path of watchPlaylistBasePaths) { + for (const id of playlistIds) { + await watchPlaylistPageTest(path + id) + } + } }) it('Should have valid twitter card on the account page', async function () { @@ -399,8 +420,10 @@ describe('Test a client controllers', function () { it('Should use the original video URL for the canonical tag', async function () { for (const basePath of watchVideoBasePaths) { - const res = await makeHTMLRequest(servers[1].url, basePath + servers[0].video.uuid) - expect(res.text).to.contain(``) + for (const id of videoIds) { + const res = await makeHTMLRequest(servers[1].url, basePath + id) + expect(res.text).to.contain(``) + } } }) @@ -426,8 +449,10 @@ describe('Test a client controllers', function () { it('Should use the original playlist URL for the canonical tag', async function () { for (const basePath of watchPlaylistBasePaths) { - const res = await makeHTMLRequest(servers[1].url, basePath + playlistUUID) - expect(res.text).to.contain(``) + for (const id of playlistIds) { + const res = await makeHTMLRequest(servers[1].url, basePath + id) + expect(res.text).to.contain(``) + } } }) }) diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts index 63f7667a1..eb0a776b8 100644 --- a/server/tools/peertube-repl.ts +++ b/server/tools/peertube-repl.ts @@ -4,7 +4,6 @@ registerTSPaths() import * as repl from 'repl' import * as path from 'path' import * as _ from 'lodash' -import { uuidv1, uuidv3, uuidv4, uuidv5 } from 'uuid' import * as Sequelize from 'sequelize' import * as YoutubeDL from 'youtube-dl' import { initDatabaseModels, sequelizeTypescript } from '../initializers/database' @@ -31,10 +30,6 @@ const start = async () => { env: process.env, lodash: _, path, - uuidv1, - uuidv3, - uuidv4, - uuidv5, cli, logger, constants, diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index d04757470..28e431e94 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts @@ -43,6 +43,7 @@ interface ServerInfo { video?: { id: number uuid: string + shortUUID: string name?: string url?: string diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts index a3a276188..469ea4d63 100644 --- a/shared/extra-utils/videos/videos.ts +++ b/shared/extra-utils/videos/videos.ts @@ -6,9 +6,9 @@ import got, { Response as GotResponse } from 'got/dist/source' import * as parseTorrent from 'parse-torrent' import { join } from 'path' import * as request from 'supertest' -import { v4 as uuidv4 } from 'uuid' import validator from 'validator' import { getLowercaseExtension } from '@server/helpers/core-utils' +import { buildUUID } from '@server/helpers/uuid' import { HttpStatusCode } from '@shared/core-utils' import { VideosCommonQuery } from '@shared/models' import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' @@ -806,7 +806,7 @@ async function uploadVideoAndGetId (options: { const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs) - return { id: res.body.video.id, uuid: res.body.video.uuid } + return res.body.video as { id: number, uuid: string, shortUUID: string } } async function getLocalIdByUUID (url: string, uuid: string) { @@ -827,7 +827,7 @@ async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) { const prefixName = additionalParams.prefixName || '' - const name = prefixName + uuidv4() + const name = prefixName + buildUUID() const data = Object.assign({ name }, additionalParams) const res = await uploadVideo(server.url, server.accessToken, data) diff --git a/shared/models/common/index.ts b/shared/models/common/index.ts new file mode 100644 index 000000000..4db85eff2 --- /dev/null +++ b/shared/models/common/index.ts @@ -0,0 +1 @@ +export * from './result-list.model' diff --git a/shared/models/result-list.model.ts b/shared/models/common/result-list.model.ts similarity index 100% rename from shared/models/result-list.model.ts rename to shared/models/common/result-list.model.ts diff --git a/shared/models/index.ts b/shared/models/index.ts index 4db1f234e..5c2bc480e 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -1,16 +1,16 @@ export * from './activitypub' export * from './actors' -export * from './moderation' -export * from './custom-markup' export * from './bulk' -export * from './redundancy' -export * from './users' -export * from './videos' +export * from './common' +export * from './custom-markup' export * from './feeds' export * from './joinpeertube' +export * from './moderation' export * from './overviews' export * from './plugins' +export * from './redundancy' export * from './search' export * from './server' -export * from './oauth-client-local.model' -export * from './result-list.model' +export * from './tokens' +export * from './users' +export * from './videos' diff --git a/shared/models/moderation/abuse/abuse-create.model.ts b/shared/models/moderation/abuse/abuse-create.model.ts index 0e7e9587f..7d35555c3 100644 --- a/shared/models/moderation/abuse/abuse-create.model.ts +++ b/shared/models/moderation/abuse/abuse-create.model.ts @@ -10,7 +10,7 @@ export interface AbuseCreate { } video?: { - id: number + id: number | string startAt?: number endAt?: number } diff --git a/shared/models/tokens/index.ts b/shared/models/tokens/index.ts new file mode 100644 index 000000000..fe130f153 --- /dev/null +++ b/shared/models/tokens/index.ts @@ -0,0 +1 @@ +export * from './oauth-client-local.model' diff --git a/shared/models/oauth-client-local.model.ts b/shared/models/tokens/oauth-client-local.model.ts similarity index 100% rename from shared/models/oauth-client-local.model.ts rename to shared/models/tokens/oauth-client-local.model.ts diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 64f2c9df6..faa9b9868 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -35,3 +35,4 @@ export * from './video-transcoding-fps.model' export * from './video-update.model' export * from './video.model' +export * from './video-create-result.model' diff --git a/shared/models/videos/playlist/index.ts b/shared/models/videos/playlist/index.ts index 99f7e9bab..f11a4bd28 100644 --- a/shared/models/videos/playlist/index.ts +++ b/shared/models/videos/playlist/index.ts @@ -1,4 +1,5 @@ export * from './video-exist-in-playlist.model' +export * from './video-playlist-create-result.model' export * from './video-playlist-create.model' export * from './video-playlist-element-create.model' export * from './video-playlist-element-update.model' diff --git a/shared/models/videos/playlist/video-playlist-create-result.model.ts b/shared/models/videos/playlist/video-playlist-create-result.model.ts new file mode 100644 index 000000000..cd9b170ae --- /dev/null +++ b/shared/models/videos/playlist/video-playlist-create-result.model.ts @@ -0,0 +1,5 @@ +export interface VideoPlaylistCreateResult { + id: number + uuid: string + shortUUID: string +} diff --git a/shared/models/videos/playlist/video-playlist.model.ts b/shared/models/videos/playlist/video-playlist.model.ts index ab4171ad1..b8a9955d9 100644 --- a/shared/models/videos/playlist/video-playlist.model.ts +++ b/shared/models/videos/playlist/video-playlist.model.ts @@ -6,6 +6,8 @@ import { VideoPlaylistType } from './video-playlist-type.model' export interface VideoPlaylist { id: number uuid: string + shortUUID: string + isLocal: boolean url: string diff --git a/shared/models/videos/video-create-result.model.ts b/shared/models/videos/video-create-result.model.ts new file mode 100644 index 000000000..a9f8e25a0 --- /dev/null +++ b/shared/models/videos/video-create-result.model.ts @@ -0,0 +1,5 @@ +export interface VideoCreateResult { + id: number + uuid: string + shortUUID: string +} diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index caefeff82..0e3e89f43 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -10,6 +10,8 @@ import { VideoStreamingPlaylist } from './video-streaming-playlist.model' export interface Video { id: number uuid: string + shortUUID: string + createdAt: Date | string updatedAt: Date | string publishedAt: Date | string diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 919905788..1f9f3d5c4 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -3003,6 +3003,8 @@ paths: $ref: '#/components/schemas/VideoPlaylist/properties/id' uuid: $ref: '#/components/schemas/VideoPlaylist/properties/uuid' + shortUUID: + $ref: '#/components/schemas/VideoPlaylist/properties/shortUUID' requestBody: content: multipart/form-data: @@ -4543,11 +4545,12 @@ components: name: id in: path required: true - description: The object id or uuid + description: The object id, uuid or short uuid schema: oneOf: - $ref: '#/components/schemas/id' - $ref: '#/components/schemas/UUIDv4' + - $ref: '#/components/schemas/shortUUID' playlistId: name: playlistId in: path @@ -4812,6 +4815,10 @@ components: pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' minLength: 36 maxLength: 36 + shortUUID: + type: string + description: translation of a uuid v4 with a bigger alphabet to have a shorter uuid + example: 2y84q2MQUMWPbiEcxNXMgC username: type: string description: immutable name of the user, used to find or mention its actor @@ -5141,6 +5148,9 @@ components: description: universal identifier for the video, that can be used across instances allOf: - $ref: '#/components/schemas/UUIDv4' + shortUUID: + allOf: + - $ref: '#/components/schemas/shortUUID' isLive: type: boolean createdAt: @@ -5520,6 +5530,9 @@ components: $ref: '#/components/schemas/id' uuid: $ref: '#/components/schemas/UUIDv4' + shortUUID: + allOf: + - $ref: '#/components/schemas/shortUUID' createdAt: type: string format: date-time @@ -6295,6 +6308,8 @@ components: $ref: '#/components/schemas/Video/properties/id' uuid: $ref: '#/components/schemas/Video/properties/uuid' + shortUUID: + $ref: '#/components/schemas/Video/properties/shortUUID' CommentThreadResponse: properties: total: diff --git a/yarn.lock b/yarn.lock index 05295ea42..f68741038 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7260,6 +7260,14 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +short-uuid@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/short-uuid/-/short-uuid-4.2.0.tgz#3706d9e7287ac589dc5ffe324d3e34817a07540b" + integrity sha512-r3cxuPPZSuF0QkKsK9bBR7u+7cwuCRzWzgjPh07F5N2iIUNgblnMHepBY16xgj5t1lG9iOP9k/TEafY1qhRzaw== + dependencies: + any-base "^1.1.0" + uuid "^8.3.2" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"