Skip to content

Commit a123bb9

Browse files
committed
Validate uploaded file to check that it's an image in PNG/JPEG formats.
Fix #128
1 parent 6eca1cf commit a123bb9

File tree

6 files changed

+192
-2
lines changed

6 files changed

+192
-2
lines changed

src/main/java/ru/mystamps/web/model/AddImageForm.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import lombok.Setter;
2727

2828
import ru.mystamps.web.service.dto.AddImageDto;
29+
import ru.mystamps.web.validation.jsr303.ImageFile;
2930
import ru.mystamps.web.validation.jsr303.NotEmptyFile;
3031
import ru.mystamps.web.validation.jsr303.NotEmptyFilename;
3132

@@ -36,11 +37,13 @@ public class AddImageForm implements AddImageDto {
3637
@NotNull
3738
@NotEmptyFilename(groups = Image1Checks.class)
3839
@NotEmptyFile(groups = Image2Checks.class)
40+
@ImageFile(groups = Image3Checks.class)
3941
private MultipartFile image;
4042

4143
@GroupSequence({
4244
Image1Checks.class,
43-
Image2Checks.class
45+
Image2Checks.class,
46+
Image3Checks.class
4447
})
4548
public interface ImageChecks {
4649
}
@@ -51,4 +54,7 @@ public interface Image1Checks {
5154
public interface Image2Checks {
5255
}
5356

57+
public interface Image3Checks {
58+
}
59+
5460
}

src/main/java/ru/mystamps/web/model/AddSeriesForm.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import ru.mystamps.web.entity.Currency;
3535
import ru.mystamps.web.service.dto.AddSeriesDto;
3636
import ru.mystamps.web.validation.jsr303.CatalogNumbers;
37+
import ru.mystamps.web.validation.jsr303.ImageFile;
3738
import ru.mystamps.web.validation.jsr303.NotEmptyFile;
3839
import ru.mystamps.web.validation.jsr303.NotEmptyFilename;
3940
import ru.mystamps.web.validation.jsr303.NotNullIfFirstField;
@@ -141,6 +142,7 @@ public class AddSeriesForm implements AddSeriesDto {
141142
@NotNull
142143
@NotEmptyFilename(groups = Image1Checks.class)
143144
@NotEmptyFile(groups = Image2Checks.class)
145+
@ImageFile(groups = Image3Checks.class)
144146
private MultipartFile image;
145147

146148

@@ -198,7 +200,8 @@ public interface GibbonsCatalog2Checks {
198200

199201
@GroupSequence({
200202
Image1Checks.class,
201-
Image2Checks.class
203+
Image2Checks.class,
204+
Image3Checks.class
202205
})
203206
public interface ImageChecks {
204207
}
@@ -209,4 +212,7 @@ public interface Image1Checks {
209212
public interface Image2Checks {
210213
}
211214

215+
public interface Image3Checks {
216+
}
217+
212218
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2009-2015 Slava Semushin <slava.semushin@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.validation.jsr303;
19+
20+
import java.lang.annotation.Documented;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.Target;
23+
24+
import javax.validation.Constraint;
25+
import javax.validation.Payload;
26+
27+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
28+
import static java.lang.annotation.ElementType.FIELD;
29+
import static java.lang.annotation.ElementType.METHOD;
30+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
31+
32+
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
33+
@Retention(RUNTIME)
34+
@Constraint(validatedBy = ImageFileValidator.class)
35+
@Documented
36+
public @interface ImageFile {
37+
String message() default "{ru.mystamps.web.validation.jsr303.ImageFile.message}";
38+
Class<?>[] groups() default {};
39+
Class<? extends Payload>[] payload() default {};
40+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright (C) 2009-2015 Slava Semushin <slava.semushin@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.validation.jsr303;
19+
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.util.Arrays;
23+
24+
import javax.validation.ConstraintValidator;
25+
import javax.validation.ConstraintValidatorContext;
26+
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
import org.springframework.web.multipart.MultipartFile;
31+
32+
public class ImageFileValidator implements ConstraintValidator<ImageFile, MultipartFile> {
33+
34+
private static final Logger LOG = LoggerFactory.getLogger(ImageFileValidator.class);
35+
36+
// see https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
37+
private static final byte[] JPEG_SIGNATURE = new byte[] {
38+
(byte)0xFF, (byte)0xD8, (byte)0xFF, (byte)0xE0
39+
};
40+
41+
// see https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
42+
private static final byte[] PNG_FIRST_PART_SIGNATURE = new byte[] {
43+
(byte)0x89, 0x50, 0x4E, 0x47
44+
};
45+
46+
private static final byte[] PNG_SECOND_PART_SIGNATURE = new byte[] {
47+
0x0D, 0x0A, 0x1A, 0x0A
48+
};
49+
50+
private static boolean isJpeg(byte[] bytes) {
51+
// TODO: also check that last 2 bytes are FF D9 (use RandomAccessFile)
52+
return Arrays.equals(bytes, JPEG_SIGNATURE);
53+
}
54+
55+
private static boolean doesItLookLikePng(byte[] bytes) {
56+
return Arrays.equals(bytes, PNG_FIRST_PART_SIGNATURE);
57+
}
58+
59+
private static boolean isItReallyPng(byte[] bytes) {
60+
return Arrays.equals(bytes, PNG_SECOND_PART_SIGNATURE);
61+
}
62+
63+
private static byte[] readFourBytes(InputStream is) {
64+
// CheckStyle: ignore MagicNumber for next 1 line
65+
byte[] bytes = new byte[4];
66+
try {
67+
int read = is.read(bytes, 0, bytes.length);
68+
if (read != bytes.length) {
69+
return null;
70+
}
71+
72+
return bytes;
73+
74+
} catch (IOException e) {
75+
LOG.warn("Error during reading from file: {}", e.getMessage());
76+
return null;
77+
}
78+
}
79+
80+
private static String formatBytes(byte[] bytes) {
81+
// CheckStyle: ignore MagicNumber for next 1 line
82+
return String.format("%02x %02x %02x %02x", bytes[0], bytes[1], bytes[2], bytes[3]);
83+
}
84+
85+
@Override
86+
public void initialize(ImageFile annotation) {
87+
// Intentionally empty: nothing to initialize
88+
}
89+
90+
@Override
91+
public boolean isValid(MultipartFile file, ConstraintValidatorContext ctx) {
92+
93+
if (file == null) {
94+
return true;
95+
}
96+
97+
if (file.isEmpty()) {
98+
return false;
99+
}
100+
101+
try (InputStream stream = file.getInputStream()) {
102+
103+
byte[] firstPart = readFourBytes(stream);
104+
if (firstPart != null) {
105+
LOG.warn("Failed to read 4 bytes from file");
106+
return false;
107+
}
108+
109+
if (isJpeg(firstPart)) {
110+
return true;
111+
}
112+
113+
if (doesItLookLikePng(firstPart)) {
114+
byte[] secondPart = readFourBytes(stream);
115+
if (isItReallyPng(secondPart)) {
116+
return true;
117+
}
118+
119+
LOG.debug(
120+
"Looks like file isn't a PNG image. First bytes: {} {}",
121+
formatBytes(firstPart),
122+
formatBytes(secondPart)
123+
);
124+
return false;
125+
}
126+
127+
LOG.debug("Looks like file isn't an image. First bytes: {}", formatBytes(firstPart));
128+
return false;
129+
130+
} catch (IOException e) {
131+
LOG.warn("Error during file type validation: {}", e.getMessage());
132+
return false;
133+
}
134+
}
135+
136+
}

src/main/resources/ru/mystamps/i18n/ValidationMessages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ru.mystamps.web.validation.jsr303.UniqueMichelNumbers.message = Series with one
2020
ru.mystamps.web.validation.jsr303.UniqueScottNumbers.message = Series with one of provided scott code already exists
2121
ru.mystamps.web.validation.jsr303.UniqueYvertNumbers.message = Series with one of provided yvert code already exists
2222
ru.mystamps.web.validation.jsr303.UniqueGibbonsNumbers.message = Series with one of provided gibbons code already exists
23+
ru.mystamps.web.validation.jsr303.ImageFile.message = Cannot detect file type. Must be image in JPEG or PNG format
2324

2425
value.too-short = Value is less than allowable minimum of {min} characters
2526
value.too-long = Value is greater than allowable maximum of {max} characters

src/main/resources/ru/mystamps/i18n/ValidationMessages_ru.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ru.mystamps.web.validation.jsr303.UniqueMichelNumbers.message = В базе уж
2020
ru.mystamps.web.validation.jsr303.UniqueScottNumbers.message = В базе уже есть серия с одним из указанных номеров по каталогу Scott
2121
ru.mystamps.web.validation.jsr303.UniqueYvertNumbers.message = В базе уже есть серия с одним из указанных номеров по каталогу Yvert
2222
ru.mystamps.web.validation.jsr303.UniqueGibbonsNumbers.message = В базе уже есть серия с одним из указанных номеров по каталогу Gibbons
23+
ru.mystamps.web.validation.jsr303.ImageFile.message = Не удалось определить тип файла. Должен быть изображением в формате JPEG или PNG
2324

2425
value.too-short = Значение должно быть не менее {min} символов
2526
value.too-long = Значение должно быть не более {max} символов

0 commit comments

Comments
 (0)