Federated video streaming platform using ActivityPub and P2P in the web browser with Angular. https://joinpeertube.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

439 lines
15 KiB

  1. import express from 'express'
  2. import { body, param, query, ValidationChain } from 'express-validator'
  3. import { ExpressPromiseHandler } from '@server/types/express-handler'
  4. import { MUserAccountId } from '@server/types/models'
  5. import {
  6. HttpStatusCode,
  7. UserRight,
  8. VideoPlaylistCreate,
  9. VideoPlaylistPrivacy,
  10. VideoPlaylistType,
  11. VideoPlaylistUpdate
  12. } from '@shared/models'
  13. import {
  14. isArrayOf,
  15. isIdOrUUIDValid,
  16. isIdValid,
  17. isUUIDValid,
  18. toCompleteUUID,
  19. toIntArray,
  20. toIntOrNull,
  21. toValueOrNull
  22. } from '../../../helpers/custom-validators/misc'
  23. import {
  24. isVideoPlaylistDescriptionValid,
  25. isVideoPlaylistNameValid,
  26. isVideoPlaylistPrivacyValid,
  27. isVideoPlaylistTimestampValid,
  28. isVideoPlaylistTypeValid
  29. } from '../../../helpers/custom-validators/video-playlists'
  30. import { isVideoImage } from '../../../helpers/custom-validators/videos'
  31. import { cleanUpReqFiles } from '../../../helpers/express-utils'
  32. import { logger } from '../../../helpers/logger'
  33. import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
  34. import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
  35. import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
  36. import { authenticatePromiseIfNeeded } from '../../auth'
  37. import {
  38. areValidationErrors,
  39. doesVideoChannelIdExist,
  40. doesVideoExist,
  41. doesVideoPlaylistExist,
  42. isValidPlaylistIdParam,
  43. VideoPlaylistFetchType
  44. } from '../shared'
  45. const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
  46. body('displayName')
  47. .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
  48. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  49. logger.debug('Checking videoPlaylistsAddValidator parameters', { parameters: req.body })
  50. if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
  51. const body: VideoPlaylistCreate = req.body
  52. if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
  53. if (
  54. !body.videoChannelId &&
  55. (body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
  56. ) {
  57. cleanUpReqFiles(req)
  58. return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
  59. }
  60. return next()
  61. }
  62. ])
  63. const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
  64. isValidPlaylistIdParam('playlistId'),
  65. body('displayName')
  66. .optional()
  67. .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
  68. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  69. logger.debug('Checking videoPlaylistsUpdateValidator parameters', { parameters: req.body })
  70. if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
  71. if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
  72. const videoPlaylist = getPlaylist(res)
  73. if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
  74. return cleanUpReqFiles(req)
  75. }
  76. const body: VideoPlaylistUpdate = req.body
  77. const newPrivacy = body.privacy || videoPlaylist.privacy
  78. if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
  79. (
  80. (!videoPlaylist.videoChannelId && !body.videoChannelId) ||
  81. body.videoChannelId === null
  82. )
  83. ) {
  84. cleanUpReqFiles(req)
  85. return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
  86. }
  87. if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
  88. cleanUpReqFiles(req)
  89. return res.fail({ message: 'Cannot update a watch later playlist.' })
  90. }
  91. if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
  92. return next()
  93. }
  94. ])
  95. const videoPlaylistsDeleteValidator = [
  96. isValidPlaylistIdParam('playlistId'),
  97. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  98. logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params })
  99. if (areValidationErrors(req, res)) return
  100. if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
  101. const videoPlaylist = getPlaylist(res)
  102. if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
  103. return res.fail({ message: 'Cannot delete a watch later playlist.' })
  104. }
  105. if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
  106. return
  107. }
  108. return next()
  109. }
  110. ]
  111. const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
  112. return [
  113. isValidPlaylistIdParam('playlistId'),
  114. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  115. logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
  116. if (areValidationErrors(req, res)) return
  117. if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
  118. const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
  119. // Video is unlisted, check we used the uuid to fetch it
  120. if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
  121. if (isUUIDValid(req.params.playlistId)) return next()
  122. return res.fail({
  123. status: HttpStatusCode.NOT_FOUND_404,
  124. message: 'Playlist not found'
  125. })
  126. }
  127. if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
  128. await authenticatePromiseIfNeeded(req, res)
  129. const user = res.locals.oauth ? res.locals.oauth.token.User : null
  130. if (
  131. !user ||
  132. (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
  133. ) {
  134. return res.fail({
  135. status: HttpStatusCode.FORBIDDEN_403,
  136. message: 'Cannot get this private video playlist.'
  137. })
  138. }
  139. return next()
  140. }
  141. return next()
  142. }
  143. ]
  144. }
  145. const videoPlaylistsSearchValidator = [
  146. query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
  147. (req: express.Request, res: express.Response, next: express.NextFunction) => {
  148. logger.debug('Checking videoPlaylists search query', { parameters: req.query })
  149. if (areValidationErrors(req, res)) return
  150. return next()
  151. }
  152. ]
  153. const videoPlaylistsAddVideoValidator = [
  154. isValidPlaylistIdParam('playlistId'),
  155. body('videoId')
  156. .customSanitizer(toCompleteUUID)
  157. .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
  158. body('startTimestamp')
  159. .optional()
  160. .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
  161. body('stopTimestamp')
  162. .optional()
  163. .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
  164. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  165. logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params })
  166. if (areValidationErrors(req, res)) return
  167. if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
  168. if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
  169. const videoPlaylist = getPlaylist(res)
  170. if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
  171. return
  172. }
  173. return next()
  174. }
  175. ]
  176. const videoPlaylistsUpdateOrRemoveVideoValidator = [
  177. isValidPlaylistIdParam('playlistId'),
  178. param('playlistElementId')
  179. .customSanitizer(toCompleteUUID)
  180. .custom(isIdValid).withMessage('Should have an element id/uuid'),
  181. body('startTimestamp')
  182. .optional()
  183. .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
  184. body('stopTimestamp')
  185. .optional()
  186. .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
  187. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  188. logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params })
  189. if (areValidationErrors(req, res)) return
  190. if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
  191. const videoPlaylist = getPlaylist(res)
  192. const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
  193. if (!videoPlaylistElement) {
  194. res.fail({
  195. status: HttpStatusCode.NOT_FOUND_404,
  196. message: 'Video playlist element not found'
  197. })
  198. return
  199. }
  200. res.locals.videoPlaylistElement = videoPlaylistElement
  201. if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
  202. return next()
  203. }
  204. ]
  205. const videoPlaylistElementAPGetValidator = [
  206. isValidPlaylistIdParam('playlistId'),
  207. param('playlistElementId')
  208. .custom(isIdValid).withMessage('Should have an playlist element id'),
  209. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  210. logger.debug('Checking videoPlaylistElementAPGetValidator parameters', { parameters: req.params })
  211. if (areValidationErrors(req, res)) return
  212. const playlistElementId = parseInt(req.params.playlistElementId + '', 10)
  213. const playlistId = req.params.playlistId
  214. const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
  215. if (!videoPlaylistElement) {
  216. res.fail({
  217. status: HttpStatusCode.NOT_FOUND_404,
  218. message: 'Video playlist element not found'
  219. })
  220. return
  221. }
  222. if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
  223. return res.fail({
  224. status: HttpStatusCode.FORBIDDEN_403,
  225. message: 'Cannot get this private video playlist.'
  226. })
  227. }
  228. res.locals.videoPlaylistElementAP = videoPlaylistElement
  229. return next()
  230. }
  231. ]
  232. const videoPlaylistsReorderVideosValidator = [
  233. isValidPlaylistIdParam('playlistId'),
  234. body('startPosition')
  235. .isInt({ min: 1 }).withMessage('Should have a valid start position'),
  236. body('insertAfterPosition')
  237. .isInt({ min: 0 }).withMessage('Should have a valid insert after position'),
  238. body('reorderLength')
  239. .optional()
  240. .isInt({ min: 1 }).withMessage('Should have a valid range length'),
  241. async (req: express.Request, res: express.Response, next: express.NextFunction) => {
  242. logger.debug('Checking videoPlaylistsReorderVideosValidator parameters', { parameters: req.params })
  243. if (areValidationErrors(req, res)) return
  244. if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
  245. const videoPlaylist = getPlaylist(res)
  246. if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
  247. const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
  248. const startPosition: number = req.body.startPosition
  249. const insertAfterPosition: number = req.body.insertAfterPosition
  250. const reorderLength: number = req.body.reorderLength
  251. if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
  252. res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
  253. return
  254. }
  255. if (reorderLength && reorderLength + startPosition > nextPosition) {
  256. res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
  257. return
  258. }
  259. return next()
  260. }
  261. ]
  262. const commonVideoPlaylistFiltersValidator = [
  263. query('playlistType')
  264. .optional()
  265. .custom(isVideoPlaylistTypeValid).withMessage('Should have a valid playlist type'),
  266. (req: express.Request, res: express.Response, next: express.NextFunction) => {
  267. logger.debug('Checking commonVideoPlaylistFiltersValidator parameters', { parameters: req.params })
  268. if (areValidationErrors(req, res)) return
  269. return next()
  270. }
  271. ]
  272. const doVideosInPlaylistExistValidator = [
  273. query('videoIds')
  274. .customSanitizer(toIntArray)
  275. .custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
  276. (req: express.Request, res: express.Response, next: express.NextFunction) => {
  277. logger.debug('Checking areVideosInPlaylistExistValidator parameters', { parameters: req.query })
  278. if (areValidationErrors(req, res)) return
  279. return next()
  280. }
  281. ]
  282. // ---------------------------------------------------------------------------
  283. export {
  284. videoPlaylistsAddValidator,
  285. videoPlaylistsUpdateValidator,
  286. videoPlaylistsDeleteValidator,
  287. videoPlaylistsGetValidator,
  288. videoPlaylistsSearchValidator,
  289. videoPlaylistsAddVideoValidator,
  290. videoPlaylistsUpdateOrRemoveVideoValidator,
  291. videoPlaylistsReorderVideosValidator,
  292. videoPlaylistElementAPGetValidator,
  293. commonVideoPlaylistFiltersValidator,
  294. doVideosInPlaylistExistValidator
  295. }
  296. // ---------------------------------------------------------------------------
  297. function getCommonPlaylistEditAttributes () {
  298. return [
  299. body('thumbnailfile')
  300. .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile'))
  301. .withMessage(
  302. 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
  303. CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
  304. ),
  305. body('description')
  306. .optional()
  307. .customSanitizer(toValueOrNull)
  308. .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
  309. body('privacy')
  310. .optional()
  311. .customSanitizer(toIntOrNull)
  312. .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
  313. body('videoChannelId')
  314. .optional()
  315. .customSanitizer(toIntOrNull)
  316. ] as (ValidationChain | ExpressPromiseHandler)[]
  317. }
  318. function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
  319. if (videoPlaylist.isOwned() === false) {
  320. res.fail({
  321. status: HttpStatusCode.FORBIDDEN_403,
  322. message: 'Cannot manage video playlist of another server.'
  323. })
  324. return false
  325. }
  326. // Check if the user can manage the video playlist
  327. // The user can delete it if s/he is an admin
  328. // Or if s/he is the video playlist's owner
  329. if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
  330. res.fail({
  331. status: HttpStatusCode.FORBIDDEN_403,
  332. message: 'Cannot manage video playlist of another user'
  333. })
  334. return false
  335. }
  336. return true
  337. }
  338. function getPlaylist (res: express.Response) {
  339. return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
  340. }