@defra/forms-engine-plugin 4.4.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.public/javascripts/application.min.js +1 -1
- package/.public/javascripts/application.min.js.map +1 -1
- package/.public/javascripts/shared.min.js +1 -1
- package/.public/javascripts/shared.min.js.map +1 -1
- package/.server/client/javascripts/file-upload.js +13 -8
- package/.server/client/javascripts/file-upload.js.map +1 -1
- package/.server/server/constants.d.ts +1 -0
- package/.server/server/constants.js +1 -0
- package/.server/server/constants.js.map +1 -1
- package/.server/server/plugins/engine/beta/form-context.d.ts +0 -1
- package/.server/server/plugins/engine/beta/form-context.js +4 -3
- package/.server/server/plugins/engine/beta/form-context.js.map +1 -1
- package/.server/server/plugins/engine/components/FileUploadField.d.ts +3 -2
- package/.server/server/plugins/engine/components/FileUploadField.js +11 -3
- package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
- package/.server/server/plugins/engine/helpers.d.ts +8 -0
- package/.server/server/plugins/engine/helpers.js +8 -0
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +2 -1
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +11 -0
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +65 -28
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/package.json +1 -1
- package/src/client/javascripts/file-upload.js +12 -8
- package/src/server/constants.js +1 -0
- package/src/server/plugins/engine/beta/form-context.test.ts +22 -8
- package/src/server/plugins/engine/beta/form-context.ts +7 -6
- package/src/server/plugins/engine/components/FileUploadField.test.ts +11 -8
- package/src/server/plugins/engine/components/FileUploadField.ts +14 -5
- package/src/server/plugins/engine/helpers.ts +17 -0
- package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +54 -0
- package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +7 -5
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +109 -5
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +69 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","names":["ControllerPath","Engine","getErrorMessage","hasComponents","isFormType","Boom","format","parseISO","StatusCodes","Liquid","createLogger","getAnswer","stripParam","FormAction","FormStatus","logger","engine","outputEscape","jsTruthy","ownPropertyOnly","registerFilter","template","globals","context","evaluated","evaluateTemplate","path","pageDef","pages","get","query","page","pageMap","undefined","getPageHref","name","componentDef","components","component","componentMap","isFormComponent","answer","relevantState","proceed","request","h","nextUrl","method","payload","returnUrl","isReturnAllowed","action","Continue","Validate","nextQuery","response","isPathRelative","redirect","redirectPath","code","SEE_OTHER","MOVED_TEMPORARILY","encodeUrl","link","URL","toString","err","error","pathOrQuery","queryOnly","Error","getHref","isRelative","params","Object","entries","filter","url","value","searchParams","set","pathname","search","href","startsWith","normalisePath","trim","replace","getPage","model","findPage","notFound","findPath","find","getStartPath","V2","startPath","def","at","Start","startPage","checkFormStatus","isPreview","state","Live","Draft","checkEmailAddressForLiveFormSubmission","emailAddress","internal","getErrors","details","length","map","getError","detail","message","key","text","createError","componentName","safeGenerateCrumb","server","plugins","crumb","generate","route","settings","getExponentialBackoffDelay","depth","BASE_DELAY_MS","CAP_DELAY_MS","delay","Math","min","pageDefMap","componentDefMap","parseAndRenderSync","getCacheService","getPluginOptions","cacheService","getSaveAndExitHelpers","saveAndExit","handleLegacyRedirect","targetUrl","permanent","takeover","setPageTitles","forEach","title","firstFormComponent","type"],"sources":["../../../../src/server/plugins/engine/helpers.ts"],"sourcesContent":["import {\n ControllerPath,\n Engine,\n getErrorMessage,\n hasComponents,\n isFormType,\n type ComponentDef,\n type FormDefinition,\n type Page\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseToolkit, type Server } from '@hapi/hapi'\nimport { format, parseISO } from 'date-fns'\nimport { StatusCodes } from 'http-status-codes'\nimport { type Schema, type ValidationErrorItem } from 'joi'\nimport { Liquid } from 'liquidjs'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport {\n getAnswer,\n type Field\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { stripParam } from '~/src/server/plugins/engine/pageControllers/helpers/state.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError\n} from '~/src/server/plugins/engine/types.js'\nimport {\n FormAction,\n FormStatus,\n type FormParams,\n type FormQuery,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst logger = createLogger()\n\nexport const engine = new Liquid({\n outputEscape: 'escape',\n jsTruthy: true,\n ownPropertyOnly: false\n})\n\nexport interface GlobalScope {\n context: FormContext\n pages: Map<string, Page>\n components: Map<string, ComponentDef>\n}\n\nengine.registerFilter('evaluate', function (template?: string) {\n if (typeof template !== 'string') {\n return template\n }\n\n const globals = this.context.globals as GlobalScope\n const evaluated = evaluateTemplate(template, globals.context)\n\n return evaluated\n})\n\nengine.registerFilter('page', function (path?: string) {\n if (typeof path !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const pageDef = globals.pages.get(path)\n\n return pageDef\n})\n\nengine.registerFilter('href', function (path: string, query?: FormQuery) {\n if (typeof path !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const page = globals.context.pageMap.get(path)\n\n if (page === undefined) {\n return\n }\n\n return getPageHref(page, query)\n})\n\nengine.registerFilter('field', function (name: string) {\n if (typeof name !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const componentDef = globals.components.get(name)\n\n return componentDef\n})\n\nengine.registerFilter('answer', function (name: string) {\n if (typeof name !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const component = globals.context.componentMap.get(name)\n\n if (!component?.isFormComponent) {\n return\n }\n\n const answer = getAnswer(component as Field, globals.context.relevantState)\n\n return answer\n})\n\nexport function proceed(\n request: Pick<FormContextRequest, 'method' | 'payload' | 'query'>,\n h: FormResponseToolkit,\n nextUrl: string\n) {\n const { method, payload, query } = request\n const { returnUrl } = query\n\n const isReturnAllowed =\n payload && 'action' in payload\n ? payload.action === FormAction.Continue ||\n payload.action === FormAction.Validate\n : false\n\n // On POST, strip all query params to prevent them persisting across pages.\n // On GET, forward params (minus returnUrl) so pre-population query params\n // survive dispatch redirects (e.g. ?formId= reaching the start page).\n const nextQuery =\n method === 'get' ? stripParam(query, 'returnUrl') : undefined\n\n // Redirect to return location (optional)\n const response =\n isReturnAllowed && isPathRelative(returnUrl)\n ? h.redirect(returnUrl)\n : h.redirect(redirectPath(nextUrl, nextQuery))\n\n // Redirect POST to GET to avoid resubmission\n return method === 'post'\n ? response.code(StatusCodes.SEE_OTHER)\n : response.code(StatusCodes.MOVED_TEMPORARILY)\n}\n\n/**\n * Encodes a URL, returning undefined if the process fails.\n */\nexport function encodeUrl(link?: string) {\n if (link) {\n try {\n return new URL(link).toString() // escape the search params without breaking the ? and & reserved characters in rfc2368\n } catch (err) {\n logger.error(\n err,\n `[urlEncodingFailed] Failed to encode URL: ${link} - ${getErrorMessage(err)}`\n )\n throw err\n }\n }\n}\n\n/**\n * Get page href\n */\nexport function getPageHref(\n page: PageControllerClass,\n query?: FormQuery\n): string\n\n/**\n * Get page href by path\n */\nexport function getPageHref(\n page: PageControllerClass,\n path: string,\n query?: FormQuery\n): string\n\nexport function getPageHref(\n page: PageControllerClass,\n pathOrQuery?: string | FormQuery,\n queryOnly: FormQuery = {}\n) {\n const path = typeof pathOrQuery === 'string' ? pathOrQuery : page.path\n const query = typeof pathOrQuery === 'object' ? pathOrQuery : queryOnly\n\n if (!isPathRelative(path)) {\n throw Error(`Only relative URLs are allowed: ${path}`)\n }\n\n // Return path with page href as base\n return redirectPath(page.getHref(path), query)\n}\n\n/**\n * Get redirect path with optional query params\n */\nexport function redirectPath(nextUrl: string, query: FormQuery = {}) {\n const isRelative = isPathRelative(nextUrl)\n\n // Filter string query params only\n const params = Object.entries(query).filter(\n (query): query is [string, string] => typeof query[1] === 'string'\n )\n\n // Build URL with relative path support\n const url = isRelative\n ? new URL(nextUrl, 'http://example.com')\n : new URL(nextUrl)\n\n // Append query params\n for (const [name, value] of params) {\n url.searchParams.set(name, value)\n }\n\n if (isRelative) {\n return `${url.pathname}${url.search}`\n }\n\n return url.href\n}\n\nexport function isPathRelative(path?: string) {\n return (path ?? '').startsWith('/')\n}\n\nexport function normalisePath(path = '') {\n return path\n .trim() // Trim empty spaces\n .replace(/^\\//, '') // Remove leading slash\n .replace(/\\/$/, '') // Remove trailing slash\n}\n\nexport function getPage(\n model: FormModel | undefined,\n request: FormContextRequest\n) {\n const { params } = request\n\n const page = findPage(model, `/${params.path}`)\n\n if (!page) {\n throw Boom.notFound(`No page found for /${params.path}`)\n }\n\n return page\n}\n\nexport function findPage(model: FormModel | undefined, path?: string) {\n const findPath = `/${normalisePath(path)}`\n return model?.pages.find(({ path }) => path === findPath)\n}\n\nexport function getStartPath(model?: FormModel) {\n if (model?.engine === Engine.V2) {\n const startPath = normalisePath(model.def.pages.at(0)?.path)\n return startPath ? `/${startPath}` : ControllerPath.Start\n }\n\n const startPath = normalisePath(model?.def.startPage)\n return startPath ? `/${startPath}` : ControllerPath.Start\n}\n\nexport function checkFormStatus(params?: FormParams) {\n const isPreview = !!params?.state\n\n let state = FormStatus.Live\n\n if (isPreview && params.state === FormStatus.Draft) {\n state = FormStatus.Draft\n }\n\n return {\n isPreview,\n state\n }\n}\n\nexport function checkEmailAddressForLiveFormSubmission(\n emailAddress: string | undefined,\n isPreview: boolean\n) {\n if (!emailAddress && !isPreview) {\n throw Boom.internal(\n 'An email address is required to complete the form submission'\n )\n }\n}\n\n/**\n * Parses the errors from {@link Schema.validate} so they can be rendered by govuk-frontend templates\n * @param [details] - provided by {@link Schema.validate}\n */\nexport function getErrors(\n details?: ValidationErrorItem[]\n): FormSubmissionError[] | undefined {\n if (!details?.length) {\n return\n }\n\n return details.map(getError)\n}\n\nexport function getError(detail: ValidationErrorItem): FormSubmissionError {\n const { context, message, path } = detail\n\n const name = context?.key ?? ''\n const href = `#${name}`\n\n const text = message.replace(\n /\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z)/,\n (text) => format(parseISO(text), 'd MMMM yyyy')\n )\n\n return {\n path,\n href,\n name,\n text,\n context\n }\n}\n\nexport function createError(componentName: string, message: string) {\n return {\n href: `#${componentName}`,\n name: componentName,\n text: message\n }\n}\n\n/**\n * A small helper to safely generate a crumb token.\n * Checks that the crumb plugin is available, that crumb\n * is not disabled on the current route, and that cookies/state are present.\n */\nexport function safeGenerateCrumb(\n request: AnyFormRequest | null\n): string | undefined {\n // no request or no .state\n if (!request?.state) {\n return undefined\n }\n\n // crumb plugin or its generate method doesn't exist\n if (!request.server.plugins.crumb.generate) {\n return undefined\n }\n\n // crumb is explicitly disabled for this route\n if (request.route.settings.plugins?.crumb === false) {\n return undefined\n }\n\n return request.server.plugins.crumb.generate(request)\n}\n\n/**\n * Calculates an exponential backoff delay (in milliseconds) based on the current retry depth,\n * using a base delay of 2000ms (2 seconds) and doubling for each additional depth, while capping the delay at 25,000ms (25 seconds).\n * @param depth - The current retry depth (1, 2, 3, …)\n * @returns The calculated delay in milliseconds.\n */\nexport function getExponentialBackoffDelay(depth: number): number {\n const BASE_DELAY_MS = 2000 // 2 seconds initial delay\n const CAP_DELAY_MS = 25000 // cap each delay to 25 seconds\n const delay = BASE_DELAY_MS * 2 ** (depth - 1)\n return Math.min(delay, CAP_DELAY_MS)\n}\n\nexport function evaluateTemplate(\n template: string,\n context: FormContext\n): string {\n const globals: GlobalScope = {\n context,\n pages: context.pageDefMap,\n components: context.componentDefMap\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return engine.parseAndRenderSync(template, context.relevantState, {\n globals\n })\n}\n\nexport function getCacheService(server: Server) {\n return getPluginOptions(server).cacheService\n}\n\nexport function getSaveAndExitHelpers(server: Server) {\n return getPluginOptions(server).saveAndExit\n}\n\nexport function getPluginOptions(server: Server) {\n return server.plugins['forms-engine-plugin']\n}\n\n/**\n * Handles logging and issuing a permanent redirect for legacy routes.\n * @param h - The Hapi response toolkit.\n * @param targetUrl - The URL to redirect to.\n * @returns The Hapi response object configured for permanent redirect.\n */\nexport function handleLegacyRedirect(h: ResponseToolkit, targetUrl: string) {\n return h.redirect(targetUrl).permanent().takeover()\n}\n\n/**\n * If the page doesn't have a title, set it from the title of the first form component\n * @param def - the form definition\n */\nexport function setPageTitles(def: FormDefinition) {\n def.pages.forEach((page) => {\n if (!page.title) {\n if (hasComponents(page)) {\n // Set the page title from the first form component\n const firstFormComponent = page.components.find((component) =>\n isFormType(component.type)\n )\n\n page.title = firstFormComponent?.title ?? ''\n }\n }\n })\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"helpers.js","names":["ControllerPath","Engine","getErrorMessage","hasComponents","isFormType","Boom","format","parseISO","StatusCodes","Liquid","createLogger","FORM_VERSION_METADATA_KEY","getAnswer","stripParam","FormAction","FormStatus","logger","engine","outputEscape","jsTruthy","ownPropertyOnly","registerFilter","template","globals","context","evaluated","evaluateTemplate","path","pageDef","pages","get","query","page","pageMap","undefined","getPageHref","name","componentDef","components","component","componentMap","isFormComponent","answer","relevantState","proceed","request","h","nextUrl","method","payload","returnUrl","isReturnAllowed","action","Continue","Validate","nextQuery","response","isPathRelative","redirect","redirectPath","code","SEE_OTHER","MOVED_TEMPORARILY","encodeUrl","link","URL","toString","err","error","pathOrQuery","queryOnly","Error","getHref","isRelative","params","Object","entries","filter","url","value","searchParams","set","pathname","search","href","startsWith","normalisePath","trim","replace","getPage","model","findPage","notFound","findPath","find","getStartPath","V2","startPath","def","at","Start","startPage","checkFormStatus","isPreview","state","Live","Draft","checkEmailAddressForLiveFormSubmission","emailAddress","internal","getErrors","details","length","map","getError","detail","message","key","text","createError","componentName","safeGenerateCrumb","server","plugins","crumb","generate","route","settings","getExponentialBackoffDelay","depth","BASE_DELAY_MS","CAP_DELAY_MS","delay","Math","min","pageDefMap","componentDefMap","parseAndRenderSync","getCacheService","getPluginOptions","cacheService","getSaveAndExitHelpers","saveAndExit","handleLegacyRedirect","targetUrl","permanent","takeover","getFormVersion","definition","metadata","setPageTitles","forEach","title","firstFormComponent","type"],"sources":["../../../../src/server/plugins/engine/helpers.ts"],"sourcesContent":["import {\n ControllerPath,\n Engine,\n getErrorMessage,\n hasComponents,\n isFormType,\n type ComponentDef,\n type FormDefinition,\n type Page\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type ResponseToolkit, type Server } from '@hapi/hapi'\nimport { format, parseISO } from 'date-fns'\nimport { StatusCodes } from 'http-status-codes'\nimport { type Schema, type ValidationErrorItem } from 'joi'\nimport { Liquid } from 'liquidjs'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport { FORM_VERSION_METADATA_KEY } from '~/src/server/constants.js'\nimport {\n getAnswer,\n type Field\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { stripParam } from '~/src/server/plugins/engine/pageControllers/helpers/state.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError\n} from '~/src/server/plugins/engine/types.js'\nimport {\n FormAction,\n FormStatus,\n type FormParams,\n type FormQuery,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst logger = createLogger()\n\nexport const engine = new Liquid({\n outputEscape: 'escape',\n jsTruthy: true,\n ownPropertyOnly: false\n})\n\nexport interface GlobalScope {\n context: FormContext\n pages: Map<string, Page>\n components: Map<string, ComponentDef>\n}\n\nengine.registerFilter('evaluate', function (template?: string) {\n if (typeof template !== 'string') {\n return template\n }\n\n const globals = this.context.globals as GlobalScope\n const evaluated = evaluateTemplate(template, globals.context)\n\n return evaluated\n})\n\nengine.registerFilter('page', function (path?: string) {\n if (typeof path !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const pageDef = globals.pages.get(path)\n\n return pageDef\n})\n\nengine.registerFilter('href', function (path: string, query?: FormQuery) {\n if (typeof path !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const page = globals.context.pageMap.get(path)\n\n if (page === undefined) {\n return\n }\n\n return getPageHref(page, query)\n})\n\nengine.registerFilter('field', function (name: string) {\n if (typeof name !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const componentDef = globals.components.get(name)\n\n return componentDef\n})\n\nengine.registerFilter('answer', function (name: string) {\n if (typeof name !== 'string') {\n return\n }\n\n const globals = this.context.globals as GlobalScope\n const component = globals.context.componentMap.get(name)\n\n if (!component?.isFormComponent) {\n return\n }\n\n const answer = getAnswer(component as Field, globals.context.relevantState)\n\n return answer\n})\n\nexport function proceed(\n request: Pick<FormContextRequest, 'method' | 'payload' | 'query'>,\n h: FormResponseToolkit,\n nextUrl: string\n) {\n const { method, payload, query } = request\n const { returnUrl } = query\n\n const isReturnAllowed =\n payload && 'action' in payload\n ? payload.action === FormAction.Continue ||\n payload.action === FormAction.Validate\n : false\n\n // On POST, strip all query params to prevent them persisting across pages.\n // On GET, forward params (minus returnUrl) so pre-population query params\n // survive dispatch redirects (e.g. ?formId= reaching the start page).\n const nextQuery =\n method === 'get' ? stripParam(query, 'returnUrl') : undefined\n\n // Redirect to return location (optional)\n const response =\n isReturnAllowed && isPathRelative(returnUrl)\n ? h.redirect(returnUrl)\n : h.redirect(redirectPath(nextUrl, nextQuery))\n\n // Redirect POST to GET to avoid resubmission\n return method === 'post'\n ? response.code(StatusCodes.SEE_OTHER)\n : response.code(StatusCodes.MOVED_TEMPORARILY)\n}\n\n/**\n * Encodes a URL, returning undefined if the process fails.\n */\nexport function encodeUrl(link?: string) {\n if (link) {\n try {\n return new URL(link).toString() // escape the search params without breaking the ? and & reserved characters in rfc2368\n } catch (err) {\n logger.error(\n err,\n `[urlEncodingFailed] Failed to encode URL: ${link} - ${getErrorMessage(err)}`\n )\n throw err\n }\n }\n}\n\n/**\n * Get page href\n */\nexport function getPageHref(\n page: PageControllerClass,\n query?: FormQuery\n): string\n\n/**\n * Get page href by path\n */\nexport function getPageHref(\n page: PageControllerClass,\n path: string,\n query?: FormQuery\n): string\n\nexport function getPageHref(\n page: PageControllerClass,\n pathOrQuery?: string | FormQuery,\n queryOnly: FormQuery = {}\n) {\n const path = typeof pathOrQuery === 'string' ? pathOrQuery : page.path\n const query = typeof pathOrQuery === 'object' ? pathOrQuery : queryOnly\n\n if (!isPathRelative(path)) {\n throw Error(`Only relative URLs are allowed: ${path}`)\n }\n\n // Return path with page href as base\n return redirectPath(page.getHref(path), query)\n}\n\n/**\n * Get redirect path with optional query params\n */\nexport function redirectPath(nextUrl: string, query: FormQuery = {}) {\n const isRelative = isPathRelative(nextUrl)\n\n // Filter string query params only\n const params = Object.entries(query).filter(\n (query): query is [string, string] => typeof query[1] === 'string'\n )\n\n // Build URL with relative path support\n const url = isRelative\n ? new URL(nextUrl, 'http://example.com')\n : new URL(nextUrl)\n\n // Append query params\n for (const [name, value] of params) {\n url.searchParams.set(name, value)\n }\n\n if (isRelative) {\n return `${url.pathname}${url.search}`\n }\n\n return url.href\n}\n\nexport function isPathRelative(path?: string) {\n return (path ?? '').startsWith('/')\n}\n\nexport function normalisePath(path = '') {\n return path\n .trim() // Trim empty spaces\n .replace(/^\\//, '') // Remove leading slash\n .replace(/\\/$/, '') // Remove trailing slash\n}\n\nexport function getPage(\n model: FormModel | undefined,\n request: FormContextRequest\n) {\n const { params } = request\n\n const page = findPage(model, `/${params.path}`)\n\n if (!page) {\n throw Boom.notFound(`No page found for /${params.path}`)\n }\n\n return page\n}\n\nexport function findPage(model: FormModel | undefined, path?: string) {\n const findPath = `/${normalisePath(path)}`\n return model?.pages.find(({ path }) => path === findPath)\n}\n\nexport function getStartPath(model?: FormModel) {\n if (model?.engine === Engine.V2) {\n const startPath = normalisePath(model.def.pages.at(0)?.path)\n return startPath ? `/${startPath}` : ControllerPath.Start\n }\n\n const startPath = normalisePath(model?.def.startPage)\n return startPath ? `/${startPath}` : ControllerPath.Start\n}\n\nexport function checkFormStatus(params?: FormParams) {\n const isPreview = !!params?.state\n\n let state = FormStatus.Live\n\n if (isPreview && params.state === FormStatus.Draft) {\n state = FormStatus.Draft\n }\n\n return {\n isPreview,\n state\n }\n}\n\nexport function checkEmailAddressForLiveFormSubmission(\n emailAddress: string | undefined,\n isPreview: boolean\n) {\n if (!emailAddress && !isPreview) {\n throw Boom.internal(\n 'An email address is required to complete the form submission'\n )\n }\n}\n\n/**\n * Parses the errors from {@link Schema.validate} so they can be rendered by govuk-frontend templates\n * @param [details] - provided by {@link Schema.validate}\n */\nexport function getErrors(\n details?: ValidationErrorItem[]\n): FormSubmissionError[] | undefined {\n if (!details?.length) {\n return\n }\n\n return details.map(getError)\n}\n\nexport function getError(detail: ValidationErrorItem): FormSubmissionError {\n const { context, message, path } = detail\n\n const name = context?.key ?? ''\n const href = `#${name}`\n\n const text = message.replace(\n /\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z)/,\n (text) => format(parseISO(text), 'd MMMM yyyy')\n )\n\n return {\n path,\n href,\n name,\n text,\n context\n }\n}\n\nexport function createError(componentName: string, message: string) {\n return {\n href: `#${componentName}`,\n name: componentName,\n text: message\n }\n}\n\n/**\n * A small helper to safely generate a crumb token.\n * Checks that the crumb plugin is available, that crumb\n * is not disabled on the current route, and that cookies/state are present.\n */\nexport function safeGenerateCrumb(\n request: AnyFormRequest | null\n): string | undefined {\n // no request or no .state\n if (!request?.state) {\n return undefined\n }\n\n // crumb plugin or its generate method doesn't exist\n if (!request.server.plugins.crumb.generate) {\n return undefined\n }\n\n // crumb is explicitly disabled for this route\n if (request.route.settings.plugins?.crumb === false) {\n return undefined\n }\n\n return request.server.plugins.crumb.generate(request)\n}\n\n/**\n * Calculates an exponential backoff delay (in milliseconds) based on the current retry depth,\n * using a base delay of 2000ms (2 seconds) and doubling for each additional depth, while capping the delay at 25,000ms (25 seconds).\n * @param depth - The current retry depth (1, 2, 3, …)\n * @returns The calculated delay in milliseconds.\n */\nexport function getExponentialBackoffDelay(depth: number): number {\n const BASE_DELAY_MS = 2000 // 2 seconds initial delay\n const CAP_DELAY_MS = 25000 // cap each delay to 25 seconds\n const delay = BASE_DELAY_MS * 2 ** (depth - 1)\n return Math.min(delay, CAP_DELAY_MS)\n}\n\nexport function evaluateTemplate(\n template: string,\n context: FormContext\n): string {\n const globals: GlobalScope = {\n context,\n pages: context.pageDefMap,\n components: context.componentDefMap\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return engine.parseAndRenderSync(template, context.relevantState, {\n globals\n })\n}\n\nexport function getCacheService(server: Server) {\n return getPluginOptions(server).cacheService\n}\n\nexport function getSaveAndExitHelpers(server: Server) {\n return getPluginOptions(server).saveAndExit\n}\n\nexport function getPluginOptions(server: Server) {\n return server.plugins['forms-engine-plugin']\n}\n\n/**\n * Handles logging and issuing a permanent redirect for legacy routes.\n * @param h - The Hapi response toolkit.\n * @param targetUrl - The URL to redirect to.\n * @returns The Hapi response object configured for permanent redirect.\n */\nexport function handleLegacyRedirect(h: ResponseToolkit, targetUrl: string) {\n return h.redirect(targetUrl).permanent().takeover()\n}\n\n/**\n * If the page doesn't have a title, set it from the title of the first form component\n * @param def - the form definition\n */\nexport interface FormVersionMetadata {\n versionNumber: number\n createdAt: Date\n}\n\n/**\n * Extracts form version metadata from a form definition\n */\nexport function getFormVersion(\n definition: Pick<FormDefinition, 'metadata'>\n): FormVersionMetadata | undefined {\n return definition.metadata?.[FORM_VERSION_METADATA_KEY] as\n | FormVersionMetadata\n | undefined\n}\n\nexport function setPageTitles(def: FormDefinition) {\n def.pages.forEach((page) => {\n if (!page.title) {\n if (hasComponents(page)) {\n // Set the page title from the first form component\n const firstFormComponent = page.components.find((component) =>\n isFormType(component.type)\n )\n\n page.title = firstFormComponent?.title ?? ''\n }\n }\n })\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getFormVersion } from "../../helpers.js";
|
|
1
2
|
import { categoriseData } from "../machine/v2.js";
|
|
2
3
|
import { FormAdapterSubmissionSchemaVersion } from "../../types/enums.js";
|
|
3
4
|
export function format(context, items, model, submitResponse, formStatus, formMetadata) {
|
|
@@ -6,7 +7,7 @@ export function format(context, items, model, submitResponse, formStatus, formMe
|
|
|
6
7
|
main: v2Main,
|
|
7
8
|
...v2Data
|
|
8
9
|
} = categoriseData(items);
|
|
9
|
-
const versionMetadata = getVersionMetadata(context.submittedVersionNumber, formMetadata);
|
|
10
|
+
const versionMetadata = getFormVersion(model.def) ?? getVersionMetadata(context.submittedVersionNumber, formMetadata);
|
|
10
11
|
const meta = {
|
|
11
12
|
schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
|
|
12
13
|
timestamp: new Date(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v1.js","names":["categoriseData","FormAdapterSubmissionSchemaVersion","format","context","items","model","submitResponse","formStatus","formMetadata","csvFiles","extractCsvFiles","main","v2Main","v2Data","versionMetadata","getVersionMetadata","submittedVersionNumber","meta","schemaVersion","V1","timestamp","Date","referenceNumber","formName","name","formId","id","formSlug","slug","status","state","isPreview","notificationEmail","Object","fromEntries","entries","map","key","value","undefined","data","result","files","payload","JSON","stringify","versions","length","submittedVersion","find","v","versionNumber","createdAt","firstVersion","repeaters"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/adapter/v1.ts"],"sourcesContent":["import {\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport {
|
|
1
|
+
{"version":3,"file":"v1.js","names":["getFormVersion","categoriseData","FormAdapterSubmissionSchemaVersion","format","context","items","model","submitResponse","formStatus","formMetadata","csvFiles","extractCsvFiles","main","v2Main","v2Data","versionMetadata","def","getVersionMetadata","submittedVersionNumber","meta","schemaVersion","V1","timestamp","Date","referenceNumber","formName","name","formId","id","formSlug","slug","status","state","isPreview","notificationEmail","Object","fromEntries","entries","map","key","value","undefined","data","result","files","payload","JSON","stringify","versions","length","submittedVersion","find","v","versionNumber","createdAt","firstVersion","repeaters"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/adapter/v1.ts"],"sourcesContent":["import {\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\n\nimport {\n getFormVersion,\n type checkFormStatus\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type DetailItem } from '~/src/server/plugins/engine/models/types.js'\nimport { categoriseData } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'\nimport { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/enums.js'\nimport {\n type FormAdapterSubmissionMessageData,\n type FormAdapterSubmissionMessageMeta,\n type FormAdapterSubmissionMessagePayload,\n type FormAdapterSubmissionMessageResult,\n type FormContext\n} from '~/src/server/plugins/engine/types.js'\n\nexport function format(\n context: FormContext,\n items: DetailItem[],\n model: FormModel,\n submitResponse: SubmitResponsePayload,\n formStatus: ReturnType<typeof checkFormStatus>,\n formMetadata?: FormMetadata\n): string {\n const csvFiles = extractCsvFiles(submitResponse)\n\n const { main: v2Main, ...v2Data } = categoriseData(items)\n\n const versionMetadata =\n getFormVersion(model.def) ??\n getVersionMetadata(context.submittedVersionNumber, formMetadata)\n\n const meta: FormAdapterSubmissionMessageMeta = {\n schemaVersion: FormAdapterSubmissionSchemaVersion.V1,\n timestamp: new Date(),\n referenceNumber: context.referenceNumber,\n formName: model.name,\n formId: formMetadata?.id ?? '',\n formSlug: formMetadata?.slug ?? '',\n status: formStatus.state,\n isPreview: formStatus.isPreview,\n notificationEmail: formMetadata?.notificationEmail ?? ''\n }\n\n if (versionMetadata) {\n meta.versionMetadata = versionMetadata\n }\n\n const main = Object.fromEntries(\n Object.entries(v2Main).map(([key, value]) => {\n if (value === undefined) {\n return [key, null]\n }\n\n return [key, value]\n })\n )\n\n const data: FormAdapterSubmissionMessageData = {\n main,\n ...v2Data\n }\n\n const result: FormAdapterSubmissionMessageResult = {\n files: csvFiles\n }\n\n const payload: FormAdapterSubmissionMessagePayload = {\n meta,\n data,\n result\n }\n\n return JSON.stringify(payload)\n}\n\nexport function getVersionMetadata(\n submittedVersionNumber: number | undefined,\n formMetadata?: FormMetadata\n): { versionNumber: number; createdAt: Date } | undefined {\n if (!formMetadata?.versions?.length) {\n return undefined\n }\n\n if (submittedVersionNumber !== undefined) {\n const submittedVersion = formMetadata.versions.find(\n (v) => v.versionNumber === submittedVersionNumber\n )\n if (submittedVersion) {\n return {\n versionNumber: submittedVersion.versionNumber,\n createdAt: submittedVersion.createdAt\n }\n }\n }\n\n // fallback to first available version\n const firstVersion = formMetadata.versions[0]\n return {\n versionNumber: firstVersion.versionNumber,\n createdAt: firstVersion.createdAt\n }\n}\n\nfunction extractCsvFiles(\n submitResponse: SubmitResponsePayload\n): FormAdapterSubmissionMessageResult['files'] {\n const result =\n submitResponse.result as Partial<FormAdapterSubmissionMessageResult>\n\n return {\n main: result.files?.main ?? '',\n repeaters: result.files?.repeaters ?? {}\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -52,6 +52,17 @@ export declare class FileUploadPageController extends QuestionPageController {
|
|
|
52
52
|
* @param depth - the number of retries so far
|
|
53
53
|
*/
|
|
54
54
|
private checkUploadStatus;
|
|
55
|
+
/**
|
|
56
|
+
* Processes the uploaded files from a CDP status response.
|
|
57
|
+
* Complete files are added to state, rejected/pending files
|
|
58
|
+
* have their error messages flashed.
|
|
59
|
+
* @param request - the hapi request
|
|
60
|
+
* @param state - the form state
|
|
61
|
+
* @param validatedItem - the Joi-validated upload item
|
|
62
|
+
* @param files - the current files array from state
|
|
63
|
+
* @param upload - the current upload initiation response
|
|
64
|
+
*/
|
|
65
|
+
private processUploadedFiles;
|
|
55
66
|
/**
|
|
56
67
|
* Checks the payload for a file getting removed
|
|
57
68
|
* and removes it from the upload files if found
|
|
@@ -125,8 +125,8 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
125
125
|
} = context;
|
|
126
126
|
const files = this.getFilesFromState(state);
|
|
127
127
|
const fileToRemove = files.find(({
|
|
128
|
-
|
|
129
|
-
}) =>
|
|
128
|
+
status
|
|
129
|
+
}) => status.form.file.fileId === params.itemId);
|
|
130
130
|
if (!fileToRemove) {
|
|
131
131
|
throw Boom.notFound('File to delete not found');
|
|
132
132
|
}
|
|
@@ -309,8 +309,7 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
309
309
|
|
|
310
310
|
// Only add to files state if the file validates.
|
|
311
311
|
// This secures against html tampering of the file input
|
|
312
|
-
//
|
|
313
|
-
// changed to a simple text field or similar.
|
|
312
|
+
// (e.g. changing it to a simple text field or similar).
|
|
314
313
|
const validationResult = tempItemSchema.validate({
|
|
315
314
|
uploadId,
|
|
316
315
|
status: statusResponse
|
|
@@ -318,13 +317,69 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
318
317
|
stripUnknown: true
|
|
319
318
|
});
|
|
320
319
|
const error = validationResult.error;
|
|
321
|
-
const fileState = validationResult.value;
|
|
322
320
|
if (error) {
|
|
323
321
|
return this.initiateAndStoreNewUpload(request, state);
|
|
324
322
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
|
|
324
|
+
// CDP returns form.file as a single object for one file,
|
|
325
|
+
// or an array for multiple files. The Joi schema normalises
|
|
326
|
+
// both to an array via .single().
|
|
327
|
+
await this.processUploadedFiles(request, state, validationResult.value, files, upload);
|
|
328
|
+
return this.initiateAndStoreNewUpload(request, state);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Processes the uploaded files from a CDP status response.
|
|
333
|
+
* Complete files are added to state, rejected/pending files
|
|
334
|
+
* have their error messages flashed.
|
|
335
|
+
* @param request - the hapi request
|
|
336
|
+
* @param state - the form state
|
|
337
|
+
* @param validatedItem - the Joi-validated upload item
|
|
338
|
+
* @param files - the current files array from state
|
|
339
|
+
* @param upload - the current upload initiation response
|
|
340
|
+
*/
|
|
341
|
+
async processUploadedFiles(request, state, validatedItem, files, upload) {
|
|
342
|
+
const {
|
|
343
|
+
uploadId
|
|
344
|
+
} = validatedItem;
|
|
345
|
+
const validatedStatus = validatedItem.status;
|
|
346
|
+
const rawFile = validatedStatus.form.file;
|
|
347
|
+
const uploadedFiles = Array.isArray(rawFile) ? rawFile : [rawFile];
|
|
348
|
+
const allErrors = [];
|
|
349
|
+
for (const file of uploadedFiles) {
|
|
350
|
+
if (file.fileStatus === FileStatus.complete) {
|
|
351
|
+
const perFileState = {
|
|
352
|
+
uploadId,
|
|
353
|
+
status: {
|
|
354
|
+
...validatedStatus,
|
|
355
|
+
form: {
|
|
356
|
+
file
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
files.unshift(prepareFileState(perFileState));
|
|
361
|
+
} else {
|
|
362
|
+
// Collect the error for rejected/pending files.
|
|
363
|
+
const {
|
|
364
|
+
fileUpload
|
|
365
|
+
} = this;
|
|
366
|
+
const name = fileUpload.name;
|
|
367
|
+
const text = file.errorMessage ?? 'Unknown error';
|
|
368
|
+
allErrors.push({
|
|
369
|
+
path: [name],
|
|
370
|
+
href: `#${name}`,
|
|
371
|
+
name,
|
|
372
|
+
text
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (allErrors.length) {
|
|
377
|
+
const cacheService = getCacheService(request.server);
|
|
378
|
+
cacheService.setFlash(request, {
|
|
379
|
+
errors: allErrors
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (uploadedFiles.some(f => f.fileStatus === FileStatus.complete)) {
|
|
328
383
|
await this.mergeState(request, state, {
|
|
329
384
|
upload: {
|
|
330
385
|
[this.path]: {
|
|
@@ -333,25 +388,7 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
333
388
|
}
|
|
334
389
|
}
|
|
335
390
|
});
|
|
336
|
-
} else {
|
|
337
|
-
// Flash the error message.
|
|
338
|
-
const {
|
|
339
|
-
fileUpload
|
|
340
|
-
} = this;
|
|
341
|
-
const cacheService = getCacheService(request.server);
|
|
342
|
-
const name = fileUpload.name;
|
|
343
|
-
const text = file.errorMessage ?? 'Unknown error';
|
|
344
|
-
const errors = [{
|
|
345
|
-
path: [name],
|
|
346
|
-
href: `#${name}`,
|
|
347
|
-
name,
|
|
348
|
-
text
|
|
349
|
-
}];
|
|
350
|
-
cacheService.setFlash(request, {
|
|
351
|
-
errors
|
|
352
|
-
});
|
|
353
391
|
}
|
|
354
|
-
return this.initiateAndStoreNewUpload(request, state);
|
|
355
392
|
}
|
|
356
393
|
|
|
357
394
|
/**
|
|
@@ -371,8 +408,8 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
371
408
|
const upload = this.getUploadFromState(state);
|
|
372
409
|
const files = this.getFilesFromState(state);
|
|
373
410
|
const filesUpdated = files.filter(({
|
|
374
|
-
|
|
375
|
-
}) =>
|
|
411
|
+
status
|
|
412
|
+
}) => status.form.file.fileId !== params.itemId);
|
|
376
413
|
if (filesUpdated.length === files.length) {
|
|
377
414
|
return;
|
|
378
415
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","StatusCodes","FileUploadField","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","at","length","badImplementation","path","indexOf","name","viewName","getStateKeys","component","pagePath","page","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","uploadId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","err","isBoom","output","statusCode","NOT_FOUND","valueOf","badRequest","uploadStatus","initiated","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","complete","unshift","mergeState","cacheService","server","setFlash","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { StatusCodes } from 'http-status-codes'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n FileUploadField,\n tempItemSchema\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n /**\n * Get supplementary state keys for clearing file upload state.\n * Returns the nested upload path for FileUploadField components only.\n * @param component - The component to get supplementary state keys for\n * @returns Array containing the nested upload path, e.g., [\"upload['/page-path']\"]\n * or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.\n */\n getStateKeys(component: FormComponent): string[] {\n // Only return upload keys for FileUploadField components\n if (!(component instanceof FileUploadField)) {\n return []\n }\n\n const pagePath = component.page?.path\n return pagePath ? [`upload['${pagePath}']`] : ['upload']\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ uploadId }) => uploadId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n\n let statusResponse\n\n try {\n statusResponse = await getUploadStatus(uploadId)\n } catch (err) {\n // if the user loads a file upload page and queries the cached upload, after the upload has\n // expired in CDP, we will get a 404 from the getUploadStatus endpoint.\n // In this case we want to initiate a new upload and return that state, so the form\n // doesn't blow up for the end user.\n if (\n Boom.isBoom(err) &&\n err.output.statusCode === StatusCodes.NOT_FOUND.valueOf()\n ) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n throw err\n }\n\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // by adding a 'multiple' attribute or it being\n // changed to a simple text field or similar.\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n const fileState = validationResult.value as FileState\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const file = fileState.status.form.file\n if (file.fileStatus === FileStatus.complete) {\n files.unshift(prepareFileState(fileState))\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n } else {\n // Flash the error message.\n const { fileUpload } = this\n const cacheService = getCacheService(request.server)\n\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n const errors: FormSubmissionError[] = [\n { path: [name], href: `#${name}`, name, text }\n ]\n cacheService.setFlash(request, { errors })\n }\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ uploadId }) => uploadId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","StatusCodes","FileUploadField","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","at","length","badImplementation","path","indexOf","name","viewName","getStateKeys","component","pagePath","page","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","fileId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","uploadId","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","err","isBoom","output","statusCode","NOT_FOUND","valueOf","badRequest","uploadStatus","initiated","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","processUploadedFiles","validatedItem","validatedStatus","rawFile","uploadedFiles","Array","isArray","allErrors","complete","perFileState","unshift","cacheService","server","setFlash","some","f","mergeState","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { StatusCodes } from 'http-status-codes'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n FileUploadField,\n tempItemSchema\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FileUpload,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n /**\n * Get supplementary state keys for clearing file upload state.\n * Returns the nested upload path for FileUploadField components only.\n * @param component - The component to get supplementary state keys for\n * @returns Array containing the nested upload path, e.g., [\"upload['/page-path']\"]\n * or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.\n */\n getStateKeys(component: FormComponent): string[] {\n // Only return upload keys for FileUploadField components\n if (!(component instanceof FileUploadField)) {\n return []\n }\n\n const pagePath = component.page?.path\n return pagePath ? [`upload['${pagePath}']`] : ['upload']\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ status }) => status.form.file.fileId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n\n let statusResponse\n\n try {\n statusResponse = await getUploadStatus(uploadId)\n } catch (err) {\n // if the user loads a file upload page and queries the cached upload, after the upload has\n // expired in CDP, we will get a 404 from the getUploadStatus endpoint.\n // In this case we want to initiate a new upload and return that state, so the form\n // doesn't blow up for the end user.\n if (\n Boom.isBoom(err) &&\n err.output.statusCode === StatusCodes.NOT_FOUND.valueOf()\n ) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n throw err\n }\n\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // (e.g. changing it to a simple text field or similar).\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n // CDP returns form.file as a single object for one file,\n // or an array for multiple files. The Joi schema normalises\n // both to an array via .single().\n await this.processUploadedFiles(\n request,\n state,\n validationResult.value,\n files,\n upload\n )\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Processes the uploaded files from a CDP status response.\n * Complete files are added to state, rejected/pending files\n * have their error messages flashed.\n * @param request - the hapi request\n * @param state - the form state\n * @param validatedItem - the Joi-validated upload item\n * @param files - the current files array from state\n * @param upload - the current upload initiation response\n */\n private async processUploadedFiles(\n request: AnyFormRequest,\n state: FormSubmissionState,\n validatedItem: FileState,\n files: FileState[],\n upload: UploadInitiateResponse | undefined\n ) {\n const { uploadId } = validatedItem\n const validatedStatus = validatedItem.status\n const rawFile = validatedStatus.form.file as unknown as\n | FileUpload\n | FileUpload[]\n const uploadedFiles = Array.isArray(rawFile) ? rawFile : [rawFile]\n\n const allErrors: FormSubmissionError[] = []\n\n for (const file of uploadedFiles) {\n if (file.fileStatus === FileStatus.complete) {\n const perFileState: FileState = {\n uploadId,\n status: {\n ...validatedStatus,\n form: { file }\n } as FileState['status']\n }\n files.unshift(prepareFileState(perFileState))\n } else {\n // Collect the error for rejected/pending files.\n const { fileUpload } = this\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n allErrors.push({ path: [name], href: `#${name}`, name, text })\n }\n }\n\n if (allErrors.length) {\n const cacheService = getCacheService(request.server)\n cacheService.setFlash(request, { errors: allErrors })\n }\n\n if (uploadedFiles.some((f) => f.fileStatus === FileStatus.complete)) {\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n }\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ status }) => status.form.file.fileId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -304,16 +304,19 @@ function pollUploadStatus(uploadId) {
|
|
|
304
304
|
* @param {HTMLInputElement} fileInput - The file input element
|
|
305
305
|
* @param {HTMLButtonElement} uploadButton - The upload button
|
|
306
306
|
* @param {HTMLButtonElement} continueButton - The continue button
|
|
307
|
-
* @param {File
|
|
307
|
+
* @param {File[]} selectedFiles - The selected files
|
|
308
308
|
*/
|
|
309
309
|
function handleStandardFormSubmission(
|
|
310
310
|
formElement,
|
|
311
311
|
fileInput,
|
|
312
312
|
uploadButton,
|
|
313
313
|
continueButton,
|
|
314
|
-
|
|
314
|
+
selectedFiles
|
|
315
315
|
) {
|
|
316
|
-
|
|
316
|
+
// Render in reverse so first file ends up at the top of the summary list
|
|
317
|
+
for (let i = selectedFiles.length - 1; i >= 0; i--) {
|
|
318
|
+
renderSummary(selectedFiles[i], 'Uploading…', formElement)
|
|
319
|
+
}
|
|
317
320
|
|
|
318
321
|
fileInput.focus()
|
|
319
322
|
|
|
@@ -403,8 +406,8 @@ function initUpload() {
|
|
|
403
406
|
}
|
|
404
407
|
|
|
405
408
|
const formElement = /** @type {HTMLFormElement} */ (form)
|
|
406
|
-
/** @type {File
|
|
407
|
-
let
|
|
409
|
+
/** @type {File[]} */
|
|
410
|
+
let selectedFiles = []
|
|
408
411
|
let isSubmitting = false
|
|
409
412
|
const uploadId = formElement.dataset.uploadId
|
|
410
413
|
|
|
@@ -414,12 +417,12 @@ function initUpload() {
|
|
|
414
417
|
}
|
|
415
418
|
|
|
416
419
|
if (fileInput.files && fileInput.files.length > 0) {
|
|
417
|
-
|
|
420
|
+
selectedFiles = Array.from(fileInput.files)
|
|
418
421
|
}
|
|
419
422
|
})
|
|
420
423
|
|
|
421
424
|
uploadButton.addEventListener('click', (event) => {
|
|
422
|
-
if (
|
|
425
|
+
if (selectedFiles.length === 0) {
|
|
423
426
|
event.preventDefault()
|
|
424
427
|
showError(
|
|
425
428
|
'Select a file',
|
|
@@ -436,12 +439,13 @@ function initUpload() {
|
|
|
436
439
|
|
|
437
440
|
isSubmitting = true
|
|
438
441
|
|
|
442
|
+
// Show all selected files in the summary table
|
|
439
443
|
handleStandardFormSubmission(
|
|
440
444
|
formElement,
|
|
441
445
|
fileInput,
|
|
442
446
|
uploadButton,
|
|
443
447
|
continueButton,
|
|
444
|
-
|
|
448
|
+
selectedFiles
|
|
445
449
|
)
|
|
446
450
|
|
|
447
451
|
handleAjaxFormSubmission(
|
package/src/server/constants.js
CHANGED
|
@@ -44,7 +44,7 @@ jest.mock('../pageControllers/index.ts', () => {
|
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
jest.mock('../helpers.ts', () => ({
|
|
47
|
-
|
|
47
|
+
...jest.requireActual('../helpers.ts'),
|
|
48
48
|
getCacheService: (...args: unknown[]) => mockGetCacheService(...args),
|
|
49
49
|
checkEmailAddressForLiveFormSubmission: (...args: unknown[]) =>
|
|
50
50
|
mockCheckEmailAddressForLiveFormSubmission(...args)
|
|
@@ -134,10 +134,17 @@ describe('getFormModel helper', () => {
|
|
|
134
134
|
class CustomController extends PageController {}
|
|
135
135
|
const controllers = { CustomController }
|
|
136
136
|
const metadata = {
|
|
137
|
-
id: 'form-meta-123'
|
|
138
|
-
|
|
137
|
+
id: 'form-meta-123'
|
|
138
|
+
}
|
|
139
|
+
const definition = {
|
|
140
|
+
pages: [{ path: '/start' }],
|
|
141
|
+
metadata: {
|
|
142
|
+
$$__formVersion: {
|
|
143
|
+
versionNumber: 17,
|
|
144
|
+
createdAt: new Date('2024-10-15T10:00:00Z')
|
|
145
|
+
}
|
|
146
|
+
}
|
|
139
147
|
}
|
|
140
|
-
const definition = { pages: [{ path: '/start' }] }
|
|
141
148
|
let formsService: FormsService
|
|
142
149
|
let services: Services
|
|
143
150
|
let formModelInstance: { id: string }
|
|
@@ -176,7 +183,7 @@ describe('getFormModel helper', () => {
|
|
|
176
183
|
definition,
|
|
177
184
|
{
|
|
178
185
|
basePath: slug,
|
|
179
|
-
versionNumber:
|
|
186
|
+
versionNumber: 17,
|
|
180
187
|
ordnanceSurveyApiKey: undefined,
|
|
181
188
|
formId: metadata.id
|
|
182
189
|
},
|
|
@@ -210,11 +217,18 @@ describe('getFormModel helper', () => {
|
|
|
210
217
|
|
|
211
218
|
describe('resolveFormModel helper', () => {
|
|
212
219
|
const slug = 'tb-origin'
|
|
213
|
-
const definition = {
|
|
220
|
+
const definition = {
|
|
221
|
+
pages: [],
|
|
222
|
+
metadata: {
|
|
223
|
+
$$__formVersion: {
|
|
224
|
+
versionNumber: 9,
|
|
225
|
+
createdAt: new Date('2024-10-15T10:00:00Z')
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
214
229
|
const metadata = {
|
|
215
230
|
id: 'metadata-123',
|
|
216
231
|
live: { updatedAt: new Date('2024-10-15T10:00:00Z') },
|
|
217
|
-
versions: [{ versionNumber: 9 }],
|
|
218
232
|
notificationEmail: 'enrique.chase@defra.gov.uk'
|
|
219
233
|
}
|
|
220
234
|
let server: Request['server']
|
|
@@ -274,7 +288,7 @@ describe('resolveFormModel helper', () => {
|
|
|
274
288
|
definition,
|
|
275
289
|
expect.objectContaining({
|
|
276
290
|
basePath: 'forms/preview/live/tb-origin',
|
|
277
|
-
versionNumber:
|
|
291
|
+
versionNumber: 9,
|
|
278
292
|
ordnanceSurveyApiKey: 'os-api-key',
|
|
279
293
|
formId: metadata.id
|
|
280
294
|
}),
|