[web] fix submit modal in Codespaces (#34137)

GitOrigin-RevId: dc057ed736e97265a901b1cf21995c1f391339a5
This commit is contained in:
Jakob Ackermann
2026-06-04 12:55:35 +02:00
committed by Copybot
parent b8fc478e1f
commit fc2abf5b24
3 changed files with 53 additions and 6 deletions
@@ -4,6 +4,10 @@ import SessionManager from '../Authentication/SessionManager.mjs'
import logger from '@overleaf/logger'
import OError from '@overleaf/o-error'
import settings from '@overleaf/settings'
import { fetchStreamWithResponse } from '@overleaf/fetch-utils'
import { isTrustedConversionJobUrl } from '../V1/V1ConversionHelper.mjs'
import { pipeline } from 'node:stream/promises'
import { parseReq, z, zz } from '../../infrastructure/Validation.mjs'
async function exportProject(req, res, next) {
const { project_id: projectId, brand_variation_id: brandVariationId } =
@@ -105,9 +109,21 @@ async function exportStatus(req, res) {
return res.json({ export_json: json })
}
const exportDownloadSchema = z.object({
params: z.object({
export_id: zz.submissionId(),
type: z.enum(['pdf', 'zip']),
}),
query: z.object({
token: z.string().optional(),
}),
})
async function exportDownload(req, res, next) {
const { type, export_id: exportId } = req.params
const { token } = req.query
const {
params: { type, export_id: exportId },
query: { token },
} = parseReq(req, exportDownloadSchema)
if (!token && settings.exports?.requireToken) {
return res.sendStatus(403)
}
@@ -119,6 +135,25 @@ async function exportDownload(req, res, next) {
type,
token
)
if (
settings.exports?.proxyDownload &&
// V1 always returns a valid URL here; throw if that assumption is violated.
isTrustedConversionJobUrl(new URL(exportFileUrl))
) {
const { stream, response } = await fetchStreamWithResponse(exportFileUrl)
res.attachment(`${token}.${type}`)
res.setHeader('X-Content-Type-Options', 'nosniff')
if (response.headers.has('Content-Length')) {
res.setHeader('Content-Length', response.headers.get('Content-Length'))
}
// Disable buffering in nginx
res.setHeader('X-Accel-Buffering', 'no')
await pipeline(stream, res)
return
}
return res.redirect(exportFileUrl)
} catch (err) {
const info = OError.getFullInfo(err)
@@ -0,0 +1,12 @@
import Settings from '@overleaf/settings'
/**
* @param {URL} url
* @return {boolean}
*/
export function isTrustedConversionJobUrl(url) {
if (!Settings.v1ConversionJobAllowedHosts.includes(url.host)) return false
return Settings.v1ConversionJobAllowedPathPrefixes.some(prefix =>
url.pathname.startsWith(prefix)
)
}
@@ -306,7 +306,7 @@ describe('ExportsController', function () {
beforeEach(function (ctx) {
ctx.req.params = {
project_id: projectId,
export_id: 897,
export_id: '897',
type: 'zip',
}
ctx.handler.fetchDownload = sinon.stub().resolves('https://example.com')
@@ -328,7 +328,7 @@ describe('ExportsController', function () {
ctx.req.query = { token: 'mock-token' }
await ctx.controller.exportDownload(ctx.req, ctx.res)
expect(ctx.handler.fetchDownload).to.have.been.calledWith(
897,
'897',
'zip',
'mock-token'
)
@@ -341,7 +341,7 @@ describe('ExportsController', function () {
ctx.req.query = {}
await ctx.controller.exportDownload(ctx.req, ctx.res)
expect(ctx.handler.fetchDownload).to.have.been.calledWith(
897,
'897',
'zip',
undefined
)
@@ -359,7 +359,7 @@ describe('ExportsController', function () {
)
await ctx.controller.exportDownload(ctx.req, ctx.res, ctx.next)
expect(ctx.handler.fetchDownload).to.have.been.calledWith(
897,
'897',
'zip',
'wrong-token'
)