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 [rejectedFiles, setRejectedFiles] = useState([]); // Store rejected files
const [completedUploadFiles, setCompletedUploadFiles] = useState(false); const [completedUploadFiles, setCompletedUploadFiles] = useState(false);
const [uploadCountMessage, setUploadCountMessage] = useState(""); const [uploadCountMessage, setUploadCountMessage] = useState("");
@@ -1060,6 +1062,15 @@ export const RenderFileUpload = (field) => {
"myrepresentations:fileupload-file-error-invalid-type-label" "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( return `${file.name} ${t(
"myrepresentations:fileupload-file-error-invalid-label" "myrepresentations:fileupload-file-error-invalid-label"
)}`; )}`;
@@ -1076,6 +1087,44 @@ export const RenderFileUpload = (field) => {
setRejectedFiles([]); // Clear any previously rejected file errorsss 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 ( return (
<> <>
<Dropzone <Dropzone
@@ -1091,9 +1140,27 @@ export const RenderFileUpload = (field) => {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
[".xlsx"], [".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); setCompletedUploadFiles(false);
const renamedAcceptedFiles = filesToUpload.map(
const renamedAcceptedFiles = acceptedFiles.map(
(file) => (file) =>
new File( new File(
[file], [file],
@@ -1115,7 +1182,16 @@ export const RenderFileUpload = (field) => {
} }
) )
); );
field.input.onChange(renamedAcceptedFiles); field.input.onChange(renamedAcceptedFiles);
if (typeof field.setFileCount === "function") {
field.setFileCount(
(field.uploadCount || 0) +
renamedAcceptedFiles.length
);
}
setUploadCountMessage(renamedAcceptedFiles.length); setUploadCountMessage(renamedAcceptedFiles.length);
uploadSingleFile( uploadSingleFile(
@@ -1196,8 +1272,6 @@ export const RenderFileUpload = (field) => {
}); });
}); });
}} }}
onDropRejected={handleDropRejected}
onDropAccepted={handleDropAccepted}
> >
{({ getRootProps, getInputProps }) => ( {({ getRootProps, getInputProps }) => (
<> <>
@@ -1224,7 +1298,7 @@ export const RenderFileUpload = (field) => {
</> </>
)} )}
</Dropzone> </Dropzone>
{rejectedFiles.length > 0 && completedUploadFiles == true && ( {rejectedFiles.length > 0 && (
<div <div
style={{ color: "red", marginTop: "10px" }} style={{ color: "red", marginTop: "10px" }}
className="govuk-body govuk-!-font-size-14 " className="govuk-body govuk-!-font-size-14 "
+135 -59
View File
@@ -1903,6 +1903,13 @@ const PickListTranslations = (optionValue) => {
}; };
export function FileUploadField(props) { export function FileUploadField(props) {
const uploadCount =
typeof props.uploadCount === "number" ? props.uploadCount : 0;
const setFileCount =
typeof props.setFileCount === "function"
? props.setFileCount
: () => {};
return ( return (
<> <>
<h2 className="govuk-heading-s"> <h2 className="govuk-heading-s">
@@ -2144,6 +2151,14 @@ const RenderFileUpload = (field) => {
"newappeal:new-appeal-fileupload-file-error-invalid-type-label" "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( return `${file.name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-label" "newappeal:new-appeal-fileupload-file-error-invalid-label"
)}`; )}`;
@@ -2160,6 +2175,44 @@ const RenderFileUpload = (field) => {
setRejectedFiles([]); // Clear any previously rejected file errorsss 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 ( return (
<> <>
<Dropzone <Dropzone
@@ -2176,25 +2229,46 @@ const RenderFileUpload = (field) => {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
[".xlsx"], [".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); setCompletedUploadFiles(false);
const renamedAcceptedFiles = filesToUpload.map(
const renamedAcceptedFiles = acceptedFiles.map(
(file) => (file) =>
new File( new File(
[file], [file],
`${getDocumentType(field.documentTypeCode)}_-_${ `${getDocumentType(field.documentTypeCode)}_-_${
file.name file.name
}`, }`,
{ { type: file.type }
type: file.type,
}
) )
); );
field.input.onChange(renamedAcceptedFiles); field.input.onChange(renamedAcceptedFiles);
field.setFileCount( if (typeof field.setFileCount === "function") {
field.uploadCount + renamedAcceptedFiles.length field.setFileCount(
); (field.uploadCount || 0) +
renamedAcceptedFiles.length
);
}
setUploadCountMessage(renamedAcceptedFiles.length); setUploadCountMessage(renamedAcceptedFiles.length);
@@ -2202,66 +2276,68 @@ const RenderFileUpload = (field) => {
renamedAcceptedFiles, renamedAcceptedFiles,
field.containerID, field.containerID,
field.ticketnumber field.ticketnumber
).then((data) => { )
console.log(data); .then((data) => {
let buildAppealFilesArray = []; let buildAppealFilesArray = [];
let values = field.form.appealForm.values; let values = field.form.appealForm.values;
let filesUploadObj = renamedAcceptedFiles; let filesUploadObj = renamedAcceptedFiles;
for (let key in filesUploadObj) { for (let key in filesUploadObj) {
typeof filesUploadObj[key].name != "undefined" && typeof filesUploadObj[key].name !==
buildAppealFilesArray.push({ "undefined" &&
"name": filesUploadObj[key].name, buildAppealFilesArray.push({
"size": filesUploadObj[key].size, 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); buildAppealFilesArray = values.hasOwnProperty(
function removeDuplicates(arr, key) { "filesList"
const seen = new Set(); )
return arr.filter((item) => { ? buildAppealFilesArray.concat(values.filesList)
const value = item[key]; : buildAppealFilesArray;
if (seen.has(value)) {
return false; // Duplicate found buildAppealFilesArray = removeDuplicates(
} else { buildAppealFilesArray,
seen.add(value); // Add value to the set "name"
return true; // Keep the item );
}
Object.assign(values, {
filesList: buildAppealFilesArray,
}); });
}
buildAppealFilesArray = values.hasOwnProperty( // keep your existing server-side invalid handling
"filesList" if (data.invalidFiles?.length > 0) {
) setRejectedFiles((prev) => [
? buildAppealFilesArray.concat(values.filesList) ...prev,
: buildAppealFilesArray; ...data.invalidFiles,
]);
setUploadCountMessage(
renamedAcceptedFiles.length -
data.invalidFiles.length
);
}
buildAppealFilesArray = removeDuplicates( return getFilesFromBlobproxy(
buildAppealFilesArray, field.containerID,
"name" field.ticketnumber
); );
})
Object.assign(values, { .then((data) => {
"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;
field.setFilesForAppeal(data); field.setFilesForAppeal(data);
setCompletedUploadFiles(true); setCompletedUploadFiles(true);
}); });
});
}} }}
onDropRejected={handleDropRejected}
onDropAccepted={handleDropAccepted}
> >
{({ getRootProps, getInputProps }) => ( {({ getRootProps, getInputProps }) => (
<> <>
@@ -2288,7 +2364,7 @@ const RenderFileUpload = (field) => {
</> </>
)} )}
</Dropzone> </Dropzone>
{rejectedFiles.length > 0 && completedUploadFiles == true && ( {rejectedFiles.length > 0 && (
<div <div
style={{ color: "red", marginTop: "10px" }} style={{ color: "red", marginTop: "10px" }}
className="govuk-body govuk-!-font-size-14 " className="govuk-body govuk-!-font-size-14 "
+2
View File
@@ -177,6 +177,8 @@ export default function BuildRow(props) {
props.props.setFilesForAppeal props.props.setFilesForAppeal
} }
maxSubField={maxSubField} maxSubField={maxSubField}
setFileCount={setFileCount}
setUploadCount={setUploadCount}
/> />
); );
// } else { // } else {
+1
View File
@@ -35,6 +35,7 @@
"fileupload-file-error-filesize-label": "yn rhy fawr. Llwythwch ffeil lai i fyny.", "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-type-label": "mae ganddo fath annilys. Uwchlwythwch fath a ganiateir.",
"fileupload-file-error-invalid-label": "yn annilys", "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-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.", "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.", "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-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-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-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-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-one": "gwrthod caniatâd cynllunio",
"s78-appeals-only-bullet-two": "methiant i benderfynu ar gais cynllunio o fewn 8 wythnos", "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-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-type-label": "has an invalid type. Please upload an allowed type.",
"fileupload-file-error-invalid-label": "is invalid", "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-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.", "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.", "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-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-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-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-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-one": "a refusal of planning permission",
"s78-appeals-only-bullet-two": "failure to determine a planning application within 8 weeks", "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, "incidentid": result.incidentID,
"appealType": result.appealType, "appealType": result.appealType,
"repDetails": result, "repDetails": result,
"filesList": result.filesList,
}) })
); );
@@ -318,7 +319,7 @@ export const getServerSideProps = wrapper.getServerSideProps(
const repsFileListObj = await getRepsFilesBlobs( const repsFileListObj = await getRepsFilesBlobs(
result.containerID, result.containerID,
result.ticketnumber, result.ticketnumber || result.caseRef,
result.repfile_name result.repfile_name
); );