{"version":3,"sources":["../../../src/uploads/endpoints/getFileFromURL.ts"],"sourcesContent":["import type { PayloadHandler } from '../../config/types.js'\n\nimport { executeAccess } from '../../auth/executeAccess.js'\nimport { APIError } from '../../errors/APIError.js'\nimport { Forbidden } from '../../errors/Forbidden.js'\nimport { getRequestCollectionWithID } from '../../utilities/getRequestEntity.js'\nimport { isURLAllowed } from '../../utilities/isURLAllowed.js'\nimport { sanitizeFilename } from '../../utilities/sanitizeFilename.js'\nimport { safeFetch } from '../safeFetch.js'\n\n// If doc id is provided, it means we are updating the doc\n// /:collectionSlug/paste-url/:doc-id?src=:fileUrl\n\n// If doc id is not provided, it means we are creating a new doc\n// /:collectionSlug/paste-url?src=:fileUrl\n\nexport const getFileFromURLHandler: PayloadHandler = async (req) => {\n  const { id, collection } = getRequestCollectionWithID(req, { optionalID: true })\n\n  if (!req.user) {\n    throw new Forbidden(req.t)\n  }\n\n  const config = collection?.config\n\n  if (!config.upload?.pasteURL) {\n    throw new APIError('Pasting from URL is not enabled for this collection.', 400)\n  }\n\n  if (id) {\n    // updating doc\n    const accessResult = await executeAccess({ req }, config.access.update)\n    if (!accessResult) {\n      throw new Forbidden(req.t)\n    }\n  } else {\n    // creating doc\n    const accessResult = await executeAccess({ req }, config.access?.create)\n    if (!accessResult) {\n      throw new Forbidden(req.t)\n    }\n  }\n\n  if (!req.url) {\n    throw new APIError('Request URL is missing.', 400)\n  }\n\n  const { searchParams } = new URL(req.url)\n  const src = searchParams.get('src')\n\n  if (!src || typeof src !== 'string') {\n    throw new APIError('A valid URL string is required.', 400)\n  }\n\n  const hasAllowList =\n    typeof config.upload.pasteURL === 'object' && Array.isArray(config.upload.pasteURL.allowList)\n\n  let fileURL: string\n  try {\n    fileURL = new URL(src).href\n  } catch {\n    throw new APIError('A valid URL string is required.', 400)\n  }\n\n  if (hasAllowList && !isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {\n    throw new APIError('The provided URL is not allowed.', 400)\n  }\n\n  let redirectCount = 0\n  const maxRedirects = 3\n  let response!: Response\n\n  while (true) {\n    if (hasAllowList && isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {\n      // Allow-listed URLs bypass SSRF filtering (e.g. internal/localhost CDNs)\n      response = await fetch(fileURL, {\n        headers: { 'Accept-Encoding': 'identity' },\n        redirect: 'manual',\n        signal: AbortSignal.timeout(30_000),\n      })\n    } else {\n      response = await safeFetch(fileURL, {\n        headers: {\n          'Accept-Encoding': 'identity',\n        },\n        signal: AbortSignal.timeout(30_000),\n      })\n    }\n\n    if (response.status >= 300 && response.status < 400) {\n      redirectCount++\n      if (redirectCount > maxRedirects) {\n        throw new APIError('Too many redirects.', 403)\n      }\n      const location = response.headers.get('location')\n      if (location) {\n        fileURL = new URL(location, fileURL).href\n        if (hasAllowList && !isURLAllowed(fileURL, config.upload.pasteURL.allowList)) {\n          throw new APIError('The provided URL is not allowed.', 400)\n        }\n        continue\n      }\n    }\n\n    break\n  }\n\n  if (!response.ok) {\n    throw new APIError('Failed to fetch the file from the provided URL.', response.status)\n  }\n\n  const rawFileName = decodeURIComponent(new URL(fileURL).pathname.split('/').pop() || '')\n  const safeFileName = sanitizeFilename(rawFileName)\n  const encodedFileName = encodeURIComponent(safeFileName).replace(\n    /['()]/g,\n    (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n  )\n  // Strip quotes, backslashes, and control chars from the ASCII fallback\n  const asciiFileName = safeFileName.replace(/[\"\\\\\\r\\n]/g, '_')\n\n  return new Response(response.body, {\n    headers: {\n      'Content-Disposition': `attachment; filename=\"${asciiFileName}\"; filename*=UTF-8''${encodedFileName}`,\n      'Content-Length': response.headers.get('content-length') || '',\n      'Content-Type': response.headers.get('content-type') || 'application/octet-stream',\n    },\n  })\n}\n"],"names":["executeAccess","APIError","Forbidden","getRequestCollectionWithID","isURLAllowed","sanitizeFilename","safeFetch","getFileFromURLHandler","req","id","collection","optionalID","user","t","config","upload","pasteURL","accessResult","access","update","create","url","searchParams","URL","src","get","hasAllowList","Array","isArray","allowList","fileURL","href","redirectCount","maxRedirects","response","fetch","headers","redirect","signal","AbortSignal","timeout","status","location","ok","rawFileName","decodeURIComponent","pathname","split","pop","safeFileName","encodedFileName","encodeURIComponent","replace","c","charCodeAt","toString","toUpperCase","asciiFileName","Response","body"],"mappings":"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"}