Merge branch 'release/v8.1.x' into develop

This commit is contained in:
Chocobozzz
2026-05-20 15:44:14 +02:00
15 changed files with 377 additions and 65 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## v8.1.6
### IMPORTANT NOTES
* Follow v8.1.0 IMPORTANT NOTES if you upgrade from PeerTube <= v8.0.2
### SECURITY
* Fix SQL injection coming from actor inbox URL when updating actor follow scores
* Reject JSON-LD objects with special properties
* Restricts role assignment to administrators only
* Prevent external auth token replay
* Prevent SSRF on import and channel sync
* Stricter rate limit to ask password reset
## v8.2.0-rc.1
### IMPORTANT NOTES
+1 -1
View File
@@ -185,7 +185,7 @@
"proxy-addr": "^2.0.7",
"pug": "^3.0.4",
"reflect-metadata": "^0.2.2",
"sanitize-html": "~2.17.3",
"sanitize-html": "~2.17.4",
"sequelize": "~6.37.8",
"sequelize-typescript": "^2.1.6",
"sharp": "^0.34.5",
+184 -13
View File
@@ -100,6 +100,27 @@ describe('Test ActivityPub security', function () {
headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD
})
async function postActivity (activity: any) {
const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
const signedBody: any = await signAndContextify({
byActor: signer,
data: activity,
contextType: 'Announce',
contextFilter: fakeFilter(),
signerFunction: signJsonLDObjectWithoutAssertion
})
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
try {
await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
return { fail: false }
} catch (err) {
return { fail: true, statusCode: err.statusCode }
}
}
// ---------------------------------------------------------------
before(async function () {
@@ -297,23 +318,173 @@ describe('Test ActivityPub security', function () {
}
})
it('Should fail with an activity using @graph payload', async function () {
const activity: any = {
'id': 'https://victim.example/users/alice/statuses/123/activity',
'type': 'Announce',
'actor': servers[2].url + '/accounts/peertube',
'published': '2026-03-30T07:18:30Z',
'to': [ 'https://www.w3.org/ns/activitystreams#Public' ],
'cc': [ 'https://victim.example/users/alice/followers' ],
'object': 'https://target.example/users/bob/statuses/456',
'@graph': [
{
id: 'https://victim.example/users/alice#announces/123/undo',
type: 'Undo',
actor: servers[2].url + '/accounts/peertube',
to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
object: 'https://victim.example/users/alice/statuses/123/activity'
}
]
}
const { fail, statusCode } = await postActivity(activity)
expect(fail).to.be.true
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
})
it('Should fail with an activity using @reverse payload', async function () {
const activity: any = {
'id': 'https://victim.example/users/alice/statuses/123/activity',
'type': 'Announce',
'actor': servers[2].url + '/accounts/peertube',
'published': '2026-03-30T07:18:30Z',
'to': [ 'https://www.w3.org/ns/activitystreams#Public' ],
'cc': [ 'https://victim.example/users/alice/followers' ],
'object': 'https://target.example/users/bob/statuses/456',
'@reverse': {
object: {
id: 'https://victim.example/users/alice#announces/123/undo',
type: 'Undo',
actor: servers[2].url + '/accounts/peertube',
to: [ 'https://www.w3.org/ns/activitystreams#Public' ]
}
}
}
const { fail, statusCode } = await postActivity(activity)
expect(fail).to.be.true
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
})
it('Should fail with an activity using @included payload', async function () {
const activity: any = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://alice.example/notes/42/activity',
'type': 'Create',
'actor': servers[2].url + '/accounts/peertube',
'cc': 'https://www.w3.org/ns/activitystreams#Public',
'object': {
id: 'https://alice.example/notes/42',
type: 'Note',
attributedTo: 'https://alice.example/actors/1',
cc: 'https://www.w3.org/ns/activitystreams#Public',
content: 'Welcome to Fediverse!'
},
'@included': {
id: 'https://alice.example/notes/42/activity',
to: 'https://bob.example/actors/1',
object: {
id: 'https://alice.example/notes/42',
to: 'https://bob.example/actors/1',
inReplyTo: 'https://bob.example/notes/1'
}
}
}
const { fail, statusCode } = await postActivity(activity)
expect(fail).to.be.true
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
})
it('Should fail with an activity using aliased @included payload', async function () {
const activity: any = {
'@context': [ 'https://www.w3.org/ns/activitystreams', { included: '@included' } ],
'id': 'https://alice.example/notes/42/activity',
'type': 'Create',
'actor': servers[2].url + '/accounts/peertube',
'cc': 'https://www.w3.org/ns/activitystreams#Public',
'object': {
id: 'https://alice.example/notes/42',
type: 'Note',
attributedTo: 'https://alice.example/actors/1',
cc: 'https://www.w3.org/ns/activitystreams#Public',
content: 'Welcome to Fediverse!'
},
'included': {
id: 'https://alice.example/notes/42/activity',
to: 'https://bob.example/actors/1',
object: {
id: 'https://alice.example/notes/42',
to: 'https://bob.example/actors/1',
inReplyTo: 'https://bob.example/notes/1'
}
}
}
const { fail, statusCode } = await postActivity(activity)
expect(fail).to.be.true
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
})
it('Should fail with an activity using inner @included payload', async function () {
const activity: any = {
'@context': [ 'https://www.w3.org/ns/activitystreams', { included: '@included' } ],
'id': 'https://alice.example/notes/42/activity',
'type': 'Create',
'actor': servers[2].url + '/accounts/peertube',
'cc': 'https://www.w3.org/ns/activitystreams#Public',
'object': {
id: 'https://alice.example/notes/42',
type: 'Note',
attributedTo: 'https://alice.example/actors/1',
cc: 'https://www.w3.org/ns/activitystreams#Public',
content: 'Welcome to Fediverse!',
included: {
id: 'https://alice.example/notes/42/activity',
to: 'https://bob.example/actors/1',
object: {
id: 'https://alice.example/notes/42',
to: 'https://bob.example/actors/1',
inReplyTo: 'https://bob.example/notes/1'
}
}
}
}
const { fail, statusCode } = await postActivity(activity)
expect(fail).to.be.true
expect(statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
})
it('Should succeed with a valid signature', async function () {
const body = getAnnounceWithoutContext(servers[1])
body.actor = servers[2].url + '/accounts/peertube'
{
const activity = getAnnounceWithoutContext(servers[1])
const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
const signedBody = await signAndContextify({
byActor: signer,
data: body,
contextType: 'Announce',
contextFilter: fakeFilter(),
signerFunction: signJsonLDObjectWithoutAssertion
})
const { fail } = await postActivity(activity)
expect(fail).to.be.false
}
const headers = buildGlobalHTTPHeaders(signedBody, buildDigest)
{
const activity: any = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'https://alice.example/notes/42/activity',
'type': 'Create',
'actor': servers[2].url + '/accounts/peertube',
'cc': 'https://www.w3.org/ns/activitystreams#Public',
'object': {
id: 'https://alice.example/notes/42',
type: 'Note',
attributedTo: 'https://alice.example/actors/1',
cc: 'https://www.w3.org/ns/activitystreams#Public',
content: 'Welcome to Fediverse!'
}
}
const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
const { fail, statusCode } = await postActivity(activity)
expect(fail).to.be.false
}
})
it('Should refresh the actor keys', async function () {
@@ -465,6 +465,18 @@ describe('Test users admin API validators', function () {
})
})
it('Should fail to update user role for a moderator', async function () {
const fields = { role: UserRole.MODERATOR }
await makePutBodyRequest({
url: server.url,
path: path + userId,
token: moderatorToken,
fields,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
})
it('Should succeed to update a user with a moderator', async function () {
const fields = {
videoQuota: 42
@@ -479,12 +491,12 @@ describe('Test users admin API validators', function () {
})
})
it('Should succeed with the correct params', async function () {
it('Should succeed with the correct params for an admin', async function () {
const fields = {
email: 'email@example.com',
emailVerified: true,
videoQuota: 42,
role: UserRole.USER
role: UserRole.MODERATOR
}
await makePutBodyRequest({
+20 -5
View File
@@ -312,8 +312,8 @@ importers:
specifier: ^0.2.2
version: 0.2.2
sanitize-html:
specifier: ~2.17.3
version: 2.17.3
specifier: ~2.17.4
version: 2.17.4
sequelize:
specifier: ~6.37.8
version: 6.37.8(pg@8.20.0)
@@ -4985,6 +4985,7 @@ packages:
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
deprecated: this version has critical issues, please update to the latest version
'@yarnpkg/lockfile@1.1.0':
resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==}
@@ -5403,6 +5404,7 @@ packages:
basic-ftp@5.3.1:
resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==}
engines: {node: '>=10.0.0'}
deprecated: Security vulnerability fixed in 5.2.1, please upgrade
bcrypt@6.0.0:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
@@ -6148,6 +6150,9 @@ packages:
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@@ -8168,6 +8173,9 @@ packages:
launch-editor@2.13.2:
resolution: {integrity: sha512-4VVDnbOpLXy/s8rdRCSXb+zfMeFR0WlJWpET1iA9CQdlZDfwyLjUuGQzXU4VeOoey6AicSAluWan7Etga6Kcmg==}
launder@1.7.1:
resolution: {integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==}
lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
@@ -10163,8 +10171,8 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sanitize-html@2.17.3:
resolution: {integrity: sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg==}
sanitize-html@2.17.4:
resolution: {integrity: sha512-2HW7v2ol/uAM7sX4hbD8Z59OGWmAPrvjL8E71UWlBcj6m+kcF6ilQBLny+cIgY214QJeJT5tQuxKKqX0SQqjGQ==}
sass-embedded-all-unknown@1.99.0:
resolution: {integrity: sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw==}
@@ -17791,6 +17799,8 @@ snapshots:
dateformat@4.6.3: {}
dayjs@1.11.20: {}
debug@2.6.9:
dependencies:
ms: 2.0.0
@@ -20298,6 +20308,10 @@ snapshots:
picocolors: 1.1.1
shell-quote: 1.8.3
launder@1.7.1:
dependencies:
dayjs: 1.11.20
lazystream@1.0.1:
dependencies:
readable-stream: 2.3.8
@@ -22701,12 +22715,13 @@ snapshots:
safer-buffer@2.1.2: {}
sanitize-html@2.17.3:
sanitize-html@2.17.4:
dependencies:
deepmerge: 4.3.1
escape-string-regexp: 4.0.0
htmlparser2: 10.1.0
is-plain-object: 5.0.0
launder: 1.7.1
parse-srcset: 1.0.2
postcss: 8.5.14
+19 -3
View File
@@ -1,6 +1,8 @@
import { pick } from '@peertube/peertube-core-utils'
import { HttpStatusCode, UserCreate, UserCreateResult, UserRight, UserUpdate } from '@peertube/peertube-models'
import { tokensRouter } from '@server/controllers/api/users/token.js'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
import { CONFIG } from '@server/initializers/config.js'
import { getResetPasswordUrl } from '@server/lib/client-urls.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
import { OAuthTokenModel } from '@server/models/oauth/oauth-token.js'
@@ -19,6 +21,7 @@ import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
buildRateLimiter,
ensureUserHasRight,
paginationValidator,
setDefaultPagination,
@@ -48,11 +51,15 @@ import { registrationsRouter } from './registrations.js'
import { twoFactorRouter } from './two-factor.js'
import { userExportsRouter } from './user-exports.js'
import { userImportRouter } from './user-imports.js'
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
const auditLogger = auditLoggerFactory('users')
const lTags = loggerTagsFactory('api', 'users')
const askResetPasswordRateLimiter = buildRateLimiter({
windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
})
const usersRouter = express.Router()
usersRouter.use(apiRateLimiter)
@@ -126,9 +133,18 @@ usersRouter.delete(
asyncMiddleware(removeUser)
)
usersRouter.post('/ask-reset-password', asyncMiddleware(usersAskResetPasswordValidator), asyncMiddleware(askResetUserPassword))
usersRouter.post(
'/ask-reset-password',
askResetPasswordRateLimiter,
asyncMiddleware(usersAskResetPasswordValidator),
asyncMiddleware(askResetUserPassword)
)
usersRouter.post('/:id/reset-password', asyncMiddleware(usersResetPasswordValidator), asyncMiddleware(resetUserPassword))
usersRouter.post(
'/:id/reset-password',
asyncMiddleware(usersResetPasswordValidator),
asyncMiddleware(resetUserPassword)
)
// ---------------------------------------------------------------------------
+2 -2
View File
@@ -4,7 +4,7 @@ import { buildUUID } from '@peertube/peertube-node-utils'
import { logger } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
import { OTP } from '@server/initializers/constants.js'
import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth.js'
import { getAuthNameFromRefreshGrant, consumeBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth.js'
import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model.js'
import { handleOAuthToken, MissingTwoFactorError } from '@server/lib/auth/oauth.js'
import { Hooks } from '@server/lib/plugins/hooks.js'
@@ -196,7 +196,7 @@ async function buildByPassLogin (req: express.Request, grantType: string): Promi
if (req.body.externalAuthToken) {
// Consistency with the getBypassFromPasswordGrant promise
return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
return consumeBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
}
return getBypassFromPasswordGrant(req.body.username, req.body.password)
+16 -2
View File
@@ -29,8 +29,8 @@ export async function compactJSONLDAndCheckRSA2017Signature (fromActor: MActor,
req.body = { ...compacted, signature: req.body.signature }
if (compacted['@include']) {
logger.warn('JSON-LD @include is not supported')
if (containInvalidJsonldKeys(compacted)) {
logger.warn('JSON-LD @included, @graph or @reverse are not supported')
return false
}
@@ -151,3 +151,17 @@ function createDocWithoutSignatureHash (doc: any) {
return hashObject(docWithoutSignature)
}
function containInvalidJsonldKeys (obj: any, depth = 1) {
if (depth > 20) return true
if (typeof obj !== 'object' || obj === null) return false
if (Array.isArray(obj)) {
return obj.some(item => containInvalidJsonldKeys(item, depth + 1))
}
if ('@included' in obj || '@graph' in obj || '@reverse' in obj) return true
return Object.values(obj).some(value => containInvalidJsonldKeys(value, depth + 1))
}
@@ -6,6 +6,7 @@ import { readdir } from 'fs/promises'
import { dirname, join } from 'path'
import { inspect } from 'util'
import { isVideoFileExtnameValid } from '../custom-validators/videos.js'
import { isResolvingToUnicastOnly } from '../dns.js'
import { t } from '../i18n.js'
import { logger, loggerTagsFactory } from '../logger.js'
import { generateVideoImportTmpPath } from '../utils.js'
@@ -103,6 +104,8 @@ export class YoutubeDLWrapper {
}): Promise<YoutubeDLInfo> {
const { userLanguage, youtubeDLArgs = [] } = options
await this.checkUnicastOrThrow(userLanguage)
const youtubeDL = await YoutubeDLCLI.safeGet()
try {
@@ -145,6 +148,8 @@ export class YoutubeDLWrapper {
}) {
const { userLanguage } = options
await this.checkUnicastOrThrow(userLanguage)
const youtubeDL = await YoutubeDLCLI.safeGet()
const list = await youtubeDL.getListInfo({
@@ -165,7 +170,11 @@ export class YoutubeDLWrapper {
return list.map(info => info.webpage_url)
}
async getSubtitles (): Promise<YoutubeDLSubs> {
async getSubtitles (options: { userLanguage: string }): Promise<YoutubeDLSubs> {
const { userLanguage } = options
await this.checkUnicastOrThrow(userLanguage)
const cwd = CONFIG.STORAGE.TMP_DIR
const youtubeDL = await YoutubeDLCLI.safeGet()
@@ -192,7 +201,15 @@ export class YoutubeDLWrapper {
return subtitles
}
async downloadVideo (fileExt: string, timeout: number): Promise<string> {
async downloadVideo (options: {
fileExt: string
timeout: number
userLanguage: string
}): Promise<string> {
const { fileExt, timeout, userLanguage } = options
await this.checkUnicastOrThrow(userLanguage)
// Leave empty the extension, youtube-dl will add it
const pathWithoutExtension = generateVideoImportTmpPath(this.url, '')
@@ -250,4 +267,13 @@ export class YoutubeDLWrapper {
return undefined
}
private async checkUnicastOrThrow (userLanguage: string) {
if (!await isResolvingToUnicastOnly(this.url)) {
throw new YoutubeDlImportError({
message: t(`URL {targetUrl} is not a unicast URL.`, userLanguage, { targetUrl: this.url }),
code: YoutubeDlImportErrorCode.NOT_ONLY_UNICAST_URL
})
}
}
}
+5 -2
View File
@@ -172,10 +172,13 @@ async function getBypassFromPasswordGrant (username: string, password: string):
return undefined
}
function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin {
function consumeBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin {
const obj = authBypassTokens.get(externalAuthToken)
if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
// Prevent replaying the same token
authBypassTokens.delete(externalAuthToken)
const { expires, user, authName, npmName } = obj
const now = new Date()
@@ -245,7 +248,7 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
export {
onExternalUserAuthenticated,
getBypassFromExternalAuth,
consumeBypassFromExternalAuth,
getAuthNameFromRefreshGrant,
getBypassFromPasswordGrant
}
@@ -91,11 +91,15 @@ export {
async function processTorrentImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportTorrentPayload) {
logger.info('Processing torrent video import in job %s.', job.id)
return processFile(
() => downloadWebTorrentVideo({ torrentPath: payload.torrentPath, uri: videoImport.magnetUri }, JOB_TTL['video-import']),
const user = await UserModel.loadById(videoImport.userId)
return processFile({
downloader: () => downloadWebTorrentVideo({ torrentPath: payload.torrentPath, uri: videoImport.magnetUri }, JOB_TTL['video-import']),
videoImport,
{ type: payload.type, generateTranscription: payload.generateTranscription, videoImportId: payload.videoImportId }
)
type: payload.type,
generateTranscription: payload.generateTranscription,
user
})
}
async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefault, payload: VideoImportYoutubeDLPayload) {
@@ -107,11 +111,17 @@ async function processYoutubeDLImport (job: Job, videoImport: MVideoImportDefaul
CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
)
return processFile(
() => youtubeDL.downloadVideo(payload.fileExt, JOB_TTL['video-import']),
const user = await UserModel.loadById(videoImport.userId)
return processFile({
downloader: () => {
return youtubeDL.downloadVideo({ fileExt: payload.fileExt, timeout: JOB_TTL['video-import'], userLanguage: user.getLanguage() })
},
videoImport,
{ type: payload.type, generateTranscription: payload.generateTranscription, videoImportId: videoImport.id }
)
type: payload.type,
generateTranscription: payload.generateTranscription,
user
})
}
async function getVideoImportOrDie (payload: VideoImportPayload) {
@@ -131,12 +141,15 @@ async function getVideoImportOrDie (payload: VideoImportPayload) {
return videoImport
}
type ProcessFileOptions = {
async function processFile (options: {
downloader: () => Promise<string>
videoImport: MVideoImportDefault
user: MUserId
type: VideoImportYoutubeDLPayloadType | VideoImportTorrentPayloadType
generateTranscription: boolean
videoImportId: number
}
async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
}) {
const { downloader, videoImport, user, type, generateTranscription } = options
let tmpVideoPath: string
let videoFile: MVideoFile
@@ -146,7 +159,6 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
// Get information about this video
const stats = await stat(tmpVideoPath)
const user = await UserModel.loadByVideoId(videoImport.videoId)
if (!user) throw new Error('Video does not exist anymore')
const isAble = await isUserQuotaValid({ channelUserId: user.id, uploadSize: stats.size })
@@ -166,7 +178,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
videoFile = await buildNewFile({ mode: 'web-video', ffprobe, path: tmpVideoPath })
videoFile.videoId = videoImport.videoId
const hookName = options.type === 'youtube-dl'
const hookName = type === 'youtube-dl'
? 'filter:api.video.post-import-url.accept.result'
: 'filter:api.video.post-import-torrent.accept.result'
@@ -248,7 +260,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
video,
videoFile,
user: videoImport.User,
generateTranscription: options.generateTranscription
generateTranscription
})
} finally {
videoFileLockReleaser()
+10 -3
View File
@@ -263,7 +263,7 @@ export async function buildYoutubeDLImport (options: {
})
// Get video subtitles
await processYoutubeSubtitles(youtubeDL, targetUrl, video)
await processYoutubeSubtitles({ youtubeDL, targetUrl, video, userLanguage: user.getLanguage() })
let fileExt = `.${youtubeDLInfo.ext}`
if (!isVideoFileExtnameValid(fileExt)) fileExt = '.mp4'
@@ -316,9 +316,16 @@ async function processThumbnails (options: {
return []
}
async function processYoutubeSubtitles (youtubeDL: YoutubeDLWrapper, targetUrl: string, video: MVideo) {
async function processYoutubeSubtitles (options: {
youtubeDL: YoutubeDLWrapper
targetUrl: string
video: MVideo
userLanguage: string
}) {
const { youtubeDL, targetUrl, video, userLanguage } = options
try {
const subtitles = await youtubeDL.getSubtitles()
const subtitles = await youtubeDL.getSubtitles({ userLanguage })
logger.info('Found %s subtitles candidates from youtube-dl import %s.', subtitles.length, targetUrl)
@@ -1,5 +1,5 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import { HttpStatusCode, ServerErrorCode, UserRole, UserUpdateMe } from '@peertube/peertube-models'
import { HttpStatusCode, ServerErrorCode, UserRole, UserRoleType, UserUpdateMe } from '@peertube/peertube-models'
import { isStringArray } from '@server/helpers/custom-validators/search.js'
import { isNSFWFlagsValid } from '@server/helpers/custom-validators/videos.js'
import { loadReservedActorName } from '@server/lib/local-actor.js'
@@ -96,13 +96,7 @@ export const usersAddValidator = [
if (areValidationErrors(req, res, { omitBodyLog: true })) return
if (!await checkUsernameOrEmailDoNotAlreadyExist({ username: req.body.username, email: req.body.email, req, res })) return
const authUser = res.locals.oauth.token.User
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'You can only create users (and not administrators or moderators)'
})
}
if (!checkCanSetRole(req.body.role, res)) return
if (req.body.channelName) {
if (req.body.channelName === req.body.username) {
@@ -213,6 +207,7 @@ export const usersUpdateValidator = [
}
if (!checkCanModerate(user, res)) return
if (req.body.role && !checkCanSetRole(req.body.role, res)) return
if (
req.body.email &&
@@ -524,3 +519,18 @@ function checkCanModerate (onUser: MUser, res: express.Response) {
return false
}
function checkCanSetRole (role: UserRoleType, res: express.Response) {
const authUser = res.locals.oauth.token.User
if (authUser.role !== UserRole.ADMINISTRATOR && role !== UserRole.USER) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Only administrators can assign admin or moderator roles'
})
return false
}
return true
}
+7 -2
View File
@@ -658,14 +658,19 @@ export class ActorFollowModel extends SequelizeModel<ActorFollowModel> {
}
static updateScore (inboxUrl: string, value: number, t?: Transaction) {
const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
const query = 'UPDATE "actorFollow" SET "score" = LEAST("score" + $value, $maxScore) ' +
'WHERE id IN (' +
'SELECT "actorFollow"."id" FROM "actorFollow" ' +
'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
`WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
'WHERE "actor"."inboxUrl" = $inboxUrl OR "actor"."sharedInboxUrl" = $inboxUrl' +
')'
const options = {
bind: {
inboxUrl,
maxScore: ACTOR_FOLLOW_SCORE.MAX,
value
},
type: QueryTypes.BULKUPDATE,
transaction: t
}
+10 -5
View File
@@ -388,19 +388,24 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
forUserId?: number
}) {
const id = forceNumber(options.id)
const bind: { id: number, forUserId?: number } = { id }
if (options.forUserId !== undefined) {
bind.forUserId = options.forUserId
}
function buildAccountWhereQuery (base: string) {
const whereSuffix = options.forUserId
? ` AND "userNotification"."userId" = ${options.forUserId}`
const whereSuffix = options.forUserId !== undefined
? ' AND "userNotification"."userId" = $forUserId'
: ''
if (options.type === 'account') {
return base +
` WHERE "account"."id" = ${id} ${whereSuffix}`
` WHERE "account"."id" = $id ${whereSuffix}`
}
return base +
` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
` WHERE "actor"."serverId" = $id ${whereSuffix}`
}
const queries = [
@@ -455,7 +460,7 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
return UserNotificationModel.sequelize.query(query)
return UserNotificationModel.sequelize.query(query, { bind })
}
toFormattedJSON (this: UserNotificationModelForApi): UserNotification {