Skip to content

Commit 4534b66

Browse files
authored
BC-10857 - Add validation for invalid characters in file name inputs (#3971)
1 parent e02539e commit 4534b66

File tree

13 files changed

+305
-9
lines changed

13 files changed

+305
-9
lines changed

src/locales/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,4 +1997,5 @@ export default {
19971997
"pages.folder.delete-multiple-confirmation": "Alle {total} Dateien wirklich löschen?",
19981998
"pages.folder.delete-confirmation": "Datei {name} wirklich löschen?",
19991999
"pages.folder.rename-file-dialog.validation.duplicate-file-name": "Der Dateiname existiert bereits.",
2000+
"pages.folder.rename-file-dialog.validation.invalid-characters": "Der Dateiname enthält ungültige Zeichen.",
20002001
};

src/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,4 +1963,5 @@ export default {
19631963
"pages.folder.delete-multiple-confirmation": "Delete all {total} files?",
19641964
"pages.folder.delete-confirmation": "Do you really want to delete file {name}?",
19651965
"pages.folder.rename-file-dialog.validation.duplicate-file-name": "The file name already exists.",
1966+
"pages.folder.rename-file-dialog.validation.invalid-characters": "The file name contains invalid characters.",
19661967
};

src/locales/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,4 +2014,6 @@ export default {
20142014
"pages.folder.delete-multiple-confirmation": "¿Borrar realmente todos los archivos {total}?",
20152015
"pages.folder.delete-confirmation": "¿Borrar realmente el archivo {name}?",
20162016
"pages.folder.rename-file-dialog.validation.duplicate-file-name": "El nombre del archivo ya existe.",
2017+
"pages.folder.rename-file-dialog.validation.invalid-characters":
2018+
"El nombre del archivo contiene caracteres no válidos.",
20172019
};

src/locales/uk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,4 +1984,5 @@ export default {
19841984
"pages.folder.delete-multiple-confirmation": "Дійсно видалити всі {total} файли?",
19851985
"pages.folder.delete-confirmation": "Дійсно видалити файл {name}?",
19861986
"pages.folder.rename-file-dialog.validation.duplicate-file-name": "Ім'я файлу вже існує.",
1987+
"pages.folder.rename-file-dialog.validation.invalid-characters": "Ім'я файлу містить недопустимі символи.",
19871988
};

src/modules/feature/board-file-element/content/inputs/file-name/FileName.unit.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,32 @@ describe("FileName", () => {
6464
});
6565
});
6666

67+
describe("when a value containing / is entered", () => {
68+
it("should display an error message", async () => {
69+
const { wrapper } = mountSetup();
70+
71+
const textField = wrapper.findComponent(VTextField);
72+
const input = textField.find("input[type='text']");
73+
await input.setValue("invalid/name");
74+
await nextTick();
75+
76+
expect(textField.text()).toContain(
77+
wrapper.vm.$t("pages.folder.rename-file-dialog.validation.invalid-characters")
78+
);
79+
});
80+
81+
it("should not emit update:name", async () => {
82+
const { wrapper } = mountSetup();
83+
84+
const textField = wrapper.findComponent(VTextField);
85+
const newFileName = "my/NewImage";
86+
await textField.setValue(newFileName);
87+
await nextTick();
88+
89+
expect(wrapper.emitted("update:name")).toBeUndefined();
90+
});
91+
});
92+
6793
describe("when an empty value is entered", () => {
6894
it("should not emit update:name", async () => {
6995
const { wrapper } = mountSetup();

src/modules/feature/board-file-element/content/inputs/file-name/FileName.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
v-model="nameRef"
44
data-testid="file-name-input"
55
:label="t('common.labels.fileName')"
6-
:rules="[rules.isRequired, rules.validateOnOpeningTag]"
6+
:rules="[rules.isRequired, rules.validateOnOpeningTag, rules.invalidCharacters]"
77
@click.stop
88
@keydown.enter.stop
99
/>
1010
</template>
1111

1212
<script setup lang="ts">
1313
import { getFileExtension, removeFileExtension } from "@/utils/fileHelper";
14-
import { isRequired, useOpeningTagValidator } from "@util-validators";
14+
import { isRequired, useInvalidCharactersValidator, useOpeningTagValidator } from "@util-validators";
1515
import { computed, ref, watch } from "vue";
1616
import { useI18n } from "vue-i18n";
1717
@@ -24,6 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
2424
});
2525
2626
const { validateOnOpeningTag } = useOpeningTagValidator();
27+
const { validateInvalidCharacters } = useInvalidCharactersValidator();
2728
2829
const emit = defineEmits<{
2930
(e: "update:name", name: string): void;
@@ -50,6 +51,7 @@ const rules = {
5051
return validateOnOpeningTag(nameWithExtension);
5152
},
5253
isRequired: (value: string) => isRequired(t("common.validation.required"))(value),
54+
invalidCharacters: (value: string) => validateInvalidCharacters(value, ["/"]),
5355
};
5456
5557
const addFileExtension = (name: string) => {
@@ -66,7 +68,10 @@ const updateName = (value: string) => {
6668
watch(nameRef, (newValue) => {
6769
const nameWithExtension = addFileExtension(newValue);
6870
69-
const isNameValid = rules.validateOnOpeningTag(nameWithExtension) === true && rules.isRequired(newValue) === true;
71+
const isNameValid =
72+
rules.validateOnOpeningTag(nameWithExtension) === true &&
73+
rules.isRequired(newValue) === true &&
74+
rules.invalidCharacters(newValue) === true;
7075
7176
if (isNameValid) {
7277
updateName(nameWithExtension);

src/modules/feature/board/shared/AddCollaboraFileDialog.unit.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ describe("CollaboraFileDialog", () => {
7676
expect(wrapper.findComponent(VForm).isVisible()).toBe(true);
7777
});
7878

79+
it("should have confirm button disabled initially", async () => {
80+
const { wrapper } = await setup();
81+
82+
const dialog = wrapper.findComponent(Dialog);
83+
expect(dialog.props("confirmBtnDisabled")).toBe(true);
84+
});
85+
7986
it("should render options", async () => {
8087
const { collaboraFileSelectionOptions, wrapper } = await setup();
8188

@@ -106,6 +113,24 @@ describe("CollaboraFileDialog", () => {
106113
expect(selectOption.action).toHaveBeenCalled();
107114
expect(selectOption.action).toHaveBeenCalledWith(FILENAME, "");
108115
});
116+
117+
it("should have confirm button enabled", async () => {
118+
const { collaboraFileSelectionOptions, wrapper } = await setup();
119+
120+
const selectOption = collaboraFileSelectionOptions[0];
121+
const FILENAME = "myDocument";
122+
123+
const typeSelect = wrapper.findComponent(VSelect);
124+
typeSelect.vm.$emit("update:modelValue", selectOption.id);
125+
await nextTick();
126+
127+
const fileNameInput = wrapper.findComponent("[data-testid='collabora-element-form-filename']");
128+
await fileNameInput.find("input").setValue(FILENAME);
129+
await nextTick();
130+
131+
const dialog = wrapper.findComponent(Dialog);
132+
expect(dialog.props("confirmBtnDisabled")).toBe(false);
133+
});
109134
});
110135

111136
describe("when filetype is not selected", () => {
@@ -123,6 +148,17 @@ describe("CollaboraFileDialog", () => {
123148
await flushPromises();
124149
expect(selectOption.action).toHaveBeenCalledTimes(0);
125150
});
151+
152+
it("should have confirm button disabled", async () => {
153+
const { wrapper } = await setup();
154+
155+
const fileNameInput = wrapper.findComponent("[data-testid='collabora-element-form-filename']");
156+
await fileNameInput.find("input").setValue("myDocument");
157+
await nextTick();
158+
159+
const dialog = wrapper.findComponent(Dialog);
160+
expect(dialog.props("confirmBtnDisabled")).toBe(true);
161+
});
126162
});
127163

128164
describe("when filename is empty", () => {
@@ -145,6 +181,61 @@ describe("CollaboraFileDialog", () => {
145181
await flushPromises();
146182
expect(selectOption.action).toHaveBeenCalledTimes(0);
147183
});
184+
185+
it("should have confirm button disabled", async () => {
186+
const { collaboraFileSelectionOptions, wrapper } = await setup();
187+
188+
const selectOption = collaboraFileSelectionOptions[0];
189+
190+
const typeSelect = wrapper.findComponent(VSelect);
191+
typeSelect.vm.$emit("update:modelValue", selectOption.id);
192+
await nextTick();
193+
194+
const dialog = wrapper.findComponent(Dialog);
195+
expect(dialog.props("confirmBtnDisabled")).toBe(true);
196+
});
197+
});
198+
199+
describe("when filename contains invalid characters", () => {
200+
it("should have confirm button disabled", async () => {
201+
const { collaboraFileSelectionOptions, wrapper } = await setup();
202+
203+
const selectOption = collaboraFileSelectionOptions[0];
204+
205+
const typeSelect = wrapper.findComponent(VSelect);
206+
typeSelect.vm.$emit("update:modelValue", selectOption.id);
207+
await nextTick();
208+
209+
const fileNameInput = wrapper.findComponent("[data-testid='collabora-element-form-filename']");
210+
await fileNameInput.find("input").setValue("invalid/filename");
211+
await nextTick();
212+
213+
const dialog = wrapper.findComponent(Dialog);
214+
expect(dialog.props("confirmBtnDisabled")).toBe(true);
215+
});
216+
});
217+
218+
describe("when caption contains opening tag", () => {
219+
it("should have confirm button disabled", async () => {
220+
const { collaboraFileSelectionOptions, wrapper } = await setup();
221+
222+
const selectOption = collaboraFileSelectionOptions[0];
223+
224+
const typeSelect = wrapper.findComponent(VSelect);
225+
typeSelect.vm.$emit("update:modelValue", selectOption.id);
226+
await nextTick();
227+
228+
const fileNameInput = wrapper.findComponent("[data-testid='collabora-element-form-filename']");
229+
await fileNameInput.find("input").setValue("validFilename");
230+
await nextTick();
231+
232+
const captionInput = wrapper.findComponent("[data-testid='collabora-element-form-caption']");
233+
await captionInput.find("textarea").setValue("<script>alert('xss')</script>");
234+
await nextTick();
235+
236+
const dialog = wrapper.findComponent(Dialog);
237+
expect(dialog.props("confirmBtnDisabled")).toBe(true);
238+
});
148239
});
149240

150241
it("should close modal on close button click", async () => {

src/modules/feature/board/shared/AddCollaboraFileDialog.vue

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<Dialog
33
v-model:is-dialog-open="isCollaboraFileDialogOpen"
44
:message="t('components.elementTypeSelection.elements.collabora.subtitle')"
5+
:confirm-btn-disabled="!isFormValid"
56
confirm-btn-lang-key="common.actions.create"
67
data-testid="collabora-element-dialog"
78
@cancel="onCancel"
@@ -43,8 +44,8 @@
4344
<script setup lang="ts">
4445
import { useAddCollaboraFile } from "./add-collabora-file.composable";
4546
import { Dialog } from "@ui-dialog";
46-
import { isRequired, useOpeningTagValidator } from "@util-validators";
47-
import { ref } from "vue";
47+
import { isRequired, useInvalidCharactersValidator, useOpeningTagValidator } from "@util-validators";
48+
import { computed, ref } from "vue";
4849
import { useI18n } from "vue-i18n";
4950
5051
type VuetifyForm = {
@@ -53,6 +54,7 @@ type VuetifyForm = {
5354
5455
const { isCollaboraFileDialogOpen, closeCollaboraFileDialog, collaboraFileSelectionOptions } = useAddCollaboraFile();
5556
const { validateOnOpeningTag } = useOpeningTagValidator();
57+
const { validateInvalidCharacters } = useInvalidCharactersValidator();
5658
5759
const { t } = useI18n();
5860
@@ -63,9 +65,28 @@ const fileName = ref<string>("");
6365
const caption = ref<string>("");
6466
6567
const docTypeRules = [isRequired(t("common.validation.required2"))];
66-
const fileNameRules = [(value: string) => validateOnOpeningTag(value), isRequired(t("common.validation.required2"))];
68+
const fileNameRules = [
69+
(value: string) => validateOnOpeningTag(value),
70+
isRequired(t("common.validation.required2")),
71+
(value: string) => validateInvalidCharacters(value, ["/"]),
72+
];
6773
const captionRules = [(value: string) => validateOnOpeningTag(value)];
6874
75+
const passesAllRules = (value: string, rules: ((value: string) => true | string)[]): boolean =>
76+
rules.every((rule) => rule(value) === true);
77+
78+
const isFormValid = computed(() => {
79+
if (!selectedDocType.value) {
80+
return false;
81+
}
82+
83+
const isFileNameValid = passesAllRules(fileName.value, fileNameRules);
84+
const isCaptionValid = passesAllRules(caption.value, captionRules);
85+
const isFormValid = isFileNameValid && isCaptionValid;
86+
87+
return isFormValid;
88+
});
89+
6990
const resetForm = () => {
7091
selectedDocType.value = null;
7192
fileName.value = "";

src/modules/feature/folder/file-table/RenameFileDialog.unit.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,44 @@ describe("RenameFileDialog", () => {
156156
expect(textField.text()).toContain("common.validation.containsOpeningTag");
157157
});
158158
});
159+
160+
describe("when a value contains a /", () => {
161+
const setup = () => {
162+
const wrapper = mount(RenameFileDialog, {
163+
props: {
164+
isDialogOpen: true,
165+
fileRecords: [],
166+
},
167+
global: {
168+
plugins: [createTestingVuetify(), createTestingI18n()],
169+
stubs: { UseFocusTrap: true },
170+
renderStubDefaultSlot: true, // to access content inside focus trap
171+
},
172+
});
173+
return { wrapper };
174+
};
175+
176+
it("should display invalid characters error message", async () => {
177+
const { wrapper } = setup();
178+
179+
const textField = wrapper.findComponent(VTextField);
180+
const input = textField.find("input[type='text']");
181+
await input.setValue("invalid/name");
182+
await input.trigger("input");
183+
184+
expect(textField.text()).toContain("pages.folder.rename-file-dialog.validation.invalid-characters");
185+
});
186+
187+
it("should disable confirm button", async () => {
188+
const { wrapper } = setup();
189+
190+
const textField = wrapper.findComponent(VTextField);
191+
const input = textField.find("input[type='text']");
192+
await input.setValue("invalid/name");
193+
await input.trigger("input");
194+
195+
const dialog = wrapper.findComponent(Dialog);
196+
expect(dialog.props("confirmBtnDisabled")).toBe(true);
197+
});
198+
});
159199
});

src/modules/feature/folder/file-table/RenameFileDialog.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
flat
1515
:aria-label="$t('common.labels.name.new')"
1616
:label="t('common.labels.name.new')"
17-
:rules="[rules.required, rules.validateOnOpeningTag, rules.checkDuplicatedNames]"
17+
:rules="[rules.required, rules.validateOnOpeningTag, rules.checkDuplicatedNames, rules.checkInvalidCharacters]"
1818
/>
1919
</template>
2020
</Dialog>
@@ -24,7 +24,7 @@
2424
import { FileRecord } from "@/types/file/File";
2525
import { getFileExtension, removeFileExtension } from "@/utils/fileHelper";
2626
import { Dialog } from "@ui-dialog";
27-
import { useOpeningTagValidator } from "@util-validators";
27+
import { useInvalidCharactersValidator, useOpeningTagValidator } from "@util-validators";
2828
import { computed, PropType, reactive, ref, watch } from "vue";
2929
import { useI18n } from "vue-i18n";
3030
@@ -59,6 +59,7 @@ watch(
5959
const { t } = useI18n();
6060
6161
const { validateOnOpeningTag } = useOpeningTagValidator();
62+
const { validateInvalidCharacters } = useInvalidCharactersValidator();
6263
6364
const rules = reactive({
6465
required: (value: string) => !!value || t("common.validation.required"),
@@ -77,13 +78,15 @@ const rules = reactive({
7778
t("pages.folder.rename-file-dialog.validation.duplicate-file-name")
7879
);
7980
},
81+
checkInvalidCharacters: (value: string) => validateInvalidCharacters(value, ["/"]),
8082
});
8183
8284
const isNameValid = computed(
8385
() =>
8486
rules.required(nameRef.value) === true &&
8587
rules.validateOnOpeningTag(nameRef.value) === true &&
86-
rules.checkDuplicatedNames(nameRef.value) === true
88+
rules.checkDuplicatedNames(nameRef.value) === true &&
89+
rules.checkInvalidCharacters(nameRef.value) === true
8790
);
8891
8992
const onCancel = () => {

0 commit comments

Comments
 (0)