From e6d5b3a0494ee04cfa9731e2341c7e8fdbe9ffba Mon Sep 17 00:00:00 2001 From: Francois Prunayre Date: Tue, 9 Jun 2026 10:28:49 +0200 Subject: [PATCH 1/7] Logo improvements * Add an API to retrieve group(existing)/sources/site/harvester logo (instead of building path to images) * Source table contains the reference to the logo for the main catalogue, portal and harvester * Group table contains the reference to the group logo * Refactor to have a single utility to return the logo * Allow using SVG for logo Follow up of https://github.com/geonetwork/core-geonetwork/pull/1565 --- .../fao/geonet/resources/FileResources.java | 2 +- .../fao/geonet/resources/ResourceFilter.java | 16 ++- .../java/org/fao/geonet/api/LogoUtils.java | 102 ++++++++++++++++ .../org/fao/geonet/api/groups/GroupsApi.java | 71 +---------- .../geonet/api/harvesting/HarvestersApi.java | 30 +++++ .../org/fao/geonet/api/site/LogosApi.java | 4 +- .../java/org/fao/geonet/api/site/SiteApi.java | 115 ++++++++++++------ .../fao/geonet/api/sources/SourcesApi.java | 58 +++++++++ .../fao/geonet/util/FileMimetypeChecker.java | 2 +- .../api/harvesting/HarvestersApiTest.java | 83 +++++++++++++ .../geonet/api/sources/SourcesApiTest.java | 18 +++ .../admin/harvester/HarvesterDirective.js | 16 +-- .../toolbar/partials/menu-sitename.html | 2 +- .../toolbar/partials/top-toolbar.html | 2 +- .../usersearches/partials/portalswitcher.html | 2 +- .../utility/partials/recordOriginLogo.html | 4 +- .../admin/harvest/harvest-settings.html | 7 +- .../templates/admin/settings/logo.html | 4 +- .../templates/admin/settings/sources.html | 2 +- .../templates/admin/usergroup/groups.html | 7 +- .../catalog/templates/top-toolbar.html | 2 +- .../views/default/templates/index.html | 2 +- .../main/java/org/fao/geonet/Geonetwork.java | 24 +++- 23 files changed, 432 insertions(+), 143 deletions(-) create mode 100644 services/src/main/java/org/fao/geonet/api/LogoUtils.java create mode 100644 services/src/test/java/org/fao/geonet/api/harvesting/HarvestersApiTest.java diff --git a/core/src/main/java/org/fao/geonet/resources/FileResources.java b/core/src/main/java/org/fao/geonet/resources/FileResources.java index 5119f936d50d..3e647c84d5bf 100644 --- a/core/src/main/java/org/fao/geonet/resources/FileResources.java +++ b/core/src/main/java/org/fao/geonet/resources/FileResources.java @@ -36,7 +36,7 @@ public class FileResources extends Resources { try (DirectoryStream possibleLogos = Files.newDirectoryStream(logosDir, imageName + ".*")) { for (final Path next: possibleLogos) { String ext = FilenameUtils.getExtension(next.getFileName().toString()); - if (IMAGE_EXTENSIONS.contains(ext.toLowerCase())) { + if (IMAGE_EXTENSIONS.contains(ext.toLowerCase()) || "svg".equalsIgnoreCase(ext)) { return next; } } diff --git a/core/src/main/java/org/fao/geonet/resources/ResourceFilter.java b/core/src/main/java/org/fao/geonet/resources/ResourceFilter.java index d89883aa0e20..a311ad73ecf3 100644 --- a/core/src/main/java/org/fao/geonet/resources/ResourceFilter.java +++ b/core/src/main/java/org/fao/geonet/resources/ResourceFilter.java @@ -28,8 +28,11 @@ import jeeves.config.springutil.JeevesDelegatingFilterProxy; import org.fao.geonet.NodeInfo; import org.fao.geonet.domain.Pair; +import org.fao.geonet.domain.Source; +import org.fao.geonet.domain.SourceType; import org.fao.geonet.kernel.GeonetworkDataDirectory; import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.repository.SourceRepository; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.http.MediaType; import org.springframework.web.context.request.ServletWebRequest; @@ -40,6 +43,8 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.attribute.FileTime; +import java.util.List; +import java.util.Optional; import java.util.concurrent.ConcurrentMap; /** @@ -52,6 +57,7 @@ * User: jeichar Date: 1/17/12 Time: 4:03 PM */ public class ResourceFilter implements Filter { + public static final String DEFAULT_LOGO = "GN3.png"; private static final int FIVE_DAYS = 60 * 60 * 24 * 5; private static final int SIX_HOURS = 60 * 60 * 6; private FilterConfig config; @@ -94,10 +100,14 @@ public Instance(ServletRequest request, ServletResponse response) throws IOExcep this.siteId = applicationContext.getBean(SettingManager.class).getSiteId(); this.resourcesDir = resources.locateResourcesDir(servletContext, applicationContext); this.schemaPublicationDir = applicationContext.getBean(GeonetworkDataDirectory.class).getSchemaPublicationDir(); + SourceRepository sourceRepository = applicationContext.getBean(SourceRepository.class); + this.nodeId = applicationContext.getBean(NodeInfo.class).getId(); if (defaultImage == null) { - defaultImage = resources.loadResource(resourcesDir, servletContext, appPath, "images/logos/" + siteId + ".png", new byte[0], -1); + Optional catalogues = sourceRepository.findById(this.nodeId); + String defaultImageName = "images/logos/" + ( + catalogues.isPresent() ? catalogues.get().getLogo() : DEFAULT_LOGO); + defaultImage = resources.loadResource(resourcesDir, servletContext, appPath, defaultImageName, new byte[0], -1); } - this.nodeId = applicationContext.getBean(NodeInfo.class).getId(); if (!faviconMap.containsKey(nodeId)) { final byte[] defaultImageBytes = defaultImage.one(); addFavIcon(nodeId, resources.loadResource(resourcesDir, servletContext, appPath, "images/logos/" + siteId + ".ico", @@ -186,6 +196,8 @@ private String extensionToMediaType(String ext) { contentType = MediaType.APPLICATION_XML_VALUE; } else if(ext.equals("txt")) { contentType = MediaType.TEXT_PLAIN_VALUE; + } else if (ext.equals("svg")) { + contentType = "image/svg+xml"; } else { contentType = "image/" + ext; } diff --git a/services/src/main/java/org/fao/geonet/api/LogoUtils.java b/services/src/main/java/org/fao/geonet/api/LogoUtils.java new file mode 100644 index 000000000000..56cee26cb69e --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/LogoUtils.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2001-2026 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ + +package org.fao.geonet.api; + +import jeeves.server.context.ServiceContext; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.fao.geonet.api.records.attachments.AttachmentsApi; +import org.fao.geonet.resources.Resources; +import org.springframework.web.context.request.WebRequest; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; + +public final class LogoUtils { + public static final String API_GET_LOGO_NOTE = "If last-modified header " + + "is present it is used to check if the logo has been modified since " + + "the header date. If it hasn't been modified returns an empty 304 Not" + + " Modified response. If modified returns the image. If there is " + + "no logo then returns a transparent 1x1 px PNG image."; + + private static final int SIX_HOURS = 60 * 60 * 6; + + private static final String TRANSPARENT_1_X_1_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR" + + "42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="; + + private static final byte[] TRANSPARENT_1_X_1_PNG = org.apache.commons.codec.binary.Base64.decodeBase64(TRANSPARENT_1_X_1_PNG_BASE64); + + private LogoUtils() { + } + + public static Resources.ResourceHolder getImage(Resources resources, + ServiceContext serviceContext, + String logoRef) throws IOException { + final Path logosDir = resources.locateLogosDir(serviceContext); + final Path harvesterLogosDir = resources.locateHarvesterLogosDir(serviceContext); + Resources.ResourceHolder image = null; + if (isLocalLogoRef(logoRef)) { + image = resources.getImage(serviceContext, logoRef, logosDir); + if (image == null) { + image = resources.getImage(serviceContext, logoRef, harvesterLogosDir); + } + } + return image; + } + + public static void writeImageOrTransparentLogo(WebRequest webRequest, + HttpServletResponse response, + Resources.ResourceHolder image) throws IOException { + if (image != null) { + FileTime lastModifiedTime = image.getLastModifiedTime(); + response.setDateHeader("Expires", System.currentTimeMillis() + SIX_HOURS * 1000L); + if (webRequest.checkNotModified(lastModifiedTime.toMillis())) { + return; + } + response.setContentType(AttachmentsApi.getFileContentType(image.getPath().getFileName().toString())); + response.setContentLength((int) Files.size(image.getPath())); + response.addHeader("Cache-Control", "max-age=" + SIX_HOURS + ", public"); + FileUtils.copyFile(image.getPath().toFile(), response.getOutputStream()); + return; + } + + if (webRequest.checkNotModified(0L)) { + return; + } + response.setContentType("image/png"); + response.setContentLength(TRANSPARENT_1_X_1_PNG.length); + response.addHeader("Cache-Control", "max-age=" + SIX_HOURS + ", public"); + response.getOutputStream().write(TRANSPARENT_1_X_1_PNG); + } + + private static boolean isLocalLogoRef(String logoRef) { + return StringUtils.isNotBlank(logoRef) + && !logoRef.startsWith("http://") + && !logoRef.startsWith("https://") + && !logoRef.startsWith("https//"); + } +} diff --git a/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java b/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java index 0add43272b26..5f811c6444b4 100644 --- a/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java +++ b/services/src/main/java/org/fao/geonet/api/groups/GroupsApi.java @@ -41,9 +41,9 @@ import org.fao.geonet.api.API; import org.fao.geonet.api.ApiParams; import org.fao.geonet.api.ApiUtils; +import org.fao.geonet.api.LogoUtils; import org.fao.geonet.api.exception.NotAllowedException; import org.fao.geonet.api.exception.ResourceNotFoundException; -import org.fao.geonet.api.records.attachments.AttachmentsApi; import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.api.tools.i18n.TranslationPackBuilder; import org.fao.geonet.constants.Geonet; @@ -77,14 +77,12 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.FileTime; import java.sql.SQLException; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.fao.geonet.api.LogoUtils.API_GET_LOGO_NOTE; import static org.springframework.data.jpa.domain.Specification.where; @RequestMapping(value = { @@ -111,27 +109,6 @@ public class GroupsApi { public static final int GROUPNAME_MAX_LENGHT = 32; - /** - * API logo note. - */ - private static final String API_GET_LOGO_NOTE = "If last-modified header " - + "is present it is used to check if the logo has been modified since " - + "the header date. If it hasn't been modified returns an empty 304 Not" - + " Modified response. If modified returns the image. If the group has " - + "no logo then returns a transparent 1x1 px PNG image."; - /** - * Six hours in seconds. - */ - private static final int SIX_HOURS = 60 * 60 * 6; - /** - * Transparent 1x1 px PNG encoded in Base64. - */ - private static final String TRANSPARENT_1_X_1_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR" - + "42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="; - /** - * Transparent 1x1 px PNG. - */ - private static final byte[] TRANSPARENT_1_X_1_PNG = org.apache.commons.codec.binary.Base64.decodeBase64(TRANSPARENT_1_X_1_PNG_BASE64); /** * Message source. */ @@ -169,20 +146,6 @@ public class GroupsApi { @Autowired private PageRepository pageRepository; - private static Resources.ResourceHolder getImage(Resources resources, ServiceContext serviceContext, Group group) throws IOException { - final Path logosDir = resources.locateLogosDir(serviceContext); - final Path harvesterLogosDir = resources.locateHarvesterLogosDir(serviceContext); - final String logoUUID = group.getLogo(); - Resources.ResourceHolder image = null; - if (StringUtils.isNotBlank(logoUUID) && !logoUUID.startsWith("http://") && !logoUUID.startsWith("https//")) { - image = resources.getImage(serviceContext, logoUUID, logosDir); - if (image == null) { - image = resources.getImage(serviceContext, logoUUID, harvesterLogosDir); - } - } - return image; - } - /** * Writes the group logo image to the response. If no image is found, it * writes a 1x1 transparent PNG. If the request contains cache-related @@ -220,35 +183,9 @@ public void getGroupLogo( } try { final Resources resources = context.getBean(Resources.class); - final String logoUUID = group.get().getLogo(); - if (StringUtils.isNotBlank(logoUUID) && !logoUUID.startsWith("http://") && !logoUUID.startsWith("https//")) { - try (Resources.ResourceHolder image = getImage(resources, serviceContext, group.get())) { - if (image != null) { - FileTime lastModifiedTime = image.getLastModifiedTime(); - response.setDateHeader("Expires", System.currentTimeMillis() + SIX_HOURS * 1000L); - if (webRequest.checkNotModified(lastModifiedTime.toMillis())) { - // webRequest.checkNotModified sets the right HTTP headers - return; - } - response.setContentType(AttachmentsApi.getFileContentType(image.getPath().getFileName().toString())); - response.setContentLength((int) Files.size(image.getPath())); - response.addHeader("Cache-Control", "max-age=" + SIX_HOURS + ", public"); - FileUtils.copyFile(image.getPath().toFile(), response.getOutputStream()); - return; - } - } - } - - // no logo image found. Return a transparent 1x1 png - FileTime lastModifiedTime = FileTime.fromMillis(0); - if (webRequest.checkNotModified(lastModifiedTime.toMillis())) { - return; + try (Resources.ResourceHolder image = LogoUtils.getImage(resources, serviceContext, group.get().getLogo())) { + LogoUtils.writeImageOrTransparentLogo(webRequest, response, image); } - response.setContentType("image/png"); - response.setContentLength(TRANSPARENT_1_X_1_PNG.length); - response.addHeader("Cache-Control", "max-age=" + SIX_HOURS + ", public"); - response.getOutputStream().write(TRANSPARENT_1_X_1_PNG); - } catch (IOException e) { Log.error(LOGGER, String.format("There was an error accessing the logo of the group with id '%d'", groupId)); diff --git a/services/src/main/java/org/fao/geonet/api/harvesting/HarvestersApi.java b/services/src/main/java/org/fao/geonet/api/harvesting/HarvestersApi.java index 58d434445f9f..c927aea2e33a 100644 --- a/services/src/main/java/org/fao/geonet/api/harvesting/HarvestersApi.java +++ b/services/src/main/java/org/fao/geonet/api/harvesting/HarvestersApi.java @@ -38,6 +38,7 @@ import org.fao.geonet.domain.HarvestHistory; import org.fao.geonet.domain.ISODate; import org.fao.geonet.domain.Source; +import org.fao.geonet.domain.SourceType; import org.fao.geonet.kernel.DataManager; import org.fao.geonet.kernel.datamanager.IMetadataManager; import org.fao.geonet.kernel.datamanager.IMetadataUtils; @@ -62,6 +63,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; @@ -173,6 +175,34 @@ public HttpEntity assignHarvestedRecordToSource( return new HttpEntity<>(HttpStatus.NO_CONTENT); } + @io.swagger.v3.oas.annotations.Operation( + summary = "Get harvester logo image.", + description = "Redirect to source logo endpoint." + ) + @RequestMapping( + value = "/{harvesterUuid}/logo", + method = RequestMethod.GET + ) + @ResponseStatus(value = HttpStatus.FOUND) + public void getHarvesterLogo( + @Parameter( + description = "The harvester UUID" + ) + @PathVariable + String harvesterUuid, + HttpServletRequest request, + HttpServletResponse response + ) throws Exception { + Source source = sourceRepository.findOneByUuid(harvesterUuid); + if (source == null || source.getType() != SourceType.harvester) { + throw new ResourceNotFoundException(String.format( + "Harvester with UUID '%s' not found.", + harvesterUuid)); + } + + response.sendRedirect(request.getRequestURI().replace("/api/harvesters/", "/api/sources/")); + } + @io.swagger.v3.oas.annotations.Operation( summary = "Reindexes all records of an harvester", diff --git a/services/src/main/java/org/fao/geonet/api/site/LogosApi.java b/services/src/main/java/org/fao/geonet/api/site/LogosApi.java index 872751d99e48..c4d0a2822b38 100644 --- a/services/src/main/java/org/fao/geonet/api/site/LogosApi.java +++ b/services/src/main/java/org/fao/geonet/api/site/LogosApi.java @@ -71,10 +71,8 @@ description = "Logos operations") @Controller("siteLogos") public class LogosApi { - private static final String[] iconExt = {".gif", ".png", ".jpg", ".jpeg"}; + private static final String[] iconExt = {".gif", ".png", ".jpg", ".jpeg", ".svg"}; - @Autowired - GeonetworkDataDirectory dataDirectory; @Autowired SettingManager settingManager; diff --git a/services/src/main/java/org/fao/geonet/api/site/SiteApi.java b/services/src/main/java/org/fao/geonet/api/site/SiteApi.java index ecb23ec77945..e69f841fcf1d 100644 --- a/services/src/main/java/org/fao/geonet/api/site/SiteApi.java +++ b/services/src/main/java/org/fao/geonet/api/site/SiteApi.java @@ -44,9 +44,11 @@ import org.fao.geonet.*; import org.fao.geonet.api.ApiParams; import org.fao.geonet.api.ApiUtils; +import org.fao.geonet.api.LogoUtils; import org.fao.geonet.api.OpenApiConfig; import org.fao.geonet.api.exception.FeatureNotEnabledException; import org.fao.geonet.api.exception.NotAllowedException; +import org.fao.geonet.api.groups.GroupsApi; import org.fao.geonet.api.site.model.SettingSet; import org.fao.geonet.api.site.model.SettingsListResponse; import org.fao.geonet.api.tools.i18n.LanguageUtils; @@ -88,16 +90,15 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.WebRequest; -import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; -import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.*; import java.util.stream.Collectors; @@ -105,6 +106,7 @@ import static org.fao.geonet.api.ApiParams.API_CLASS_CATALOG_TAG; import static org.fao.geonet.constants.Geonet.Path.IMPORT_STYLESHEETS_SCHEMA_PREFIX; import static org.fao.geonet.kernel.setting.Settings.SYSTEM_FEEDBACK_EMAIL; +import static org.fao.geonet.resources.ResourceFilter.DEFAULT_LOGO; /** * @@ -852,51 +854,86 @@ public void setLogo( String nodeUuid = settingManager.getSiteId(); - Resources.ResourceHolder holder = resources.getImage(serviceContext, file, logoDirectory); - final Path resourcesDir = - resources.locateResourcesDir(request.getServletContext(), serviceContext.getApplicationContext()); - if (holder == null || holder.getPath() == null) { - holder = resources.getImage(serviceContext, "images/harvesting/" + file, resourcesDir); + Optional siteSourceOpt = sourceRepository.findById(nodeUuid); + if (siteSourceOpt.isEmpty()) { + throw new Exception("Unable to find main catalog in source table with uuid '" + nodeUuid + "'."); } - try { - try (InputStream inputStream = Files.newInputStream(holder.getPath())) { - BufferedImage source = ImageIO.read(inputStream); + Source siteSource = siteSourceOpt.get(); + if (siteSource.getType() != SourceType.portal) { + throw new Exception("Source with uuid '" + nodeUuid + "' is not of the type portal (main catalogue)."); + } + boolean isSvg = file.endsWith(".svg"); + + if (asFavicon) { + if (isSvg) { + throw new IllegalArgumentException("SVG logo can not be used to generate favicon."); + } + + Resources.ResourceHolder holder = resources.getImage(serviceContext, file, logoDirectory); + final Path resourcesDir = + resources.locateResourcesDir(request.getServletContext(), serviceContext.getApplicationContext()); + + if (holder == null || holder.getPath() == null) { + holder = resources.getImage(serviceContext, "images/harvesting/" + file, resourcesDir); + } - if (asFavicon) { + if (holder == null || holder.getPath() == null) { + throw new FileNotFoundException("Logo file '" + file + "' not found in resources."); + } + + try { + + try (InputStream inputStream = Files.newInputStream(holder.getPath())) { + java.awt.image.BufferedImage source = javax.imageio.ImageIO.read(inputStream); try (Resources.ResourceHolder favicon = resources.getWritableImage(serviceContext, "images/logos/favicon.png", resourcesDir)) { ApiUtils.createFavicon(source, favicon.getPath()); } - } else { - try (Resources.ResourceHolder logo = - resources.getWritableImage(serviceContext, - "images/logos/" + nodeUuid + ".png", - resourcesDir); - Resources.ResourceHolder defaultLogo = - resources.getWritableImage(serviceContext, - "images/logo.png", resourcesDir)) { - if (!file.endsWith(".png")) { - try ( - OutputStream logoOut = Files.newOutputStream(logo.getPath()); - OutputStream defLogoOut = Files.newOutputStream(defaultLogo.getPath()) - ) { - ImageIO.write(source, "png", logoOut); - ImageIO.write(source, "png", defLogoOut); - } - } else { - Files.copy(holder.getPath(), logo.getPath(), StandardCopyOption.REPLACE_EXISTING); - Files.copy(holder.getPath(), defaultLogo.getPath(), - StandardCopyOption.REPLACE_EXISTING); - } - } } + } finally { + holder.close(); } - } catch (Exception e) { - throw new Exception( - "Unable to move uploaded thumbnail to destination directory. Error: " + e.getMessage()); - } finally { - holder.close(); + } else { + String actualLogo = siteSource.getLogo(); + if (StringUtils.isNotEmpty(actualLogo)) { + resources.deleteImageIfExists(actualLogo, dataDirectory.getResourcesDir().resolve("images").resolve("logos")); + } + String logoFile = resources.copyLogo(serviceContext, + "images" + File.separator + "harvesting" + File.separator + file, nodeUuid); + + siteSource.setLogo(logoFile); + sourceRepository.save(siteSource); + } + } + + @io.swagger.v3.oas.annotations.Operation( + summary = "Get catalog logo image.", + description = LogoUtils.API_GET_LOGO_NOTE) + @RequestMapping( + path = "/logo", + method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public void getLogo( + @Parameter(hidden = true) WebRequest webRequest, + HttpServletRequest request, + HttpServletResponse response + ) { + ServiceContext serviceContext = ApiUtils.createServiceContext(request); + final Resources resources = serviceContext.getBean(Resources.class); + final String siteUuid = settingManager.getSiteId(); + final String logoRef = sourceRepository.findById(siteUuid) + .map(Source::getLogo) + .filter(StringUtils::isNotBlank) + .orElse(siteUuid); + + try (Resources.ResourceHolder image = LogoUtils.getImage(resources, serviceContext, logoRef)) { + LogoUtils.writeImageOrTransparentLogo(webRequest, response, image); + } catch (IOException e) { + Log.error(Geonet.GEONETWORK + ".api.site", + String.format("There was an error accessing the logo for site uuid '%s'", siteUuid)); + throw new RuntimeException(e); } } diff --git a/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java b/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java index 9edd5a11f274..d2771b74c716 100644 --- a/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java +++ b/services/src/main/java/org/fao/geonet/api/sources/SourcesApi.java @@ -37,13 +37,16 @@ import org.fao.geonet.api.ApiParams; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.api.exception.ResourceNotFoundException; +import org.fao.geonet.api.LogoUtils; import org.fao.geonet.api.tools.i18n.TranslationPackBuilder; +import org.fao.geonet.constants.Geonet; import org.fao.geonet.domain.*; import org.fao.geonet.guiapi.search.XsltResponseWriter; import org.fao.geonet.repository.LanguageRepository; import org.fao.geonet.repository.SortUtils; import org.fao.geonet.repository.SourceRepository; import org.fao.geonet.resources.Resources; +import org.fao.geonet.utils.Log; import org.jdom.Element; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -53,6 +56,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.WebRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -73,6 +77,7 @@ description = "Source catalogue operations") @Controller("sources") public class SourcesApi { + private static final String LOGGER = Geonet.GEONETWORK + ".api.sources"; @Autowired SourceRepository sourceRepository; @@ -185,6 +190,50 @@ public Source getSource( } } + @io.swagger.v3.oas.annotations.Operation( + summary = "Get source logo image.", + description = LogoUtils.API_GET_LOGO_NOTE) + @RequestMapping( + value = "/{sourceIdentifier}/logo", + method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Logo returned."), + @ApiResponse(responseCode = "404", description = "Source not found.", + content = @Content(schema = @Schema(implementation = ApiError.class))) + }) + @ResponseBody + public void getSourceLogo( + @Parameter( + description = "Source identifier", + required = true + ) + @PathVariable + String sourceIdentifier, + @Parameter(hidden = true) WebRequest webRequest, + HttpServletRequest request, + HttpServletResponse response + ) throws ResourceNotFoundException { + Optional source = sourceRepository.findById(sourceIdentifier); + if (source.isEmpty()) { + throw new ResourceNotFoundException(String.format( + "Source with uuid '%s' does not exist.", + sourceIdentifier + )); + } + + ServiceContext serviceContext = ApiUtils.createServiceContext(request); + Resources resources = serviceContext.getBean(Resources.class); + String logoRef = getLogoReference(source.get()); + try (Resources.ResourceHolder image = LogoUtils.getImage(resources, serviceContext, logoRef)) { + LogoUtils.writeImageOrTransparentLogo(webRequest, response, image); + } catch (IOException e) { + Log.error(LOGGER, String.format("There was an error accessing the logo of the source with id '%s'", + sourceIdentifier)); + throw new RuntimeException(e); + } + } + @io.swagger.v3.oas.annotations.Operation( summary = "Add a source", description = "") @@ -366,4 +415,13 @@ private void updateSource(String sourceIdentifier, }); } + + private String getLogoReference(Source source) { + if (source.getType() == SourceType.portal + || source.getType() == SourceType.subportal + || source.getType() == SourceType.harvester) { + return source.getLogo(); + } + return StringUtils.defaultIfBlank(source.getLogo(), source.getUuid()); + } } diff --git a/services/src/main/java/org/fao/geonet/util/FileMimetypeChecker.java b/services/src/main/java/org/fao/geonet/util/FileMimetypeChecker.java index 8a1a0674690f..00af11b3b0a2 100644 --- a/services/src/main/java/org/fao/geonet/util/FileMimetypeChecker.java +++ b/services/src/main/java/org/fao/geonet/util/FileMimetypeChecker.java @@ -32,7 +32,7 @@ @Component public class FileMimetypeChecker { - private static final String[] validImagesMimeTypes = {"image/gif", "image/png", "image/jpeg"}; + private static final String[] validImagesMimeTypes = {"image/gif", "image/png", "image/jpeg", "image/svg+xml"}; // application/xml --> SDMX files, application/rdf+xml --> RDF, OWL files private static final String[] validThesaurusMimeTypes = {"application/xml", "application/rdf+xml"}; diff --git a/services/src/test/java/org/fao/geonet/api/harvesting/HarvestersApiTest.java b/services/src/test/java/org/fao/geonet/api/harvesting/HarvestersApiTest.java new file mode 100644 index 000000000000..bb217e61602e --- /dev/null +++ b/services/src/test/java/org/fao/geonet/api/harvesting/HarvestersApiTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2001-2026 Food and Agriculture Organization of the + * United Nations (FAO-UN), United Nations World Food Programme (WFP) + * and United Nations Environment Programme (UNEP) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + * + * Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2, + * Rome - Italy. email: geonetwork@osgeo.org + */ +package org.fao.geonet.api.harvesting; + +import junit.framework.Assert; +import org.fao.geonet.domain.ISODate; +import org.fao.geonet.domain.Source; +import org.fao.geonet.domain.SourceType; +import org.fao.geonet.repository.SourceRepository; +import org.fao.geonet.services.AbstractServiceIntegrationTest; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HarvestersApiTest extends AbstractServiceIntegrationTest { + @Autowired + private WebApplicationContext wac; + + @Autowired + private SourceRepository sourceRepo; + + private MockMvc mockMvc; + + @Before + public void setUp() { + createTestData(); + } + + @Test + public void getHarvesterLogoRedirectsToSourceLogo() throws Exception { + Source source = sourceRepo.findOneByName("harvester-source"); + Assert.assertNotNull(source); + + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc.perform(get("/srv/api/harvesters/" + source.getUuid() + "/logo")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/srv/api/sources/" + source.getUuid() + "/logo")); + } + + @Test + public void getHarvesterLogoWithNonExistingHarvester() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc.perform(get("/srv/api/harvesters/not-an-existing-harvester/logo")) + .andExpect(status().isNotFound()); + } + + private void createTestData() { + Source source = new Source(); + source.setName("harvester-source"); + source.setUuid("harvester-source-uuid"); + source.setCreationDate(new ISODate()); + source.setType(SourceType.harvester); + sourceRepo.save(source); + } +} diff --git a/services/src/test/java/org/fao/geonet/api/sources/SourcesApiTest.java b/services/src/test/java/org/fao/geonet/api/sources/SourcesApiTest.java index 9e7ca4b79775..33857f2fcc25 100644 --- a/services/src/test/java/org/fao/geonet/api/sources/SourcesApiTest.java +++ b/services/src/test/java/org/fao/geonet/api/sources/SourcesApiTest.java @@ -86,6 +86,24 @@ public void getSource() throws Exception { .andExpect(content().contentType(API_JSON_EXPECTED_ENCODING)); } + @Test + public void getSourceLogo() throws Exception { + Source source = sourceRepo.findOneByName("source-test"); + Assert.assertNotNull(source); + + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc.perform(get("/srv/api/sources/" + source.getUuid() + "/logo")) + .andExpect(status().isOk()) + .andExpect(content().contentType(API_PNG_EXPECTED_ENCODING)); + } + + @Test + public void getNonExistingSourceLogo() throws Exception { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockMvc.perform(get("/srv/api/sources/source-test-2/logo")) + .andExpect(status().is(404)); + } + @Test public void getNonExistingSource() throws Exception { Source sourceToUpdate = sourceRepo.findOneByName("source-test-2"); diff --git a/web-ui/src/main/resources/catalog/components/admin/harvester/HarvesterDirective.js b/web-ui/src/main/resources/catalog/components/admin/harvester/HarvesterDirective.js index 26ee2d40a6aa..fac053d5795f 100644 --- a/web-ui/src/main/resources/catalog/components/admin/harvester/HarvesterDirective.js +++ b/web-ui/src/main/resources/catalog/components/admin/harvester/HarvesterDirective.js @@ -61,11 +61,9 @@ } $("#translationModal").modal("show"); }; - $http - .get("admin.harvester.info?type=icons&_content_type=json", { cache: true }) - .then(function (response) { - scope.icons = response.data[0]; - }); + $http.get("../api/logos", { cache: true }).then(function (response) { + scope.icons = response.data; + }); $http .get("info?_content_type=json&type=languages", { cache: true }) .then(function (response) { @@ -338,11 +336,9 @@ "../../catalog/components/admin/harvester/partials/" + "logopicker.html", link: function (scope, element, attrs) { - $http - .get("admin.harvester.info?type=icons&_content_type=json", { cache: true }) - .then(function (response) { - scope.icons = response.data[0]; - }); + $http.get("../api/logos", { cache: true }).then(function (response) { + scope.icons = response.data; + }); /** * Set the icon diff --git a/web-ui/src/main/resources/catalog/components/toolbar/partials/menu-sitename.html b/web-ui/src/main/resources/catalog/components/toolbar/partials/menu-sitename.html index 35c0e52d428f..b8e18c7e519f 100644 --- a/web-ui/src/main/resources/catalog/components/toolbar/partials/menu-sitename.html +++ b/web-ui/src/main/resources/catalog/components/toolbar/partials/menu-sitename.html @@ -4,7 +4,7 @@ class="gn-logo" data-ng-hide="{{gnCfg.mods.header.isLogoInHeader}}" alt="{{'siteLogo' | translate}}" - data-ng-src="{{gnUrl}}../images/logos/{{info['node/id'] || info['system/site/siteId']}}.png?random{{info['system/site/lastUpdate']}}" + data-ng-src="{{gnUrl + 'api/sources/' + info['node/id'] + '/logo?' +info['system/site/lastUpdate']}}" />