diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java index 1dd42903118..138af722e93 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetWidgetsPage.java @@ -9,12 +9,16 @@ import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.JsfHelper; +import edu.harvard.iq.dataverse.util.SystemConfig; + import java.io.File; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import jakarta.ejb.EJB; +import jakarta.faces.application.FacesMessage; +import jakarta.faces.context.FacesContext; import jakarta.faces.view.ViewScoped; import jakarta.inject.Inject; import jakarta.inject.Named; @@ -32,6 +36,9 @@ public class DatasetWidgetsPage implements java.io.Serializable { @EJB EjbDataverseEngine commandEngine; + + @EJB + SystemConfig systemConfig; @Inject DataverseRequestServiceBean dvRequestService; @@ -131,6 +138,12 @@ public void flagDatasetThumbnailForRemoval() { public void handleImageFileUpload(FileUploadEvent event) { logger.fine("handleImageFileUpload clicked"); UploadedFile uploadedFile = event.getFile(); + long maxSize = systemConfig.getThumbnailSizeLimitImage(); + if (!FileUtil.isUploadedFileAnImage(uploadedFile, maxSize)) { + FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Only image files are allowed.", "Only image files under " + maxSize + " bytes are allowed."); + FacesContext.getCurrentInstance().addMessage(null, msg); + return; + } try { updateDatasetThumbnailCommand = new UpdateDatasetThumbnailCommand(dvRequestService.getDataverseRequest(), dataset, UpdateDatasetThumbnailCommand.UserIntent.setNonDatasetFileAsThumbnail, null, uploadedFile.getInputStream()); } catch (IOException ex) { diff --git a/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java b/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java index 2474b43183f..5efe68fdc15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/ThemeWidgetFragment.java @@ -9,16 +9,25 @@ import edu.harvard.iq.dataverse.engine.command.impl.UpdateDataverseThemeCommand; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.FileUtil; import edu.harvard.iq.dataverse.util.JsfHelper; +import edu.harvard.iq.dataverse.util.SystemConfig; + +import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; + +import javax.imageio.ImageIO; + import jakarta.annotation.PreDestroy; import jakarta.ejb.EJB; import jakarta.faces.application.FacesMessage; @@ -51,7 +60,8 @@ public class ThemeWidgetFragment implements java.io.Serializable { public static final String LOGOS_SUBDIR = "logos"; public static final String LOGOS_TEMP_SUBDIR = LOGOS_SUBDIR + File.separator + "temp"; - + private long maxSize = 0; + private File tempDir; private File uploadedFile; private File uploadedFileThumbnail; @@ -65,6 +75,7 @@ public class ThemeWidgetFragment implements java.io.Serializable { EjbDataverseEngine commandEngine; @EJB DataverseServiceBean dataverseServiceBean; + @EJB SystemConfig systemConfig; @Inject DataverseRequestServiceBean dvRequestService; @@ -228,6 +239,11 @@ public void handleImageThumbnailFileUpload(FileUploadEvent event) { logger.finer("created tempDir"); } final UploadedFile uFile = event.getFile(); + if(!FileUtil.isUploadedFileAnImage(uFile, getMaxSize())) { + FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Only image files are allowed.", "Only image files under " + getMaxSize() + " bytes are allowed."); + FacesContext.getCurrentInstance().addMessage(null, msg); + return; + } try { this.uploadedFileThumbnail = new File(tempDir, uFile.getFileName()); if (!this.uploadedFileThumbnail.exists()) { @@ -258,6 +274,12 @@ public void handleImageFooterFileUpload(FileUploadEvent event) { logger.finer("created tempDir"); } UploadedFile uFile = event.getFile(); + if (!FileUtil.isUploadedFileAnImage(uFile, getMaxSize())) { + FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Only image files are allowed.", "Only image files under " + getMaxSize() + " bytes are allowed."); + FacesContext.getCurrentInstance().addMessage(null, msg); + return; + } + try { uploadedFileFooter = new File(tempDir, uFile.getFileName()); if (!uploadedFileFooter.exists()) { @@ -283,8 +305,13 @@ public void handleImageFileUpload(FileUploadEvent event) { logger.finer("created tempDir"); } UploadedFile uFile = event.getFile(); - try { - uploadedFile = new File(tempDir, uFile.getFileName()); + if (!FileUtil.isUploadedFileAnImage(uFile, getMaxSize())) { + FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Only image files are allowed.", "Only image files under " + getMaxSize() + " bytes are allowed."); + FacesContext.getCurrentInstance().addMessage(null, msg); + return; + } + try { + uploadedFile = new File(tempDir, uFile.getFileName()); if (!uploadedFile.exists()) { uploadedFile.createNewFile(); } @@ -430,6 +457,13 @@ public boolean exectThemeCommand(Command cmd){ return true; } + // Initialize maxSize from systemConfig + public long getMaxSize() { + if (maxSize == 0) { + maxSize = systemConfig.getUploadLogoSizeLimit(); + } + return maxSize; + } } diff --git a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java index 7502658444a..4116d058cb1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java @@ -34,13 +34,11 @@ import static edu.harvard.iq.dataverse.datasetutility.FileSizeChecker.bytesToHumanReadable; import edu.harvard.iq.dataverse.ingest.IngestReport; import edu.harvard.iq.dataverse.ingest.IngestServiceBean; -import edu.harvard.iq.dataverse.ingest.IngestServiceShapefileHelper; import edu.harvard.iq.dataverse.ingest.IngestableDataChecker; import edu.harvard.iq.dataverse.license.License; import edu.harvard.iq.dataverse.settings.ConfigCheckService; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.file.BagItFileHandler; -import edu.harvard.iq.dataverse.util.file.CreateDataFileResult; import edu.harvard.iq.dataverse.util.file.BagItFileHandlerFactory; import edu.harvard.iq.dataverse.util.xml.XmlUtil; import edu.harvard.iq.dataverse.util.xml.html.HtmlFormatUtil; @@ -54,6 +52,7 @@ import static edu.harvard.iq.dataverse.util.xml.html.HtmlFormatUtil.formatTableCellAlignRight; import static edu.harvard.iq.dataverse.util.xml.html.HtmlFormatUtil.formatTableRow; +import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; @@ -64,7 +63,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -82,7 +80,7 @@ import java.util.HashMap; import java.util.List; import java.util.Optional; -import java.util.ResourceBundle; +import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -92,6 +90,7 @@ import jakarta.json.JsonArray; import jakarta.json.JsonObject; +import javax.imageio.ImageIO; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; @@ -106,9 +105,10 @@ import edu.harvard.iq.dataverse.util.file.FileExceedsStorageQuotaException; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.tika.Tika; +import org.primefaces.model.file.UploadedFile; + import ucar.nc2.NetcdfFile; import ucar.nc2.NetcdfFiles; @@ -1900,5 +1900,63 @@ public static String decodeFileName(String originalFileName) { } return new String(originalFileName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); } + + /** + * Verifies that an uploaded file is a valid png or jpg image file. Performs both MIME type checking and content validation. + * + * Note: This is similar to the isFileOfImageType which is used for collection feature items. This method works with PrimeFaces UploadedFile vs File, limits to jpg and png (as the UI states), uses + * ImageIO to read the content, and checks size (as the caller of isFileOfImageType does). It avoids using Tika in the core (as we once tried to do) and is potentially slower but more thorough as it + * will confirm the image is not corrupt. Work could be done to merge the two use cases. + * + * @param uploadedFile + * the file to verify + * @param maxSize + * maximum allowed file size in bytes + */ + public static boolean isUploadedFileAnImage(UploadedFile uploadedFile, long maxSize) { + if (uploadedFile == null) { + return false; + } + + // Pre-filter: Check MIME type first (fast rejection) + String contentType = uploadedFile.getContentType(); + if (contentType == null || !contentType.startsWith("image/")) { + return false; + } + + // Check against allowed MIME types + Set allowedMimeTypes = Set.of( + "image/jpeg", + "image/jpg", + "image/png"); + + if (!allowedMimeTypes.contains(contentType.toLowerCase())) { + return false; + } + + // Validate actual image content (security check) + try (InputStream inputStream = uploadedFile.getInputStream()) { + BufferedImage image = ImageIO.read(inputStream); + if (image == null) { + return false; + } + + // Optional: Check file size limit (similar to DataverseFeaturedItemServiceBean) + + if (uploadedFile.getSize() > maxSize) { + return false; + } + + // Optional: Check image dimensions if needed + int width = image.getWidth(); + int height = image.getHeight(); + logger.fine("Uploaded image dimensions: " + width + "x" + height); + + } catch (IOException e) { + logger.log(Level.WARNING, "Error reading uploaded image file", e); + return false; + } + return true; + } } diff --git a/src/main/webapp/dataset-widgets.xhtml b/src/main/webapp/dataset-widgets.xhtml index 6ab1751ebd1..0255fbf8f0b 100644 --- a/src/main/webapp/dataset-widgets.xhtml +++ b/src/main/webapp/dataset-widgets.xhtml @@ -73,7 +73,7 @@ -