Răsfoiți Sursa

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

GitOrigin-RevId: dc057ed736e97265a901b1cf21995c1f391339a5
Jakob Ackermann 4 luni în urmă
părinte
comite
fc2abf5b24

+ 37 - 2
services/web/app/src/Features/Exports/ExportsController.mjs

@@ -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)

+ 12 - 0
services/web/app/src/Features/V1/V1ConversionHelper.mjs

@@ -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)
+  )
+}

+ 4 - 4
services/web/test/unit/src/Exports/ExportsController.test.mjs

@@ -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'
         )