Merged PR 2051: Update filename validation

Related work items: #21099, #21154
This commit is contained in:
Robert Bond
2026-01-20 15:22:40 +00:00
8 changed files with 222 additions and 65 deletions
@@ -1042,7 +1042,9 @@ export const RenderFileUpload = (field) => {
});
};
const [errorMessage, setErrorMessage] = useState(null);
const [rejectedFiles, setRejectedFiles] = useState([]); // Store rejected files
const [completedUploadFiles, setCompletedUploadFiles] = useState(false);
const [uploadCountMessage, setUploadCountMessage] = useState("");
@@ -1060,6 +1062,15 @@ export const RenderFileUpload = (field) => {
"myrepresentations:fileupload-file-error-invalid-type-label"
)}`;
}
if (error.code === "filename-invalid-chars") {
return (
error.message ||
`${file.name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-filename-label"
)}`
);
}
return `${file.name} ${t(
"myrepresentations:fileupload-file-error-invalid-label"
)}`;
@@ -1076,6 +1087,44 @@ export const RenderFileUpload = (field) => {
setRejectedFiles([]); // Clear any previously rejected file errorsss
};
const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-()]+$/;
const validateFilename = (file, t) => {
const name = file.name;
// block #
if (name.includes("#")) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"myrepresentations:fileupload-file-error-invalid-filename-label"
)}`,
};
}
// broader blocklist (optional but recommended)
if (!FILENAME_ALLOWED.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"myrepresentations:fileupload-file-error-invalid-filename-label"
)}`,
};
}
// block characters < > : " / \ | ? *
if (/[<>:"/\\|?*]/.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"myrepresentations:fileupload-file-error-invalid-filename-label"
)}`,
};
}
return null; // valid
};
return (
<>
<Dropzone
@@ -1091,9 +1140,27 @@ export const RenderFileUpload = (field) => {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
[".xlsx"],
}}
onDrop={(filesToUpload, e) => {
validator={(file) => validateFilename(file, t)}
onDrop={(acceptedFiles, fileRejections, e) => {
// build + display rejected messages (ALWAYS, even if some accepted)
if (fileRejections?.length) {
handleDropRejected(fileRejections);
} else {
// only clear rejections when nothing was rejected on this drop
setRejectedFiles([]);
setErrorMessage(null);
}
// if nothing accepted, stop (prevents showing only "uploading" when all invalid)
if (!acceptedFiles || acceptedFiles.length === 0) {
setCompletedUploadFiles(true); // optional: depends on your UX
setUploadCountMessage(0);
return;
}
setCompletedUploadFiles(false);
const renamedAcceptedFiles = filesToUpload.map(
const renamedAcceptedFiles = acceptedFiles.map(
(file) =>
new File(
[file],
@@ -1115,7 +1182,16 @@ export const RenderFileUpload = (field) => {
}
)
);
field.input.onChange(renamedAcceptedFiles);
if (typeof field.setFileCount === "function") {
field.setFileCount(
(field.uploadCount || 0) +
renamedAcceptedFiles.length
);
}
setUploadCountMessage(renamedAcceptedFiles.length);
uploadSingleFile(
@@ -1196,8 +1272,6 @@ export const RenderFileUpload = (field) => {
});
});
}}
onDropRejected={handleDropRejected}
onDropAccepted={handleDropAccepted}
>
{({ getRootProps, getInputProps }) => (
<>
@@ -1224,7 +1298,7 @@ export const RenderFileUpload = (field) => {
</>
)}
</Dropzone>
{rejectedFiles.length > 0 && completedUploadFiles == true && (
{rejectedFiles.length > 0 && (
<div
style={{ color: "red", marginTop: "10px" }}
className="govuk-body govuk-!-font-size-14 "
+135 -59
View File
@@ -1903,6 +1903,13 @@ const PickListTranslations = (optionValue) => {
};
export function FileUploadField(props) {
const uploadCount =
typeof props.uploadCount === "number" ? props.uploadCount : 0;
const setFileCount =
typeof props.setFileCount === "function"
? props.setFileCount
: () => {};
return (
<>
<h2 className="govuk-heading-s">
@@ -2144,6 +2151,14 @@ const RenderFileUpload = (field) => {
"newappeal:new-appeal-fileupload-file-error-invalid-type-label"
)}`;
}
if (error.code === "filename-invalid-chars") {
return (
error.message ||
`${file.name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-filename-label"
)}`
);
}
return `${file.name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-label"
)}`;
@@ -2160,6 +2175,44 @@ const RenderFileUpload = (field) => {
setRejectedFiles([]); // Clear any previously rejected file errorsss
};
const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-()]+$/;
const validateFilename = (file, t) => {
const name = file.name;
// block #
if (name.includes("#")) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-filename-label"
)}`,
};
}
// broader blocklist (optional but recommended)
if (!FILENAME_ALLOWED.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-filename-label"
)}`,
};
}
// block characters < > : " / \ | ? *
if (/[<>:"/\\|?*]/.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-filename-label"
)}`,
};
}
return null; // valid
};
return (
<>
<Dropzone
@@ -2176,25 +2229,46 @@ const RenderFileUpload = (field) => {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
[".xlsx"],
}}
onDrop={(filesToUpload, e) => {
validator={(file) => validateFilename(file, t)}
onDrop={(acceptedFiles, fileRejections, e) => {
// build + display rejected messages (ALWAYS, even if some accepted)
if (fileRejections?.length) {
handleDropRejected(fileRejections);
} else {
// only clear rejections when nothing was rejected on this drop
setRejectedFiles([]);
setErrorMessage(null);
}
// if nothing accepted, stop (prevents showing only "uploading" when all invalid)
if (!acceptedFiles || acceptedFiles.length === 0) {
setCompletedUploadFiles(true); // optional: depends on your UX
setUploadCountMessage(0);
return;
}
// continue with your existing upload logic for accepted files
setCompletedUploadFiles(false);
const renamedAcceptedFiles = filesToUpload.map(
const renamedAcceptedFiles = acceptedFiles.map(
(file) =>
new File(
[file],
`${getDocumentType(field.documentTypeCode)}_-_${
file.name
}`,
{
type: file.type,
}
{ type: file.type }
)
);
field.input.onChange(renamedAcceptedFiles);
field.setFileCount(
field.uploadCount + renamedAcceptedFiles.length
);
if (typeof field.setFileCount === "function") {
field.setFileCount(
(field.uploadCount || 0) +
renamedAcceptedFiles.length
);
}
setUploadCountMessage(renamedAcceptedFiles.length);
@@ -2202,66 +2276,68 @@ const RenderFileUpload = (field) => {
renamedAcceptedFiles,
field.containerID,
field.ticketnumber
).then((data) => {
console.log(data);
let buildAppealFilesArray = [];
let values = field.form.appealForm.values;
let filesUploadObj = renamedAcceptedFiles;
)
.then((data) => {
let buildAppealFilesArray = [];
let values = field.form.appealForm.values;
let filesUploadObj = renamedAcceptedFiles;
for (let key in filesUploadObj) {
typeof filesUploadObj[key].name != "undefined" &&
buildAppealFilesArray.push({
"name": filesUploadObj[key].name,
"size": filesUploadObj[key].size,
for (let key in filesUploadObj) {
typeof filesUploadObj[key].name !==
"undefined" &&
buildAppealFilesArray.push({
name: filesUploadObj[key].name,
size: filesUploadObj[key].size,
});
}
function removeDuplicates(arr, key) {
const seen = new Set();
return arr.filter((item) => {
const value = item[key];
if (seen.has(value)) return false;
seen.add(value);
return true;
});
}
}
//console.log(buildAppealFilesArray);
function removeDuplicates(arr, key) {
const seen = new Set();
return arr.filter((item) => {
const value = item[key];
if (seen.has(value)) {
return false; // Duplicate found
} else {
seen.add(value); // Add value to the set
return true; // Keep the item
}
buildAppealFilesArray = values.hasOwnProperty(
"filesList"
)
? buildAppealFilesArray.concat(values.filesList)
: buildAppealFilesArray;
buildAppealFilesArray = removeDuplicates(
buildAppealFilesArray,
"name"
);
Object.assign(values, {
filesList: buildAppealFilesArray,
});
}
buildAppealFilesArray = values.hasOwnProperty(
"filesList"
)
? buildAppealFilesArray.concat(values.filesList)
: buildAppealFilesArray;
// keep your existing server-side invalid handling
if (data.invalidFiles?.length > 0) {
setRejectedFiles((prev) => [
...prev,
...data.invalidFiles,
]);
setUploadCountMessage(
renamedAcceptedFiles.length -
data.invalidFiles.length
);
}
buildAppealFilesArray = removeDuplicates(
buildAppealFilesArray,
"name"
);
Object.assign(values, {
"filesList": buildAppealFilesArray,
});
data.invalidFiles.length > 0 &&
(setRejectedFiles(data.invalidFiles),
setUploadCountMessage(
renamedAcceptedFiles.length -
data.invalidFiles.length
));
getFilesFromBlobproxy(
field.containerID,
field.ticketnumber
).then((data) => {
field.input.value == null;
return getFilesFromBlobproxy(
field.containerID,
field.ticketnumber
);
})
.then((data) => {
field.setFilesForAppeal(data);
setCompletedUploadFiles(true);
});
});
}}
onDropRejected={handleDropRejected}
onDropAccepted={handleDropAccepted}
>
{({ getRootProps, getInputProps }) => (
<>
@@ -2288,7 +2364,7 @@ const RenderFileUpload = (field) => {
</>
)}
</Dropzone>
{rejectedFiles.length > 0 && completedUploadFiles == true && (
{rejectedFiles.length > 0 && (
<div
style={{ color: "red", marginTop: "10px" }}
className="govuk-body govuk-!-font-size-14 "
+2
View File
@@ -177,6 +177,8 @@ export default function BuildRow(props) {
props.props.setFilesForAppeal
}
maxSubField={maxSubField}
setFileCount={setFileCount}
setUploadCount={setUploadCount}
/>
);
// } else {
+1
View File
@@ -35,6 +35,7 @@
"fileupload-file-error-filesize-label": "yn rhy fawr. Llwythwch ffeil lai i fyny.",
"fileupload-file-error-invalid-type-label": "mae ganddo fath annilys. Uwchlwythwch fath a ganiateir.",
"fileupload-file-error-invalid-label": "yn annilys",
"fileupload-file-error-invalid-filename-label": "mae ganddo enw ffeil annilys",
"capacity-para-one": "Mae'r ffurflen hon yn eich galluogi i gyflwyno sylwadau ar achos i Benderfyniadau Cynllunio ac Amgylchedd Cymru",
"capacity-para-two": "Sylwch fod angen cyflwyno sylwadau gan bartïon â diddordeb o fewn yr amserlen. Mae hwn i'w weld ar y dudalen \"Crynodeb Achos\" flaenorol. Gall sylwadau a gyflwynir ar ôl y dyddiad hwn gael eu hystyried yn annilys.",
"sips-capacity-para-one": "Mae'r ffurflen hon yn eich galluogi i gyflwyno ymateb i ymgynghoriad ar achos i Benderfyniadau Cynllunio ac Amgylchedd Cymru.",
+1
View File
@@ -86,6 +86,7 @@
"new-appeal-fileupload-file-error-filesize-label": "yn rhy fawr. Llwythwch ffeil lai i fyny.",
"new-appeal-fileupload-file-error-invalid-type-label": "mae ganddo fath annilys. Uwchlwythwch fath a ganiateir.",
"new-appeal-fileupload-file-error-invalid-label": "yn annilys",
"new-appeal-fileupload-file-error-invalid-filename-label": "mae ganddo enw ffeil annilys",
"s78-appeals-only-label": "Gallwch ddefnyddio'r porth hwn i gyflwyno apêl yn erbyn y canlynol yn unig:",
"s78-appeals-only-bullet-one": "gwrthod caniatâd cynllunio",
"s78-appeals-only-bullet-two": "methiant i benderfynu ar gais cynllunio o fewn 8 wythnos",
+1
View File
@@ -35,6 +35,7 @@
"fileupload-file-error-filesize-label": "is too large. Please upload a smaller file.",
"fileupload-file-error-invalid-type-label": "has an invalid type. Please upload an allowed type.",
"fileupload-file-error-invalid-label": "is invalid",
"fileupload-file-error-invalid-filename-label": "has invalid filename",
"capacity-para-one": "This form enables you to submit representations on a case to Planning and Environment Decisions Wales.",
"capacity-para-two": "Please note that representations from interested parties need to be made within the timetable. This can be found on the previous \"Case Summary\" page. Representations submitted after this date may be considered invalid.",
"sips-capacity-para-one": "This form enables you to submit a consultation repsonse on a case to Planning and Environment Decisions Wales.",
+1
View File
@@ -86,6 +86,7 @@
"new-appeal-fileupload-file-error-filesize-label": "is too large. Please upload a smaller file.",
"new-appeal-fileupload-file-error-invalid-type-label": "has an invalid type. Please upload an allowed type.",
"new-appeal-fileupload-file-error-invalid-label": "is invalid",
"new-appeal-fileupload-file-error-invalid-filename-label": "has invalid filename",
"s78-appeals-only-label": "You can only use this portal to submit an appeal against:",
"s78-appeals-only-bullet-one": "a refusal of planning permission",
"s78-appeals-only-bullet-two": "failure to determine a planning application within 8 weeks",
+2 -1
View File
@@ -304,6 +304,7 @@ export const getServerSideProps = wrapper.getServerSideProps(
"incidentid": result.incidentID,
"appealType": result.appealType,
"repDetails": result,
"filesList": result.filesList,
})
);
@@ -318,7 +319,7 @@ export const getServerSideProps = wrapper.getServerSideProps(
const repsFileListObj = await getRepsFilesBlobs(
result.containerID,
result.ticketnumber,
result.ticketnumber || result.caseRef,
result.repfile_name
);