Thibaut Valentin aba8d8a202
All checks were successful
Deploy Production Server / if_merged (pull_request) Successful in 8m10s
Re-update to 3.30.5
2025-12-31 13:19:31 +01:00

417 lines
17 KiB
Java

package fr.titionfire.ffsaf.utils;
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.domain.service.VirusScannerService;
import fr.titionfire.ffsaf.rest.exception.DBadRequestException;
import fr.titionfire.ffsaf.rest.exception.DInternalError;
import fr.titionfire.ffsaf.rest.exception.DetailException;
import io.quarkiverse.antivirus.runtime.AntivirusScanResult;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jodd.net.MimeTypes;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public class Utils {
private static final Logger LOGGER = Logger.getLogger(Utils.class);
public static String HTML_HEADER = """
<!DOCTYPE html>
<html data-lt-installed="true">
<head>
<meta charset="UTF-8">
<title>%s</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #003366;
color: white;
padding: 20px;
text-align: center;
border-radius: 5px 5px 0 0;
}
.content {
padding: 20px;
background-color: #f9f9f9;
border-radius: 0 0 5px 5px;
border: 1px solid #ddd;
border-top: none;
}
.button {
display: inline-block;
padding: 10px 20px;
background-color: #003366;
color: white !important;
text-decoration: none;
border-radius: 5px;
margin: 15px 0;
}
.footer {
margin-top: 20px;
font-size: 0.9em;
color: #666;
text-align: center;
}
.highlight {
font-weight: bold;
color: #003366;
}
</style>
</head>
<body data-gramm="false" data-lt-tmp-id="lt-957854">
<div class="header">
<div><img src="https://intra.ffsaf.fr/Logo-FFSAF-2023.png" alt="ffsaf" height="128">
<h1>F&eacute;d&eacute;ration France Soft Armored Fighting</h1>
</div>
</div>
<div class="content">
<p>Bonjour,</p>
""";
public static String HTML_FOOTER = """
<p>Cordialement,<br>L&rsquo;&eacute;quipe de la FFSAF</p>
</div>
</body>
</html>
""";
public static int getSaison() {
return getSaison(new Date());
}
public static int getSaison(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
if (calendar.get(Calendar.MONTH) >= Calendar.SEPTEMBER) {
return calendar.get(Calendar.YEAR);
} else {
return calendar.get(Calendar.YEAR) - 1;
}
}
public static Calendar toCalendar(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal;
}
public static Categorie getCategoryFormBirthDate(Date birth_date, Date currentDate) {
int currentSaison = getSaison(currentDate);
int birthYear = toCalendar(birth_date).get(Calendar.YEAR);
int diff = currentSaison - birthYear;
if (diff < 6) {
return Categorie.SUPER_MINI;
} else if (diff < 8) {
return Categorie.MINI_POUSSIN;
} else if (diff < 10) {
return Categorie.POUSSIN;
} else if (diff < 12) {
return Categorie.BENJAMIN;
} else if (diff < 14) {
return Categorie.MINIME;
} else if (diff < 16) {
return Categorie.CADET;
} else if (diff < 18) {
return Categorie.JUNIOR;
} else if (diff < 25) {
return Categorie.SENIOR1;
} else if (diff < 35) {
return Categorie.SENIOR2;
} else if (diff < 45) {
return Categorie.VETERAN1;
} else {
return Categorie.VETERAN2;
}
}
public static Uni<String> moveMedia(long idSrc, long idDest, String media, String dirSrc, String dirDst) {
return Uni.createFrom().nullItem().map(__ -> {
File dirFile = new File(media, dirSrc);
if (!dirFile.exists())
return "Not found";
File dirDestFile = new File(media, dirDst);
if (!dirDestFile.exists())
if (!dirDestFile.mkdirs())
return "Fail to create directory " + dirDestFile;
FilenameFilter filter = (directory, filename) -> filename.startsWith(idSrc + ".");
File[] files = dirFile.listFiles(filter);
if (files == null || files.length == 0)
return "Not found";
FilenameFilter filter2 = (directory, filename) -> filename.startsWith(idDest + ".");
File[] files2 = dirDestFile.listFiles(filter2);
if (files2 != null) {
for (File file : files2) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.renameTo(new File(dirDestFile,
file.getName().replaceFirst(String.valueOf(idSrc), String.valueOf(idDest))));
}
return "Ok";
});
}
public static Uni<String> uploadFile(VirusScannerService ss, FileUpload file, long id, String media, String dir) {
if (file == null || file.size() == 0)
return Uni.createFrom().item("Ok");
LOGGER.infof("Received file upload request for: %s (size: %d bytes)", file.fileName(), file.size());
return Uni.createFrom().<InputStream>item(() -> {
try {
// Read the entire file into memory for virus scanning
// ByteArrayInputStream fully supports mark/reset operations required by ClamAV
// This ensures we can scan the file content before any filesystem storage
InputStream fileStream = java.nio.file.Files.newInputStream(file.uploadedFile());
byte[] fileBytes = fileStream.readAllBytes();
fileStream.close(); // Close the file stream immediately
return new ByteArrayInputStream(fileBytes);
} catch (IOException e) {
throw new RuntimeException("Failed to read uploaded file: " + e.getMessage(), e);
}
}).runSubscriptionOn(io.smallrye.mutiny.infrastructure.Infrastructure.getDefaultWorkerPool()).onItem()
.transformToUni(inputStream -> {
// Perform virus scanning reactively using the input stream
return ss.scanFileReactive(file.fileName(), inputStream).onItem().invoke(() -> {
try {
inputStream.close();
} catch (IOException e) {
LOGGER.warn("Warning: Failed to close input stream: " + e.getMessage());
}
});
})
.onItem().transformToUni(scanResults -> Uni.createFrom().item(Unchecked.supplier(() -> {
// Check if any scanner found a threat
for (AntivirusScanResult result : scanResults) {
if (result.getStatus() != Response.Status.OK.getStatusCode()) {
LOGGER.warnf("THREAT DETECTED in %s: %s", file.fileName(), result.getMessage());
throw new DetailException(Response.Status.fromStatusCode(result.getStatus()),
"THREAT_DETECTED on File " + file.fileName() + " is infected: " + result.getMessage());
}
}
// File is clean - now we can safely process it
LOGGER.infof("File is clean: %s (size=%d) (contentType=%s)", file.fileName(), file.size(),
file.contentType());
String[] detectedExtensions = MimeTypes.findExtensionsByMimeTypes(file.contentType(), false);
if (detectedExtensions.length == 0)
throw new DBadRequestException(
"Fail to detect file extension for MIME type " + file.contentType());
File dirFile = new File(media, dir);
if (!dirFile.exists())
if (!dirFile.mkdirs())
throw new DInternalError("Fail to create directory " + dir);
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = dirFile.listFiles(filter);
if (files != null) {
for (File f : files) {
//noinspection ResultOfMethodCallIgnored
f.delete();
}
}
File f = file.filePath().toFile();
//noinspection ResultOfMethodCallIgnored
f.renameTo(new File(dirFile, id + "." + detectedExtensions[0]));
return "ok";
})));
}
public static Uni<Response> getMediaFile(long id, String media, String dirname,
Uni<?> uniBase) throws URISyntaxException {
return getMediaFile(id, media, dirname, null, uniBase, true);
}
public static Uni<Response> getMediaFileNoDefault(long id, String media, String dirname,
Uni<?> uniBase) throws URISyntaxException {
return getMediaFile(id, media, dirname, null, uniBase, false);
}
public static Uni<Response> getMediaFile(long id, String media, String dirname, String out_filename,
Uni<?> uniBase, boolean default_) throws URISyntaxException {
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = new File(media, dirname).listFiles(filter);
if (files != null && files.length > 0) {
File file = files[0];
try {
byte[] data = Files.readAllBytes(file.toPath());
return new Pair<>(file, data);
} catch (IOException ignored) {
}
}
return null;
});
Future<byte[]> future2 = CompletableFuture.supplyAsync(() -> {
try (InputStream st = Utils.class.getClassLoader().getResourceAsStream("asset/blank-profile-picture.png")) {
if (st == null)
return null;
return st.readAllBytes();
} catch (IOException ignored) {
}
return null;
});
return uniBase.chain(__ -> Uni.createFrom().future(future)
.chain(filePair -> {
if (filePair == null) {
if (default_) {
return Uni.createFrom().future(future2).map(data -> {
if (data == null)
return Response.noContent().build();
String mimeType = "image/apng";
Response.ResponseBuilder resp = Response.ok(data);
resp.type(MediaType.APPLICATION_OCTET_STREAM);
resp.header(HttpHeaders.CONTENT_LENGTH, data.length);
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
resp.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; " + ((out_filename == null) ? "" : "filename=\"" + out_filename + "\""));
return resp.build();
});
} else {
Response.ResponseBuilder resp = Response.status(404);
resp.header(HttpHeaders.CACHE_CONTROL, "max-age=600");
return Uni.createFrom().item(resp.build());
}
} else {
return Uni.createFrom().item(() -> {
String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName());
String filename = out_filename;
if (filename != null && (filename.length() < 5 || filename.lastIndexOf(
'.') <= filename.length() - 6)) {
filename += filePair.getKey().getName()
.substring(filePair.getKey().getName().lastIndexOf('.'));
}
Response.ResponseBuilder resp = Response.ok(filePair.getValue());
resp.type(MediaType.APPLICATION_OCTET_STREAM);
resp.header(HttpHeaders.CONTENT_LENGTH, filePair.getValue().length);
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
resp.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; " + ((filename == null) ? "" : "filename=\"" + filename + "\""));
return resp.build();
});
}
}));
}
public static Uni<?> deleteMedia(long id, String media, String dir) {
return Uni.createFrom().nullItem().map(__ -> {
File dirFile = new File(media, dir);
if (!dirFile.exists())
return "OK";
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
File[] files = dirFile.listFiles(filter);
if (files != null) {
for (File file : files) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
return "Ok";
});
}
public static int getDaysBeforeCompetition(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
Calendar now = Calendar.getInstance();
now.set(Calendar.HOUR_OF_DAY, 0);
now.set(Calendar.MINUTE, 0);
now.set(Calendar.SECOND, 0);
return (int) ((calendar.getTimeInMillis() - now.getTimeInMillis()) / (1000 * 60 * 60 * 24));
}
public static String formatPrenom(String input) {
if (input == null || input.isEmpty()) {
return input;
}
StringBuilder result = new StringBuilder();
String[] mots = input.split(" ");
for (String mot : mots) {
if (!mot.isEmpty()) {
String[] parties = mot.split("[-']");
StringBuilder motFormate = new StringBuilder();
for (int i = 0; i < parties.length; i++) {
if (!parties[i].isEmpty()) {
String premiereLettre = parties[i].substring(0, 1).toUpperCase();
String reste = parties[i].substring(1).toLowerCase();
motFormate.append(premiereLettre).append(reste);
}
if (i < parties.length - 1) {
motFormate.append(mot.charAt(mot.indexOf(parties[i]) + parties[i].length()));
}
}
result.append(motFormate).append(" ");
}
}
return result.toString().trim();
}
public static String getFullName(Object... models) {
for (Object model : models) {
if (model == null)
continue;
if (model instanceof MembreModel membreModel)
return membreModel.getFname() + " " + membreModel.getLname();
if (model instanceof CompetitionGuestModel guestModel)
return guestModel.getFname() + " " + guestModel.getLname();
}
return "";
}
}