Skip to content

Commit 3d451de

Browse files
fix(file): added file extension validation
ref: MANAGER-15044 Signed-off-by: Jacques Larique <jacques.larique.ext@ovhcloud.com>
1 parent f6a0eb8 commit 3d451de

File tree

3 files changed

+114
-14
lines changed

3 files changed

+114
-14
lines changed

packages/components/field/src/js/field.provider.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default class {
1414
minlength: 'Too short ({{minlength}} characters min).',
1515
maxlength: 'Too high ({{maxlength}} characters max).',
1616
maxsize: 'This file exceeds the size limit',
17+
type: 'This file extension is not supported',
1718
pattern: 'Invalid format.',
1819
},
1920
};

packages/components/file/src/js/file.controller.js

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,29 @@ export default class {
3232
file.errors.maxsize = true;
3333
}
3434

35+
// Check extension / type
36+
if (this.accept) {
37+
const [fileType, fileExtension] = file.type.split('/');
38+
const acceptedTypes = this.accept.split(',');
39+
file.errors.type = !acceptedTypes.some((acceptedType) => {
40+
const [type, extension] = acceptedType.split('/');
41+
if (extension) {
42+
const isTypeValid = type === '*' || type.toLowerCase() === fileType.toLowerCase();
43+
const isExtensionValid = extension === '*' || extension.toLowerCase() === fileExtension.toLowerCase();
44+
return isTypeValid && isExtensionValid;
45+
}
46+
return type === '*' || type.replace('.', '').toLowerCase() === fileExtension.toLowerCase();
47+
});
48+
}
49+
3550
// Set form validation
3651
if (this.form && this.form[this.name]) {
37-
this.form[this.name].$setValidity('maxsize', !file.errors.maxsize);
52+
if (file.errors.maxsize) {
53+
this.form[this.name].$setValidity('maxsize', false);
54+
}
55+
if (file.errors.type) {
56+
this.form[this.name].$setValidity('type', false);
57+
}
3858
this.form[this.name].$setDirty();
3959
}
4060

@@ -68,6 +88,7 @@ export default class {
6888
}
6989

7090
addFile(file) {
91+
if (!file) return;
7192
this.getFileInfos(file);
7293
this.checkFileValidity(file);
7394

@@ -103,9 +124,15 @@ export default class {
103124
removeFile(file) {
104125
if (angular.isArray(this.model)) {
105126
remove(this.model, (item) => item === file);
106-
if (file.errors && this.model.every((item) => !item.errors)
107-
&& this.form && this.form[this.name]) {
108-
this.form[this.name].$setValidity('maxsize', true);
127+
if (file.errors && this.form && this.form[this.name]) {
128+
let hasMaxsizeErrors = false;
129+
let hasTypeErrors = false;
130+
this.model.forEach((item) => {
131+
hasMaxsizeErrors = hasMaxsizeErrors || item.errors?.maxsize;
132+
hasTypeErrors = hasTypeErrors || item.errors?.type;
133+
});
134+
this.form[this.name].$setValidity('maxsize', !hasMaxsizeErrors);
135+
this.form[this.name].$setValidity('type', !hasTypeErrors);
109136
}
110137
this.onRemove({ modelValue: this.model });
111138
}
@@ -118,6 +145,7 @@ export default class {
118145

119146
if (this.form && this.form[this.name]) {
120147
this.form[this.name].$setValidity('maxsize', true);
148+
this.form[this.name].$setValidity('type', true);
121149
}
122150
}
123151

@@ -176,6 +204,21 @@ export default class {
176204
}
177205
}
178206

207+
parseAcceptAttribute() {
208+
const acceptedTypes = this.accept?.split(',') || [];
209+
let accept = '';
210+
acceptedTypes.forEach((acceptedType) => {
211+
const isExtension = acceptedType.indexOf('/') === -1;
212+
const isValid = isExtension
213+
? acceptedType.startsWith('.')
214+
: /^([\x20-\x7F]+|\*)\/([\x20-\x7F]+|\*)$/.test(acceptedType);// match "string/string","*/string" and "string/*"
215+
if (isValid) {
216+
accept = accept ? `${accept},${acceptedType}` : acceptedType;
217+
}
218+
});
219+
return accept;
220+
}
221+
179222
$onInit() {
180223
addBooleanParameter(this, 'disabled');
181224
addBooleanParameter(this, 'required');
@@ -190,6 +233,8 @@ export default class {
190233
this.selectorId = `${this.id}Selector`;
191234
this.dropareaId = `${this.id}Droparea`;
192235
this.attachments = Boolean(this.multiple || this.droparea || this.preview);
236+
237+
this.accept = this.parseAcceptAttribute();
193238
}
194239

195240
$postLink() {

packages/components/file/src/js/file.spec.js

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ describe('ouiFile', () => {
1717
type: 'image/png',
1818
};
1919

20+
const invalidMockFile = {
21+
name: 'test_invalid.png',
22+
size: 500000,
23+
type: 'image/jpeg',
24+
};
25+
2026
const mockFiles = [mockFile];
2127

2228
beforeEach(angular.mock.module('oui.file'));
@@ -263,14 +269,6 @@ describe('ouiFile', () => {
263269
expect(controller.model.length).toBe(0);
264270
expect(onRemoveSpy).toHaveBeenCalledWith(controller.model);
265271
});
266-
267-
it('should clear errors after file removal', () => {
268-
controller.maxsize = 100000;
269-
controller.addFiles(mockFiles);
270-
expect(controller.form[controller.name].$invalid).toBe(true);
271-
controller.removeFile(mockFile);
272-
expect(controller.form[controller.name].$invalid).toBe(false);
273-
});
274272
});
275273

276274
describe('Form controls', () => {
@@ -302,6 +300,22 @@ describe('ouiFile', () => {
302300
expect(element.find('oui-file').attr('name')).toBeUndefined();
303301
});
304302

303+
// Due to an unexplained issue, file component's controller is correctly updated but the
304+
// variable controller used here is not, to bypass this we're triggering the $onInit by hand
305+
it('should ignore invalid accept attribute', () => {
306+
controller.accept = 'test';
307+
controller.$onInit();
308+
expect(controller.accept).toBe('');
309+
310+
controller.accept = '.png';
311+
controller.$onInit();
312+
expect(controller.accept).toBe('.png');
313+
314+
controller.accept = '*/png';
315+
controller.$onInit();
316+
expect(controller.accept).toBe('*/png');
317+
});
318+
305319
it('should set input form $touched', () => {
306320
const label = angular.element(element[0].querySelector('.oui-file-selector__label'));
307321

@@ -315,16 +329,56 @@ describe('ouiFile', () => {
315329

316330
controller.maxsize = 200000;
317331
controller.checkFileValidity(mockFile);
318-
expect(controller.form[name].$error.maxsize).toBeUndefined();
332+
expect(controller.form[name].$error.maxsize).toBeFalsy();
319333

320334
expect(controller.form[name].$dirty).toBeTruthy();
321335

322336
controller.maxsize = 100000;
323337
controller.checkFileValidity(mockFile);
324338
expect(controller.form[name].$error.maxsize).toBeTruthy();
325339

340+
// Valid extension tests
341+
controller.accept = 'image/png';
342+
controller.checkFileValidity(mockFile);
343+
expect(controller.form[name].$error.type).toBeFalsy();
344+
345+
controller.accept = 'image/*';
346+
controller.checkFileValidity(mockFile);
347+
expect(controller.form[name].$error.type).toBeFalsy();
348+
349+
controller.accept = '*/png';
350+
controller.checkFileValidity(mockFile);
351+
expect(controller.form[name].$error.type).toBeFalsy();
352+
353+
controller.accept = '.png';
354+
controller.checkFileValidity(mockFile);
355+
expect(controller.form[name].$error.type).toBeFalsy();
356+
357+
// Invalid extension tests
358+
controller.accept = 'image/jpeg';
359+
controller.checkFileValidity(mockFile);
360+
expect(controller.form[name].$error.type).toBeTruthy();
361+
326362
controller.resetFile();
327-
expect(controller.form[name].$error.maxsize).toBeUndefined();
363+
expect(controller.form[name].$error.maxsize).toBeFalsy();
364+
expect(controller.form[name].$error.type).toBeFalsy();
365+
});
366+
367+
it('should not set form as valid after uploading an invalid and then a valid file', () => {
368+
controller.maxsize = 200000;
369+
controller.addFile(invalidMockFile);
370+
expect(controller.form[controller.name].$invalid).toBe(true);
371+
controller.addFile(mockFile);
372+
expect(controller.form[controller.name].$invalid).toBe(true);
373+
});
374+
375+
it('should clear errors after file removal', () => {
376+
controller.maxsize = 200000;
377+
controller.accept = 'image/png';
378+
controller.addFiles([mockFile, invalidMockFile]);
379+
expect(controller.form[controller.name].$invalid).toBe(true);
380+
controller.removeFile(invalidMockFile);
381+
expect(controller.form[controller.name].$invalid).toBe(false);
328382
});
329383
});
330384
});

0 commit comments

Comments
 (0)