Skip to content

Commit

Permalink
feat: storage submission with virus scans
Browse files Browse the repository at this point in the history
  • Loading branch information
LinHuiqing committed Oct 2, 2023
1 parent 721c181 commit 9ee97bb
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 37 deletions.
21 changes: 17 additions & 4 deletions frontend/src/features/public-form/PublicFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ import get from 'lodash/get'
import simplur from 'simplur'

import {
ENCRYPTION_BOUNDARY_SHIFT_SUBMISSION_VERSION,
featureFlags,
PAYMENT_CONTACT_FIELD_ID,
PAYMENT_PRODUCT_FIELD_ID,
PAYMENT_VARIABLE_INPUT_AMOUNT_FIELD_ID,
VIRUS_SCANNER_SUBMISSION_VERSION,
} from '~shared/constants'
import { BasicField, PaymentType } from '~shared/types'
import { CaptchaTypes } from '~shared/types/captcha'
Expand Down Expand Up @@ -560,8 +558,23 @@ export const PublicFormProvider = ({
return (
submitStorageModeClearFormWithVirusScanningMutation
.mutateAsync(formData, {
onSuccess: async (presignedUrls) => {
console.log('presignedUrls', presignedUrls)
onSuccess: ({
submissionId,
timestamp,
// payment forms will have non-empty paymentData field
paymentData,
}) => {
trackSubmitForm(form)

if (paymentData) {
navigate(getPaymentPageUrl(formId, paymentData.paymentId))
storePaymentMemory(paymentData.paymentId)
return
}
setSubmissionData({
id: submissionId,
timestamp,
})
},
})
// Using catch since we are using mutateAsync and react-hook-form will continue bubbling this up.
Expand Down
66 changes: 61 additions & 5 deletions frontend/src/features/public-form/PublicFormService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { PresignedPost } from 'aws-sdk/clients/s3'
import axios from 'axios'

import { ENCRYPTION_BOUNDARY_SHIFT_SUBMISSION_VERSION } from '~shared/constants'
import {
ENCRYPTION_BOUNDARY_SHIFT_SUBMISSION_VERSION,
VIRUS_SCANNER_SUBMISSION_VERSION,
} from '~shared/constants'
import { SubmitFormIssueBodyDto, SuccessMessageDto } from '~shared/types'
import {
AttachmentPresignedPostDataMapType,
Expand Down Expand Up @@ -35,6 +38,7 @@ import { FormFieldValues } from '~templates/Field'

import {
createClearSubmissionFormData,
createClearSubmissionWithVirusScanningFormData,
createEncryptedSubmissionData,
getAttachmentsMap,
} from './utils/createSubmission'
Expand Down Expand Up @@ -110,6 +114,16 @@ export type SubmitStorageFormClearArgs = SubmitEmailFormArgs & {
version?: number
}

export type FieldIdToQuarantineKeyType = {
fieldId: string
quarantineBucketKey: string
}

export type SubmitStorageFormWithVirusScanningArgs =
SubmitStorageFormClearArgs & {
fieldIdToQuarantineKeyMap: FieldIdToQuarantineKeyType[]
}

export const submitEmailModeForm = async ({
formFields,
formLogics,
Expand Down Expand Up @@ -192,7 +206,6 @@ export const submitStorageModeClearForm = async ({
responseMetadata,
paymentProducts,
payments,
version = ENCRYPTION_BOUNDARY_SHIFT_SUBMISSION_VERSION,
}: SubmitStorageFormClearArgs) => {
const filteredInputs = filterHiddenInputs({
formFields,
Expand All @@ -207,7 +220,7 @@ export const submitStorageModeClearForm = async ({
paymentReceiptEmail,
paymentProducts,
payments,
version,
version: ENCRYPTION_BOUNDARY_SHIFT_SUBMISSION_VERSION,
})

return ApiService.post<SubmissionResponseDto>(
Expand All @@ -234,7 +247,6 @@ export const submitStorageModeClearFormWithFetch = async ({
responseMetadata,
paymentProducts,
payments,
version = ENCRYPTION_BOUNDARY_SHIFT_SUBMISSION_VERSION,
}: SubmitStorageFormClearArgs) => {
const filteredInputs = filterHiddenInputs({
formFields,
Expand All @@ -249,7 +261,7 @@ export const submitStorageModeClearFormWithFetch = async ({
paymentReceiptEmail,
paymentProducts,
payments,
version,
version: ENCRYPTION_BOUNDARY_SHIFT_SUBMISSION_VERSION,
})

// Add captcha response to query string
Expand All @@ -272,6 +284,50 @@ export const submitStorageModeClearFormWithFetch = async ({
return processFetchResponse(response)
}

export const submitStorageModeClearFormWithVirusScanning = async ({
formFields,
formLogics,
formInputs,
formId,
captchaResponse = null,
captchaType = '',
paymentReceiptEmail,
responseMetadata,
paymentProducts,
payments,
fieldIdToQuarantineKeyMap,
}: SubmitStorageFormWithVirusScanningArgs) => {
const filteredInputs = filterHiddenInputs({
formFields,
formInputs,
formLogics,
})

const formData = createClearSubmissionWithVirusScanningFormData(
{
formFields,
formInputs: filteredInputs,
responseMetadata,
paymentReceiptEmail,
paymentProducts,
payments,
version: VIRUS_SCANNER_SUBMISSION_VERSION,
},
fieldIdToQuarantineKeyMap,
)

return ApiService.post<SubmissionResponseDto>(
`${PUBLIC_FORMS_ENDPOINT}/${formId}/submissions/storage`,
formData,
{
params: {
captchaResponse: String(captchaResponse),
captchaType: captchaType,
},
},
).then(({ data }) => data)
}

// TODO (#5826): Fallback mutation using Fetch. Remove once network error is resolved
export const submitEmailModeFormWithFetch = async ({
formFields,
Expand Down
83 changes: 55 additions & 28 deletions frontend/src/features/public-form/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useToast } from '~hooks/useToast'

import { useStorePrefillQuery } from './hooks/useStorePrefillQuery'
import {
FieldIdToQuarantineKeyType,
getAttachmentPresignedPostData,
getPublicFormAuthRedirectUrl,
logoutPublicForm,
Expand All @@ -22,6 +23,7 @@ import {
SubmitStorageFormClearArgs,
submitStorageModeClearForm,
submitStorageModeClearFormWithFetch,
submitStorageModeClearFormWithVirusScanning,
submitStorageModeForm,
submitStorageModeFormWithFetch,
uploadAttachmentToQuarantine,
Expand Down Expand Up @@ -123,34 +125,59 @@ export const usePublicFormMutations = (
const submitStorageModeClearFormWithVirusScanningMutation = useMutation(
(args: Omit<SubmitStorageFormClearArgs, 'formId'>) => {
// Step 1: Get presigned post data for all attachment fields
return getAttachmentPresignedPostData({ ...args, formId }).then(
// Step 2: Upload attachments to quarantine bucket asynchronously
(presignedPostDataList) =>
Promise.all(
presignedPostDataList.map(async (presignedPostData) => {
const attachmentFile = args.formInputs[presignedPostData.id]

// Check if response is a File object (from an attachment field)
if (!(attachmentFile instanceof File))
throw new Error('Field is not attachment')

const uploadResponse = await uploadAttachmentToQuarantine(
presignedPostData.presignedPostData,
attachmentFile,
)

// // If status code is not 200-299, throw error
if (uploadResponse.status < 200 || uploadResponse.status > 299)
throw new Error(
`Attachment upload failed - ${uploadResponse.statusText}`,
)

return {
fieldId: presignedPostData.id,
uploadResponse,
}
}),
),
return (
getAttachmentPresignedPostData({ ...args, formId })
.then(
// Step 2: Upload attachments to quarantine bucket asynchronously
(fieldToPresignedPostDataMap) =>
Promise.all(
fieldToPresignedPostDataMap.map(
async (fieldToPresignedPostData) => {
const attachmentFile =
args.formInputs[fieldToPresignedPostData.id]

// Check if response is a File object (from an attachment field)
if (!(attachmentFile instanceof File))
throw new Error('Field is not attachment')

const uploadResponse = await uploadAttachmentToQuarantine(
fieldToPresignedPostData.presignedPostData,
attachmentFile,
)

// If status code is not 200-299, throw error
if (
uploadResponse.status < 200 ||
uploadResponse.status > 299
)
throw new Error(
`Attachment upload failed - ${uploadResponse.statusText}`,
)

const quarantineBucketKey =
fieldToPresignedPostData.presignedPostData.fields.key

if (!quarantineBucketKey)
throw new Error(
'key is not defined in presigned post data',
)

return {
fieldId: fieldToPresignedPostData.id,
quarantineBucketKey,
} as FieldIdToQuarantineKeyType
},
),
),
)
// Step 3: Submit form with keys to quarantine bucket attachments
.then((fieldIdToQuarantineKeyMap) => {
return submitStorageModeClearFormWithVirusScanning({
...args,
fieldIdToQuarantineKeyMap,
formId,
})
})
)
},
)
Expand Down
52 changes: 52 additions & 0 deletions frontend/src/features/public-form/utils/createSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import fileArrayBuffer from '~/utils/fileArrayBuffer'
import formsgSdk from '~utils/formSdk'
import { AttachmentFieldSchema, FormFieldValues } from '~templates/Field'

import { FieldIdToQuarantineKeyType } from '../PublicFormService'

import { transformInputsToOutputs } from './inputTransformation'
import { validateResponses } from './validateResponses'

Expand Down Expand Up @@ -121,6 +123,56 @@ export const createClearSubmissionFormData = (
return formData
}

export const createClearSubmissionWithVirusScanningFormData = (
formDataArgs:
| CreateEmailSubmissionFormDataArgs
| CreateStorageSubmissionFormDataArgs,
fieldIdToQuarantineKeyMap: FieldIdToQuarantineKeyType[],
) => {
const { formFields, formInputs, ...formDataArgsRest } = formDataArgs
const responses = createResponsesArray(formFields, formInputs).map(
(response) => {
if (response.fieldType === BasicField.Attachment && response.answer) {
const fieldIdToQuarantineKeyEntry = fieldIdToQuarantineKeyMap.find(
(v) => v.fieldId === response._id,
)
if (!fieldIdToQuarantineKeyEntry)
throw new Error(
`Attachment response with fieldId ${response._id} not found among attachments uploaded to quarantine bucket`,
)
response.answer = fieldIdToQuarantineKeyEntry.quarantineBucketKey
}
return response
},
)
const attachments = getAttachmentsMap(formFields, formInputs)

// Convert content to FormData object.
const formData = new FormData()
formData.append(
'body',
JSON.stringify({
responses,
...formDataArgsRest,
}),
)

if (!isEmpty(attachments)) {
forOwn(attachments, (attachment, fieldId) => {
if (attachment) {
formData.append(
attachment.name,
// Set content as empty array buffer.
new File([], attachment.name, { type: attachment.type }),
fieldId,
)
}
})
}

return formData
}

const createResponsesArray = (
formFields: FormFieldDto[],
formInputs: FormFieldValues,
Expand Down

0 comments on commit 9ee97bb

Please sign in to comment.