Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions designer/server/src/routes/forms/contact/email.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Scopes,
emailAddressSchema,
UNICODE_EMAIL_ERROR_MESSAGE,
emailAddressNoUnicodeSchema,
emailResponseTimeSchema
} from '@defra/forms-model'
import { StatusCodes } from 'http-status-codes'
Expand All @@ -20,13 +21,14 @@ export const ROUTE_PATH_EDIT_EMAIL_CONTACT =
'/library/{slug}/edit/contact/email'

export const emailContactSchema = Joi.object().keys({
address: emailAddressSchema
address: emailAddressNoUnicodeSchema
.required()
.when('_delete', { is: true, then: Joi.allow('') })
.messages({
'string.empty': 'Enter an email address for dedicated support',
'string.email':
'Enter an email address for dedicated support in the correct format'
'Enter an email address for dedicated support in the correct format',
'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE
}),
responseTime: emailResponseTimeSchema
.required()
Expand Down
4 changes: 3 additions & 1 deletion designer/server/src/routes/forms/create.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Scopes,
UNICODE_EMAIL_ERROR_MESSAGE,
getErrorMessage,
organisationSchema,
slugify,
Expand Down Expand Up @@ -41,7 +42,8 @@ export const schema = Joi.object().keys({
}),
teamEmail: teamEmailSchema.messages({
'string.empty': 'Enter a shared team email address',
'string.email': 'Enter a shared team email address in the correct format'
'string.email': 'Enter a shared team email address in the correct format',
'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE
})
})

Expand Down
9 changes: 7 additions & 2 deletions designer/server/src/routes/forms/notification-email.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Scopes, notificationEmailAddressSchema } from '@defra/forms-model'
import {
Scopes,
UNICODE_EMAIL_ERROR_MESSAGE,
notificationEmailAddressSchema
} from '@defra/forms-model'
import { StatusCodes } from 'http-status-codes'
import Joi from 'joi'

Expand All @@ -21,7 +25,8 @@ export const schema = Joi.object().keys({
notificationEmail: notificationEmailAddressSchema.required().messages({
'string.empty': EMPTY_MESSAGE,
'string.email': INCORRECT_FORMAT_MESSAGE,
'string.pattern.base': INCORRECT_FORMAT_MESSAGE
'string.pattern.base': INCORRECT_FORMAT_MESSAGE,
'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE
})
})

Expand Down
12 changes: 8 additions & 4 deletions designer/server/src/routes/manage/user.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Roles, Scopes } from '@defra/forms-model'
import {
Roles,
Scopes,
UNICODE_EMAIL_ERROR_MESSAGE,
emailAddressNoUnicodeSchema
} from '@defra/forms-model'
import Boom from '@hapi/boom'
import { StatusCodes } from 'http-status-codes'
import Joi from 'joi'
Expand All @@ -15,11 +20,10 @@ import * as userService from '~/src/services/userService.js'
const errorKey = sessionNames.validationFailure.manageUsers

const addUserSchema = Joi.object({
emailAddress: Joi.string()
.email()
.trim()
emailAddress: emailAddressNoUnicodeSchema
.required()
.messages({
'string.unicode': UNICODE_EMAIL_ERROR_MESSAGE,
'*': 'Enter an email address in the correct format'
})
.description('Email address of user'),
Expand Down
8 changes: 4 additions & 4 deletions model/src/form/form-audit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ import {
type FormUploadedMessageData,
type FormsBackupRequestedMessageData
} from '~/src/form/form-audit/types.js'
import { emailAddressNoUnicodeSchema } from '~/src/form/form-editor/index.js'
import {
contactSchema,
emailAddressSchema,
emailResponseTimeSchema,
idSchema,
notificationEmailAddressSchema,
Expand Down Expand Up @@ -181,7 +181,7 @@ export const formSupportOnlineChanges = Joi.object<FormSupportOnlineChanges>()

export const formSupportEmailChanges = Joi.object<FormSupportEmailChanges>()
.keys({
address: emailAddressSchema.optional(),
address: emailAddressNoUnicodeSchema.optional(),
responseTime: emailResponseTimeSchema.optional()
})
.description('Changes schema for FORM_SUPPORT_EMAIL_UPDATED event')
Expand Down Expand Up @@ -238,7 +238,7 @@ export const entitlementMessageData = Joi.object<EntitlementMessageData>().keys(
{
userId: Joi.string().required(),
displayName: Joi.string().required(),
email: Joi.string().email().required(),
email: emailAddressNoUnicodeSchema.required(),
roles: Joi.array().items(Joi.string())
}
)
Expand All @@ -260,7 +260,7 @@ export const excelGenerationMessageData =
.keys({
formId: Joi.string().required(),
formName: Joi.string().required(),
notificationEmail: Joi.string().email().required()
notificationEmail: emailAddressNoUnicodeSchema.required()
})
.required()
.description(
Expand Down
2 changes: 2 additions & 0 deletions model/src/form/form-definition/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MIN_NUMBER_OF_REPEAT_ITEMS = 1
export const MAX_NUMBER_OF_REPEAT_ITEMS = 200
27 changes: 13 additions & 14 deletions model/src/form/form-definition/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import {
type RelativeDateValueData,
type RelativeDateValueDataV2
} from '~/src/conditions/types.js'
import {
MAX_NUMBER_OF_REPEAT_ITEMS,
MIN_NUMBER_OF_REPEAT_ITEMS
} from '~/src/form/form-definition/constants.js'
import {
isConditionListItemRefValueData,
isFormDefinition
Expand All @@ -47,6 +51,7 @@ import {
type RepeatSchema,
type Section
} from '~/src/form/form-definition/types.js'
import { emailAddressNoUnicodeSchema } from '~/src/form/form-editor/index.js'
import { checkErrors } from '~/src/form/form-manager/errors.js'
import {
FormDefinitionError,
Expand Down Expand Up @@ -348,9 +353,6 @@ const conditionSchema = Joi.object<ConditionData>()
)
})

export const MIN_NUMBER_OF_REPEAT_ITEMS = 1
export const MAX_NUMBER_OF_REPEAT_ITEMS = 200

export const conditionDataSchemaV2 = Joi.object<ConditionDataV2>()
.description('Condition definition')
.keys({
Expand Down Expand Up @@ -1041,13 +1043,7 @@ const feedbackSchema = Joi.object<FormDefinition['feedback']>()
.description(
'URL to an external feedback form when not using built-in feedback'
),
emailAddress: Joi.string()
.trim()
.email({
tlds: {
allow: false
}
})
emailAddress: emailAddressNoUnicodeSchema
.optional()
.description('Email address where feedback is sent')
})
Expand Down Expand Up @@ -1155,8 +1151,7 @@ export const formDefinitionSchema = Joi.object<FormDefinition>()
.optional()
.description('Phase banner configuration'),
options: optionsSchema.optional().description('Options for the form'),
outputEmail: Joi.string()
.trim()
outputEmail: emailAddressNoUnicodeSchema
.email({ tlds: { allow: ['uk'] } })
.optional()
.description('Email address where form submissions are sent'),
Expand All @@ -1165,8 +1160,7 @@ export const formDefinitionSchema = Joi.object<FormDefinition>()
.description('Configuration for submission output format'),
outputs: Joi.array()
.items({
emailAddress: Joi.string()
.trim()
emailAddress: emailAddressNoUnicodeSchema
.email({ tlds: { allow: ['uk'] } })
.description('Email address where form submissions are sent'),
audience: Joi.string()
Expand Down Expand Up @@ -1244,6 +1238,11 @@ export const formDefinitionV2Schema = formDefinitionSchema
})
.description('Form definition schema for V2')

export {
MAX_NUMBER_OF_REPEAT_ITEMS,
MIN_NUMBER_OF_REPEAT_ITEMS
} from '~/src/form/form-definition/constants.js'

// Maintain compatibility with legacy named export
// E.g. `import { Schema } from '@defra/forms-model'`
export const Schema = formDefinitionSchema
12 changes: 11 additions & 1 deletion model/src/form/form-editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ComponentType } from '~/src/components/enums.js'
import {
MAX_NUMBER_OF_REPEAT_ITEMS,
MIN_NUMBER_OF_REPEAT_ITEMS
} from '~/src/form/form-definition/index.js'
} from '~/src/form/form-definition/constants.js'
import {
type FormEditorInputCheckAnswersSettings,
type FormEditorInputPage,
Expand All @@ -16,6 +16,16 @@ import {
type GovukFieldUsePostcodeLookup,
type GovukStringField
} from '~/src/form/form-editor/types.js'
import { preventUnicodeInEmail } from '~/src/form/utils/prevent-unicode.js'

export const emailAddressNoUnicodeSchema = Joi.string()
.trim()
.email()
.custom((value, helpers) => preventUnicodeInEmail(value, helpers))
.description('Email address preventing unicode characters')

export const UNICODE_EMAIL_ERROR_MESSAGE =
'The email address you entered includes invalid characters, for example, long dashes'

export enum QuestionTypeSubGroup {
WrittenAnswerSubGroup = 'writtenAnswerSub',
Expand Down
15 changes: 7 additions & 8 deletions model/src/form/form-metadata/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Joi from 'joi'

import { emailAddressNoUnicodeSchema } from '~/src/form/form-editor/index.js'
import {
type FormMetadata,
type FormMetadataAuthor,
Expand Down Expand Up @@ -53,7 +54,7 @@ export const teamNameSchema = Joi.string()
.required()
.description('Name of the team responsible for the form')

export const teamEmailSchema = Joi.string()
export const teamEmailSchema = emailAddressNoUnicodeSchema
.email({ tlds: { allow: ['uk'] } })
.trim()
.required()
Expand All @@ -63,9 +64,7 @@ export const phoneSchema = Joi.string()
.trim()
.description('Phone number for form-related inquiries')

export const emailAddressSchema = Joi.string()
.email()
.trim()
export const emailAddressSchema = emailAddressNoUnicodeSchema
.required()
.description('Email address for form-related inquiries')

Expand Down Expand Up @@ -132,10 +131,10 @@ export const termsAndConditionsAgreedSchema = Joi.boolean().description(
'Whether the data protection terms and conditions have been agreed to'
)

export const notificationEmailAddressSchema = Joi.string()
.email()
.trim()
.description('Email address to receive form submission notifications')
export const notificationEmailAddressSchema =
emailAddressNoUnicodeSchema.description(
'Email address to receive form submission notifications'
)

export const authoredAtSchema = Joi.date()
.iso()
Expand Down
1 change: 1 addition & 0 deletions model/src/form/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { findStartPage } from '~/src/form/utils/find-start-page.js'
export { findDefinitionListFromComponent } from '~/src/form/utils/list.js'
export { preventUnicodeInEmail } from '~/src/form/utils/prevent-unicode.js'
19 changes: 19 additions & 0 deletions model/src/form/utils/prevent-unicode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Custom Joi validator to prevent Unicode characters in say an email address.
* Although Joi has Joi.email({ allowUnicode: false }), we can't differentiate from a general
* email format error or a Unicode error - hance the custom validator to allow a different error message
* @param {unknown} value
* @param {CustomHelpers<string>} helpers
*/
export function preventUnicodeInEmail(value, helpers) {
if (!value || typeof value !== 'string') {
return helpers.error('string.empty')
}
const invalidCharsRegex = /[^a-zA-Z0-9.!#$%&'*+/=?^_`{|}~@-]/
const invalid = invalidCharsRegex.exec(value)
return invalid ? helpers.error('string.unicode') : value
}

/**
* @import { type CustomHelpers } from 'joi'
*/
44 changes: 44 additions & 0 deletions model/src/form/utils/prevent-unicode.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { preventUnicodeInEmail } from '~/src/form/utils/prevent-unicode.js'

describe('preventUnicodeInEmail', () => {
const mockHelpers = {
error: jest.fn()
}

beforeEach(() => {
jest.clearAllMocks()
})

it('should handle undefined', () => {
// @ts-expect-error - partial helper mock
preventUnicodeInEmail(undefined, mockHelpers)
expect(mockHelpers.error).toHaveBeenCalledWith('string.empty')
})

it('should handle not a string', () => {
// @ts-expect-error - partial helper mock
preventUnicodeInEmail({ abc: 123 }, mockHelpers)
expect(mockHelpers.error).toHaveBeenCalledWith('string.empty')
})

it.each([
'first.last@domain.co.uk',
'first-last@domain.uk',
'only@domain.uk'
])('handles a valid emails', (/** @type {string} */ email) => {
// @ts-expect-error - partial helper mock
expect(preventUnicodeInEmail(email, mockHelpers)).toBe(email)
expect(mockHelpers.error).not.toHaveBeenCalled()
})

const enDash = '\u2013'
const emDash = '\u2014'
it.each([`first${enDash}last@domain.co.uk`, `first${emDash}}last@domain.uk`])(
'handles an invalid email',
(/** @type {string} */ email) => {
// @ts-expect-error - partial helper mock
preventUnicodeInEmail(email, mockHelpers)
expect(mockHelpers.error).toHaveBeenCalledWith('string.unicode')
}
)
})
9 changes: 0 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading