From ab94f233b37fb86326bba99d4f497dae21ce5dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Tue, 18 Jun 2024 15:41:05 +0200 Subject: [PATCH 01/76] Metadata extents API - fix service for metadata with working copy The API accepts an optional parameter to retrieve the approved or the working copy versions. --- .../api/records/extent/MetadataExtentApi.java | 37 ++++++++++--------- .../metadata/MetadataRegionSearchRequest.java | 26 ++++++------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/services/src/main/java/org/fao/geonet/api/records/extent/MetadataExtentApi.java b/services/src/main/java/org/fao/geonet/api/records/extent/MetadataExtentApi.java index a1039d588bd..a3f0515c673 100644 --- a/services/src/main/java/org/fao/geonet/api/records/extent/MetadataExtentApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/extent/MetadataExtentApi.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2021 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -45,7 +45,6 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.NativeWebRequest; @@ -65,7 +64,7 @@ }) @Tag(name = API_CLASS_RECORD_TAG, description = API_CLASS_RECORD_OPS) -@Controller("recordExtent") +@RestController("recordExtent") @ReadWriteController public class MetadataExtentApi { @@ -118,12 +117,11 @@ public class MetadataExtentApi { @io.swagger.v3.oas.annotations.Operation( summary = "Get record extents as image", description = API_EXTENT_DESCRIPTION) - @RequestMapping( + @GetMapping( value = "/{metadataUuid}/extents.png", produces = { MediaType.IMAGE_PNG_VALUE - }, - method = RequestMethod.GET) + }) public HttpEntity getAllRecordExtentAsImage( @Parameter( description = API_PARAM_RECORD_UUID, @@ -144,6 +142,8 @@ public HttpEntity getAllRecordExtentAsImage( @Parameter(description = API_PARAM_STROKE_DESCRIPTION) @RequestParam(value = "", required = false, defaultValue = "0,0,0,255") String strokeColor, + @RequestParam(required = false, defaultValue = "true") + Boolean approved, @Parameter(hidden = true) NativeWebRequest nativeWebRequest, @Parameter(hidden = true) @@ -157,7 +157,7 @@ public HttpEntity getAllRecordExtentAsImage( "EPSG:4326"); } - return getExtent(metadataUuid, srs, width, height, background, fillColor, strokeColor, null, nativeWebRequest, request); + return getExtent(metadataUuid, srs, width, height, background, fillColor, strokeColor, null, approved, nativeWebRequest, request); } @@ -165,13 +165,11 @@ public HttpEntity getAllRecordExtentAsImage( @io.swagger.v3.oas.annotations.Operation( summary = "Get list of record extents", description = API_EXTENT_DESCRIPTION) - @RequestMapping( + @GetMapping( value = "/{metadataUuid}/extents.json", produces = { MediaType.APPLICATION_JSON_VALUE - }, - method = RequestMethod.GET) - @ResponseBody + }) public List getAllRecordExtentAsJson( @Parameter( description = API_PARAM_RECORD_UUID, @@ -215,7 +213,7 @@ public List getAllRecordExtentAsJson( String.format("%sapi/records/%s/extents/%d.png", settingManager.getNodeURL(), metadataUuid, index), extentElement.getName(), - XPath.getXPath(xmlData, (Element) extent), + XPath.getXPath(xmlData, extent), description)); index ++; } @@ -227,12 +225,11 @@ public List getAllRecordExtentAsJson( @io.swagger.v3.oas.annotations.Operation( summary = "Get one record extent as image", description = API_EXTENT_DESCRIPTION) - @RequestMapping( + @GetMapping( value = "/{metadataUuid}/extents/{geometryIndex}.png", produces = { MediaType.IMAGE_PNG_VALUE - }, - method = RequestMethod.GET) + }) public HttpEntity getOneRecordExtentAsImage( @Parameter( description = API_PARAM_RECORD_UUID, @@ -256,6 +253,8 @@ public HttpEntity getOneRecordExtentAsImage( @Parameter(description = API_PARAM_STROKE_DESCRIPTION) @RequestParam(value = "", required = false, defaultValue = "0,0,0,255") String strokeColor, + @RequestParam(required = false, defaultValue = "true") + Boolean approved, @Parameter(hidden = true) NativeWebRequest nativeWebRequest, @Parameter(hidden = true) @@ -269,11 +268,13 @@ public HttpEntity getOneRecordExtentAsImage( "EPSG:4326"); } - return getExtent(metadataUuid, srs, width, height, background, fillColor, strokeColor, geometryIndex, nativeWebRequest, request); + return getExtent(metadataUuid, srs, width, height, background, fillColor, strokeColor, geometryIndex, approved, nativeWebRequest, request); } - private HttpEntity getExtent(String metadataUuid, String srs, Integer width, Integer height, String background, String fillColor, String strokeColor, Integer extentOrderOfAppearance, NativeWebRequest nativeWebRequest, HttpServletRequest request) throws Exception { - AbstractMetadata metadata = ApiUtils.canViewRecord(metadataUuid, request); + private HttpEntity getExtent(String metadataUuid, String srs, Integer width, Integer height, String background, String fillColor, String strokeColor, + Integer extentOrderOfAppearance, Boolean approved, + NativeWebRequest nativeWebRequest, HttpServletRequest request) throws Exception { + AbstractMetadata metadata = ApiUtils.canViewRecord(metadataUuid, approved, request); ServiceContext context = ApiUtils.createServiceContext(request); if (width != null && height != null) { diff --git a/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java b/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java index 739f9f3bd7c..d68e8777054 100644 --- a/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java +++ b/services/src/main/java/org/fao/geonet/api/regions/metadata/MetadataRegionSearchRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -24,14 +24,12 @@ package org.fao.geonet.api.regions.metadata; import com.google.common.base.Optional; -import com.google.common.collect.Lists; import jeeves.server.context.ServiceContext; import org.fao.geonet.ApplicationContextHolder; import org.fao.geonet.GeonetContext; import org.fao.geonet.api.regions.MetadataRegion; import org.fao.geonet.constants.Geonet; import org.fao.geonet.domain.ISODate; -import org.fao.geonet.domain.Metadata; import org.fao.geonet.domain.ReservedOperation; import org.fao.geonet.kernel.DataManager; import org.fao.geonet.kernel.datamanager.IMetadataSchemaUtils; @@ -39,9 +37,9 @@ import org.fao.geonet.kernel.region.Region; import org.fao.geonet.kernel.region.Request; import org.fao.geonet.kernel.schema.MetadataSchema; +import org.fao.geonet.kernel.search.EsSearchManager; import org.fao.geonet.kernel.search.spatial.ErrorHandler; import org.fao.geonet.lib.Lib; -import org.fao.geonet.repository.MetadataRepository; import org.fao.geonet.util.GMLParsers; import org.fao.geonet.utils.Log; import org.fao.geonet.utils.Xml; @@ -97,16 +95,16 @@ public Collection execute() throws Exception { if (label == null && id == null || (id != null && !id.startsWith(PREFIX))) { return Collections.emptySet(); } - List regions = new ArrayList(); + List regions = new ArrayList<>(); if (label != null) { loadAll(regions, Id.create(label)); } else if (id != null) { String[] parts = id.split(":", 3); String mdId = parts[1]; - String id; + String identifier; if (parts.length > 2) { - id = parts[2]; - loadOnly(regions, Id.create(mdId), id); + identifier = parts[2]; + loadOnly(regions, Id.create(mdId), identifier); } else { loadSpatialExtent(regions, Id.create(mdId)); } @@ -189,13 +187,13 @@ private double getCoordinateValue(String coord, Element bbox) { } } return 0; - }; + } Region parseRegion(Id mdId, Element extentObj) throws Exception { GeonetContext gc = (GeonetContext) context.getHandlerContext(Geonet.CONTEXT_NAME); gc.getBean(DataManager.class).getEditLib().removeEditingInfo(extentObj); - String id = null; + String regionId = null; Geometry geometry = null; Namespace objNamespace = extentObj.getNamespace(); if ("polygon".equals(extentObj.getName())) { @@ -227,9 +225,9 @@ Region parseRegion(Id mdId, Element extentObj) throws Exception { if (geometry != null) { Element element = extentObj.getChild("element", Geonet.Namespaces.GEONET); if (element != null) { - id = element.getAttributeValue("ref"); + regionId = element.getAttributeValue("ref"); } - return new MetadataRegion(mdId, id, geometry); + return new MetadataRegion(mdId, regionId, geometry); } else { return null; } @@ -281,9 +279,7 @@ public Optional getLastModified() throws Exception { final String mdId = MetadataRegionSearchRequest.Id.create(idParts[0]).getMdId(); if (mdId != null) { - Metadata metadata = ApplicationContextHolder.get().getBean(MetadataRepository.class).findOneById(Integer.valueOf(mdId)); - - final ISODate docChangeDate = metadata.getDataInfo().getChangeDate(); + final ISODate docChangeDate = ApplicationContextHolder.get().getBean(EsSearchManager.class).getDocChangeDate(mdId); if (docChangeDate != null) { return Optional.of(docChangeDate.toDate().getTime()); } From e79f2a30fd362c786cf384b93b1a52b6b559f730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Wed, 19 Jun 2024 08:21:59 +0200 Subject: [PATCH 02/76] Register user / allow to select the group where the user wants to register (#8176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Register user / allow to select the group that wishes to register * Register user / API integration tests * Update web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties Co-authored-by: Jody Garnett * Update web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties Co-authored-by: Jody Garnett * Update core/src/test/resources/org/fao/geonet/api/Messages.properties Co-authored-by: Jody Garnett * Update core/src/test/resources/org/fao/geonet/api/Messages_fre.properties Co-authored-by: Jody Garnett * Register user / disable group selection for profile Administrator * Register user / form improvements * Register user / surname not required * Register user / Update new account text * Update core/src/test/resources/org/fao/geonet/api/Messages_fre.properties Co-authored-by: François Prunayre --------- Co-authored-by: Jody Garnett Co-authored-by: François Prunayre --- .../org/fao/geonet/api/Messages.properties | 19 +++ .../fao/geonet/api/Messages_fre.properties | 16 ++ .../img/selfregistration-form.png | Bin 17721 -> 51422 bytes .../user-self-registration.md | 5 +- services/pom.xml | 19 +++ .../org/fao/geonet/api/users/RegisterApi.java | 151 ++++++++++++----- .../api/users/model/UserRegisterDto.java | 15 +- .../fao/geonet/api/users/RegisterApiTest.java | 156 ++++++++++++++++++ .../resources/catalog/js/LoginController.js | 18 ++ .../resources/catalog/locales/en-core.json | 3 +- .../catalog/templates/new-account.html | 72 ++++++-- .../org/fao/geonet/api/Messages.properties | 19 +++ .../fao/geonet/api/Messages_fre.properties | 16 ++ 13 files changed, 448 insertions(+), 61 deletions(-) create mode 100644 services/src/test/java/org/fao/geonet/api/users/RegisterApiTest.java diff --git a/core/src/test/resources/org/fao/geonet/api/Messages.properties b/core/src/test/resources/org/fao/geonet/api/Messages.properties index 9bd5be836db..1cde2820cbb 100644 --- a/core/src/test/resources/org/fao/geonet/api/Messages.properties +++ b/core/src/test/resources/org/fao/geonet/api/Messages.properties @@ -61,6 +61,10 @@ register_email_admin_message=Dear Admin,\n\ Newly registered user %s has requested %s access for %s.\n\ Yours sincerely,\n\ The %s team. +register_email_group_admin_message=Dear Admin,\n\ + Newly registered user %s has requested %s access in group %s for %s.\n\ + Yours sincerely,\n\ + The %s team. register_email_subject=%s / Your account as %s register_email_message=Dear User,\n\ Your registration at %s was successful.\n\ @@ -77,6 +81,21 @@ register_email_message=Dear User,\n\ \n\ Yours sincerely,\n\ The %s team. +register_email_group_message=Dear User,\n\ + Your registration at %s was successful.\n\ + Your account is: \n\ + * username: %s\n\ + * password: %s\n\ + * profile: %s\n\ + \n\ + We have sent your request for %s to the group %s administrator. You will be contacted shortly.\n\ + To log in and access your account, please click on the link below.\n\ + %s\n\ + \n\ + Thanks for your registration.\n\ + \n\ + Yours sincerely,\n\ + The %s team. new_user_rating=%s / New user rating on %s new_user_rating_text=See record %s user_feedback_title=%s / User feedback on %s / %s diff --git a/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties b/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties index 6abe49058d6..55a00915b44 100644 --- a/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties +++ b/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties @@ -52,6 +52,10 @@ register_email_admin_message=Cher administrateur,\n\ L'utilisateur %s vient de demander une cr\u00E9ation de compte pour %s.\n\ Salutation,\n\ L'\u00E9quipe %s. +register_email_group_admin_message=Cher administrateur,\n\ + L'utilisateur %s vient de demander une cr\u00E9ation de compte pour %s en groupe %s.\n\ + Salutation,\n\ + L'\u00E9quipe %s. register_email_subject=%s / Votre compte %s register_email_message=Cher utilisateur,\n\ Votre compte a \u00E9t\u00E9 cr\u00E9\u00E9 avec succ\u00E9s pour %s.\n\ @@ -65,6 +69,18 @@ register_email_message=Cher utilisateur,\n\ \n\ Salutations,\n\ L'\u00E9quipe %s. +register_email_group_message=Cher utilisateur,\n\ + Votre compte a \u00E9t\u00E9 cr\u00E9\u00E9 avec succ\u00E9s pour %s.\n\ + * Nom d'utilisateur : %s\n\ + * Mot de passe : %s\n\ + * Profil : %s\n\ + \n\ +Nous avons envoy\u00E9 votre demande de %s à l'administrateur du groupe %s. Vous serez contact\u00E9 rapidement.\n\ + Vous pouvez d\u00E9s \u00E0 pr\u00E9sent vous connecter.\n\ + %s\n\ + \n\ + Salutations,\n\ + L'\u00E9quipe %s. new_user_rating=%s / Nouvelle \u00E9valuation faite pour %s new_user_rating_text=Consulter la fiche %s user_feedback_title=%s / Nouveau commentaire sur %s / %s diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-form.png b/docs/manual/docs/administrator-guide/managing-users-and-groups/img/selfregistration-form.png index 09321a4e4b52516b44c5a02ee28f6e234654a94d..d718b94a019074f5e4a415cabe8e6256243f35fc 100644 GIT binary patch literal 51422 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 NSJOJ5t7;waKLDgrBYFS; literal 17721 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 BE6V@? diff --git a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md index 2594a81f727..fe3cb2d0142 100644 --- a/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md +++ b/docs/manual/docs/administrator-guide/managing-users-and-groups/user-self-registration.md @@ -11,9 +11,12 @@ Click the `Create an account` button and fill out the registration form: The fields in this form are self-explanatory except for the following: - **Email**: The user's email address. This is mandatory and will be used as the username. -- **Profile**: By default, self-registered users are given the `Registered User` profile (see previous section). If any other profile is selected: +- **Requested profile**: By default, self-registered users are given the `Registered User` profile (see previous section). If any other profile is selected: - the user will still be given the `Registered User` profile - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the request for a more privileged profile +- **Requested group**: By default, self-registered users are not assigned to any group. If a group is selected: + - the user will still not be assigned to any group + - an email will be sent to the Email address nominated in the Feedback section of the 'System Administration' menu, informing them of the requested group. ## What happens when a user self-registers? diff --git a/services/pom.xml b/services/pom.xml index 08a35139de6..e0ccd7873f1 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -48,6 +48,14 @@ test + + + org.jvnet.mock-javamail + mock-javamail + 1.9 + test + + org.springframework spring-web @@ -391,6 +399,17 @@ + + + maven-surefire-plugin + + + org.jvnet.mock_javamail.MockTransport + org.jvnet.mock_javamail.MockStore + org.jvnet.mock_javamail.MockStore + + + diff --git a/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java b/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java index e5eeb703f1d..848f6e53e5d 100644 --- a/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java +++ b/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2021 Food and Agriculture Organization of the +//=== Copyright (C) 2001-2024 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== @@ -26,7 +26,6 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jeeves.server.context.ServiceContext; -import org.fao.geonet.api.API; import org.fao.geonet.api.ApiUtils; import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.api.users.model.UserRegisterDto; @@ -45,17 +44,14 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import javax.servlet.http.HttpServletRequest; -import java.sql.SQLException; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.ResourceBundle; +import java.util.*; @EnableWebMvc @Service @@ -72,12 +68,20 @@ public class RegisterApi { @Autowired(required=false) SecurityProviderConfiguration securityProviderConfiguration; + @Autowired + GroupRepository groupRepository; + + @Autowired + UserGroupRepository userGroupRepository; + + @Autowired + SettingManager settingManager; + @io.swagger.v3.oas.annotations.Operation(summary = "Create user account", description = "User is created with a registered user profile. username field is ignored and the email is used as " + "username. Password is sent by email. Catalog administrator is also notified.") - @RequestMapping( + @PutMapping( value = "/actions/register", - method = RequestMethod.PUT, produces = MediaType.TEXT_PLAIN_VALUE) @ResponseStatus(value = HttpStatus.CREATED) @ResponseBody @@ -101,19 +105,18 @@ public ResponseEntity registerUser( ServiceContext context = ApiUtils.createServiceContext(request); - SettingManager sm = context.getBean(SettingManager.class); - boolean selfRegistrationEnabled = sm.getValueAsBool(Settings.SYSTEM_USERSELFREGISTRATION_ENABLE); + boolean selfRegistrationEnabled = settingManager.getValueAsBool(Settings.SYSTEM_USERSELFREGISTRATION_ENABLE); if (!selfRegistrationEnabled) { return new ResponseEntity<>(String.format( messages.getString("self_registration_disabled") ), HttpStatus.PRECONDITION_FAILED); } - boolean recaptchaEnabled = sm.getValueAsBool(Settings.SYSTEM_USERSELFREGISTRATION_RECAPTCHA_ENABLE); + boolean recaptchaEnabled = settingManager.getValueAsBool(Settings.SYSTEM_USERSELFREGISTRATION_RECAPTCHA_ENABLE); if (recaptchaEnabled) { boolean validRecaptcha = RecaptchaChecker.verify(userRegisterDto.getCaptcha(), - sm.getValue(Settings.SYSTEM_USERSELFREGISTRATION_RECAPTCHA_SECRETKEY)); + settingManager.getValue(Settings.SYSTEM_USERSELFREGISTRATION_RECAPTCHA_SECRETKEY)); if (!validRecaptcha) { return new ResponseEntity<>( messages.getString("recaptcha_not_valid"), HttpStatus.PRECONDITION_FAILED); @@ -144,7 +147,7 @@ public ResponseEntity registerUser( ), HttpStatus.PRECONDITION_FAILED); } - if (userRepository.findByUsernameIgnoreCase(userRegisterDto.getEmail()).size() != 0) { + if (!userRepository.findByUsernameIgnoreCase(userRegisterDto.getEmail()).isEmpty()) { // username is ignored and the email is used as username in selfregister return new ResponseEntity<>(String.format( messages.getString("user_with_that_username_found"), @@ -153,8 +156,6 @@ public ResponseEntity registerUser( } User user = new User(); - - // user.setUsername(userRegisterDto.getUsername()); user.setName(userRegisterDto.getName()); user.setSurname(userRegisterDto.getSurname()); user.setOrganisation(userRegisterDto.getOrganisation()); @@ -162,7 +163,6 @@ public ResponseEntity registerUser( user.getAddresses().add(userRegisterDto.getAddress()); user.getEmailAddresses().add(userRegisterDto.getEmail()); - String password = User.getRandomPassword(); user.getSecurity().setPassword( PasswordUtil.encode(context, password) @@ -172,48 +172,78 @@ public ResponseEntity registerUser( user.setProfile(Profile.RegisteredUser); user = userRepository.save(user); - Group targetGroup = getGroup(context); + Group targetGroup = getGroup(); + if (targetGroup != null) { UserGroup userGroup = new UserGroup().setUser(user).setGroup(targetGroup).setProfile(Profile.RegisteredUser); - context.getBean(UserGroupRepository.class).save(userGroup); + userGroupRepository.save(userGroup); } - - String catalogAdminEmail = sm.getValue(Settings.SYSTEM_FEEDBACK_EMAIL); + String catalogAdminEmail = settingManager.getValue(Settings.SYSTEM_FEEDBACK_EMAIL); String subject = String.format( messages.getString("register_email_admin_subject"), - sm.getSiteName(), + settingManager.getSiteName(), user.getEmail(), requestedProfile ); - String message = String.format( - messages.getString("register_email_admin_message"), - user.getEmail(), - requestedProfile, - sm.getNodeURL(), - sm.getSiteName() - ); - if (!MailUtil.sendMail(catalogAdminEmail, subject, message, null, sm)) { + Group requestedGroup = getRequestedGroup(userRegisterDto.getGroup()); + String message; + if (requestedGroup != null) { + message = String.format( + messages.getString("register_email_group_admin_message"), + user.getEmail(), + requestedProfile, + requestedGroup.getLabelTranslations().get(context.getLanguage()), + settingManager.getNodeURL(), + settingManager.getSiteName() + ); + } else { + message = String.format( + messages.getString("register_email_admin_message"), + user.getEmail(), + requestedProfile, + settingManager.getNodeURL(), + settingManager.getSiteName() + ); + + } + + if (Boolean.FALSE.equals(MailUtil.sendMail(catalogAdminEmail, subject, message, null, settingManager))) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } subject = String.format( messages.getString("register_email_subject"), - sm.getSiteName(), + settingManager.getSiteName(), user.getProfile() ); - message = String.format( - messages.getString("register_email_message"), - sm.getSiteName(), - user.getUsername(), - password, - Profile.RegisteredUser, - requestedProfile, - sm.getNodeURL(), - sm.getSiteName() - ); - if (!MailUtil.sendMail(user.getEmail(), subject, message, null, sm)) { + if (requestedGroup != null) { + message = String.format( + messages.getString("register_email_group_message"), + settingManager.getSiteName(), + user.getUsername(), + password, + Profile.RegisteredUser, + requestedProfile, + requestedGroup.getLabelTranslations().get(context.getLanguage()), + settingManager.getNodeURL(), + settingManager.getSiteName() + ); + } else { + message = String.format( + messages.getString("register_email_message"), + settingManager.getSiteName(), + user.getUsername(), + password, + Profile.RegisteredUser, + requestedProfile, + settingManager.getNodeURL(), + settingManager.getSiteName() + ); + } + + if (Boolean.FALSE.equals(MailUtil.sendMail(user.getEmail(), subject, message, null, settingManager))) { return new ResponseEntity<>(String.format( messages.getString("mail_error")), HttpStatus.PRECONDITION_FAILED); } @@ -224,8 +254,39 @@ public ResponseEntity registerUser( ), HttpStatus.CREATED); } - Group getGroup(ServiceContext context) throws SQLException { - final GroupRepository bean = context.getBean(GroupRepository.class); - return bean.findById(ReservedGroup.guest.getId()).get(); + /** + * Returns the group (GUEST) to assign to the registered user. + * + * @return + */ + private Group getGroup() { + Optional targetGroupOpt = groupRepository.findById(ReservedGroup.guest.getId()); + + if (targetGroupOpt.isPresent()) { + return targetGroupOpt.get(); + } + + return null; + } + + /** + * Returns the group requested by the registered user. + * + * @param requestedGroup Requested group identifier for the user. + * @return + */ + private Group getRequestedGroup(String requestedGroup) { + Group targetGroup = null; + + if (StringUtils.hasLength(requestedGroup)) { + Optional targetGroupOpt = groupRepository.findById(Integer.parseInt(requestedGroup)); + + // Don't allow reserved groups + if (targetGroupOpt.isPresent() && !targetGroupOpt.get().isReserved()) { + targetGroup = targetGroupOpt.get(); + } + } + + return targetGroup; } } diff --git a/services/src/main/java/org/fao/geonet/api/users/model/UserRegisterDto.java b/services/src/main/java/org/fao/geonet/api/users/model/UserRegisterDto.java index c77a7b66b6c..5131c99a241 100644 --- a/services/src/main/java/org/fao/geonet/api/users/model/UserRegisterDto.java +++ b/services/src/main/java/org/fao/geonet/api/users/model/UserRegisterDto.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -27,7 +27,6 @@ /** * DTO class for user register information. * - * @author Jose García */ public class UserRegisterDto { private String profile; @@ -39,6 +38,8 @@ public class UserRegisterDto { private Address address; private String captcha; + private String group; + public String getProfile() { return profile; } @@ -103,6 +104,14 @@ public void setCaptcha(String captcha) { this.captcha = captcha; } + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -110,6 +119,7 @@ public boolean equals(Object o) { UserRegisterDto that = (UserRegisterDto) o; + if (group != null ? !group.equals(that.group) : that.group != null) return false; if (profile != null ? !profile.equals(that.profile) : that.profile != null) return false; if (username != null ? !username.equals(that.username) : that.username != null) return false; if (email != null ? !email.equals(that.email) : that.email != null) return false; @@ -123,6 +133,7 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = profile != null ? profile.hashCode() : 0; + result = 31 * result + (group != null ? group.hashCode() : 0); result = 31 * result + (username != null ? username.hashCode() : 0); result = 31 * result + (email != null ? email.hashCode() : 0); result = 31 * result + (name != null ? name.hashCode() : 0); diff --git a/services/src/test/java/org/fao/geonet/api/users/RegisterApiTest.java b/services/src/test/java/org/fao/geonet/api/users/RegisterApiTest.java new file mode 100644 index 00000000000..c7e02ad3308 --- /dev/null +++ b/services/src/test/java/org/fao/geonet/api/users/RegisterApiTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2001-2024 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.users; + +import com.google.gson.Gson; +import org.fao.geonet.api.users.model.UserRegisterDto; +import org.fao.geonet.domain.Profile; +import org.fao.geonet.domain.User; +import org.fao.geonet.kernel.setting.SettingManager; +import org.fao.geonet.kernel.setting.Settings; +import org.fao.geonet.services.AbstractServiceIntegrationTest; +import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +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.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +public class RegisterApiTest extends AbstractServiceIntegrationTest { + @Autowired + private WebApplicationContext wac; + + @Autowired + private SettingManager settingManager; + + @Autowired + StandardPBEStringEncryptor encryptor; + + private MockMvc mockMvc; + + private MockHttpSession mockHttpSession; + + + @Test + public void testFeatureDisabled() throws Exception { + encryptor.initialize(); + + settingManager.setValue(Settings.SYSTEM_USERSELFREGISTRATION_ENABLE, false); + + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockHttpSession = loginAsAnonymous(); + + UserRegisterDto userRegister = new UserRegisterDto(); + userRegister.setName("John"); + userRegister.setSurname("Doe"); + userRegister.setUsername("test@mail.com"); + userRegister.setEmail("test@mail.com"); + userRegister.setProfile("Editor"); + userRegister.setGroup("2"); + + Gson gson = new Gson(); + String json = gson.toJson(userRegister); + + this.mockMvc.perform(put("/srv/api/user/actions/register") + .session(this.mockHttpSession) + .content(json) + .contentType(API_JSON_EXPECTED_ENCODING) + .accept(MediaType.parseMediaType("text/plain"))) + .andExpect(status().isPreconditionFailed()); + } + + @Test + public void testCreateUser() throws Exception { + encryptor.initialize(); + + settingManager.setValue(Settings.SYSTEM_USERSELFREGISTRATION_ENABLE, true); + settingManager.setValue(Settings.SYSTEM_FEEDBACK_MAILSERVER_HOST, "localhost"); + settingManager.setValue(Settings.SYSTEM_FEEDBACK_MAILSERVER_PORT, "25"); + + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockHttpSession = loginAsAnonymous(); + + UserRegisterDto userRegister = new UserRegisterDto(); + userRegister.setName("John"); + userRegister.setSurname("Doe"); + userRegister.setUsername("test@mail.com"); + userRegister.setEmail("test@mail.com"); + userRegister.setProfile("Editor"); + userRegister.setGroup("2"); + + Gson gson = new Gson(); + String json = gson.toJson(userRegister); + + this.mockMvc.perform(put("/srv/api/user/actions/register") + .session(this.mockHttpSession) + .content(json) + .contentType(API_JSON_EXPECTED_ENCODING) + .accept(MediaType.parseMediaType("text/plain"))) + .andExpect(status().isCreated()) + .andExpect(content().string(String.format("User '%s' registered.", userRegister.getUsername()))) + .andReturn(); + } + + + @Test + public void testCreateExistingUser() throws Exception { + encryptor.initialize(); + + settingManager.setValue(Settings.SYSTEM_USERSELFREGISTRATION_ENABLE, true); + settingManager.setValue(Settings.SYSTEM_FEEDBACK_MAILSERVER_HOST, "localhost"); + settingManager.setValue(Settings.SYSTEM_FEEDBACK_MAILSERVER_PORT, "25"); + + User testUserEditor2 = new User(); + testUserEditor2.setUsername("test@mail.com"); + testUserEditor2.setProfile(Profile.Editor); + testUserEditor2.setEnabled(true); + _userRepo.save(testUserEditor2); + + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + this.mockHttpSession = loginAsAnonymous(); + + UserRegisterDto userRegister = new UserRegisterDto(); + userRegister.setName("John"); + userRegister.setSurname("Doe"); + userRegister.setUsername("test@mail.com"); + userRegister.setEmail("test@mail.com"); + userRegister.setProfile("Editor"); + userRegister.setGroup("2"); + + Gson gson = new Gson(); + String json = gson.toJson(userRegister); + + this.mockMvc.perform(put("/srv/api/user/actions/register") + .content(json) + .contentType(API_JSON_EXPECTED_ENCODING) + .session(this.mockHttpSession) + .accept(MediaType.parseMediaType("text/plain"))) + .andExpect(status().isPreconditionFailed()); + } +} diff --git a/web-ui/src/main/resources/catalog/js/LoginController.js b/web-ui/src/main/resources/catalog/js/LoginController.js index ec4df79ad46..9a6a5de8f80 100644 --- a/web-ui/src/main/resources/catalog/js/LoginController.js +++ b/web-ui/src/main/resources/catalog/js/LoginController.js @@ -101,6 +101,8 @@ $scope.userToRemind = gnUtilityService.getUrlParameter("username"); $scope.changeKey = gnUtilityService.getUrlParameter("changeKey"); } + + $scope.retrieveGroups(); } // TODO: https://github.com/angular/angular.js/issues/1460 @@ -134,6 +136,7 @@ email: "", organisation: "", profile: "RegisteredUser", + group: "", address: { address: "", city: "", @@ -143,6 +146,21 @@ } }; + $scope.retrieveGroups = function () { + $http.get("../api/groups").then( + function (response) { + $scope.groups = response.data; + }, + function (response) {} + ); + }; + + $scope.updateGroupSelection = function () { + if ($scope.userInfo.profile === "Administrator") { + $scope.userInfo.group = ""; + } + }; + $scope.register = function () { if ($scope.recaptchaEnabled) { if (vcRecaptchaService.getResponse() === "") { diff --git a/web-ui/src/main/resources/catalog/locales/en-core.json b/web-ui/src/main/resources/catalog/locales/en-core.json index 834426eec24..e18cb2c1325 100644 --- a/web-ui/src/main/resources/catalog/locales/en-core.json +++ b/web-ui/src/main/resources/catalog/locales/en-core.json @@ -170,7 +170,7 @@ "needAnAccount": "Need an account?", "needAnAccountInfo": "Then sign right up, it only takes a minute.", "needHelp": "Need help", - "newAccountInfo": "When you request an account an email will be sent to you with your user details. If an advanced user profile is requested, the catalog administrator will analyze your request and get back to you.", + "newAccountInfo": "When registering, an email will be sent to you with account details. The catalogue administrator will review group and profile requests.", "next": "Next", "noFileSelected": "No file selected", "noRecordFound": "No record found.", @@ -234,6 +234,7 @@ "register": "Register", "rememberMe": "Remember me", "requestedProfile": "Requested profile", + "requestedGroup": "Requested group", "resetPassword": "Reset password", "resetPasswordTitle": "Reset {{user}} password.", "resetPasswordError": "Error occurred while resetting password", diff --git a/web-ui/src/main/resources/catalog/templates/new-account.html b/web-ui/src/main/resources/catalog/templates/new-account.html index 2b6e10404ff..9b6323c54df 100644 --- a/web-ui/src/main/resources/catalog/templates/new-account.html +++ b/web-ui/src/main/resources/catalog/templates/new-account.html @@ -6,97 +6,141 @@

createAnAccount

newAccountInfo

-
+
personal -
+
- +
-
- +
+
- +
- +
+ +
+ + +
address
- +
- +
- + createAnAccount
- + createAnAccount
\n\ self_registration_disabled=User self-registration is disabled +self_registration_no_valid_mail=The email address is not allowed recaptcha_not_valid=Recaptcha is not valid metadata.title.createdFromTemplate=Copy of template %s created at %s metadata.title.createdFromRecord=Copy of record %s created at %s username.field.required=Username is required password.field.length=Password size should be between {min} and {max} characters password.field.invalid=Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?'); +field.required.name=Name is required +field.required.email=Email address is required +field.notvalid.email=Email address is not valid api.exception.forbidden=Access denied api.exception.forbidden.description=Access is denied. To access, try again with a user containing more privileges. api.exception.resourceNotFound=Resource not found diff --git a/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties b/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties index 55a00915b44..6fe2d2fc083 100644 --- a/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties +++ b/core/src/test/resources/org/fao/geonet/api/Messages_fre.properties @@ -145,10 +145,14 @@ user_watchlist_message=Les fiches suivantes ont \u00E9t\u00E9 mises \u00e0 jour \n\ self_registration_disabled=La cr\u00E9ation de compte par les utilisateurs est d\u00E9sactiv\u00E9e +self_registration_no_valid_mail=L''adresse email n''est pas autoris\u00E9e recaptcha_not_valid=Recaptcha invalide metadata.title.createdFromTemplate=Copie du mod\u00e8le %s cr\u00E9\u00E9e le %s metadata.title.createdFromRecord=Copie de la fiche %s cr\u00E9\u00E9e le %s username.field.required=Le nom d''utilisateur est requis +field.required.name=Nom est obligatoire +field.required.email=L''adresse mail est obligatoire +field.notvalid.email=L''adresse mail n''est pas valide password.field.length=Le mot de passe doit contenir entre {min} et {max} caract\u00E8res password.field.invalid=Le mot de passe doit contenir a minima 1 lettre majuscule, 1 minuscule, 1 chiffre et 1 symbole (ie. `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?')); api.exception.forbidden=L''acc\u00E8s est refus\u00E9 diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index 9d923dd46d8..8f9f567d1e4 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -91,6 +91,9 @@ See [Configuring Shibboleth](../managing-users-and-groups/authentication-mode.md Enable the self registration form. See [User Self-Registration](../managing-users-and-groups/user-self-registration.md). +You can configure optionally re-Captcha, to protect you and your users from spam and abuse. And a list of email domains (separated by commas) +that can request an account. If not configured any email address is allowed. + ## system/userFeedback !!! warning "Deprecated" diff --git a/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java b/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java index 848f6e53e5d..d8974a61687 100644 --- a/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java +++ b/services/src/main/java/org/fao/geonet/api/users/RegisterApi.java @@ -30,6 +30,7 @@ import org.fao.geonet.api.tools.i18n.LanguageUtils; import org.fao.geonet.api.users.model.UserRegisterDto; import org.fao.geonet.api.users.recaptcha.RecaptchaChecker; +import org.fao.geonet.api.users.validation.UserRegisterDtoValidator; import org.fao.geonet.domain.*; import org.fao.geonet.kernel.security.SecurityProviderConfiguration; import org.fao.geonet.kernel.setting.SettingManager; @@ -46,7 +47,6 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -68,6 +68,9 @@ public class RegisterApi { @Autowired(required=false) SecurityProviderConfiguration securityProviderConfiguration; + @Autowired + UserRepository userRepository; + @Autowired GroupRepository groupRepository; @@ -123,39 +126,30 @@ public ResponseEntity registerUser( } } - // Validate the user registration - if (bindingResult.hasErrors()) { - List errorList = bindingResult.getAllErrors(); - - StringBuilder sb = new StringBuilder(); - Iterator it = errorList.iterator(); - while (it.hasNext()) { - sb.append(messages.getString(it.next().getDefaultMessage())); - if (it.hasNext()) { - sb.append(", "); - } - } - - return new ResponseEntity<>(sb.toString(), HttpStatus.PRECONDITION_FAILED); + // Validate userDto data + UserRegisterDtoValidator userRegisterDtoValidator = new UserRegisterDtoValidator(); + userRegisterDtoValidator.validate(userRegisterDto, bindingResult); + String errorMessage = ApiUtils.processRequestValidation(bindingResult, messages); + if (org.apache.commons.lang.StringUtils.isNotEmpty(errorMessage)) { + return new ResponseEntity<>(errorMessage, HttpStatus.PRECONDITION_FAILED); } - final UserRepository userRepository = context.getBean(UserRepository.class); - if (userRepository.findOneByEmail(userRegisterDto.getEmail()) != null) { - return new ResponseEntity<>(String.format( - messages.getString("user_with_that_email_found"), - userRegisterDto.getEmail() - ), HttpStatus.PRECONDITION_FAILED); - } - if (!userRepository.findByUsernameIgnoreCase(userRegisterDto.getEmail()).isEmpty()) { - // username is ignored and the email is used as username in selfregister - return new ResponseEntity<>(String.format( - messages.getString("user_with_that_username_found"), - userRegisterDto.getEmail() - ), HttpStatus.PRECONDITION_FAILED); + String emailDomainsAllowed = settingManager.getValue(Settings.SYSTEM_USERSELFREGISTRATION_EMAIL_DOMAINS); + if (StringUtils.hasLength(emailDomainsAllowed)) { + List emailDomainsAllowedList = Arrays.asList(emailDomainsAllowed.split(",")); + + String userEmailDomain = userRegisterDto.getEmail().split("@")[1]; + + if (!emailDomainsAllowedList.contains(userEmailDomain)) { + return new ResponseEntity<>(String.format( + messages.getString("self_registration_no_valid_mail") + ), HttpStatus.PRECONDITION_FAILED); + } } User user = new User(); + user.setName(userRegisterDto.getName()); user.setSurname(userRegisterDto.getSurname()); user.setOrganisation(userRegisterDto.getOrganisation()); diff --git a/services/src/main/java/org/fao/geonet/api/users/validation/UserRegisterDtoValidator.java b/services/src/main/java/org/fao/geonet/api/users/validation/UserRegisterDtoValidator.java new file mode 100644 index 00000000000..2ba53946b18 --- /dev/null +++ b/services/src/main/java/org/fao/geonet/api/users/validation/UserRegisterDtoValidator.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2001-2024 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.users.validation; + +import org.fao.geonet.ApplicationContextHolder; +import org.fao.geonet.api.users.model.UserRegisterDto; +import org.fao.geonet.constants.Params; +import org.fao.geonet.repository.UserRepository; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +/** + * Validator for UserRegisterDto. + * + */ +public class UserRegisterDtoValidator implements Validator { + private static final String OWASP_EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; + private static final java.util.regex.Pattern OWASP_EMAIL_PATTERN = java.util.regex.Pattern.compile(OWASP_EMAIL_REGEX); + + @Override + public boolean supports(Class clazz) { + return UserRegisterDto.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + UserRegisterDto userRegisterDto = (UserRegisterDto) target; + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "field.required", Params.NAME + + " is required"); + + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "field.required", Params.EMAIL + + " is required"); + + if (StringUtils.hasLength(userRegisterDto.getEmail()) && !OWASP_EMAIL_PATTERN.matcher(userRegisterDto.getEmail()).matches()) { + errors.rejectValue("email", "field.notvalid", "Email address is not valid"); + } + + UserRepository userRepository = ApplicationContextHolder.get().getBean(UserRepository.class); + if (userRepository.findOneByEmail(userRegisterDto.getEmail()) != null) { + errors.rejectValue("", "user_with_that_email_found", "A user with this email or username already exists."); + } + + if (userRepository.findByUsernameIgnoreCase(userRegisterDto.getEmail()).size() != 0) { + errors.rejectValue("", "user_with_that_username_found", "A user with this email or username already exists."); + } + } +} diff --git a/web-ui/src/main/resources/catalog/locales/en-admin.json b/web-ui/src/main/resources/catalog/locales/en-admin.json index 111798db13d..ec570ee7c95 100644 --- a/web-ui/src/main/resources/catalog/locales/en-admin.json +++ b/web-ui/src/main/resources/catalog/locales/en-admin.json @@ -843,6 +843,8 @@ "system/userSelfRegistration": "User self-registration", "system/userSelfRegistration/enable": "Enable self-registration", "system/userSelfRegistration/enable-help": "When enabled, make sure a mail server is also configured.", + "system/userSelfRegistration/domainsAllowed": "Email domains allowed", + "system/userSelfRegistration/domainsAllowed-help": "Comma separated list of email domains that can request an account. If not configured, any email address is allowed.", "system/userSelfRegistration/recaptcha/enable": "Enable re-captcha", "system/userSelfRegistration/recaptcha/enable-help": "Enabling re-captcha will protect you and your users from spam and abuse. This is highly recommended when you enable feedback or self-registration. Create your re-captcha key on https://www.google.com/recaptcha/", "system/userSelfRegistration/recaptcha/publickey": "Re-captcha public key", diff --git a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties index f1ce00195c7..1b25459a27f 100644 --- a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties +++ b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages.properties @@ -159,12 +159,16 @@ user_watchlist_message=The following records have been updated:\n\ \n\ self_registration_disabled=User self-registration is disabled +self_registration_no_valid_mail=The email address is not allowed recaptcha_not_valid=Recaptcha is not valid metadata.title.createdFromTemplate=Copy of template %s created at %s metadata.title.createdFromRecord=Copy of record %s created at %s username.field.required=Username is required password.field.length=Password size should be between {min} and {max} characters password.field.invalid=Password must contain at least 1 uppercase, 1 lowercase, 1 number and 1 symbol. Symbols include: `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?'); +field.required.name=Name is required +field.required.email=Email address is required +field.notvalid.email=Email address is not valid api.exception.forbidden=Access denied api.exception.forbidden.description=Access is denied. To access, try again with a user containing more privileges. api.exception.resourceNotFound=Resource not found diff --git a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties index bdf83dc0908..cdcb274f49b 100644 --- a/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties +++ b/web/src/main/webapp/WEB-INF/classes/org/fao/geonet/api/Messages_fre.properties @@ -145,12 +145,16 @@ user_watchlist_message=Les fiches suivantes ont \u00E9t\u00E9 mises \u00e0 jour \n\ self_registration_disabled=La cr\u00E9ation de compte par les utilisateurs est d\u00E9sactiv\u00E9e +self_registration_no_valid_mail=L''adresse email n''est pas autoris\u00E9e recaptcha_not_valid=Recaptcha invalide metadata.title.createdFromTemplate=Copie du mod\u00e8le %s cr\u00E9\u00E9e le %s metadata.title.createdFromRecord=Copie de la fiche %s cr\u00E9\u00E9e le %s username.field.required=Le nom d''utilisateur est requis password.field.length=Le mot de passe doit contenir entre {min} et {max} caract\u00E8res password.field.invalid=Le mot de passe doit contenir a minima 1 lettre majuscule, 1 minuscule, 1 chiffre et 1 symbole (ie. `~!@#$%^&*()-_=+[]{}\\|;:'",.<>/?')); +field.required.name=Nom est obligatoire +field.required.email=L''adresse mail est obligatoire +field.notvalid.email=L''adresse mail n''est pas valide api.exception.forbidden=L''acc\u00E8s est refus\u00E9 api.exception.forbidden.description=L''acc\u00E8s est refus\u00E9. Pour y acc\u00E9der, r\u00E9essayez avec un utilisateur disposant de plus de privil\u00E8ges. api.exception.resourceNotFound=Ressource introuvable diff --git a/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql b/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql index 5c9e89a1858..ce49770366f 100644 --- a/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql +++ b/web/src/main/webapp/WEB-INF/classes/setup/sql/data/data-db-default.sql @@ -726,10 +726,11 @@ INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('metada INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/ui/defaultView', 'default', 0, 10100, 'n'); + INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/enable', 'false', 2, 1910, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/publickey', '', 0, 1910, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/recaptcha/secretkey', '', 0, 1910, 'y'); - +INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/domainsAllowed', '', 0, 1911, 'y'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doienabled', 'false', 2, 100191, 'n'); INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/publication/doi/doiurl', '', 0, 100192, 'n'); diff --git a/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql b/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql index 12551f10c8e..cdac905504d 100644 --- a/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql +++ b/web/src/main/webapp/WEB-INF/classes/setup/sql/migrate/v446/migrate-default.sql @@ -1,2 +1,4 @@ UPDATE Settings SET value='4.4.6' WHERE name='system/platform/version'; UPDATE Settings SET value='SNAPSHOT' WHERE name='system/platform/subVersion'; + +INSERT INTO Settings (name, value, datatype, position, internal) VALUES ('system/userSelfRegistration/domainsAllowed', '', 0, 1911, 'y'); From 0ec4547e32a606e27b6a7c77413735e9a7694163 Mon Sep 17 00:00:00 2001 From: KoalaGeo Date: Wed, 19 Jun 2024 11:39:05 +0100 Subject: [PATCH 11/76] Update SECURITY.md (#8172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update SECURITY.md Clarify 3.12 is end of life and no longer supported * Update SECURITY.md --------- Co-authored-by: Jose García --- SECURITY.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index fda55f12dad..8ca2726ee51 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,11 +11,11 @@ Each GeoNetwork release is supported with bug fixes for a limited period, with p - We recommend to update to latest incremental release as soon as possible to address security vulnerabilities. - Some overlap is provided when major versions are announced with both a current version and a maintenance version being made available to provide time for organizations to upgrade. -| Version | Supported | Comment | -|---------|--------------------|---------------------| -| 4.4.x | :white_check_mark: | Latest version | -| 4.2.x | :white_check_mark: | Stable version | -| 3.12.x | :white_check_mark: | Maintenance version | +| Version | Supported | Comment | +|---------|--------------------|---------------------------------------------| +| 4.4.x | :white_check_mark: | Latest version | +| 4.2.x | :white_check_mark: | Stable version | +| 3.12.x | ❌ | End Of Life 2024-03-31 | If your organisation is making use of a GeoNetwork version that is no longer in use by the community all is not lost. You can volunteer on the developer list to make additional releases, or engage with one of our [Commercial Support](https://www.osgeo.org/service-providers/?p=geonetwork) providers. From 85d2f96b783630493d8d791be0980ce0557b7470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Wed, 19 Jun 2024 12:42:05 +0200 Subject: [PATCH 12/76] Fix user application feedback (#7769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix user application feedback * Update setting help text and change in the footer the 'Contact' after the 'About' link * Conflict fix (#103) For https://github.com/geonetwork/core-geonetwork/pull/7769/files --------- Co-authored-by: François Prunayre --- .../img/application-feedback-link.png | Bin 0 -> 3172 bytes .../img/application-feedback.png | Bin 0 -> 16222 bytes .../system-configuration.md | 11 +- .../java/org/fao/geonet/api/site/SiteApi.java | 135 +++++++++++++----- .../contactus/ContactUsDirective.js | 83 +++++++++-- .../contactus/partials/contactusform.html | 73 ++++++++-- .../resources/catalog/js/CatController.js | 6 +- .../catalog/js/ContactUsController.js | 12 +- .../resources/catalog/locales/en-admin.json | 4 +- .../resources/catalog/locales/en-core.json | 1 + .../default/less/gn_contact_us_default.less | 15 ++ .../views/default/templates/footer.html | 5 + .../org/fao/geonet/api/Messages.properties | 9 +- .../fao/geonet/api/Messages_fre.properties | 7 + .../webapp/xslt/base-layout-cssjs-loader.xsl | 2 +- .../webapp/xslt/common/base-variables.xsl | 2 + 16 files changed, 287 insertions(+), 78 deletions(-) create mode 100644 docs/manual/docs/administrator-guide/configuring-the-catalog/img/application-feedback-link.png create mode 100644 docs/manual/docs/administrator-guide/configuring-the-catalog/img/application-feedback.png create mode 100644 web-ui/src/main/resources/catalog/views/default/less/gn_contact_us_default.less diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/img/application-feedback-link.png b/docs/manual/docs/administrator-guide/configuring-the-catalog/img/application-feedback-link.png new file mode 100644 index 0000000000000000000000000000000000000000..a52132a35dd6ce4af2796dcd55406c71f3accf33 GIT binary patch literal 3172 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 literal 0 HcmV?d00001 diff --git a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md index 8f9f567d1e4..d56eb454ffa 100644 --- a/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md +++ b/docs/manual/docs/administrator-guide/configuring-the-catalog/system-configuration.md @@ -91,15 +91,18 @@ See [Configuring Shibboleth](../managing-users-and-groups/authentication-mode.md Enable the self registration form. See [User Self-Registration](../managing-users-and-groups/user-self-registration.md). -You can configure optionally re-Captcha, to protect you and your users from spam and abuse. And a list of email domains (separated by commas) +You can configure optionally re-Captcha, to protect you and your users from spam and abuse. And a list of email domains (separated by commas) that can request an account. If not configured any email address is allowed. -## system/userFeedback +## User application feedback -!!! warning "Deprecated" +Enabling the setting, displays in the application footer a link to a page that allows sending comments about the application. + +![](img/application-feedback-link.png) - 3.0.0 +![](img/application-feedback.png) +It requires an email server configured. ## Link in metadata records 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 d39d42f5134..a2bd724fa59 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2023 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -36,17 +36,16 @@ import jeeves.server.context.ServiceContext; import jeeves.xlink.Processor; import org.apache.commons.lang3.StringUtils; -import org.fao.geonet.ApplicationContextHolder; -import org.fao.geonet.GeonetContext; -import org.fao.geonet.NodeInfo; -import org.fao.geonet.SystemInfo; +import org.fao.geonet.*; import org.fao.geonet.api.ApiParams; import org.fao.geonet.api.ApiUtils; 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.site.model.SettingSet; import org.fao.geonet.api.site.model.SettingsListResponse; import org.fao.geonet.api.tools.i18n.LanguageUtils; +import org.fao.geonet.api.users.recaptcha.RecaptchaChecker; import org.fao.geonet.constants.Geonet; import org.fao.geonet.doi.client.DoiManager; import org.fao.geonet.domain.*; @@ -69,6 +68,7 @@ import org.fao.geonet.repository.*; import org.fao.geonet.repository.specification.MetadataSpecs; import org.fao.geonet.resources.Resources; +import org.fao.geonet.util.MailUtil; import org.fao.geonet.utils.FilePathChecker; import org.fao.geonet.utils.Log; import org.fao.geonet.utils.ProxyInfo; @@ -78,15 +78,10 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.*; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; @@ -100,19 +95,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Optional; -import java.util.TimeZone; +import java.util.*; import static org.apache.commons.fileupload.util.Streams.checkFileName; 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; /** * @@ -201,7 +189,7 @@ public static void reloadServices(ServiceContext context) throws Exception { @ResponseBody public SettingsListResponse getSiteOrPortalDescription( @Parameter(hidden = true) - HttpServletRequest request + HttpServletRequest request ) throws Exception { SettingsListResponse response = new SettingsListResponse(); response.setSettings(settingManager.getSettings(new String[]{ @@ -267,7 +255,7 @@ public SettingsListResponse getSettingsSet( @RequestParam( required = false ) - SettingSet[] set, + SettingSet[] set, @Parameter( description = "Setting key", required = false @@ -275,11 +263,11 @@ public SettingsListResponse getSettingsSet( @RequestParam( required = false ) - String[] key, + String[] key, @Parameter( hidden = true ) - HttpSession httpSession + HttpSession httpSession ) throws Exception { ConfigurableApplicationContext appContext = ApplicationContextHolder.get(); UserSession session = ApiUtils.getUserSession(httpSession); @@ -353,7 +341,7 @@ public List getSettingsDetails( @RequestParam( required = false ) - SettingSet[] set, + SettingSet[] set, @Parameter( description = "Setting key", required = false @@ -361,9 +349,9 @@ public List getSettingsDetails( @RequestParam( required = false ) - String[] key, + String[] key, @Parameter(hidden = true) - HttpSession httpSession + HttpSession httpSession ) throws Exception { UserSession session = ApiUtils.getUserSession(httpSession); Profile profile = session == null ? null : session.getProfile(); @@ -415,7 +403,7 @@ public List getSettingsDetails( public void saveSettings( @Parameter(hidden = false) @RequestParam - Map allRequestParams, + Map allRequestParams, HttpServletRequest request ) throws Exception { ApplicationContext applicationContext = ApplicationContextHolder.get(); @@ -450,7 +438,7 @@ public void saveSettings( // Update the system default timezone. If the setting is blank use the timezone user.timezone property from command line or // TZ environment variable String zoneId = StringUtils.defaultIfBlank(settingManager.getValue(Settings.SYSTEM_SERVER_TIMEZONE, true), - SettingManager.DEFAULT_SERVER_TIMEZONE.getId()); + SettingManager.DEFAULT_SERVER_TIMEZONE.getId()); TimeZone.setDefault(TimeZone.getTimeZone(zoneId)); @@ -534,7 +522,7 @@ public boolean isCasEnabled( @PreAuthorize("hasAuthority('Administrator')") public void updateStagingProfile( @PathVariable - SystemInfo.Staging profile) { + SystemInfo.Staging profile) { this.info.setStagingProfile(profile.toString()); } @@ -582,22 +570,22 @@ public HttpEntity indexSite( @Parameter(description = "Drop and recreate index", required = false) @RequestParam(required = false, defaultValue = "true") - boolean reset, + boolean reset, @Parameter(description = "Asynchronous mode (only on all records. ie. no selection bucket)", required = false) @RequestParam(required = false, defaultValue = "false") - boolean asynchronous, + boolean asynchronous, @Parameter(description = "Index. By default only remove record index.", required = false) @RequestParam(required = false, defaultValue = "records") - String[] indices, + String[] indices, @Parameter( description = ApiParams.API_PARAM_BUCKET_NAME, required = false) @RequestParam( required = false ) - String bucket, + String bucket, HttpServletRequest request ) throws Exception { ServiceContext context = ApiUtils.createServiceContext(request); @@ -779,7 +767,7 @@ public ProxyConfiguration getProxyConfiguration( public void setLogo( @Parameter(description = "Logo to use for the catalog") @RequestParam("file") - String file, + String file, @Parameter( description = "Create favicon too", required = false @@ -788,7 +776,7 @@ public void setLogo( defaultValue = "false", required = false ) - boolean asFavicon, + boolean asFavicon, HttpServletRequest request ) throws Exception { @@ -901,4 +889,77 @@ public List getXslTransformations( return list; } } + + + @io.swagger.v3.oas.annotations.Operation( + summary = "Send an email to catalogue administrator with feedback about the application", + description = "") + @PostMapping( + value = "/userfeedback", + produces = MediaType.APPLICATION_JSON_VALUE + ) + @ResponseStatus(HttpStatus.CREATED) + @ResponseBody + public ResponseEntity sendApplicationUserFeedback( + @Parameter( + description = "Recaptcha validation key." + ) + @RequestParam(required = false, defaultValue = "") final String recaptcha, + @Parameter( + description = "User name.", + required = true + ) + @RequestParam final String name, + @Parameter( + description = "User organisation.", + required = true + ) + @RequestParam final String org, + @Parameter( + description = "User email address.", + required = true + ) + @RequestParam final String email, + @Parameter( + description = "A comment or question.", + required = true + ) + @RequestParam final String comments, + @Parameter(hidden = true) final HttpServletRequest request + ) throws Exception { + Locale locale = languageUtils.parseAcceptLanguage(request.getLocales()); + ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", locale); + + boolean feedbackEnabled = settingManager.getValueAsBool(Settings.SYSTEM_USERFEEDBACK_ENABLE, false); + if (!feedbackEnabled) { + throw new FeatureNotEnabledException( + "Application feedback is not enabled.") + .withMessageKey("exception.resourceNotEnabled.applicationFeedback") + .withDescriptionKey("exception.resourceNotEnabled.applicationFeedback.description"); + } + + boolean recaptchaEnabled = settingManager.getValueAsBool(Settings.SYSTEM_USERSELFREGISTRATION_RECAPTCHA_ENABLE); + + if (recaptchaEnabled) { + boolean validRecaptcha = RecaptchaChecker.verify(recaptcha, + settingManager.getValue(Settings.SYSTEM_USERSELFREGISTRATION_RECAPTCHA_SECRETKEY)); + if (!validRecaptcha) { + return new ResponseEntity<>( + messages.getString("recaptcha_not_valid"), HttpStatus.PRECONDITION_FAILED); + } + } + + String to = settingManager.getValue(SYSTEM_FEEDBACK_EMAIL); + + Set toAddress = new HashSet<>(); + toAddress.add(to); + + MailUtil.sendMail(new ArrayList<>(toAddress), + messages.getString("site_user_feedback_title"), + String.format( + messages.getString("site_user_feedback_text"), + name, email, org, comments), + settingManager); + return new ResponseEntity<>(HttpStatus.CREATED); + } } diff --git a/web-ui/src/main/resources/catalog/components/contactus/ContactUsDirective.js b/web-ui/src/main/resources/catalog/components/contactus/ContactUsDirective.js index 1eed76abf55..10df5d42a19 100644 --- a/web-ui/src/main/resources/catalog/components/contactus/ContactUsDirective.js +++ b/web-ui/src/main/resources/catalog/components/contactus/ContactUsDirective.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -31,28 +31,81 @@ */ module.directive("gnContactUsForm", [ "$http", - function ($http) { + "$translate", + "vcRecaptchaService", + "gnConfigService", + "gnConfig", + function ($http, $translate, vcRecaptchaService, gnConfigService, gnConfig) { return { restrict: "A", replace: true, scope: { user: "=" }, - templateUrl: "../../catalog/components/share/" + "partials/contactusform.html", + templateUrl: + "../../catalog/components/contactus/" + "partials/contactusform.html", link: function (scope, element, attrs) { - scope.send = function (formId) { - $http({ - url: "contact.send@json", - method: "POST", - data: $(formId).serialize(), - headers: { "Content-Type": "application/x-www-form-urlencoded" } - }).then(function (response) { - // TODO: report no email sent - if (response.status === 200) { - scope.success = true; - } else { + gnConfigService.load().then(function (c) { + scope.recaptchaEnabled = + gnConfig["system.userSelfRegistration.recaptcha.enable"]; + scope.recaptchaKey = + gnConfig["system.userSelfRegistration.recaptcha.publickey"]; + }); + + scope.resolveRecaptcha = false; + + function initModel() { + scope.feedbackModel = { + name: scope.user.name, + email: scope.user.email, + org: "", + comments: "" + }; + } + + initModel(); + + scope.send = function (form, formId) { + if (scope.recaptchaEnabled) { + if (vcRecaptchaService.getResponse() === "") { + scope.resolveRecaptcha = true; + + var deferred = $q.defer(); + deferred.resolve(""); + return deferred.promise; } - }); + scope.resolveRecaptcha = false; + scope.captcha = vcRecaptchaService.getResponse(); + $("#recaptcha").val(scope.captcha); + } + + if (form.$valid) { + $http({ + url: "../api/site/userfeedback", + method: "POST", + data: $(formId).serialize(), + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }).then( + function (response) { + scope.$emit("StatusUpdated", { + msg: $translate.instant("feebackSent"), + timeout: 2, + type: "info" + }); + initModel(); + }, + function (response) { + scope.success = false; + scope.$emit("StatusUpdated", { + msg: $translate.instant("feebackSentError"), + timeout: 0, + type: "danger" + }); + } + ); + } }; } }; diff --git a/web-ui/src/main/resources/catalog/components/contactus/partials/contactusform.html b/web-ui/src/main/resources/catalog/components/contactus/partials/contactusform.html index 716d5e9dc26..059b1bcd449 100644 --- a/web-ui/src/main/resources/catalog/components/contactus/partials/contactusform.html +++ b/web-ui/src/main/resources/catalog/components/contactus/partials/contactusform.html @@ -1,31 +1,74 @@ -
+ -
-

{{date.type | translate}}

+

{{date.type | translate}}

@@ -79,9 +79,7 @@

updateFrequency

-

- {{(t.multilingualTitle.default || t.title || key) | translate}} -

+

{{(t.multilingualTitle.default || t.title || key) | translate}}

From 9378975832f3fd70d29f9cd90176f1d86306b580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= Date: Tue, 25 Jun 2024 08:46:59 +0200 Subject: [PATCH 21/76] API / Client code generation / Avoid reserved word (#8214) When using tools to convert OpenAPI to client code eg. ```bash npx openapi-ts -i ./src/gapi/gapi.json -o src/gapi ``` Some reserved word may create invalid generated code. eg. when building typescript: ``` TS1102: 'delete' cannot be called on an identifier in strict mode. [ ``` --- .../src/main/java/org/fao/geonet/api/processing/ProcessApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java b/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java index c832ab913a0..775874d572d 100644 --- a/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java +++ b/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java @@ -105,7 +105,7 @@ public List getProcessReport() throws Exception { @ResponseBody @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("isAuthenticated()") - public void delete() throws Exception { + public void deleteProcessReport() throws Exception { registry.clear(); } From 4c63865fea9e23745d9071ab6188751e1df51087 Mon Sep 17 00:00:00 2001 From: wangf1122 <74916635+wangf1122@users.noreply.github.com> Date: Tue, 25 Jun 2024 07:00:11 -0400 Subject: [PATCH 22/76] Add info logs to make transaction of working copy merge more traceable (#8178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add info logs to make transaction of working copy merge more traceable * build fix * Update listeners/src/main/java/org/fao/geonet/listener/metadata/draft/DraftUtilities.java Co-authored-by: Jose García * Update listeners/src/main/java/org/fao/geonet/listener/metadata/draft/DraftUtilities.java Co-authored-by: Jose García * Update datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java Co-authored-by: Jose García * Update datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java Co-authored-by: Jose García * add more logs for delete folder * build fix * build fix * Update core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java Co-authored-by: Jose García --------- Co-authored-by: Jose García Co-authored-by: Ian --- .../fao/geonet/api/records/attachments/FilesystemStore.java | 1 + .../org/fao/geonet/api/records/attachments/CMISStore.java | 5 +++++ .../org/fao/geonet/api/records/attachments/JCloudStore.java | 1 + .../java/org/fao/geonet/api/records/attachments/S3Store.java | 4 ++++ .../fao/geonet/listener/metadata/draft/DraftUtilities.java | 2 ++ 5 files changed, 13 insertions(+) diff --git a/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java b/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java index 4fc31b3f7a2..fb0577bc8bd 100644 --- a/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java +++ b/core/src/main/java/org/fao/geonet/api/records/attachments/FilesystemStore.java @@ -232,6 +232,7 @@ private Path getPath(ServiceContext context, int metadataId, MetadataResourceVis public String delResources(ServiceContext context, int metadataId) throws Exception { Path metadataDir = Lib.resource.getMetadataDir(getDataDirectory(context), metadataId); try { + Log.info(Geonet.RESOURCES, String.format("Deleting all files from metadataId '%d'", metadataId)); IO.deleteFileOrDirectory(metadataDir, true); return String.format("Metadata '%s' directory removed.", metadataId); } catch (Exception e) { diff --git a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java index cc39c8d2d78..de258b3a711 100644 --- a/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java +++ b/datastorages/cmis/src/main/java/org/fao/geonet/api/records/attachments/CMISStore.java @@ -391,6 +391,7 @@ public String delResources(final ServiceContext context, final int metadataId) t folderKey = getMetadataDir(context, metadataId); final Folder folder = cmisUtils.getFolderCache(folderKey, true); + Log.info(Geonet.RESOURCES, String.format("Deleting the folder of '%s' and the files within the folder", folderKey)); folder.deleteTree(true, UnfileObject.DELETE, true); cmisUtils.invalidateFolderCache(folderKey); @@ -508,7 +509,9 @@ public MetadataResourceContainer getResourceContainerDescription(final ServiceCo @Override public void copyResources(ServiceContext context, String sourceUuid, String targetUuid, MetadataResourceVisibility metadataResourceVisibility, boolean sourceApproved, boolean targetApproved) throws Exception { final int sourceMetadataId = canEdit(context, sourceUuid, metadataResourceVisibility, sourceApproved); + final int targetMetadataId = canEdit(context, sourceUuid, metadataResourceVisibility, targetApproved); final String sourceResourceTypeDir = getMetadataDir(context, sourceMetadataId) + cmisConfiguration.getFolderDelimiter() + metadataResourceVisibility.toString(); + final String targetResourceTypeDir = getMetadataDir(context, targetMetadataId) + cmisConfiguration.getFolderDelimiter() + metadataResourceVisibility.toString(); try { Folder sourceParentFolder = cmisUtils.getFolderCache(sourceResourceTypeDir, true); @@ -522,6 +525,8 @@ public void copyResources(ServiceContext context, String sourceUuid, String targ for (Map.Entry sourceEntry : sourceDocumentMap.entrySet()) { Document sourceDocument = sourceEntry.getValue(); + + Log.info(Geonet.RESOURCES, String.format("Copying %s to %s" , sourceResourceTypeDir+cmisConfiguration.getFolderDelimiter()+sourceDocument.getName(), targetResourceTypeDir)); // Get cmis properties from the source document Map sourceProperties = getProperties(sourceDocument); putResource(context, targetUuid, sourceDocument.getName(), sourceDocument.getContentStream().getStream(), null, metadataResourceVisibility, targetApproved, sourceProperties); diff --git a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java index ac9f80d243d..067809526d1 100644 --- a/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java +++ b/datastorages/jcloud/src/main/java/org/fao/geonet/api/records/attachments/JCloudStore.java @@ -243,6 +243,7 @@ public String delResources(final ServiceContext context, final int metadataId) t ListContainerOptions opts = new ListContainerOptions(); opts.prefix(getMetadataDir(context, metadataId) + jCloudConfiguration.getFolderDelimiter()).recursive(); + Log.info(Geonet.RESOURCES, String.format("Deleting all files from metadataId '%s'", metadataId)); // Page through the data String marker = null; do { diff --git a/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java b/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java index aa59182c9b6..27df07a4532 100644 --- a/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java +++ b/datastorages/s3/src/main/java/org/fao/geonet/api/records/attachments/S3Store.java @@ -34,11 +34,13 @@ import com.amazonaws.services.s3.model.S3ObjectSummary; import jeeves.server.context.ServiceContext; import org.fao.geonet.api.exception.ResourceNotFoundException; +import org.fao.geonet.constants.Geonet; import org.fao.geonet.domain.MetadataResource; import org.fao.geonet.domain.MetadataResourceContainer; import org.fao.geonet.domain.MetadataResourceVisibility; import org.fao.geonet.kernel.setting.SettingManager; import org.fao.geonet.resources.S3Credentials; +import org.fao.geonet.utils.Log; import org.springframework.beans.factory.annotation.Autowired; import java.io.File; @@ -186,6 +188,8 @@ public String delResources(final ServiceContext context, final int metadataId) t try { final ListObjectsV2Result objects = s3.getClient().listObjectsV2( s3.getBucket(), getMetadataDir(metadataId)); + + Log.info(Geonet.RESOURCES, String.format("Deleting all files from metadataId '%s'", metadataId)); for (S3ObjectSummary object: objects.getObjectSummaries()) { s3.getClient().deleteObject(s3.getBucket(), object.getKey()); } diff --git a/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/DraftUtilities.java b/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/DraftUtilities.java index c3f6cf7704c..ce418b4062f 100644 --- a/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/DraftUtilities.java +++ b/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/DraftUtilities.java @@ -87,6 +87,7 @@ public AbstractMetadata replaceMetadataWithDraft(AbstractMetadata md) { * @return */ public AbstractMetadata replaceMetadataWithDraft(AbstractMetadata md, AbstractMetadata draft) { + Log.info(Geonet.DATA_MANAGER, String.format("Replacing metadata approved record (%d) with draft record (%d)", md.getId(), draft.getId())); Log.trace(Geonet.DATA_MANAGER, "Found approved record with id " + md.getId()); Log.trace(Geonet.DATA_MANAGER, "Found draft with id " + draft.getId()); // Reassign metadata validations @@ -131,6 +132,7 @@ public AbstractMetadata replaceMetadataWithDraft(AbstractMetadata md, AbstractMe } // Reassign file uploads + Log.info(Geonet.DATA_MANAGER, String.format("Copying draft record '%d' resources to approved record '%d'", draft.getId(), md.getId())); draftMetadataUtils.replaceFiles(draft, md); metadataFileUploadRepository.deleteAll(MetadataFileUploadSpecs.hasMetadataId(md.getId())); From f0debae33f4ca6bbd4214f5921fa38a1924dfb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= Date: Fri, 28 Jun 2024 16:21:19 +0200 Subject: [PATCH 23/76] Editor / Polygon not saved (#8230) Related to JQuery update. --- .../components/edit/bounding/partials/boundingpolygon.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/main/resources/catalog/components/edit/bounding/partials/boundingpolygon.html b/web-ui/src/main/resources/catalog/components/edit/bounding/partials/boundingpolygon.html index 6d6d612364b..7ca025f75a9 100644 --- a/web-ui/src/main/resources/catalog/components/edit/bounding/partials/boundingpolygon.html +++ b/web-ui/src/main/resources/catalog/components/edit/bounding/partials/boundingpolygon.html @@ -90,7 +90,7 @@ placeholder="{{'inputGeometryText' | translate}}" ng-change="ctrl.handleInputChange()" ng-readonly="ctrl.readOnly" - /> + > {{ 'inputGeometryIsInvalid' | translate}} {{ctrl.parseError}} From e22bce74f42ac1b3ad5945f6daf4cedfcb17a946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Mon, 1 Jul 2024 07:56:36 +0200 Subject: [PATCH 24/76] Fix wrong HTML self closing tags (#8232) --- .../directives/partials/facet-temporalrange.html | 6 +++--- .../ng-skos/templates/skos-concept-thesaurus.html | 2 +- .../resources/catalog/templates/admin/usergroup/users.html | 2 +- .../main/resources/catalog/templates/formatter-viewer.html | 2 +- .../views/default/directives/partials/attributetable.html | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html b/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html index 02cf7c2e574..8029f53fbf8 100644 --- a/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html +++ b/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html @@ -15,7 +15,7 @@ name="start" /> - + - + diff --git a/web-ui/src/main/resources/catalog/components/ng-skos/templates/skos-concept-thesaurus.html b/web-ui/src/main/resources/catalog/components/ng-skos/templates/skos-concept-thesaurus.html index 0dae7eea07c..f8407b708ec 100644 --- a/web-ui/src/main/resources/catalog/components/ng-skos/templates/skos-concept-thesaurus.html +++ b/web-ui/src/main/resources/catalog/components/ng-skos/templates/skos-concept-thesaurus.html @@ -44,7 +44,7 @@ Related Terms:
  • - +
diff --git a/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html b/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html index 3c7271c9d70..39a7163574e 100644 --- a/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html +++ b/web-ui/src/main/resources/catalog/templates/admin/usergroup/users.html @@ -614,7 +614,7 @@

UserAdmin

data-gn-saved-selections="" data-ng-if="userSelected.id > 0" data-gn-saved-selections-panel="userSelected" - /> + > diff --git a/web-ui/src/main/resources/catalog/templates/formatter-viewer.html b/web-ui/src/main/resources/catalog/templates/formatter-viewer.html index e0a7427ae43..7a04c8b83b4 100644 --- a/web-ui/src/main/resources/catalog/templates/formatter-viewer.html +++ b/web-ui/src/main/resources/catalog/templates/formatter-viewer.html @@ -3,7 +3,7 @@
+ >
{{attribute.definition}} - + From cddac34e74d78fd65ab5664dd8e784ad37c23e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= Date: Wed, 3 Jul 2024 08:07:56 +0200 Subject: [PATCH 25/76] Standard / ISO19139 / Formatter / Do not display extent if none available (#8229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Standard / ISO19139 / Formatter / Do not display extent if none available * Update schemas/iso19139/src/main/plugin/iso19139/formatter/xsl-view/view.xsl Co-authored-by: Jose García --------- Co-authored-by: Jose García --- .../iso19139/formatter/xsl-view/view.xsl | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/schemas/iso19139/src/main/plugin/iso19139/formatter/xsl-view/view.xsl b/schemas/iso19139/src/main/plugin/iso19139/formatter/xsl-view/view.xsl index 65dc278704e..4f5f259dc21 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/formatter/xsl-view/view.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/formatter/xsl-view/view.xsl @@ -193,23 +193,25 @@ -
-

- - - - -

- - - - - - - - - -
+ +
+

+ + + + +

+ + + + + + + + + +
+
From b5c29b8a4c17bffc297b2fe16c03cd8d539e3568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= Date: Wed, 3 Jul 2024 10:22:09 +0200 Subject: [PATCH 26/76] ISO19115-3.2018 / Remove duplicated fields for metadata identifier and uuid in CSV export (#8238) --- .../src/main/plugin/iso19115-3.2018/layout/tpl-csv.xsl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/layout/tpl-csv.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/layout/tpl-csv.xsl index 0ab5bd3adad..d5ae0576c7d 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/layout/tpl-csv.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/layout/tpl-csv.xsl @@ -29,12 +29,6 @@ priority="2"> - - - - - - <xsl:apply-templates mode="localised" select="mdb:identificationInfo/*/mri:citation/*/cit:title"> From 4b9864ad3fe4da33d93497f79ee5bd3e28bd6b9d Mon Sep 17 00:00:00 2001 From: wangf1122 <74916635+wangf1122@users.noreply.github.com> Date: Tue, 9 Jul 2024 11:16:22 -0400 Subject: [PATCH 27/76] Broadcasting error when delete record (#8212) Show a warning to the user when an error happens while deleting one or more metadata records. --- .../components/metadataactions/MetadataActionService.js | 4 ++++ .../main/resources/catalog/js/edit/EditorBoardController.js | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js index 84a9d9a2492..100ebfca458 100644 --- a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js +++ b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js @@ -286,6 +286,10 @@ deferred.resolve(data); }, function (data) { + gnAlertService.addAlert({ + msg: data.data.message || data.data.description, + type: "danger" + }); deferred.reject(data); } ); diff --git a/web-ui/src/main/resources/catalog/js/edit/EditorBoardController.js b/web-ui/src/main/resources/catalog/js/edit/EditorBoardController.js index bb5c459b0a8..15ed806c00a 100644 --- a/web-ui/src/main/resources/catalog/js/edit/EditorBoardController.js +++ b/web-ui/src/main/resources/catalog/js/edit/EditorBoardController.js @@ -135,7 +135,8 @@ }, function (reason) { $rootScope.$broadcast("StatusUpdated", { - title: reason.data.description, //returned error JSON obj + title: reason.data.message, //returned error JSON obj + error: reason.data.description, timeout: 0, type: "danger" }); From 46188341d243ec2858a6d3f147d2b23d50e51ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Luis=20Rodr=C3=ADguez=20Ponce?= <juanluisrp@geocat.net> Date: Wed, 10 Jul 2024 08:16:12 +0200 Subject: [PATCH 28/76] Fix infinite "Please wait" message on error (#8249) If an error happens while using the delete, validate or other option of the editor dashboard metadata actions menu then remove the text "Please wait" and the spinner from the button to allow to perform a new action. --- .../metadataactions/MetadataActionService.js | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js index 100ebfca458..77429954b58 100644 --- a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js +++ b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js @@ -275,24 +275,28 @@ ); } else { $rootScope.$broadcast("operationOnSelectionStart"); - $http.delete("../api/records?" + "bucket=" + bucket).then( - function (data) { - $rootScope.$broadcast("mdSelectNone"); - $rootScope.$broadcast("operationOnSelectionStop"); - $rootScope.$broadcast("search"); - $timeout(function () { + $http + .delete("../api/records?" + "bucket=" + bucket) + .then( + function (data) { + $rootScope.$broadcast("mdSelectNone"); $rootScope.$broadcast("search"); - }, 5000); - deferred.resolve(data); - }, - function (data) { - gnAlertService.addAlert({ - msg: data.data.message || data.data.description, - type: "danger" - }); - deferred.reject(data); - } - ); + $timeout(function () { + $rootScope.$broadcast("search"); + }, 5000); + deferred.resolve(data); + }, + function (data) { + gnAlertService.addAlert({ + msg: data.data.message || data.data.description, + type: "danger" + }); + deferred.reject(data); + } + ) + .finally(function () { + $rootScope.$broadcast("operationOnSelectionStop"); + }); } return deferred.promise; }; @@ -644,8 +648,10 @@ }) .then(function (data) { $rootScope.$broadcast("inspireMdValidationStop"); - $rootScope.$broadcast("operationOnSelectionStop"); $rootScope.$broadcast("search"); + }) + .finally(function () { + $rootScope.$broadcast("operationOnSelectionStop"); }); }; @@ -657,8 +663,10 @@ method: "DELETE" }) .then(function (data) { - $rootScope.$broadcast("operationOnSelectionStop"); $rootScope.$broadcast("search"); + }) + .finally(function () { + $rootScope.$broadcast("operationOnSelectionStop"); }); }; From 9b19b580af85953cbaeb0b3ea9c36601491e5e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Wed, 10 Jul 2024 09:05:30 +0200 Subject: [PATCH 29/76] Editor / Configuration / Improve deletion in forEach section (#8244) Improve documentation and properly target node to delete in case the forEach loop element is not the one to remove. eg. ```xml <section name="Axe - Time" forEach="/mdb:MD_Metadata/mdb:spatialRepresentationInfo/*/msr:axisDimensionProperties/*[msr:dimensionName/*/@codeListValue = 'time']" del="ancestor::msr:axisDimensionProperties"> <field xpath="msr:dimensionSize"/> <field xpath="msr:resolution"/> </section> ``` Before the change, the remove button was not displayed. Most of the time the loop element is the one to delete and this case was fine. --- schemas/config-editor.xsd | 16 +++++++++------- .../xslt/ui-metadata/form-configurator.xsl | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/schemas/config-editor.xsd b/schemas/config-editor.xsd index ac697476f41..31455f489a3 100644 --- a/schemas/config-editor.xsd +++ b/schemas/config-editor.xsd @@ -906,7 +906,7 @@ Define if this tab is the default one for the view. Only one tab should be the d <xs:annotation> <xs:documentation><![CDATA[ The tab key used in URL parameter to activate that tab. The key is also use for the tab label as defined in ``{schema}/loc/{lang}/strings.xml``. -It has to be unique for all views. A good practice is to use same prefix for all tabs in the same view. +It has to be unique for all views. A good practice is to use same prefix for all tabs in the same view. ]]></xs:documentation> </xs:annotation> </xs:attribute> @@ -1128,9 +1128,11 @@ Note: Only sections with forEach support del attribute. <section forEach="/gmd:MD_Metadata/gmd:distributionInfo"> <text><h2>Distribution</h2></text> - <section forEach="*/gmd:transferOptions/*/gmd:onLine/gmd:CI_OnlineResource" name="A link"> - <field xpath="gmd:linkage"/> - <field xpath="gmd:name" or="name" in="."/> + <section forEach="*/gmd:transferOptions/*/gmd:onLine" + name="A link" + del="."> + <field xpath="*/gmd:linkage"/> + <field xpath="*/gmd:name" or="name" in="."/> </section> <text><hr/></text> </section> @@ -2111,9 +2113,9 @@ An autocompletion list based on a thesaurus. This field facilitates users in selecting a `subtemplate` (also known as xml-snippet) from the catalogue. Subtemplates are mostly used to store contact details, but can also be used to store snippets of xml having Quality reports, Access constraints, CRS definitions, etc. - -`data-insert-modes` can be `text` and/or `xlink` depending on how the subtemplate is encoded. -Contact can be forced to be `xlink` but some other type of subtemplates (eg. DQ report) are usually just simple default values + +`data-insert-modes` can be `text` and/or `xlink` depending on how the subtemplate is encoded. +Contact can be forced to be `xlink` but some other type of subtemplates (eg. DQ report) are usually just simple default values that need to be detailed by editors and in that case `text` mode is recommended. For `xlink`, the XLink resolver needs to be enabled in the admin settings. diff --git a/web/src/main/webapp/xslt/ui-metadata/form-configurator.xsl b/web/src/main/webapp/xslt/ui-metadata/form-configurator.xsl index ce17645dc19..2e423b7ea75 100644 --- a/web/src/main/webapp/xslt/ui-metadata/form-configurator.xsl +++ b/web/src/main/webapp/xslt/ui-metadata/form-configurator.xsl @@ -151,7 +151,7 @@ <xsl:variable name="originalNode" select="gn-fn-metadata:getOriginalNode($metadata, .)"/> - <xsl:variable name="refToDelete"> + <xsl:variable name="refToDelete" as="node()?"> <xsl:call-template name="get-ref-element-to-delete"> <xsl:with-param name="node" select="$originalNode"/> <xsl:with-param name="delXpath" select="$del"/> @@ -159,7 +159,7 @@ </xsl:variable> <xsl:call-template name="render-form-field-control-remove"> - <xsl:with-param name="editInfo" select="gn:element"/> + <xsl:with-param name="editInfo" select="$refToDelete"/> </xsl:call-template> </xsl:if> </legend> @@ -329,7 +329,7 @@ select="gn-fn-metadata:check-elementandsession-visibility( $schema, $base, $serviceInfo, @if, @displayIfServiceInfo)"/> - <!-- + <!-- <xsl:message> Field: <xsl:value-of select="@name"/></xsl:message> <xsl:message>Xpath: <xsl:copy-of select="@xpath"/></xsl:message> <xsl:message>TemplateModeOnly: <xsl:value-of select="@templateModeOnly"/></xsl:message> From 427eae7fb084d49c15f1b8f857855a22381f6da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Thu, 11 Jul 2024 09:05:31 +0200 Subject: [PATCH 30/76] Standard / ISO19115-3 / Formatter / Fix namespace declaration (#8223) `xmlns:srv` was declared 2 times. Fixes https://github.com/geonetwork/core-geonetwork/issues/8221 Related to https://github.com/geonetwork/core-geonetwork/commit/27a69dfa7c0214b4e3c9971b2e652dbba9742d36#diff-1f83f3131214c0592eea4a9f65ed3a58a889689a64e9f07bc0753544c0427b29 --- .../src/main/plugin/iso19115-3.2018/formatter/iso19139/view.xsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/formatter/iso19139/view.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/formatter/iso19139/view.xsl index d388bbff7f2..95c9643d53f 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/formatter/iso19139/view.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/formatter/iso19139/view.xsl @@ -39,7 +39,7 @@ xmlns:gcx="http://standards.iso.org/iso/19115/-3/gcx/1.0" xmlns:gex="http://standards.iso.org/iso/19115/-3/gex/1.0" xmlns:lan="http://standards.iso.org/iso/19115/-3/lan/1.0" - xmlns:srv="http://standards.iso.org/iso/19115/-3/srv/2.0" + xmlns:srv2="http://standards.iso.org/iso/19115/-3/srv/2.0" xmlns:mac="http://standards.iso.org/iso/19115/-3/mac/2.0" xmlns:mas="http://standards.iso.org/iso/19115/-3/mas/1.0" xmlns:mcc="http://standards.iso.org/iso/19115/-3/mcc/1.0" From 111a1d7f79fe23b5663e727aeaf126277576fe89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Thu, 11 Jul 2024 09:14:32 +0200 Subject: [PATCH 31/76] Standard / ISO19115-3 / Formatters / ISO19139 / Fix scope code (#8224) Fixes "md:scope will be gmd:MD_Scope when it should be gmd:DQ_Scope" See https://github.com/geonetwork/core-geonetwork/issues/8220 --- .../convert/ISO19139/toISO19139.xsl | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl index 66500e82d1f..4955ec576ca 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl @@ -553,24 +553,30 @@ <xsl:template match="mdb:resourceLineage[not(../mdb:dataQualityInfo)]"> <gmd:dataQualityInfo> <gmd:DQ_DataQuality> - <xsl:apply-templates select="mrl:LI_Lineage/mrl:scope"/> - <gmd:lineage> - <gmd:LI_Lineage> - <xsl:call-template name="writeCharacterStringElement"> - <xsl:with-param name="elementName" - select="'gmd:statement'"/> - <xsl:with-param name="nodeWithStringToWrite" - select="mrl:LI_Lineage/mrl:statement"/> - </xsl:call-template> - <xsl:apply-templates select="mrl:LI_Lineage/mrl:processStep"/> - <xsl:apply-templates select="mrl:LI_Lineage/mrl:source"/> - </gmd:LI_Lineage> - </gmd:lineage> + <xsl:if test="mrl:LI_Lineage/mrl:scope"> + <gmd:scope> + <gmd:DQ_Scope> + <xsl:apply-templates select="mrl:LI_Lineage/mrl:scope/@*"/> + <xsl:apply-templates select="mrl:LI_Lineage/mrl:scope/mcc:MD_Scope/*"/> + </gmd:DQ_Scope> + </gmd:scope> + </xsl:if> + <gmd:lineage> + <gmd:LI_Lineage> + <xsl:call-template name="writeCharacterStringElement"> + <xsl:with-param name="elementName" + select="'gmd:statement'"/> + <xsl:with-param name="nodeWithStringToWrite" + select="mrl:LI_Lineage/mrl:statement"/> + </xsl:call-template> + <xsl:apply-templates select="mrl:LI_Lineage/mrl:processStep"/> + <xsl:apply-templates select="mrl:LI_Lineage/mrl:source"/> + </gmd:LI_Lineage> + </gmd:lineage> </gmd:DQ_DataQuality> </gmd:dataQualityInfo> </xsl:template> - <xsl:template match="mmi:maintenanceDate"> <gmd:dateOfNextUpdate> <xsl:apply-templates select="cit:CI_Date/cit:date/*"/> From 564771f40c24ca1e5756428127aff17cebfcd0e1 Mon Sep 17 00:00:00 2001 From: Ian <ianwallen@hotmail.com> Date: Fri, 19 Jul 2024 12:00:20 -0300 Subject: [PATCH 32/76] Fixed issue with working copy not being returned from getRecordAS api (#8265) --- .../src/main/java/org/fao/geonet/api/records/MetadataApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java index 64bb64c4200..c62a7c4a76c 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataApi.java @@ -366,7 +366,7 @@ private Object getRecordAs( throws Exception { AbstractMetadata metadata; try { - metadata = ApiUtils.canViewRecord(metadataUuid, request); + metadata = ApiUtils.canViewRecord(metadataUuid, approved, request); } catch (ResourceNotFoundException e) { Log.debug(API.LOG_MODULE_NAME, e.getMessage(), e); throw e; From fb966e37c43615be218de532842449992a8a09ea Mon Sep 17 00:00:00 2001 From: Ian <ianwallen@hotmail.com> Date: Mon, 22 Jul 2024 03:05:06 -0300 Subject: [PATCH 33/76] Fixed issue with working copy not being returned from /api/records/{metadataUuid}/formatters/{formatterId:.+} (#8269) This affect PDF exports on working copies. --- .../org/fao/geonet/api/records/formatters/FormatterApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterApi.java b/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterApi.java index 1a9830c3ef6..068c63f60c7 100644 --- a/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/formatters/FormatterApi.java @@ -252,7 +252,7 @@ public void getRecordFormattedBy( language = isoLanguagesMapper.iso639_2T_to_iso639_2B(locale.getISO3Language()); } - AbstractMetadata metadata = ApiUtils.canViewRecord(metadataUuid, servletRequest); + AbstractMetadata metadata = ApiUtils.canViewRecord(metadataUuid, approved, servletRequest); if (approved) { metadata = ApplicationContextHolder.get().getBean(MetadataRepository.class).findOneByUuid(metadataUuid); From f78fda51b89480ea2705e2902a92dce571a9719c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Tue, 23 Jul 2024 14:01:47 +0200 Subject: [PATCH 34/76] Use UI language for metadata selection export to CSV / PDF. Fixes #7969 (#8262) Use the UI language for the metadata information, when exporting a metadata selection to CSV / PDF. The API for these services accept a new optional parameter language that defaults to English as previously. Fixes #7969. --- .../fao/geonet/api/records/CatalogApi.java | 85 ++++++++++++------- .../guiapi/search/XsltResponseWriter.java | 19 ++--- .../metadataactions/MetadataActionService.js | 12 ++- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java b/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java index e7547678245..e6751f8e3ef 100644 --- a/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2023 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -125,8 +125,8 @@ public class CatalogApi { .add("geom") .add(SOURCE_CATALOGUE) .add(Geonet.IndexFieldNames.DATABASE_CHANGE_DATE) - .add("resourceTitleObject.default") // TODOES multilingual - .add("resourceAbstractObject.default").build(); + .add(Geonet.IndexFieldNames.RESOURCETITLE + "Object") + .add(Geonet.IndexFieldNames.RESOURCEABSTRACT + "Object").build(); } @Autowired @@ -167,7 +167,7 @@ private static String paramsAsString(Map<String, String> requestParams) { StringBuilder paramNonPaging = new StringBuilder(); for (Entry<String, String> pair : requestParams.entrySet()) { if (!pair.getKey().equals("from") && !pair.getKey().equals("to")) { - paramNonPaging.append(paramNonPaging.toString().equals("") ? "" : "&").append(pair.getKey()).append("=").append(pair.getValue()); + paramNonPaging.append(paramNonPaging.toString().isEmpty() ? "" : "&").append(pair.getKey()).append("=").append(pair.getValue()); } } return paramNonPaging.toString(); @@ -364,6 +364,11 @@ public void exportAsPdf( required = false ) String bucket, + @RequestParam( + required = false, + defaultValue = "eng" + ) + String language, @Parameter(hidden = true) @RequestParam Map<String, String> allRequestParams, @@ -392,68 +397,74 @@ public void exportAsPdf( Map<String, Object> params = new HashMap<>(); Element request = new Element("request"); - allRequestParams.entrySet().forEach(e -> { - Element n = new Element(e.getKey()); - n.setText(e.getValue()); + allRequestParams.forEach((key, value) -> { + Element n = new Element(key); + n.setText(value); request.addContent(n); }); + if (!languageUtils.getUiLanguages().contains(language)) { + language = languageUtils.getDefaultUiLanguage(); + } + + String langCode = "lang" + language; + Element response = new Element("response"); ObjectMapper objectMapper = new ObjectMapper(); searchResponse.hits().hits().forEach(h1 -> { Hit h = (Hit) h1; Element r = new Element("metadata"); final Map<String, Object> source = objectMapper.convertValue(h.source(), Map.class); - source.entrySet().forEach(e -> { - Object v = e.getValue(); + source.forEach((key, v) -> { if (v instanceof String) { - Element t = new Element(e.getKey()); + Element t = new Element(key); t.setText((String) v); r.addContent(t); - } else if (v instanceof HashMap && e.getKey().endsWith("Object")) { - Element t = new Element(e.getKey()); - Map<String, String> textFields = (HashMap) e.getValue(); - t.setText(textFields.get("default")); + } else if (v instanceof HashMap && key.endsWith("Object")) { + Element t = new Element(key); + Map<String, String> textFields = (HashMap) v; + String textValue = textFields.get(langCode) != null ? textFields.get(langCode) : textFields.get("default"); + t.setText(textValue); r.addContent(t); - } else if (v instanceof ArrayList && e.getKey().equals("link")) { + } else if (v instanceof ArrayList && key.equals("link")) { //landform|Physiography of North and Central Eurasia Landform|http://geonetwork3.fao.org/ows/7386_landf|OGC:WMS-1.1.1-http-get-map|application/vnd.ogc.wms_xml ((ArrayList) v).forEach(i -> { - Element t = new Element(e.getKey()); + Element t = new Element(key); Map<String, String> linkProperties = (HashMap) i; t.setText(linkProperties.get("description") + "|" + linkProperties.get("name") + "|" + linkProperties.get("url") + "|" + linkProperties.get("protocol")); r.addContent(t); }); - } else if (v instanceof HashMap && e.getKey().equals("overview")) { - Element t = new Element(e.getKey()); + } else if (v instanceof HashMap && key.equals("overview")) { + Element t = new Element(key); Map<String, String> overviewProperties = (HashMap) v; t.setText(overviewProperties.get("url") + "|" + overviewProperties.get("name")); r.addContent(t); } else if (v instanceof ArrayList) { ((ArrayList) v).forEach(i -> { - if (i instanceof HashMap && e.getKey().equals("overview")) { - Element t = new Element(e.getKey()); + if (i instanceof HashMap && key.equals("overview")) { + Element t = new Element(key); Map<String, String> overviewProperties = (HashMap) i; t.setText(overviewProperties.get("url") + "|" + overviewProperties.get("name")); r.addContent(t); } else if (i instanceof HashMap) { - Element t = new Element(e.getKey()); + Element t = new Element(key); Map<String, String> tags = (HashMap) i; t.setText(tags.get("default")); // TODOES: Multilingual support r.addContent(t); } else { - Element t = new Element(e.getKey()); + Element t = new Element(key); t.setText((String) i); r.addContent(t); } }); - } else if (v instanceof HashMap && e.getKey().equals("geom")) { - Element t = new Element(e.getKey()); + } else if (v instanceof HashMap && key.equals("geom")) { + Element t = new Element(key); t.setText(((HashMap) v).get("coordinates").toString()); r.addContent(t); } else if (v instanceof HashMap) { // Skip. } else { - Element t = new Element(e.getKey()); + Element t = new Element(key); t.setText(v.toString()); r.addContent(t); } @@ -461,14 +472,13 @@ public void exportAsPdf( response.addContent(r); }); - Locale locale = languageUtils.parseAcceptLanguage(httpRequest.getLocales()); - String language = IsoLanguagesMapper.iso639_2T_to_iso639_2B(locale.getISO3Language()); - language = XslUtil.twoCharLangCode(language, "eng").toLowerCase(); - new XsltResponseWriter("env", "search") - .withJson(String.format("catalog/locales/%s-v4.json", language)) - .withJson(String.format("catalog/locales/%s-core.json", language)) - .withJson(String.format("catalog/locales/%s-search.json", language)) + String language2Code = XslUtil.twoCharLangCode(language, "eng").toLowerCase(); + + new XsltResponseWriter("env", "search", language) + .withJson(String.format("catalog/locales/%s-v4.json", language2Code)) + .withJson(String.format("catalog/locales/%s-core.json", language2Code)) + .withJson(String.format("catalog/locales/%s-search.json", language2Code)) .withXml(response) .withParams(params) .withXsl("xslt/services/pdf/portal-present-fop.xsl") @@ -504,6 +514,11 @@ public void exportAsCsv( required = false ) String bucket, + @RequestParam( + required = false, + defaultValue = "eng" + ) + String language, @Parameter(description = "XPath pointing to the XML element to loop on.", required = false, example = "Use . for the metadata, " + @@ -575,7 +590,11 @@ public void exportAsCsv( } }); - Element r = new XsltResponseWriter(null, "search") + if (!languageUtils.getUiLanguages().contains(language)) { + language = languageUtils.getDefaultUiLanguage(); + } + + Element r = new XsltResponseWriter(null, "search", language) .withParams(allRequestParams.entrySet().stream() .collect(Collectors.toMap( Entry::getKey, diff --git a/services/src/main/java/org/fao/geonet/guiapi/search/XsltResponseWriter.java b/services/src/main/java/org/fao/geonet/guiapi/search/XsltResponseWriter.java index 3623a242f02..fd6a7c78bd5 100644 --- a/services/src/main/java/org/fao/geonet/guiapi/search/XsltResponseWriter.java +++ b/services/src/main/java/org/fao/geonet/guiapi/search/XsltResponseWriter.java @@ -1,6 +1,6 @@ /* * ============================================================================= - * === Copyright (C) 2001-2023 Food and Agriculture Organization of the + * === Copyright (C) 2001-2024 Food and Agriculture Organization of the * === United Nations (FAO-UN), United Nations World Food Programme (WFP) * === and United Nations Environment Programme (UNEP) * === @@ -36,9 +36,7 @@ import org.fao.geonet.utils.Log; import org.fao.geonet.utils.Xml; import org.jdom.Element; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -50,16 +48,17 @@ /** * Utility to mimic what Jeeves was doing */ -@Component public class XsltResponseWriter { public static final String TRANSLATIONS = "translations"; - @Autowired - GeonetworkDataDirectory dataDirectory; Element xml; Path xsl; Map<String, Object> xslParams = new HashMap<>(); public XsltResponseWriter(String envTagName, String serviceName) { + this(envTagName, serviceName, "eng"); + } + + public XsltResponseWriter(String envTagName, String serviceName, String lang) { SettingManager settingManager = ApplicationContextHolder.get().getBean(SettingManager.class); String url = settingManager.getBaseURL(); Element gui = new Element("gui"); @@ -70,8 +69,7 @@ public XsltResponseWriter(String envTagName, String serviceName) { gui.addContent(new Element("baseUrl").setText(settingManager.getBaseURL())); gui.addContent(new Element("serverUrl").setText(settingManager.getServerURL())); gui.addContent(new Element("nodeId").setText(settingManager.getNodeId())); - // TODO: set language based on header - gui.addContent(new Element("language").setText("eng")); + gui.addContent(new Element("language").setText(lang)); Element settings = settingManager.getAllAsXML(true); @@ -94,8 +92,7 @@ public XsltResponseWriter withXml(Element xml) { public XsltResponseWriter withXsl(String xsl) { ApplicationContext applicationContext = ApplicationContextHolder.get(); GeonetworkDataDirectory dataDirectory = applicationContext.getBean(GeonetworkDataDirectory.class); - Path xslt = dataDirectory.getWebappDir().resolve(xsl); - this.xsl = xslt; + this.xsl = dataDirectory.getWebappDir().resolve(xsl); return this; } @@ -153,7 +150,7 @@ public XsltResponseWriter withJson(String json) { }); } catch (IOException e) { Log.warning(Geonet.GEONETWORK, String.format( - "Can't find JSON file '%s'.", jsonPath.toString() + "Can't find JSON file '%s'.", jsonPath )); } diff --git a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js index 77429954b58..98d4793b3ca 100644 --- a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js +++ b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js @@ -51,6 +51,7 @@ "$q", "$http", "gnConfig", + "gnLangs", function ( $rootScope, $timeout, @@ -67,7 +68,8 @@ $translate, $q, $http, - gnConfig + gnConfig, + gnLangs ) { var windowName = "geonetwork"; var windowOption = ""; @@ -154,7 +156,7 @@ if (params.sortOrder) { url += "&sortOrder=" + params.sortOrder; } - url += "&bucket=" + bucket; + url += "&bucket=" + bucket + "&language=" + gnLangs.current; location.replace(url); } else if (angular.isString(params)) { gnMdFormatter.getFormatterUrl(null, null, params).then(function (url) { @@ -194,7 +196,11 @@ }; this.exportCSV = function (bucket) { - window.open("../api/records/csv" + "?bucket=" + bucket, windowName, windowOption); + window.open( + "../api/records/csv" + "?bucket=" + bucket + "&language=" + gnLangs.current, + windowName, + windowOption + ); }; this.validateMdLinks = function (bucket) { $rootScope.$broadcast("operationOnSelectionStart"); From f9d8f0dd533d4d51c137d2acea0e8d5bea07a484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Thu, 25 Jul 2024 08:28:33 +0200 Subject: [PATCH 35/76] Zoom to map popup remains active on non-map pages. (#8267) * Zoom to map popup remains active on non-map pages. Fixes #8260 Fixes also wrong values for add alert delay, provided in some pages in milliseconds, but the method expects seconds. * Zoom to map popup remains active on non-map pages - remove alerts only when switching from the map * Increase timeout to hide layer added popup to 15 seconds --- .../components/common/alert/AlertDirective.js | 6 ++++++ .../catalog/components/common/map/mapService.js | 10 +++++----- .../search/resultsview/SelectionDirective.js | 2 +- .../search/searchmanager/LocationService.js | 10 ++++++---- .../catalog/components/viewer/ViewerDirective.js | 4 ++-- .../viewer/wmsimport/WmsImportDirective.js | 2 +- .../catalog/js/edit/DirectoryController.js | 2 +- .../main/resources/catalog/views/default/module.js | 13 +++++++++++-- 8 files changed, 33 insertions(+), 16 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/common/alert/AlertDirective.js b/web-ui/src/main/resources/catalog/components/common/alert/AlertDirective.js index 449f8563f6d..f1406c99c4f 100644 --- a/web-ui/src/main/resources/catalog/components/common/alert/AlertDirective.js +++ b/web-ui/src/main/resources/catalog/components/common/alert/AlertDirective.js @@ -69,6 +69,12 @@ } } }; + + this.closeAlerts = function () { + if (gnAlertValue.length) { + gnAlertValue.splice(0, gnAlertValue.length); + } + }; } ]); diff --git a/web-ui/src/main/resources/catalog/components/common/map/mapService.js b/web-ui/src/main/resources/catalog/components/common/map/mapService.js index 9c6ebc03599..a2af666d1f6 100644 --- a/web-ui/src/main/resources/catalog/components/common/map/mapService.js +++ b/web-ui/src/main/resources/catalog/components/common/map/mapService.js @@ -1390,7 +1390,7 @@ } else { gnAlertService.addAlert({ msg: $translate.instant("layerCRSNotFound"), - delay: 5000, + delay: 5, type: "warning" }); } @@ -1400,7 +1400,7 @@ msg: $translate.instant("layerNotAvailableInMapProj", { proj: mapProjection }), - delay: 5000, + delay: 5, type: "warning" }); } @@ -1981,7 +1981,7 @@ type: "wmts", url: encodeURIComponent(url) }), - delay: 20000, + delay: 20, type: "warning" }); var o = { @@ -2079,7 +2079,7 @@ type: "wfs", url: encodeURIComponent(url) }), - delay: 20000, + delay: 20, type: "warning" }); var o = { @@ -2159,7 +2159,7 @@ } catch (e) { gnAlertService.addAlert({ msg: $translate.instant("wmtsLayerNoUsableMatrixSet"), - delay: 5000, + delay: 5, type: "danger" }); return; diff --git a/web-ui/src/main/resources/catalog/components/search/resultsview/SelectionDirective.js b/web-ui/src/main/resources/catalog/components/search/resultsview/SelectionDirective.js index 500ece65fd0..ae8fedf50bb 100644 --- a/web-ui/src/main/resources/catalog/components/search/resultsview/SelectionDirective.js +++ b/web-ui/src/main/resources/catalog/components/search/resultsview/SelectionDirective.js @@ -99,7 +99,7 @@ function (r) { gnAlertService.addAlert({ msg: r.data.message || r.data.description, - delay: 20000, + delay: 20, type: "danger" }); if (r.id) { diff --git a/web-ui/src/main/resources/catalog/components/search/searchmanager/LocationService.js b/web-ui/src/main/resources/catalog/components/search/searchmanager/LocationService.js index c04e9b9ff4b..7d1a9c252e4 100644 --- a/web-ui/src/main/resources/catalog/components/search/searchmanager/LocationService.js +++ b/web-ui/src/main/resources/catalog/components/search/searchmanager/LocationService.js @@ -77,12 +77,14 @@ return p.indexOf(this.METADATA) == 0 || p.indexOf(this.DRAFT) == 0; }; - this.isMap = function () { - return $location.path() == this.MAP; + this.isMap = function (path) { + var p = path || $location.path(); + return p == this.MAP; }; - this.isHome = function () { - return $location.path() == this.HOME; + this.isHome = function (path) { + var p = path || $location.path(); + return p == this.HOME; }; this.isUndefined = function () { diff --git a/web-ui/src/main/resources/catalog/components/viewer/ViewerDirective.js b/web-ui/src/main/resources/catalog/components/viewer/ViewerDirective.js index cb6f63ed0fe..150d12bf1a4 100644 --- a/web-ui/src/main/resources/catalog/components/viewer/ViewerDirective.js +++ b/web-ui/src/main/resources/catalog/components/viewer/ViewerDirective.js @@ -255,7 +255,7 @@ }), type: "success" }, - 5000 + 5 ); } }, @@ -293,7 +293,7 @@ url: config.url, extent: extent ? extent.join(",") : "" }), - delay: 5000, + delay: 5, type: "warning" }); // TODO: You may want to add more than one time diff --git a/web-ui/src/main/resources/catalog/components/viewer/wmsimport/WmsImportDirective.js b/web-ui/src/main/resources/catalog/components/viewer/wmsimport/WmsImportDirective.js index bdc1eb1f285..9d4dec15053 100644 --- a/web-ui/src/main/resources/catalog/components/viewer/wmsimport/WmsImportDirective.js +++ b/web-ui/src/main/resources/catalog/components/viewer/wmsimport/WmsImportDirective.js @@ -105,7 +105,7 @@ }), type: "success" }, - 4 + 15 ); gnMap.feedLayerMd(layer); return layer; diff --git a/web-ui/src/main/resources/catalog/js/edit/DirectoryController.js b/web-ui/src/main/resources/catalog/js/edit/DirectoryController.js index 280d0e27e36..3baa7a89bd0 100644 --- a/web-ui/src/main/resources/catalog/js/edit/DirectoryController.js +++ b/web-ui/src/main/resources/catalog/js/edit/DirectoryController.js @@ -455,7 +455,7 @@ .then(refreshEntriesInfo, function (e) { gnAlertService.addAlert({ msg: $translate.instant("directoryEntry-removeError-referenced"), - delay: 5000, + delay: 5, type: "danger" }); }); diff --git a/web-ui/src/main/resources/catalog/views/default/module.js b/web-ui/src/main/resources/catalog/views/default/module.js index a3e72354245..74a74fc1c7a 100644 --- a/web-ui/src/main/resources/catalog/views/default/module.js +++ b/web-ui/src/main/resources/catalog/views/default/module.js @@ -412,7 +412,7 @@ msg: $translate.instant("layerProtocolNotSupported", { type: link.protocol }), - delay: 20000, + delay: 20, type: "warning" }); return; @@ -536,7 +536,7 @@ setActiveTab(); $scope.$on("$locationChangeSuccess", setActiveTab); - $scope.$on("$locationChangeSuccess", function (next, current) { + $scope.$on("$locationChangeSuccess", function (event, next, current) { if ( gnSearchLocation.isSearch() && (!angular.isArray(searchMap.getSize()) || searchMap.getSize()[0] < 0) @@ -545,6 +545,15 @@ searchMap.updateSize(); }, 0); } + + // Changing from the map to search pages, hide alerts + var currentUrlHash = + current.indexOf("#") > -1 ? current.slice(current.indexOf("#") + 1) : ""; + if (gnSearchLocation.isMap(currentUrlHash)) { + setTimeout(function () { + gnAlertService.closeAlerts(); + }, 0); + } }); var sortConfig = gnSearchSettings.sortBy.split("#"); From aa525736c20055ed8d0a102f1e4ed20068753a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Luis=20Rodr=C3=ADguez=20Ponce?= <juanluisrp@geocat.net> Date: Tue, 30 Jul 2024 15:55:17 +0200 Subject: [PATCH 36/76] Fix a problem with recaptcha not shown sometimes (#8285) In the register user, metadata feedback and user feedback forms, sometimes the settings with the recaptcha keys takes a bit longer to load making the recaptcha widget not to load. This commit fixes that updating the values of the recaptcha settings when the settings have finished loading. It also resets the recaptcha widget if there is any problem returned by the server after sending the form. --- .../userfeedback/GnUserfeedbackDirective.js | 14 +++++--- .../userfeedback/GnmdFeedbackDirective.js | 3 ++ .../resources/catalog/js/LoginController.js | 35 ++++++++++++------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/userfeedback/GnUserfeedbackDirective.js b/web-ui/src/main/resources/catalog/components/userfeedback/GnUserfeedbackDirective.js index de197b907ac..80d6c7964e6 100644 --- a/web-ui/src/main/resources/catalog/components/userfeedback/GnUserfeedbackDirective.js +++ b/web-ui/src/main/resources/catalog/components/userfeedback/GnUserfeedbackDirective.js @@ -303,6 +303,7 @@ "Metadata", "vcRecaptchaService", "gnConfig", + "gnConfigService", function ( $http, gnUserfeedbackService, @@ -311,7 +312,8 @@ $rootScope, Metadata, vcRecaptchaService, - gnConfig + gnConfig, + gnConfigService ) { return { restrict: "AEC", @@ -322,10 +324,12 @@ templateUrl: "../../catalog/components/" + "userfeedback/partials/userfeedbacknew.html", link: function (scope) { - scope.recaptchaEnabled = - gnConfig["system.userSelfRegistration.recaptcha.enable"]; - scope.recaptchaKey = - gnConfig["system.userSelfRegistration.recaptcha.publickey"]; + gnConfigService.loadPromise.then(function () { + scope.recaptchaEnabled = + gnConfig["system.userSelfRegistration.recaptcha.enable"]; + scope.recaptchaKey = + gnConfig["system.userSelfRegistration.recaptcha.publickey"]; + }); scope.resolveRecaptcha = false; scope.userName = null; diff --git a/web-ui/src/main/resources/catalog/components/userfeedback/GnmdFeedbackDirective.js b/web-ui/src/main/resources/catalog/components/userfeedback/GnmdFeedbackDirective.js index 9405de99f71..f693181de2a 100644 --- a/web-ui/src/main/resources/catalog/components/userfeedback/GnmdFeedbackDirective.js +++ b/web-ui/src/main/resources/catalog/components/userfeedback/GnmdFeedbackDirective.js @@ -139,6 +139,9 @@ scope.mdFeedbackOpen = false; } else { scope.success = false; + if (scope.recaptchaEnabled) { + vcRecaptchaService.reload(); + } } }); } diff --git a/web-ui/src/main/resources/catalog/js/LoginController.js b/web-ui/src/main/resources/catalog/js/LoginController.js index 9a6a5de8f80..42097136212 100644 --- a/web-ui/src/main/resources/catalog/js/LoginController.js +++ b/web-ui/src/main/resources/catalog/js/LoginController.js @@ -45,6 +45,7 @@ "$window", "$timeout", "gnUtilityService", + "gnConfigService", "gnConfig", "gnGlobalSettings", "vcRecaptchaService", @@ -59,6 +60,7 @@ $window, $timeout, gnUtilityService, + gnConfigService, gnConfig, gnGlobalSettings, vcRecaptchaService, @@ -73,8 +75,23 @@ $scope.userToRemind = null; $scope.changeKey = null; - $scope.recaptchaEnabled = gnConfig["system.userSelfRegistration.recaptcha.enable"]; - $scope.recaptchaKey = gnConfig["system.userSelfRegistration.recaptcha.publickey"]; + gnConfigService.loadPromise.then(function () { + $scope.recaptchaEnabled = + gnConfig["system.userSelfRegistration.recaptcha.enable"]; + $scope.recaptchaKey = gnConfig["system.userSelfRegistration.recaptcha.publickey"]; + + // take the bigger of the two values + $scope.passwordMinLength = Math.max( + gnConfig["system.security.passwordEnforcement.minLength"], + 6 + ); + $scope.passwordMaxLength = Math.max( + gnConfig["system.security.passwordEnforcement.maxLength"], + 6 + ); + $scope.passwordPattern = gnConfig["system.security.passwordEnforcement.pattern"]; + }); + $scope.resolveRecaptcha = false; $scope.redirectUrl = gnUtilityService.getUrlParameter("redirect"); @@ -84,17 +101,6 @@ $scope.isShowLoginAsLink = gnGlobalSettings.isShowLoginAsLink; $scope.isUserProfileUpdateEnabled = gnGlobalSettings.isUserProfileUpdateEnabled; - // take the bigger of the two values - $scope.passwordMinLength = Math.max( - gnConfig["system.security.passwordEnforcement.minLength"], - 6 - ); - $scope.passwordMaxLength = Math.max( - gnConfig["system.security.passwordEnforcement.maxLength"], - 6 - ); - $scope.passwordPattern = gnConfig["system.security.passwordEnforcement.pattern"]; - function initForm() { if ($window.location.pathname.indexOf("new.password") !== -1) { // Retrieve username from URL parameter @@ -196,6 +202,9 @@ timeout: 0, type: "danger" }); + if ($scope.recaptchaEnabled) { + vcRecaptchaService.reload(); + } } ); }; From 4b0e20d3ffc53b354cc86a78c24a0f351853559b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Fri, 9 Aug 2024 06:47:44 +0200 Subject: [PATCH 37/76] Standard / ISO19139 / Fix removal of online source when multiple transfer options block are used. (#8281) * Standard / ISO19139 / Fix removal of online source when multiple transfer options block are used. Follow up of https://github.com/geonetwork/core-geonetwork/pull/7431 * Fix online resource update/delete so that it supports multiple gmd:MD_DigitalTransferOptions blocks. --------- Co-authored-by: Ian Allen <ianwallen@hotmail.com> --- .../plugin/iso19139/extract-relations.xsl | 5 ++-- .../plugin/iso19139/index-fields/index.xsl | 6 ++-- .../plugin/iso19139/process/onlinesrc-add.xsl | 28 +++++++++++++------ .../iso19139/process/onlinesrc-remove.xsl | 22 +++++++++++---- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/schemas/iso19139/src/main/plugin/iso19139/extract-relations.xsl b/schemas/iso19139/src/main/plugin/iso19139/extract-relations.xsl index 10e10352ab8..7aa2b12d7a1 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/extract-relations.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/extract-relations.xsl @@ -35,7 +35,6 @@ xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:util="java:org.fao.geonet.util.XslUtil" xmlns:digestUtils="java:org.apache.commons.codec.digest.DigestUtils" - xmlns:exslt="http://exslt.org/common" xmlns:gn-fn-rel="http://geonetwork-opensource.org/xsl/functions/relations" version="2.0" exclude-result-prefixes="#all"> @@ -110,7 +109,7 @@ <xsl:value-of select="position()"/> </idx> <hash> - <xsl:value-of select="digestUtils:md5Hex(string(exslt:node-set(normalize-space(.))))"/> + <xsl:value-of select="digestUtils:md5Hex(normalize-space(.))"/> </hash> <url> <xsl:apply-templates mode="get-iso19139-localized-string" @@ -142,7 +141,7 @@ <xsl:value-of select="position()"/> </idx> <hash> - <xsl:value-of select="digestUtils:md5Hex(string(exslt:node-set(normalize-space(.))))"/> + <xsl:value-of select="digestUtils:md5Hex(normalize-space(.))"/> </hash> <title> <xsl:apply-templates mode="get-iso19139-localized-string" diff --git a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl index d2fdc3f4a78..15f081abb99 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl @@ -34,7 +34,6 @@ xmlns:gn-fn-index="http://geonetwork-opensource.org/xsl/functions/index" xmlns:index="java:org.fao.geonet.kernel.search.EsSearchManager" xmlns:digestUtils="java:org.apache.commons.codec.digest.DigestUtils" - xmlns:exslt="http://exslt.org/common" xmlns:util="java:org.fao.geonet.util.XslUtil" xmlns:date-util="java:org.fao.geonet.utils.DateUtil" xmlns:daobs="http://daobs.org" @@ -1122,8 +1121,7 @@ <xsl:copy-of select="gn-fn-index:add-multilingual-field('orderingInstructions', ., $allLanguages)"/> </xsl:for-each> - <xsl:for-each select="gmd:transferOptions/*/ - gmd:onLine/*[gmd:linkage/gmd:URL != '']"> + <xsl:for-each select=".//gmd:onLine/*[gmd:linkage/gmd:URL != '']"> <xsl:variable name="transferGroup" select="count(ancestor::gmd:transferOptions/preceding-sibling::gmd:transferOptions)"/> @@ -1147,7 +1145,7 @@ <atomfeed><xsl:value-of select="gmd:linkage/gmd:URL"/></atomfeed> </xsl:if> <link type="object">{ - "hash": "<xsl:value-of select="digestUtils:md5Hex(string(exslt:node-set(normalize-space(.))))"/>", + "hash": "<xsl:value-of select="digestUtils:md5Hex(normalize-space(.))"/>", "idx": <xsl:value-of select="position()"/>, "protocol":"<xsl:value-of select="util:escapeForJson((gmd:protocol/*/text())[1])"/>", "mimeType":"<xsl:value-of select="if (*/gmx:MimeFileType) diff --git a/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-add.xsl b/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-add.xsl index 0afe80eaa3d..e9c86757ca4 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-add.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-add.xsl @@ -187,19 +187,29 @@ Note: It assumes that it will be adding new items in <!-- Updating the gmd:onLine based on update parameters --> <!-- Note: first part of the match needs to match the xsl:for-each select from extract-relations.xsl in order to get the position() to match --> <!-- The unique identifier is marked with resourceIdx which is the position index and resourceHash which is hash code of the current node (combination of url, resource name, and description) --> - <xsl:template - match="*//gmd:MD_DigitalTransferOptions/gmd:onLine - [gmd:CI_OnlineResource[gmd:linkage/gmd:URL!=''] and ($resourceIdx = '' or position() = xs:integer($resourceIdx))] - [($resourceHash != '' or ($updateKey != '' and normalize-space($updateKey) = concat( + <!-- Template to match all gmd:onLine elements --> + <xsl:template match="//gmd:MD_DigitalTransferOptions/gmd:onLine" priority="2"> + <!-- Calculate the global position of the current gmd:onLine element --> + <xsl:variable name="position" select="count(//gmd:MD_DigitalTransferOptions/gmd:onLine[current() >> .]) + 1" /> + + <xsl:choose> + <xsl:when test="gmd:CI_OnlineResource[gmd:linkage/gmd:URL != ''] and + ($resourceIdx = '' or $position = xs:integer($resourceIdx)) and + ($resourceHash != '' or ($updateKey != '' and normalize-space($updateKey) = concat( gmd:CI_OnlineResource/gmd:linkage/gmd:URL, gmd:CI_OnlineResource/gmd:protocol/*, gmd:CI_OnlineResource/gmd:name/gco:CharacterString))) - and ($resourceHash = '' or digestUtils:md5Hex(string(exslt:node-set(normalize-space(.)))) = $resourceHash)]" - priority="2"> - <xsl:call-template name="createOnlineSrc"/> + and ($resourceHash = '' or digestUtils:md5Hex(normalize-space(.)) = $resourceHash)"> + <xsl:call-template name="createOnlineSrc"/> + </xsl:when> + <xsl:otherwise> + <xsl:copy> + <xsl:apply-templates select="@*|node()"/> + </xsl:copy> + </xsl:otherwise> + </xsl:choose> </xsl:template> - <xsl:template name="createOnlineSrc"> <!-- Add all online source from the target metadata to the current one --> @@ -243,7 +253,7 @@ Note: It assumes that it will be adding new items in </gmd:URL> </gmd:linkage> <gmd:protocol> - <xsl:call-template name="setProtocol"/> + <xsl:call-template name="setProtocol"/> </gmd:protocol> <xsl:if test="$applicationProfile != ''"> diff --git a/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-remove.xsl b/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-remove.xsl index 718f483eced..5ea7b210773 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-remove.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/process/onlinesrc-remove.xsl @@ -53,15 +53,25 @@ Stylesheet used to remove a reference to a online resource. <!-- Remove the gmd:onLine define in url parameter --> <!-- Note: first part of the match needs to match the xsl:for-each select from extract-relations.xsl in order to get the position() to match --> <!-- The unique identifier is marked with resourceIdx which is the position index and resourceHash which is hash code of the current node (combination of url, resource name, and description) --> - <xsl:template - match="*//gmd:MD_DigitalTransferOptions/gmd:onLine - [gmd:CI_OnlineResource[gmd:linkage/gmd:URL!=''] and ($resourceIdx = '' or (count(preceding::gmd:onLine) + 1) = xs:integer($resourceIdx))] - [($resourceHash != '' or ($url != null and (normalize-space(gmd:CI_OnlineResource/gmd:linkage/gmd:URL) = $url and normalize-space(gmd:CI_OnlineResource/gmd:name/gco:CharacterString) = normalize-space($name) + <xsl:template match="//gmd:MD_DigitalTransferOptions/gmd:onLine" priority="2"> + + <!-- Calculate the global position of the current gmd:onLine element --> + <xsl:variable name="position" select="count(//gmd:MD_DigitalTransferOptions/gmd:onLine[current() >> .]) + 1" /> + + <xsl:if test="not( + gmd:CI_OnlineResource[gmd:linkage/gmd:URL != ''] and + ($resourceIdx = '' or $position = xs:integer($resourceIdx)) and + ($resourceHash != '' or ($url != null and (normalize-space(gmd:CI_OnlineResource/gmd:linkage/gmd:URL) = $url and normalize-space(gmd:CI_OnlineResource/gmd:name/gco:CharacterString) = normalize-space($name) or normalize-space(gmd:CI_OnlineResource/gmd:linkage/gmd:URL) = $url and count(gmd:CI_OnlineResource/gmd:name/gmd:PT_FreeText/gmd:textGroup[gmd:LocalisedCharacterString = $name]) > 0 or normalize-space(gmd:CI_OnlineResource/gmd:linkage/gmd:URL) = $url and normalize-space(gmd:CI_OnlineResource/gmd:protocol/*) = 'WWW:DOWNLOAD-1.0-http--download')) ) - and ($resourceHash = '' or digestUtils:md5Hex(string(exslt:node-set(normalize-space(.)))) = $resourceHash)]" - priority="2"/> + and ($resourceHash = '' or digestUtils:md5Hex(normalize-space(.)) = $resourceHash) + )"> + <xsl:copy> + <xsl:apply-templates select="@*|node()"/> + </xsl:copy> + </xsl:if> + </xsl:template> <!-- Do a copy of every node and attribute --> <xsl:template match="@*|node()"> From 79c57690ea604da26c3ea97db7adb4bad7867ed9 Mon Sep 17 00:00:00 2001 From: tylerjmchugh <163562062+tylerjmchugh@users.noreply.github.com> Date: Wed, 14 Aug 2024 06:02:51 -0400 Subject: [PATCH 38/76] Update batch PDF export to skip working copies (#8292) * Update batch PDF export to skip working copies * Capitalize boolean operators in lucene query --- .../src/main/java/org/fao/geonet/api/records/CatalogApi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java b/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java index e6751f8e3ef..08d060e3ca5 100644 --- a/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/CatalogApi.java @@ -389,8 +389,8 @@ public void exportAsPdf( final SearchResponse searchResponse = searchManager.query( String.format( - "uuid:(\"%s\")", - String.join("\" or \"", uuidList)), + "uuid:(\"%s\") AND NOT draft:\"y\"", // Skip working copies as duplicate UUIDs cause the PDF xslt to fail + String.join("\" OR \"", uuidList)), EsFilterBuilder.buildPermissionsFilter(ApiUtils.createServiceContext(httpRequest)), searchFieldsForPdf, 0, maxhits); From 57bc1d5541bf03c8bdf6c452b4654fe06fb9895a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Thu, 8 Aug 2024 12:48:15 +0200 Subject: [PATCH 39/76] Record view / Improve layout of table (eg. quality measures) Related to https://github.com/geonetwork/core-geonetwork/pull/5879#issuecomment-2269506779 --- web-ui/src/main/resources/catalog/style/gn_metadata.less | 2 -- 1 file changed, 2 deletions(-) diff --git a/web-ui/src/main/resources/catalog/style/gn_metadata.less b/web-ui/src/main/resources/catalog/style/gn_metadata.less index 33ac3d63140..a830b0fe0fe 100644 --- a/web-ui/src/main/resources/catalog/style/gn_metadata.less +++ b/web-ui/src/main/resources/catalog/style/gn_metadata.less @@ -551,8 +551,6 @@ ul.container-list { margin-bottom: 0.5em; } td { - padding-left: 40px; - word-break: break-word; ul { padding-left: 0; } From b874f86e6e53a8c09d31154216174148addae4dd Mon Sep 17 00:00:00 2001 From: Francois Prunayre <fx.prunayre@gmail.com> Date: Mon, 5 Aug 2024 18:18:57 +0200 Subject: [PATCH 40/76] Index / Add maintenance details. Before only maintenance frequency was displayed but more information may be provided about the maintenance (eg. custom frequency, next update date, note). --- .../geonet/kernel/search/EsSearchManager.java | 1 + .../iso19115-3.2018/index-fields/index.xsl | 16 ++++++++ .../plugin/iso19139/index-fields/index.xsl | 16 ++++++++ .../main/resources/catalog/locales/en-v4.json | 3 ++ .../templates/recordView/maintenance.html | 38 +++++++++++++++++++ .../templates/recordView/technical.html | 17 +-------- 6 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 web-ui/src/main/resources/catalog/views/default/templates/recordView/maintenance.html diff --git a/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java b/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java index bfd783bc5f4..122e7e2930a 100644 --- a/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java +++ b/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java @@ -573,6 +573,7 @@ private void checkIndexResponse(BulkResponse bulkItemResponses, .add("status_text") .add("coordinateSystem") .add("identifier") + .add("maintenance") .add("responsibleParty") .add("mdLanguage") .add("otherLanguage") diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl index fb86b5a225a..35ed82ac5bf 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl @@ -712,6 +712,22 @@ </spatialRepresentationType> </xsl:for-each> + <xsl:for-each select="*:resourceMaintenance/*"> + <maintenance type="object">{ + "frequency": "<xsl:value-of select="*:maintenanceAndUpdateFrequency/*/@codeListValue"/>" + <xsl:for-each select="*:dateOfNextUpdate[*/text() != '']"> + ,"nextUpdateDate": "<xsl:value-of select="*/text()"/>" + </xsl:for-each> + <xsl:for-each select="*:userDefinedMaintenanceFrequency[*/text() != '']"> + ,"userDefinedFrequency": "<xsl:value-of select="*/text()"/>" + </xsl:for-each> + <xsl:for-each select="*:maintenanceNote[*/text() != '']"> + ,"noteObject": + <xsl:value-of select="gn-fn-index:add-multilingual-field('maintenanceNote', ., $allLanguages, true())"/> + </xsl:for-each> + }</maintenance> + </xsl:for-each> + <xsl:for-each select="mri:resourceConstraints/*"> <xsl:variable name="fieldPrefix" select="local-name()"/> diff --git a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl index 15f081abb99..c2a7ee33ec8 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl @@ -656,6 +656,22 @@ </xsl:for-each> + <xsl:for-each select="gmd:resourceMaintenance/*"> + <maintenance type="object">{ + "frequency": "<xsl:value-of select="*:maintenanceAndUpdateFrequency/*/@codeListValue"/>" + <xsl:for-each select="gmd:dateOfNextUpdate[*/text() != '']"> + ,"nextUpdateDate": "<xsl:value-of select="*/text()"/>" + </xsl:for-each> + <xsl:for-each select="gmd:userDefinedMaintenanceFrequency[*/text() != '']"> + ,"userDefinedFrequency": "<xsl:value-of select="*/text()"/>" + </xsl:for-each> + <xsl:for-each select="gmd:maintenanceNote[*/text() != '']"> + ,"noteObject": + <xsl:value-of select="gn-fn-index:add-multilingual-field('maintenanceNote', ., $allLanguages, true())"/> + </xsl:for-each> + }</maintenance> + </xsl:for-each> + <xsl:for-each select="gmd:resourceConstraints/*"> <xsl:variable name="fieldPrefix" select="local-name()"/> <xsl:for-each select="gmd:otherConstraints"> diff --git a/web-ui/src/main/resources/catalog/locales/en-v4.json b/web-ui/src/main/resources/catalog/locales/en-v4.json index e412c79f700..266f5e5bf43 100644 --- a/web-ui/src/main/resources/catalog/locales/en-v4.json +++ b/web-ui/src/main/resources/catalog/locales/en-v4.json @@ -405,6 +405,9 @@ "measureDescription": "Description", "measureValue": "Value", "measureDate": "Date", + "nextUpdateDate": "Next update", + "userDefinedFrequency": "Update frequency", + "maintenanceNote": "Maintenance note", "switchPortals": "Switch to another Portal", "dataPreview": "Discover data", "tableOfContents": "Table of Contents", diff --git a/web-ui/src/main/resources/catalog/views/default/templates/recordView/maintenance.html b/web-ui/src/main/resources/catalog/views/default/templates/recordView/maintenance.html new file mode 100644 index 00000000000..a2672e5df09 --- /dev/null +++ b/web-ui/src/main/resources/catalog/views/default/templates/recordView/maintenance.html @@ -0,0 +1,38 @@ +<div data-ng-repeat="maintenance in mdView.current.record.maintenance"> + <div data-ng-if="maintenance.frequency" class="gn-margin-bottom flex-row"> + <span class="badge badge-rounded" title="{{'updateFrequency' | translate}}"> + <i class="fa fa-fw fa-rotate"></i> + </span> + <div> + <h3 data-translate="">updateFrequency</h3> + <p>{{maintenance.frequency | translate}}</p> + </div> + </div> + <div data-ng-if="maintenance.noteObject.default" class="gn-margin-bottom flex-row"> + <span class="badge badge-rounded" title="{{'maintenanceNote' | translate}}"> + <i class="fa fa-fw fa-rotate"></i> + </span> + <div> + <h3 data-translate="">maintenanceNote</h3> + <p>{{maintenance.noteObject.default}}</p> + </div> + </div> + <div data-ng-if="maintenance.nextUpdateDate" class="gn-margin-bottom flex-row"> + <span class="badge badge-rounded" title="{{'nextUpdateDate' | translate}}"> + <i class="fa fa-fw fa-calendar-plus"></i> + </span> + <div> + <h3 data-translate="">nextUpdateDate</h3> + <p>{{maintenance.nextUpdateDate}}</p> + </div> + </div> + <div data-ng-if="maintenance.userDefinedFrequency" class="gn-margin-bottom flex-row"> + <span class="badge badge-rounded" title="{{'userDefinedFrequency' | translate}}"> + <i class="fa fa-fw fa-rotate"></i> + </span> + <div> + <h3 data-translate="">userDefinedFrequency</h3> + <p data-gn-field-duration-div="{{maintenance.userDefinedFrequency}}"></p> + </div> + </div> +</div> diff --git a/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html b/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html index a6bfc157cba..5c68e76a547 100644 --- a/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html +++ b/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html @@ -22,6 +22,8 @@ <h3>{{date.type | translate}}</h3> </ul> </section> + <div ng-include="'../../catalog/views/default/templates/recordView/maintenance.html'"></div> + <div data-ng-if="mdView.current.record.serviceType" class="gn-margin-bottom flex-row"> <span class="badge badge-rounded" title="{{'serviceType' | translate}}"> <i class="fa fa-fw fa-cloud"></i> @@ -49,21 +51,6 @@ <h3 data-translate="">cl_couplingType</h3> <p data-ng-repeat="c in mdView.current.record.cl_couplingType">{{c.default}}</p> </div> </div> - - <div - data-ng-if="mdView.current.record.cl_maintenanceAndUpdateFrequency.length > 0" - class="gn-margin-bottom flex-row" - > - <span class="badge badge-rounded" title="{{'updateFrequency' | translate}}"> - <i class="fa fa-fw fa-language"></i> - </span> - <div> - <h3 data-translate="">updateFrequency</h3> - <p data-ng-repeat="c in mdView.current.record.cl_maintenanceAndUpdateFrequency"> - {{c.default}} - </p> - </div> - </div> </div> <div> From 0331328b2bdacb00e2b2edc17caa8e5cca6932a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Thu, 22 Aug 2024 08:40:14 +0200 Subject: [PATCH 41/76] Standard / ISO19139 / i18n / Missing french translation (#8298) On application startup, the translation pack may report an error like the following ``` org.fao.geonet.api.exception.ResourceNotFoundException at org.fao.geonet.api.standards.StandardsUtils.getCodelistOrLabel(StandardsUtils.java:72) at org.fao.geonet.api.standards.StandardsUtils.getLabel(StandardsUtils.java:55) at org.fao.geonet.api.tools.i18n.TranslationPackBuilder.getStandardLabel(TranslationPackBuilder.java:217) ``` due to a missing french translation for element `DQ_EvaluationMethodTypeCode` Follow up of https://github.com/geonetwork/core-geonetwork/pull/7180 --- schemas/iso19139/src/main/plugin/iso19139/loc/fre/labels.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/schemas/iso19139/src/main/plugin/iso19139/loc/fre/labels.xml b/schemas/iso19139/src/main/plugin/iso19139/loc/fre/labels.xml index b2e388944d9..ff12a90122d 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/loc/fre/labels.xml +++ b/schemas/iso19139/src/main/plugin/iso19139/loc/fre/labels.xml @@ -359,7 +359,10 @@ n’est pas respectée. </help> <condition/> - + </element> + <element name="gmd:DQ_EvaluationMethodTypeCode"> + <label>Code du type de méthode d'évaluation</label> + <description>Type de méthode d'évaluation d'une mesure de qualité de données identifiée</description> </element> <element name="gmd:DQ_FormatConsistency" id="114.0"> <label>Cohérence du format</label> From 4a57a0278ff9b109555cb23c4751491e993aacab Mon Sep 17 00:00:00 2001 From: "mel-rie (CONET ISB / LGL)" <56172653+rime1014@users.noreply.github.com> Date: Thu, 22 Aug 2024 08:54:20 +0200 Subject: [PATCH 42/76] harvesting CSW: changed loglevel for invalid metadata to info (#8303) - other harvester have same or higher loglevel as well - helps to identify invalid metadata --- .../org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java b/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java index 8ba9e1e31af..88942e4ec86 100644 --- a/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java +++ b/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java @@ -539,7 +539,7 @@ private Element retrieveMetadata(String uuid) { params.getValidate().validate(dataMan, context, response, groupIdVal); } catch (Exception e) { - log.debug("Ignoring invalid metadata with uuid " + uuid); + log.info("Ignoring invalid metadata with uuid " + uuid); result.doesNotValidate++; return null; } From 347dc96f123dc30b9ab4685ea64ebd90c8d14ef7 Mon Sep 17 00:00:00 2001 From: tylerjmchugh <Tyler.McHugh@dfo-mpo.gc.ca> Date: Thu, 22 Aug 2024 14:43:32 -0400 Subject: [PATCH 43/76] Modify GnMdViewController to set recordIdentifierRequested using the getUuid function --- .../resources/catalog/components/search/mdview/mdviewModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/main/resources/catalog/components/search/mdview/mdviewModule.js b/web-ui/src/main/resources/catalog/components/search/mdview/mdviewModule.js index 1310e4a4810..a58b4ae60f3 100644 --- a/web-ui/src/main/resources/catalog/components/search/mdview/mdviewModule.js +++ b/web-ui/src/main/resources/catalog/components/search/mdview/mdviewModule.js @@ -85,7 +85,7 @@ $scope.gnMetadataActions = gnMetadataActions; $scope.url = location.href; $scope.compileScope = $scope.$new(); - $scope.recordIdentifierRequested = gnSearchLocation.uuid; + $scope.recordIdentifierRequested = gnSearchLocation.getUuid(); $scope.isUserFeedbackEnabled = false; $scope.isRatingEnabled = false; $scope.showCitation = false; From 22a87f68d2ec74ddbed86a83e0208fc2a8ce262d Mon Sep 17 00:00:00 2001 From: tylerjmchugh <163562062+tylerjmchugh@users.noreply.github.com> Date: Tue, 27 Aug 2024 05:49:22 -0400 Subject: [PATCH 44/76] Modify record not found message to only link to signin if user is not logged in (#8312) --- .../resources/catalog/locales/en-search.json | 4 ++-- .../default/templates/recordView/recordView.html | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/web-ui/src/main/resources/catalog/locales/en-search.json b/web-ui/src/main/resources/catalog/locales/en-search.json index 65a4d70eba6..9a2a2be6837 100644 --- a/web-ui/src/main/resources/catalog/locales/en-search.json +++ b/web-ui/src/main/resources/catalog/locales/en-search.json @@ -366,8 +366,8 @@ "shareOnLinkedIn": "Share on LinkedIn", "shareByEmail": "Share by email", "zoomto": "Zoom To", - "recordNotFound": "The record with identifier <strong>{{uuid}}</strong> was not found or is not shared with you. Try to <a href=\"catalog.signin?redirect={{url}}\">sign in</a> if you've an account.", - "intersectWith": "Intersects with", + "recordNotFound": "The record with identifier <strong>{{uuid}}</strong> was not found or is not shared with you.", + "trySignIn": "Try to <a href=\"catalog.signin?redirect={{url}}\">sign in</a> if you've an account.", "intersectWith": "Intersects with", "fullyOutsideOf": "Fully outside of", "encloses": "Enclosing", "within": "Within", diff --git a/web-ui/src/main/resources/catalog/views/default/templates/recordView/recordView.html b/web-ui/src/main/resources/catalog/views/default/templates/recordView/recordView.html index 846ef56531a..9d6ec37f733 100644 --- a/web-ui/src/main/resources/catalog/views/default/templates/recordView/recordView.html +++ b/web-ui/src/main/resources/catalog/views/default/templates/recordView/recordView.html @@ -35,10 +35,20 @@ <div class="alert alert-warning" data-ng-hide="!mdView.loadDetailsFinished || mdView.current.record" - data-translate="" - data-translate-values="{uuid: '{{recordIdentifierRequested | htmlToPlaintext}}', url: '{{url | encodeURIComponent}}'}" > - recordNotFound + <span + data-translate="" + data-translate-values="{uuid: '{{recordIdentifierRequested | htmlToPlaintext}}'}" + > + recordNotFound + </span> + <span + data-ng-hide="user" + data-translate="" + data-translate-values="{url: '{{url | encodeURIComponent}}'}" + > + trySignIn + </span> </div> <div class="row" data-ng-show="!mdView.loadDetailsFinished"> <i class="fa fa-spinner fa-spin fa-3x fa-fw"></i> From 36951d179d5b63b2e9134cf84987dc402acda86b Mon Sep 17 00:00:00 2001 From: Jody Garnett <jody.garnett@gmail.com> Date: Thu, 29 Aug 2024 03:10:34 -0700 Subject: [PATCH 45/76] Repository Citation.cff metadata for DOI registration with Zenodo (#8317) * Repository Citation.cff metadata for DUI regestration with zenodo * Update CITATION.cff Minor updates, additions and clean up * Update CITATION.cff Simplified the list to only include active PSC members --------- Co-authored-by: Jeroen Ticheler <Jeroen.Ticheler@GeoCat.net> --- CITATION.cff | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000000..1cdaa3768cf --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,88 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: GeoNetwork opensource +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: François + family-names: Prunayre + affiliation: Titellus + - given-names: Jose + family-names: García + affiliation: GeoCat BV + - given-names: Jeroen + family-names: Ticheler + affiliation: GeoCat BV + orcid: 'https://orcid.org/0009-0003-3896-0437' + email: jeroen.ticheler@geocat.net + - given-names: Florent + family-names: Gravin + affiliation: CamptoCamp + - given-names: Simon + family-names: Pigot + affiliation: CSIRO Australia + - name: GeoCat BV + address: Veenderweg 13 + city: Bennekom + country: NL + post-code: 6721 WD + tel: +31 (0) 318 416 664 + website: 'https://www.geocat.net/' + email: info@geocat.net + - name: Titellus + address: 321 Route de la Mollière + city: Saint Pierre de Genebroz + country: FR + post-code: 73360 + website: 'https://titellus.net/' + email: fx.prunayre@titellus.net + - name: CamptoCamp + address: QG Center Rte de la Chaux 4 + city: Bussigny + country: CH + post-code: 1030 + tel: +41 (21) 619 10 10 + website: 'https://camptocamp.com/' + email: info@camptocamp.com + - name: Open Source Geospatial Foundation - OSGeo + address: '9450 SW Gemini Dr. #42523' + location: Beaverton + region: Oregon + post-code: '97008' + country: US + email: info@osgeo.org + website: 'https://www.osgeo.org/' +repository-code: 'http://github.com/geonetwork/core-geonetwork' +url: 'https://geonetwork-opensource.org' +repository-artifact: >- + https://sourceforge.net/projects/geonetwork/files/GeoNetwork_opensource/ +abstract: >- + GeoNetwork is a catalog application to manage spatial and + non-spatial resources. It is compliant with critical + international standards from ISO, OGC and INSPIRE. It + provides powerful metadata editing and search functions as + well as an interactive web map viewer. +keywords: + - catalog + - gis + - sdi + - spatial data infrastructure + - dataspace + - search + - open data + - standards + - spatial + - CSW + - OGCAPI Records + - DCAT + - GeoDCAT-AP + - Catalog Service + - OGC + - open geospatial consortium + - osgeo + - open source geospatial foundation +license: GPL-2.0 From 849619b5574301d2af69440ca9943e8dcc76dce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Thu, 29 Aug 2024 16:46:59 +0200 Subject: [PATCH 46/76] Social links in metadata page doesn't have the metadata page permalink. Fixes #8322 --- .../search/mdview/mdviewDirective.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/search/mdview/mdviewDirective.js b/web-ui/src/main/resources/catalog/components/search/mdview/mdviewDirective.js index 4f9f2e6546c..f91c671e018 100644 --- a/web-ui/src/main/resources/catalog/components/search/mdview/mdviewDirective.js +++ b/web-ui/src/main/resources/catalog/components/search/mdview/mdviewDirective.js @@ -588,15 +588,20 @@ }, link: function (scope, element, attrs) { scope.mdService = gnUtilityService; - scope.$watch(scope.md, function (newVal, oldVal) { - if (newVal !== null && newVal !== oldVal) { - $http - .get("../api/records/" + scope.md.getUuid() + "/permalink") - .then(function (r) { - scope.socialMediaLink = r.data; - }); - } - }); + + scope.$watch( + "md", + function (newVal, oldVal) { + if (newVal !== null && newVal !== oldVal) { + $http + .get("../api/records/" + scope.md.getUuid() + "/permalink") + .then(function (r) { + scope.socialMediaLink = r.data; + }); + } + }, + true + ); } }; } From a9a9b5bb2c1b0314af73c8c449078e8b46f56cab Mon Sep 17 00:00:00 2001 From: Tobias Hotz <tobias.hotz@conet-isb.de> Date: Mon, 26 Aug 2024 08:34:03 +0200 Subject: [PATCH 47/76] Do not try to request clipboard permissions Clipboard permissions are only implemented in Chromium-based browsers. See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Interact_with_the_clipboard for more details Instead, check if the browser supports the writeText/readText clipboard functions (some mobile browsers and older firefox versions do not). With these change, both Chromium-based browsers and other browsers such as firefox work correctly --- .../components/utility/UtilityDirective.js | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js b/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js index 80834e2d4ec..425ce230238 100644 --- a/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js +++ b/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js @@ -1143,45 +1143,35 @@ return { copy: function (toCopy) { var deferred = $q.defer(); - navigator.permissions.query({ name: "clipboard-write" }).then( - function (result) { - if (result.state == "granted" || result.state == "prompt") { - navigator.clipboard.writeText(toCopy).then( - function () { - deferred.resolve(); - }, - function (r) { - console.warn(r); - deferred.reject(); - } - ); + if (navigator.clipboard?.writeText) { + navigator.clipboard.writeText(toCopy).then( + function () { + deferred.resolve(); + }, + function (r) { + console.warn(r); + deferred.reject(); } - }, - function () { - deferred.reject(); - } - ); + ); + } else { + deferred.reject(); + } return deferred.promise; }, paste: function () { var deferred = $q.defer(); - navigator.permissions.query({ name: "clipboard-read" }).then( - function (result) { - if (result.state == "granted" || result.state == "prompt") { - navigator.clipboard.readText().then( - function (text) { - deferred.resolve(text); - }, - function () { - deferred.reject(); - } - ); + if (navigator.clipboard?.readText) { + navigator.clipboard.readText().then( + function (text) { + deferred.resolve(text); + }, + function () { + deferred.reject(); } - }, - function () { - deferred.reject(); - } - ); + ); + } else { + deferred.reject(); + } return deferred.promise; } }; From 568c4d723f79378a9dc9c3511464e1721cdabc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Fri, 23 Aug 2024 08:58:35 +0200 Subject: [PATCH 48/76] Special characters in the cookie causing 400 bad requests from Spring Security. Fixes #8275 --- .../web/GeoNetworkStrictHttpFirewall.java | 47 +++++++++++++++++++ .../config-security/config-security-core.xml | 6 +++ 2 files changed, 53 insertions(+) create mode 100644 core/src/main/java/org/fao/geonet/web/GeoNetworkStrictHttpFirewall.java diff --git a/core/src/main/java/org/fao/geonet/web/GeoNetworkStrictHttpFirewall.java b/core/src/main/java/org/fao/geonet/web/GeoNetworkStrictHttpFirewall.java new file mode 100644 index 00000000000..cdf34c45f18 --- /dev/null +++ b/core/src/main/java/org/fao/geonet/web/GeoNetworkStrictHttpFirewall.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2001-2024 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.web; + +import org.springframework.security.web.firewall.StrictHttpFirewall; + +import java.util.regex.Pattern; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Spring Security HttpFirewall that allows parsing UTF8 header values. + */ +public class GeoNetworkStrictHttpFirewall extends StrictHttpFirewall { + private static final Pattern ALLOWED_HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*"); + + public GeoNetworkStrictHttpFirewall() { + super(); + + this.setAllowedHeaderValues(header -> { + String parsed = new String(header.getBytes(ISO_8859_1), UTF_8); + return ALLOWED_HEADER_VALUE_PATTERN.matcher(parsed).matches(); + }); + } +} diff --git a/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml b/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml index f83fa3e0bc9..c769833cefa 100644 --- a/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml +++ b/web/src/main/webapp/WEB-INF/config-security/config-security-core.xml @@ -65,8 +65,14 @@ <ref bean="coreFilterChain"/> </list> </constructor-arg> + + <property name="firewall" ref="httpFirewall"/> </bean> + <!-- HttpFirewall that parses UTF8 header values --> + <bean id="httpFirewall" + class="org.fao.geonet.web.GeoNetworkStrictHttpFirewall"> + </bean> <bean id="coreFilterChain" class="org.springframework.security.web.DefaultSecurityFilterChain"> From 60d54f0d1bf82aa35790d7fb38ededae202aed28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Wed, 5 Jun 2024 11:17:32 +0200 Subject: [PATCH 49/76] INSPIRE Atom harvester / process only public datasets by resource identifier --- .../fao/geonet/inspireatom/util/InspireAtomUtil.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/inspire-atom/src/main/java/org/fao/geonet/inspireatom/util/InspireAtomUtil.java b/inspire-atom/src/main/java/org/fao/geonet/inspireatom/util/InspireAtomUtil.java index a452d0733d0..622f8fe4ca3 100644 --- a/inspire-atom/src/main/java/org/fao/geonet/inspireatom/util/InspireAtomUtil.java +++ b/inspire-atom/src/main/java/org/fao/geonet/inspireatom/util/InspireAtomUtil.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2023 Food and Agriculture Organization of the +//=== Copyright (C) 2001-2024 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== @@ -63,7 +63,7 @@ * @author Jose García */ public class InspireAtomUtil { - private final static String EXTRACT_DATASETS_FROM_SERVICE_XSLT = "extract-datasetinfo-from-service-feed.xsl"; + private static final String EXTRACT_DATASETS_FROM_SERVICE_XSLT = "extract-datasetinfo-from-service-feed.xsl"; /** * Xslt process to get the related datasets in service metadata. @@ -395,7 +395,15 @@ public static String retrieveDatasetUuidFromIdentifier(EsSearchManager searchMan " \"value\": \"%s\"" + " }" + " }" + + " }," + + " {" + + " \"term\": {" + + " \"isPublishedToAll\": {" + + " \"value\": \"true\"" + + " }" + + " }" + " }" + + " ]" + " }" + "}"; From 2615fa7eea71bdfd2b599b3a992c8ea241fc0559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Mon, 2 Sep 2024 10:45:41 +0200 Subject: [PATCH 50/76] Workflow / update notification level based on user profile when cancelling a submission (#8264) * Workflow / update notification level based on user profile when cancelling a submission: - cancel working copy (from editor) --> should be notified the reviewer. - rejection (from reviewer) --> should be notified the editor. * Update core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java Co-authored-by: Ian <ianwallen@hotmail.com> * Fix code suggestion missing bracket --------- Co-authored-by: Ian <ianwallen@hotmail.com> --- .../kernel/metadata/DefaultStatusActions.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java b/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java index cdb7a8bf8f7..58cc82a4459 100644 --- a/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java +++ b/core/src/main/java/org/fao/geonet/kernel/metadata/DefaultStatusActions.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2023 Food and Agriculture Organization of the +//=== Copyright (C) 2001-2024 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== @@ -369,6 +369,25 @@ protected List<User> getUserToNotify(MetadataStatus status) { return new ArrayList<>(); } + // If status is DRAFT and previous status is SUBMITTED, which means either: + // - a cancel working copy (from editor) --> should be notified the reviewer. + // - rejection (from reviewer) --> should be notified the editor. + // and the notification level is recordUserAuthor or recordProfileReviewer, + // then adjust the notification level, depending on the user role + if ((status.getStatusValue().getId() == Integer.parseInt(StatusValue.Status.DRAFT)) && + (!StringUtils.isEmpty(status.getPreviousState()) && + (status.getPreviousState().equals(StatusValue.Status.SUBMITTED))) && + (notificationLevel.equals(StatusValueNotificationLevel.recordUserAuthor) || (notificationLevel.equals(StatusValueNotificationLevel.recordProfileReviewer)))) { + UserRepository userRepository = ApplicationContextHolder.get().getBean(UserRepository.class); + Optional<User> user = userRepository.findById(status.getUserId()); + if (user.isPresent()) { + if (user.get().getProfile() == Profile.Editor) { + notificationLevel = StatusValueNotificationLevel.recordProfileReviewer; + } else { + notificationLevel = StatusValueNotificationLevel.recordUserAuthor; + } + } + } // TODO: Status does not provide batch update // So taking care of one record at a time. // Currently the code could notify a mix of reviewers From 10c99f6eb832cf26fca415267417839d557b7229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Mon, 2 Sep 2024 10:47:01 +0200 Subject: [PATCH 51/76] Editor / Dublin core / Fix extent coordinates (#8258) Fixes https://github.com/geonetwork/core-geonetwork/issues/8255 Co-authored-by: ByronCinNZ <cochranes4@eml.cc> --- .../resources/catalog/components/common/map/mapService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/common/map/mapService.js b/web-ui/src/main/resources/catalog/components/common/map/mapService.js index a2af666d1f6..ab9420fb2a6 100644 --- a/web-ui/src/main/resources/catalog/components/common/map/mapService.js +++ b/web-ui/src/main/resources/catalog/components/common/map/mapService.js @@ -747,10 +747,10 @@ extent[1] + ", " + "East " + - extent[0] + + extent[2] + ", " + "West " + - extent[2]; + extent[0]; if (location) { dc += ". " + location; } From 1e643bd49404315f390bd19054c9f8edff5dd32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Mon, 2 Sep 2024 10:48:01 +0200 Subject: [PATCH 52/76] Indexing / Draft field MUST not be an array (#8242) In case of XSL error, index document was containing an array instead of single value ``` "draft": [ "n", "n" ], ``` Draft field is added by the database info so no need to add it again on XSL error. Properly set it in case of indexing error. not causing issue in current app (but will be more strict in index document model). --- .../geonet/kernel/search/EsSearchManager.java | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java b/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java index 122e7e2930a..978ab63a750 100644 --- a/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java +++ b/core/src/main/java/org/fao/geonet/kernel/search/EsSearchManager.java @@ -29,8 +29,8 @@ import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; import co.elastic.clients.elasticsearch.core.bulk.UpdateOperation; import co.elastic.clients.elasticsearch.core.search.Hit; -import co.elastic.clients.elasticsearch.indices.*; import co.elastic.clients.elasticsearch.indices.ExistsRequest; +import co.elastic.clients.elasticsearch.indices.*; import co.elastic.clients.transport.endpoints.BooleanResponse; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -73,8 +73,7 @@ import java.util.*; import static org.fao.geonet.constants.Geonet.IndexFieldNames.IS_TEMPLATE; -import static org.fao.geonet.kernel.search.IndexFields.INDEXING_ERROR_FIELD; -import static org.fao.geonet.kernel.search.IndexFields.INDEXING_ERROR_MSG; +import static org.fao.geonet.kernel.search.IndexFields.*; public class EsSearchManager implements ISearchManager { @@ -216,7 +215,6 @@ private void addMDFields(Element doc, Path schemaDir, doc.addContent(new Element(INDEXING_ERROR_FIELD).setText("true")); doc.addContent(createIndexingErrorMsgElement("indexingErrorMsg-indexingStyleSheetError", "error", Map.of("message", e.getMessage()))); - doc.addContent(new Element(IndexFields.DRAFT).setText("n")); } } @@ -225,7 +223,7 @@ private void addMoreFields(Element doc, Multimap<String, Object> fields) { fields.entries().forEach(e -> { Element newElement = new Element(e.getKey()) .setText(String.valueOf(e.getValue())); - if(objectFields.contains(e.getKey())) { + if (objectFields.contains(e.getKey())) { newElement.setAttribute("type", "object"); } doc.addContent(newElement); @@ -349,6 +347,7 @@ public BulkResponse updateFields(String id, Multimap<String, Object> fields, Set fields.asMap().forEach((e, v) -> fieldMap.put(e, v.toArray())); return updateFields(id, fieldMap, fieldsToRemove); } + public BulkResponse updateFields(String id, Map<String, Object> fieldMap, Set<String> fieldsToRemove) throws IOException { fieldMap.put(Geonet.IndexFieldNames.INDEXING_DATE, new Date()); @@ -404,7 +403,7 @@ public void updateFieldsAsynch(String id, Map<String, Object> fields) { if (exception != null) { LOGGER.error("Failed to index {}", exception); } else { - LOGGER.info("Updated fields for document {}", id); + LOGGER.info("Updated fields for document {}", id); } }); } @@ -479,7 +478,7 @@ private void sendDocumentsToIndex() { } catch (Exception e) { LOGGER.error( "An error occurred while indexing {} documents in current indexing list. Error is {}.", - listOfDocumentsToIndex.size(), e.getMessage()); + listOfDocumentsToIndex.size(), e.getMessage()); } finally { // TODO: Trigger this async ? documents.keySet().forEach(uuid -> overviewFieldUpdater.process(uuid)); @@ -502,6 +501,7 @@ private void checkIndexResponse(BulkResponse bulkItemResponses, String id = ""; String uuid = ""; String isTemplate = ""; + String isDraft = ""; String failureDoc = documents.get(e.id()); try { @@ -510,13 +510,14 @@ private void checkIndexResponse(BulkResponse bulkItemResponses, id = node.get(IndexFields.DBID).asText(); uuid = node.get("uuid").asText(); isTemplate = node.get(IS_TEMPLATE).asText(); + isDraft = node.get(DRAFT).asText(); } catch (Exception ignoredException) { } docWithErrorInfo.put(IndexFields.DBID, id); docWithErrorInfo.put("uuid", uuid); docWithErrorInfo.put(IndexFields.RESOURCE_TITLE, resourceTitle); docWithErrorInfo.put(IS_TEMPLATE, isTemplate); - docWithErrorInfo.put(IndexFields.DRAFT, "n"); + docWithErrorInfo.put(IndexFields.DRAFT, isDraft); docWithErrorInfo.put(INDEXING_ERROR_FIELD, true); ArrayNode errors = docWithErrorInfo.putArray(INDEXING_ERROR_MSG); errors.add(createIndexingErrorMsgObject(e.error().reason(), "error", Map.of())); @@ -539,7 +540,7 @@ private void checkIndexResponse(BulkResponse bulkItemResponses, BulkResponse response = client.bulkRequest(defaultIndex, listErrorOfDocumentsToIndex); if (response.errors()) { LOGGER.error("Failed to save error documents {}.", - Arrays.toString(errorDocumentIds.toArray())); + Arrays.toString(errorDocumentIds.toArray())); } } } @@ -675,7 +676,7 @@ public ObjectNode documentToJson(Element xml) { mapper.readTree(node.getTextNormalize())); } catch (IOException e) { LOGGER.error("Parsing invalid JSON node {} for property {}. Error is: {}", - node.getTextNormalize(), propertyName, e.getMessage()); + node.getTextNormalize(), propertyName, e.getMessage()); } } else { arrayNode.add( @@ -694,7 +695,7 @@ public ObjectNode documentToJson(Element xml) { )); } catch (IOException e) { LOGGER.error("Parsing invalid JSON node {} for property {}. Error is: {}", - nodeElements.get(0).getTextNormalize(), propertyName, e.getMessage()); + nodeElements.get(0).getTextNormalize(), propertyName, e.getMessage()); } } else { doc.put(propertyName, @@ -707,7 +708,8 @@ public ObjectNode documentToJson(Element xml) { } - /** Field starting with _ not supported in Kibana + /** + * Field starting with _ not supported in Kibana * Those are usually GN internal fields */ private String getPropertyName(String name) { @@ -935,12 +937,12 @@ public boolean isIndexWritable(String indexName) throws IOException, Elasticsear String indexBlockRead = "index.blocks.read_only_allow_delete"; GetIndicesSettingsRequest request = GetIndicesSettingsRequest.of( - b -> b.index(indexName) - .name(indexBlockRead) + b -> b.index(indexName) + .name(indexBlockRead) ); GetIndicesSettingsResponse settings = this.client.getClient() - .indices().getSettings(request); + .indices().getSettings(request); IndexState indexState = settings.get(indexBlockRead); @@ -951,7 +953,7 @@ public boolean isIndexWritable(String indexName) throws IOException, Elasticsear /** * Make a JSON Object that properly represents an indexingErrorMsg, to be used in the index. * - * @param type either 'error' or 'warning' + * @param type either 'error' or 'warning' * @param string a string that is translatable (see, e.g., en-search.json) * @param values values that replace the placeholders in the `string` parameter * @return a json object that represents an indexingErrorMsg @@ -962,7 +964,7 @@ public ObjectNode createIndexingErrorMsgObject(String string, String type, Map<S indexingErrorMsg.put("string", string); indexingErrorMsg.put("type", type); ObjectNode valuesObject = objectMapper.createObjectNode(); - values.forEach((k,v) -> valuesObject.put(k, String.valueOf(v))); + values.forEach((k, v) -> valuesObject.put(k, String.valueOf(v))); indexingErrorMsg.set("values", valuesObject); return indexingErrorMsg; } @@ -970,7 +972,7 @@ public ObjectNode createIndexingErrorMsgObject(String string, String type, Map<S /** * Create an Element that represents an indexingErrorMsg object, to be used in the index. * - * @param type either 'error' or 'warning' + * @param type either 'error' or 'warning' * @param string a string that is translatable (see, e.g., en-search.json) * @param values values that replace the placeholders in the `string` parameter * @return an Element that represents an indexingErrorMsg From 1094237d90dde96f233e4199af89907ba442fc1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Mon, 2 Sep 2024 13:56:40 +0200 Subject: [PATCH 53/76] Fix Clipboard copy/paste on Firefox - use ES5 (#8332) Follow up of #8320, that causes issues in 4.2.x. Added the change to main branch to avoid issues in future backports of changes in this file. --- .../resources/catalog/components/utility/UtilityDirective.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js b/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js index 425ce230238..493c85a99f6 100644 --- a/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js +++ b/web-ui/src/main/resources/catalog/components/utility/UtilityDirective.js @@ -1143,7 +1143,7 @@ return { copy: function (toCopy) { var deferred = $q.defer(); - if (navigator.clipboard?.writeText) { + if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(toCopy).then( function () { deferred.resolve(); @@ -1160,7 +1160,7 @@ }, paste: function () { var deferred = $q.defer(); - if (navigator.clipboard?.readText) { + if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard.readText().then( function (text) { deferred.resolve(text); From dc0f78a585934e5cdfbfb082eeb9c6d2864127c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Mon, 2 Sep 2024 14:47:40 +0200 Subject: [PATCH 54/76] Standard / ISO19115-3 / Formatters / ISO19139 / Ignore mcc linkage for overview (#8225) Overview file name is stored in `mcc:fileName` --- .../plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl index 4955ec576ca..764011f4819 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/ISO19139/toISO19139.xsl @@ -1115,5 +1115,6 @@ mcc:MD_Identifier/mcc:description| mrl:LI_Source/mrl:scope| mrl:sourceSpatialResolution| - mdq:derivedElement" priority="2"/> + mdq:derivedElement| + mri:graphicOverview/mcc:MD_BrowseGraphic/mcc:linkage" priority="2"/> </xsl:stylesheet> From 6f4e79ae4f59a4e80c2cfd08c40aabdc1c44dd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Mon, 2 Sep 2024 16:48:57 +0200 Subject: [PATCH 55/76] Admin / Source / Improve dirty state (#8222) * Admin / Source / Improve dirty state Due to wrong `form` tag placement, dirty state was only set when changing portal id and filter - not UI config, service or logo. Reorganize the form to improve this, so that save action is enabled when changing the form. Also keep the logo form upload not nested (because it would not work) * Update SourcesController.js * Admin / Source / Hide logo if not available. --- .../catalog/js/admin/SourcesController.js | 2 +- .../templates/admin/settings/sources.html | 221 +++++++++--------- 2 files changed, 112 insertions(+), 111 deletions(-) diff --git a/web-ui/src/main/resources/catalog/js/admin/SourcesController.js b/web-ui/src/main/resources/catalog/js/admin/SourcesController.js index 2a6c5e86e62..c83ff220c7c 100644 --- a/web-ui/src/main/resources/catalog/js/admin/SourcesController.js +++ b/web-ui/src/main/resources/catalog/js/admin/SourcesController.js @@ -211,7 +211,7 @@ $scope.deleteSourceLogo = function () { $scope.source.logo = null; - // $scope.updateSource(); + $scope.gnSourceForm.$setDirty(); }; // upload directive options diff --git a/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html b/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html index a0315d50b0f..56edf2a2bb6 100644 --- a/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html +++ b/web-ui/src/main/resources/catalog/templates/admin/settings/sources.html @@ -50,6 +50,7 @@ {{::s.name}} <img class="gn-source-logo" + onerror="this.style.display='none'" data-ng-src="{{'../../images/' + (s.type === 'subportal' ? 'harvesting/' + s.logo : 'logos/' + s.uuid + '.png')}}" /> </a> @@ -169,6 +170,20 @@ </p> </div> + <table class="table table-striped"> + <tr data-ng-repeat="(key, value) in source.label"> + <td>{{key | translate}}</td> + <td> + <input + type="text" + class="form-control" + value="{{value}}" + data-ng-model="source.label[key]" + /> + </td> + </tr> + </table> + <label data-translate="">sourceFilter</label> <input type="text" class="form-control" data-ng-model="source.filter" /> <p class="help-block" data-translate="">sourceFilter-help</p> @@ -177,130 +192,116 @@ <span data-translate="">displayInHeaderSwitcher</span> </label> <p class="help-block" data-translate="">displayInHeaderSwitcher-help</p> - </form> - <div> - <label data-translate="">sourceLogo</label> + <label data-translate="">sourceUiConfig</label> + <select + id="uiConfigurationList" + class="form-control" + data-ng-options="c.id as c.id for (key, c) in uiConfigurations | orderBy: 'id'" + data-ng-model="source.uiConfig" + ></select> + <p class="help-block" data-translate="">sourceUiConfig-help</p> - <div class="row" data-ng-show="source.logo"> - <div class="col-md-6 gn-nopadding-left"> - <img - data-ng-show="source.logo" - src="../../images/harvesting/{{ source.logo }}" - class="img-thumbnail form-group" - data-ng-attr-title="{{ source.logo }}" - /> - </div> - <div class="col-md-6 gn-nopadding-left"> - <a href="" data-ng-click="deleteSourceLogo()" class="text-danger"> - <i data-ng-show="source.logo" class="fa fa-times delete"></i> - </a> - </div> + <div> + <label for="serviceList" + >{{'system/csw/capabilityRecordUuid' | translate}}</label + > + + <div + data-gn-suggest="serviceRecordSearchObj" + data-gn-suggest-model="source.serviceRecord" + data-gn-suggest-property="_id" + data-gn-suggest-display-title="span" + ></div> + + <p class="help-block"> + {{'system/csw/capabilityRecordUuid-help' | translate}} + </p> </div> - <!--Display logo picker from harvester logos--> - <div class="row" data-ng-show="queue.length == 0"> - <div class="col-md-12 gn-nopadding-left gn-margin-bottom" translate> - selectExistingLogo - </div> - <div class="col-md-12 gn-nopadding-left gn-margin-bottom"> - <div class="form-group" gn-logo-picker="source.logo"></div> - </div> + <div data-ng-show="groups.length"> + <label class="control-label" data-translate="">subPortalGroupOwner</label> + <div + data-groups-combo="" + data-owner-group="source.groupOwner" + data-set-default-value="false" + data-optional="{{::$parent.user.isAdministrator()}}" + lang="lang" + groups="groups" + data-exclude-special-groups="true" + ></div> + + <p class="help-block" data-translate="">subPortalGroupOwnerHelp</p> </div> - <!--Display logo upload input--> - <form - id="gn-group-edit" - name="gnGroupEdit" - method="POST" - data-file-upload="logoUploadOptions" - role="form" - > - <input type="hidden" name="_csrf" value="{{csrf}}" /> - <div class="row" data-ng-show="!source.logo" id="group-logo-upload"> - <div class="col-md-12 gn-nopadding-left gn-margin-bottom" translate> - addNewLogo + <div> + <label data-translate="">sourceLogo</label> + + <div class="row" data-ng-show="source.logo"> + <div class="col-md-6 gn-nopadding-left"> + <img + data-ng-show="source.logo" + src="../../images/harvesting/{{ source.logo }}" + class="img-thumbnail form-group" + data-ng-attr-title="{{ source.logo }}" + /> </div> - <div class="col-md-12 gn-nopadding-left gn-nopadding-right"> - <div class="panel panel-default"> - <div class="panel-heading" data-translate="">upload</div> - <div class="panel-body"> - <span class="btn btn-success btn-block fileinput-button"> - <i class="fa fa-plus fa-white"></i> - <span data-translate="">chooseLogos</span> - <input type="file" id="source-logo" name="file" /> - </span> - <ul style="list-style: none"> - <li data-ng-repeat="file in queue"> - <div class="preview" data-file-upload-preview="file"></div> - {{file.name}} ({{file.type}} / {{file.size | formatFileSize}}) - <i class="fa fa-trash-o" data-ng-click="clear(file)"></i> - </li> - </ul> - </div> - </div> + <div class="col-md-6 gn-nopadding-left"> + <a href="" data-ng-click="deleteSourceLogo()" class="text-danger"> + <i data-ng-show="source.logo" class="fa fa-times delete"></i> + </a> </div> </div> - </form> - - <p class="help-block" data-translate="">sourceLogo-help</p> - </div> - - <label data-translate="">sourceUiConfig</label> - <select - id="uiConfigurationList" - class="form-control" - data-ng-options="c.id as c.id for (key, c) in uiConfigurations | orderBy: 'id'" - data-ng-model="source.uiConfig" - ></select> - <p class="help-block" data-translate="">sourceUiConfig-help</p> - - <div> - <label for="serviceList" - >{{'system/csw/capabilityRecordUuid' | translate}}</label - > - - <div - data-gn-suggest="serviceRecordSearchObj" - data-gn-suggest-model="source.serviceRecord" - data-gn-suggest-property="_id" - data-gn-suggest-display-title="span" - ></div> + </div> + </form> - <p class="help-block"> - {{'system/csw/capabilityRecordUuid-help' | translate}} - </p> + <!--Display logo picker from harvester logos--> + <div class="row" data-ng-show="queue.length == 0"> + <div class="col-md-12 gn-nopadding-left gn-margin-bottom" translate> + selectExistingLogo + </div> + <div class="col-md-12 gn-nopadding-left gn-margin-bottom"> + <div class="form-group" gn-logo-picker="source.logo"></div> + </div> </div> - <div data-ng-show="groups.length"> - <label class="control-label" data-translate="">subPortalGroupOwner</label> - <div - data-groups-combo="" - data-owner-group="source.groupOwner" - data-set-default-value="false" - data-optional="{{::$parent.user.isAdministrator()}}" - lang="lang" - groups="groups" - data-exclude-special-groups="true" - ></div> + <!--Display logo upload input--> + <form + id="gn-group-edit" + name="gnGroupEdit" + method="POST" + data-file-upload="logoUploadOptions" + role="form" + > + <input type="hidden" name="_csrf" value="{{csrf}}" /> + <div class="row" data-ng-show="!source.logo" id="group-logo-upload"> + <div class="col-md-12 gn-nopadding-left gn-margin-bottom" translate> + addNewLogo + </div> + <div class="col-md-12 gn-nopadding-left gn-nopadding-right"> + <div class="panel panel-default"> + <div class="panel-heading" data-translate="">upload</div> + <div class="panel-body"> + <span class="btn btn-success btn-block fileinput-button"> + <i class="fa fa-plus fa-white"></i> + <span data-translate="">chooseLogos</span> + <input type="file" id="source-logo" name="file" /> + </span> + <ul style="list-style: none"> + <li data-ng-repeat="file in queue"> + <div class="preview" data-file-upload-preview="file"></div> + {{file.name}} ({{file.type}} / {{file.size | formatFileSize}}) + <i class="fa fa-trash-o" data-ng-click="clear(file)"></i> + </li> + </ul> + </div> + </div> + </div> + </div> + </form> - <p class="help-block" data-translate="">subPortalGroupOwnerHelp</p> - </div> + <p class="help-block" data-translate="">sourceLogo-help</p> </div> - - <table class="table table-striped"> - <tr data-ng-repeat="(key, value) in source.label"> - <td>{{key | translate}}</td> - <td> - <input - type="text" - class="form-control" - value="{{value}}" - data-ng-model="source.label[key]" - /> - </td> - </tr> - </table> </div> </div> </div> From c9164d0e1f1106f5a9d26dca81962b954508a533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Mon, 2 Sep 2024 16:50:27 +0200 Subject: [PATCH 56/76] API / Improve parameter check for XSL conversion. (#8201) --- .../kernel/GeonetworkDataDirectory.java | 13 +++++++++ .../AbstractGeonetworkDataDirectoryTest.java | 27 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/fao/geonet/kernel/GeonetworkDataDirectory.java b/core/src/main/java/org/fao/geonet/kernel/GeonetworkDataDirectory.java index 86a0cdca444..cc5296232bd 100644 --- a/core/src/main/java/org/fao/geonet/kernel/GeonetworkDataDirectory.java +++ b/core/src/main/java/org/fao/geonet/kernel/GeonetworkDataDirectory.java @@ -27,8 +27,11 @@ import jeeves.server.sources.http.JeevesServlet; import org.fao.geonet.ApplicationContextHolder; import org.fao.geonet.constants.Geonet; +import org.fao.geonet.exceptions.BadParameterEx; +import org.fao.geonet.utils.FilePathChecker; import org.fao.geonet.utils.IO; import org.fao.geonet.utils.Log; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.ConfigurableApplicationContext; @@ -63,6 +66,9 @@ public class GeonetworkDataDirectory { */ public static final String GEONETWORK_BEAN_KEY = "GeonetworkDataDirectory"; + @Autowired + SchemaManager schemaManager; + private Path webappDir; private Path systemDataDir; private Path indexConfigDir; @@ -797,11 +803,18 @@ public Path getXsltConversion(String conversionId) { if (conversionId.startsWith(IMPORT_STYLESHEETS_SCHEMA_PREFIX)) { String[] pathToken = conversionId.split(":"); if (pathToken.length == 3) { + String schema = pathToken[1]; + if (!schemaManager.existsSchema(schema)) { + throw new BadParameterEx(String.format( + "Conversion not found. Schema '%s' is not registered in this catalog.", schema)); + } + FilePathChecker.verify(pathToken[2]); return this.getSchemaPluginsDir() .resolve(pathToken[1]) .resolve(pathToken[2] + ".xsl"); } } else { + FilePathChecker.verify(conversionId); return this.getWebappDir().resolve(Geonet.Path.IMPORT_STYLESHEETS). resolve(conversionId + ".xsl"); } diff --git a/core/src/test/java/org/fao/geonet/kernel/AbstractGeonetworkDataDirectoryTest.java b/core/src/test/java/org/fao/geonet/kernel/AbstractGeonetworkDataDirectoryTest.java index 7f7f4b26b4a..63624516b09 100644 --- a/core/src/test/java/org/fao/geonet/kernel/AbstractGeonetworkDataDirectoryTest.java +++ b/core/src/test/java/org/fao/geonet/kernel/AbstractGeonetworkDataDirectoryTest.java @@ -26,6 +26,8 @@ import jeeves.server.ServiceConfig; import org.fao.geonet.AbstractCoreIntegrationTest; +import org.fao.geonet.constants.Geonet; +import org.fao.geonet.exceptions.BadParameterEx; import org.jdom.Element; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -34,7 +36,7 @@ import java.nio.file.Path; import java.util.ArrayList; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * Abstract class for GeonetworkDataDirectory tests where the data directory layout is a default @@ -76,6 +78,29 @@ public void testInit() throws Exception { assertSystemDirSubFolders(expectedDataDir); } + @Test + public void testGetXsltConversion() { + Path xsltConversion = dataDirectory.getXsltConversion("conversion"); + assertEquals(dataDirectory.getWebappDir().resolve(Geonet.Path.IMPORT_STYLESHEETS).resolve("conversion.xsl"), xsltConversion); + try { + dataDirectory.getXsltConversion("../conversion"); + } catch (BadParameterEx e) { + assertEquals("../conversion is not a valid value for: Invalid character found in path.", e.getMessage()); + } + + xsltConversion = dataDirectory.getXsltConversion("schema:iso19115-3.2018:convert/fromISO19115-3.2014"); + assertNotNull(xsltConversion); + try { + dataDirectory.getXsltConversion("schema:notExistingSchema:convert/fromISO19115-3.2014"); + } catch (BadParameterEx e) { + assertEquals("Conversion not found. Schema 'notExistingSchema' is not registered in this catalog.", e.getMessage()); + } + try { + dataDirectory.getXsltConversion("schema:iso19115-3.2018:../../custom/path"); + } catch (BadParameterEx e) { + assertEquals("../../custom/path is not a valid value for: Invalid character found in path.", e.getMessage()); + } + } private void assertSystemDirSubFolders(Path expectedDataDir) { final Path expectedConfigDir = expectedDataDir.resolve("config"); assertEquals(expectedConfigDir, dataDirectory.getConfigDir()); From debb6ac31609f2def88dfe2e82967f285bc75590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Tue, 3 Sep 2024 15:40:54 +0200 Subject: [PATCH 57/76] Editor / DOI search / Improve label (#8338) --- web-ui/src/main/resources/catalog/locales/en-v4.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/main/resources/catalog/locales/en-v4.json b/web-ui/src/main/resources/catalog/locales/en-v4.json index 266f5e5bf43..dc8e786767d 100644 --- a/web-ui/src/main/resources/catalog/locales/en-v4.json +++ b/web-ui/src/main/resources/catalog/locales/en-v4.json @@ -435,7 +435,7 @@ "overviewUrl": "Overview URL", "restApiUrl": "REST API URL", "filterHelp": "Please click on one of the buttons below to activate the filter", - "selectDOIResource": "Choose a DOI resource", + "selectDOIResource": "Search for a DOI", "httpStatus--200": "Invalid status", "httpStatus-200": "200: Valid status", "httpStatus-404": "404: Not found", From 304d90cfbb510b59224717c2bddbeb1b3c50a738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Tue, 3 Sep 2024 16:05:14 +0200 Subject: [PATCH 58/76] Editor / Associated resource / Avoid empty label (#8339) When adding a source for example, the button label may be empty. Add a default one. --- .../catalog/components/edit/onlinesrc/partials/linkToMd.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/linkToMd.html b/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/linkToMd.html index d4cd3f89131..698421f1769 100644 --- a/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/linkToMd.html +++ b/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/linkToMd.html @@ -62,7 +62,7 @@ ng-disabled="!canEnableLinkButton(selectRecords)" > <i class="fa gn-icon-{{mode}}"></i> - <i class="icon-external-link"></i>  {{btn.label}} + <i class="icon-external-link"></i>  {{btn.label || ('saveLinkToSibling' | translate)}} </button> <div data-gn-need-help="linking-records" class="pull-right"></div> </div> From b5bc47436b34b08ff244edc2bae445bdfa161d0b Mon Sep 17 00:00:00 2001 From: wangf1122 <74916635+wangf1122@users.noreply.github.com> Date: Wed, 4 Sep 2024 03:48:00 -0400 Subject: [PATCH 59/76] publish status not refreshing fix (#8344) --- .../geonet/listener/metadata/draft/ApprovePublishedRecord.java | 2 +- .../catalog/components/metadataactions/MetadataActionService.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/ApprovePublishedRecord.java b/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/ApprovePublishedRecord.java index b335fc9cdec..546571cec96 100644 --- a/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/ApprovePublishedRecord.java +++ b/listeners/src/main/java/org/fao/geonet/listener/metadata/draft/ApprovePublishedRecord.java @@ -121,7 +121,7 @@ private void changeToApproved(AbstractMetadata md, MetadataStatus previousStatus status.setChangeDate(new ISODate()); status.setUserId(ServiceContext.get().getUserSession().getUserIdAsInt()); - metadataStatus.setStatusExt(status, false); + metadataStatus.setStatusExt(status, true); Log.trace(Geonet.DATA_MANAGER, "Metadata with id " + md.getId() + " automatically approved due to publishing."); } diff --git a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js index 98d4793b3ca..51d51978d82 100644 --- a/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js +++ b/web-ui/src/main/resources/catalog/components/metadataactions/MetadataActionService.js @@ -537,6 +537,7 @@ } if (md) { + gnMetadataManager.updateMdObj(md); md.publish(publicationType); } }, From ee23e5430ef839c90ab6d08890e1176481465741 Mon Sep 17 00:00:00 2001 From: Ian <ianwallen@hotmail.com> Date: Thu, 5 Sep 2024 06:11:43 -0300 Subject: [PATCH 60/76] iso19139 - Update thumbnail add/update and remove to support index update/removal (#8348) --- .../plugin/iso19139/process/thumbnail-add.xsl | 48 +++++++++++++------ .../iso19139/process/thumbnail-remove.xsl | 36 ++++++++++++-- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-add.xsl b/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-add.xsl index 16b975a837c..4c173556039 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-add.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-add.xsl @@ -27,7 +27,9 @@ xmlns:srv="http://www.isotc211.org/2005/srv" xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:geonet="http://www.fao.org/geonetwork" - exclude-result-prefixes="#all" + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns:digestUtils="java:org.apache.commons.codec.digest.DigestUtils" + exclude-result-prefixes="#all" version="2.0"> <!-- @@ -41,11 +43,22 @@ <xsl:param name="thumbnail_desc" select="''"/> <xsl:param name="thumbnail_type" select="''"/> - <!-- Target element to update. The key is based on the concatenation - of URL+Name --> - <xsl:param name="updateKey"/> - - <xsl:template match="gmd:identificationInfo/*"> + <!-- Target element to update. + updateKey is used to identify the resource name to be updated - it is for backwards compatibility. Will not be used if resourceHash is set. + The key is based on the concatenation of URL+Name + resourceHash is hash value of the object to be removed which will ensure the correct value is removed. It will override the usage of updateKey + resourceIdx is the index location of the object to be removed - can be used when duplicate entries exists to ensure the correct one is removed. +--> + <xsl:param name="updateKey" select="''"/> + <xsl:param name="resourceHash" select="''"/> + <xsl:param name="resourceIdx" select="''"/> + + <xsl:variable name="update_flag"> + <xsl:value-of select="boolean($updateKey != '' or $resourceHash != '' or $resourceIdx != '')"/> + </xsl:variable> + + <!-- Add new gmd:graphicOverview --> + <xsl:template match="gmd:identificationInfo/*[$update_flag = false()]"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:apply-templates select="gmd:citation"/> @@ -56,9 +69,7 @@ <xsl:apply-templates select="gmd:pointOfContact"/> <xsl:apply-templates select="gmd:resourceMaintenance"/> - <xsl:if test="$updateKey = ''"> - <xsl:call-template name="fill"/> - </xsl:if> + <xsl:call-template name="fill"/> <xsl:apply-templates select="gmd:graphicOverview"/> @@ -83,12 +94,19 @@ </xsl:copy> </xsl:template> - - <xsl:template match="gmd:graphicOverview[concat( - */gmd:fileName/gco:CharacterString, - */gmd:fileName/gmd:PT_FreeText/gmd:textGroup/gmd:LocalisedCharacterString[@locale = '#DE'], - */gmd:fileDescription/gmd:PT_FreeText/gmd:textGroup/gmd:LocalisedCharacterString[@locale = '#DE'], - */gmd:fileDescription/gco:CharacterString) = normalize-space($updateKey)]"> + <!-- Updating the gmd:graphicOverview based on update parameters --> + <!-- Note: first part of the match needs to match the xsl:for-each select from extract-relations.xsl in order to get the position() to match --> + <!-- The unique identifier is marked with resourceIdx which is the position index and resourceHash which is hash code of the current node (combination of url, resource name, and description) --> + <xsl:template + priority="2" + match="*//gmd:graphicOverview + [$resourceIdx = '' or position() = xs:integer($resourceIdx)] + [ ($resourceHash != '' or ($updateKey != '' and normalize-space($updateKey) = concat( + */gmd:fileName/gco:CharacterString, + */gmd:fileName/gmd:PT_FreeText/gmd:textGroup/gmd:LocalisedCharacterString[@locale = '#DE'], + */gmd:fileDescription/gmd:PT_FreeText/gmd:textGroup/gmd:LocalisedCharacterString[@locale = '#DE'], + */gmd:fileDescription/gco:CharacterString))) + and ($resourceHash = '' or digestUtils:md5Hex(normalize-space(.)) = $resourceHash)]"> <xsl:call-template name="fill"/> </xsl:template> diff --git a/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-remove.xsl b/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-remove.xsl index a856ed23dd1..de77616984c 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-remove.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/process/thumbnail-remove.xsl @@ -25,22 +25,48 @@ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:gmd="http://www.isotc211.org/2005/gmd" xmlns:gco="http://www.isotc211.org/2005/gco" + xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:geonet="http://www.fao.org/geonetwork" exclude-result-prefixes="#all" + xmlns:digestUtils="java:org.apache.commons.codec.digest.DigestUtils" version="2.0"> <!-- Usage: + thumbnail_url is the url to be removed - it is for backwards compatibility. Will not be used if resourceHash is set. + resourceHash is hash value of the object to be removed which will ensure the correct value is removed. It will override the usage of thumbnail_url + resourceIdx is the index location of the object to be removed - can be used when duplicate entries exists to ensure the correct one is removed. + + example: thumbnail-from-url-remove?thumbnail_url=http://geonetwork.org/thumbnails/image.png --> - <xsl:param name="thumbnail_url"/> + <xsl:param name="thumbnail_url" select="''"/> + <xsl:param name="resourceHash" select="''"/> + <xsl:param name="resourceIdx" select="''"/> <!-- Remove the thumbnail define in thumbnail_url parameter --> - <xsl:template - priority="2" - match="gmd:graphicOverview[normalize-space(gmd:MD_BrowseGraphic/gmd:fileName/gco:CharacterString) = normalize-space($thumbnail_url)]"/> + <!-- Note: first part of the match needs to match the xsl:for-each select from extract-relations.xsl in order to get the position() to match --> + <!-- The unique identifier is marked with resourceIdx which is the position index and resourceHash which is hash code of the current node (combination of url, resource name, and description) --> + + <xsl:template match="//gmd:graphicOverview" priority="2"> + + <!-- Calculate the global position of the current gmd:onLine element --> + <xsl:variable name="position" select="count(//gmd:graphicOverview[current() >> .]) + 1" /> + + <xsl:if test="not( + gmd:MD_BrowseGraphic[gmd:fileName/gco:CharacterString != ''] and + ($resourceIdx = '' or $position = xs:integer($resourceIdx)) and + ($resourceHash != '' or ($thumbnail_url != null and (normalize-space(gmd:MD_BrowseGraphic/gmd:fileName/gco:CharacterString) = normalize-space($thumbnail_url)))) + and ($resourceHash = '' or digestUtils:md5Hex(normalize-space(.)) = $resourceHash) + )"> + <xsl:copy> + <xsl:apply-templates select="@*|node()"/> + </xsl:copy> + </xsl:if> + </xsl:template> + - <!-- Do a copy of every nodes and attributes --> + <!-- Do a copy of every node and attribute --> <xsl:template match="@*|node()"> <xsl:copy> <xsl:apply-templates select="@*|node()"/> From 05b2e43dd5ce5a340081269461560af480b85af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Thu, 5 Sep 2024 15:40:44 +0200 Subject: [PATCH 61/76] CSW Harvester / Avoid increment 2 metrics for a single metadata in certain conditions (#8069) * CSW Harvester / Avoid increment 2 metrics for a single metadata in certain conditions. Retrieve metadata method increments metrics under certain conditions and returns null. Methods calling the retrieve metadata method increments additionally the unretrievable metric if null is returned. Sonarlint fixeas are already applied. Fixes #8039 * CSW Harvester / Use primitive boolean for 'force' parameter --- .../kernel/harvest/harvester/csw/Aligner.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java b/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java index 88942e4ec86..5097d9a600c 100644 --- a/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java +++ b/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/csw/Aligner.java @@ -1,5 +1,5 @@ //============================================================================= -//=== Copyright (C) 2001-2007 Food and Agriculture Organization of the +//=== Copyright (C) 2001-2024 Food and Agriculture Organization of the //=== United Nations (FAO-UN), United Nations World Food Programme (WFP) //=== and United Nations Environment Programme (UNEP) //=== @@ -232,7 +232,7 @@ private void insertOrUpdate(Collection<RecordInfo> records, Collection<HarvestEr } result.totalMetadata++; - } catch (Throwable t) { + } catch (Exception t) { errors.add(new HarvestError(this.context, t)); log.error("Unable to process record from csw (" + this.params.getName() + ")"); log.error(" Record failed: " + ri.uuid + ". Error is: " + t.getMessage()); @@ -285,7 +285,6 @@ private void addMetadata(RecordInfo ri, String uuidToAssign) throws Exception { Element md = retrieveMetadata(ri.uuid); if (md == null) { - result.unretrievable++; return; } @@ -404,7 +403,7 @@ public static void applyBatchEdits( if (StringUtils.isNotEmpty(batchEditParameter.getCondition())) { applyEdit = false; final Object node = Xml.selectSingle(md, batchEditParameter.getCondition(), metadataSchema.getNamespaces()); - if (node != null && node instanceof Boolean && (Boolean)node == true) { + if (node instanceof Boolean && Boolean.TRUE.equals(node)) { applyEdit = true; } } @@ -424,7 +423,7 @@ public static void applyBatchEdits( } } } - private void updateMetadata(RecordInfo ri, String id, Boolean force) throws Exception { + private void updateMetadata(RecordInfo ri, String id, boolean force) throws Exception { String date = localUuids.getChangeDate(ri.uuid); if (date == null && !force) { @@ -443,11 +442,10 @@ private void updateMetadata(RecordInfo ri, String id, Boolean force) throws Exce } } @Transactional(value = TxType.REQUIRES_NEW) - boolean updatingLocalMetadata(RecordInfo ri, String id, Boolean force) throws Exception { + boolean updatingLocalMetadata(RecordInfo ri, String id, boolean force) throws Exception { Element md = retrieveMetadata(ri.uuid); if (md == null) { - result.unchangedMetadata++; return false; } @@ -500,8 +498,11 @@ boolean updatingLocalMetadata(RecordInfo ri, String id, Boolean force) throws Ex } /** - * Does CSW GetRecordById request. If validation is requested and the metadata does not - * validate, null is returned. + * Does CSW GetRecordById request. Returns null on error conditions: + * - If validation is requested and the metadata does not validate. + * - No metadata is retrieved. + * - If metadata resource is duplicated. + * - An exception occurs retrieving the metadata. * * @param uuid uuid of metadata to request * @return metadata the metadata @@ -524,6 +525,7 @@ private Element retrieveMetadata(String uuid) { //--- maybe the metadata has been removed if (list.isEmpty()) { + result.unretrievable++; return null; } @@ -544,11 +546,10 @@ private Element retrieveMetadata(String uuid) { return null; } - if (params.rejectDuplicateResource) { - if (foundDuplicateForResource(uuid, response)) { + if (params.rejectDuplicateResource && (foundDuplicateForResource(uuid, response))) { result.unchangedMetadata++; return null; - } + } return response; @@ -612,7 +613,7 @@ private boolean foundDuplicateForResource(String uuid, Element response) { } } } - } catch (Throwable e) { + } catch (Exception e) { log.warning(" - Error when searching for resource duplicate " + uuid + ". Error is: " + e.getMessage()); } } From d560b2a9aef3a6d1ab0d18bda96cd9b604882b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michel=20Gabri=C3=ABl?= <michel.gabriel@geocat.net> Date: Mon, 9 Sep 2024 08:21:35 +0200 Subject: [PATCH 62/76] Put the image name in the `alt` attribute in the thumbnail on the metadata page. (#8290) This commit adds the `<img>` in the `alt` attribute when there is a name, otherwise a default string is used. The image name used to be the caption underneath the thumbnail, but is now removed Extra: the `max-height` is increased in order to have the full width of the image. --- .../views/default/less/gn_result_default.less | 2 +- .../default/templates/recordView/thumbnails.html | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/web-ui/src/main/resources/catalog/views/default/less/gn_result_default.less b/web-ui/src/main/resources/catalog/views/default/less/gn_result_default.less index 4e26b7506c4..09867678db0 100644 --- a/web-ui/src/main/resources/catalog/views/default/less/gn_result_default.less +++ b/web-ui/src/main/resources/catalog/views/default/less/gn_result_default.less @@ -147,7 +147,7 @@ .img-thumbnail { padding: 0; border: none; - max-height: 300px; + max-height: 500px; min-height: 150px; max-width: 100%; } diff --git a/web-ui/src/main/resources/catalog/views/default/templates/recordView/thumbnails.html b/web-ui/src/main/resources/catalog/views/default/templates/recordView/thumbnails.html index 87fab96123f..6338bf0f4d5 100644 --- a/web-ui/src/main/resources/catalog/views/default/templates/recordView/thumbnails.html +++ b/web-ui/src/main/resources/catalog/views/default/templates/recordView/thumbnails.html @@ -1,13 +1,22 @@ <ul class="gn-thumbnails" data-ng-if="mdView.current.record.overview.length > 0"> <li data-ng-repeat="img in mdView.current.record.overview"> <img + data-ng-if="img.name" data-gn-img-modal="img" class="img-thumbnail" - alt="{{'overview' | translate}}" + alt="{{img.name}}" title="{{img.name}}" data-ng-src="{{mdView.current.record.draft === 'y'? img.url + (img.url.indexOf('?') > 0 ? '&' : '?') + 'approved=false' : img.url}}" onerror="this.onerror=null; this.parentElement.style.display='none';" /> - <p class="text-center" data-ng-if="img.name != ''">{{img.name}}</p> + <img + data-ng-if="!img.name" + data-gn-img-modal="img" + class="img-thumbnail" + alt="{{'overview' | translate}}" + title="{{'overview' | translate}}" + data-ng-src="{{mdView.current.record.draft === 'y'? img.url + (img.url.indexOf('?') > 0 ? '&' : '?') + 'approved=false' : img.url}}" + onerror="this.onerror=null; this.parentElement.style.display='none';" + /> </li> </ul> From 39838a9d5b2995d39dfadc20ec9009f6c30c0538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Mon, 9 Sep 2024 14:21:26 +0200 Subject: [PATCH 63/76] Don't add file content to the exception when requesting XML documents, if the content is not XML (#8360) --- common/src/main/java/org/fao/geonet/utils/XmlRequest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/fao/geonet/utils/XmlRequest.java b/common/src/main/java/org/fao/geonet/utils/XmlRequest.java index 7b6a3b69c59..cba8608a556 100644 --- a/common/src/main/java/org/fao/geonet/utils/XmlRequest.java +++ b/common/src/main/java/org/fao/geonet/utils/XmlRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -124,13 +124,13 @@ protected final Element executeAndReadResponse(HttpRequestBase httpMethod) throw " -- Response Code: " + httpResponse.getRawStatusCode()); } - byte[] data = null; + byte[] data; try { data = IOUtils.toByteArray(httpResponse.getBody()); return Xml.loadStream(new ByteArrayInputStream(data)); } catch (JDOMException e) { - throw new BadXmlResponseEx("Response: '" + new String(data, "UTF8") + "' (from URI " + httpMethod.getURI() + ")"); + throw new BadXmlResponseEx("Invalid XML document from URI: " + httpMethod.getURI()); } finally { httpMethod.releaseConnection(); From c2a3f5fd6295676e1e523e7bed233907df3b5680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Wed, 5 Jul 2023 13:15:53 +0200 Subject: [PATCH 64/76] Harvester / Simple URL / ODS / Improve mapping Follow up of https://github.com/geonetwork/core-geonetwork/pull/7059 * Add elements taking into account the API version 1 or 2 * Do not put free text in an ISO field which is an enumeration (which avoids to mix facet icons and translations for topic category) * Provide a mapping based on default ODS values for french and english * Add a dedicated keyword block with the free text values --- .../convert/fromJsonOpenDataSoft.xsl | 84 +++++++----- .../convert/odstheme-mapping.xsl | 43 ++++++ .../convert/protocol-mapping.xsl | 123 +++++++++--------- 3 files changed, 155 insertions(+), 95 deletions(-) create mode 100644 schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/odstheme-mapping.xsl diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/fromJsonOpenDataSoft.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/fromJsonOpenDataSoft.xsl index a14cd81ca4a..04e69e18483 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/fromJsonOpenDataSoft.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/fromJsonOpenDataSoft.xsl @@ -45,7 +45,8 @@ xmlns:java-xsl-util="java:org.fao.geonet.util.XslUtil" exclude-result-prefixes="#all"> - <xsl:import href="protocol-mapping.xsl"></xsl:import> + <xsl:import href="protocol-mapping.xsl"/> + <xsl:import href="odstheme-mapping.xsl"/> <xsl:output method="xml" indent="yes"/> @@ -130,26 +131,30 @@ </cit:CI_Responsibility> </mdb:contact> - <mdb:dateInfo> - <cit:CI_Date> - <cit:date> - <gco:DateTime><xsl:value-of select="(metas/modified|dataset/metas/default/metadata_processed)[1]"/></gco:DateTime> - </cit:date> - <cit:dateType> - <cit:CI_DateTypeCode codeList="codeListLocation#CI_DateTypeCode" codeListValue="publication"/> - </cit:dateType> - </cit:CI_Date> - </mdb:dateInfo> - <mdb:dateInfo> - <cit:CI_Date> - <cit:date> - <gco:DateTime><xsl:value-of select="metas/metadata_processed"/></gco:DateTime> - </cit:date> - <cit:dateType> - <cit:CI_DateTypeCode codeList="codeListLocation#CI_DateTypeCode" codeListValue="revision"/> - </cit:dateType> - </cit:CI_Date> - </mdb:dateInfo> + <xsl:for-each select="metas/modified"> + <mdb:dateInfo> + <cit:CI_Date> + <cit:date> + <gco:DateTime><xsl:value-of select="."/></gco:DateTime> + </cit:date> + <cit:dateType> + <cit:CI_DateTypeCode codeList="codeListLocation#CI_DateTypeCode" codeListValue="publication"/> + </cit:dateType> + </cit:CI_Date> + </mdb:dateInfo> + </xsl:for-each> + <xsl:for-each select="metas/metadata_processed|dataset/metas/default/metadata_processed"> + <mdb:dateInfo> + <cit:CI_Date> + <cit:date> + <gco:DateTime><xsl:value-of select="."/></gco:DateTime> + </cit:date> + <cit:dateType> + <cit:CI_DateTypeCode codeList="codeListLocation#CI_DateTypeCode" codeListValue="revision"/> + </cit:dateType> + </cit:CI_Date> + </mdb:dateInfo> + </xsl:for-each> <mdb:metadataStandard> <cit:CI_Citation> <cit:title> @@ -264,15 +269,33 @@ </mri:pointOfContact> </xsl:for-each> - <!-- ODS themes copied as topicCategory --> - <xsl:if test="metas/theme"> - <xsl:for-each select="metas/theme"> - <mri:topicCategory> - <mri:MD_TopicCategoryCode> - <xsl:value-of select="."/> - </mri:MD_TopicCategoryCode> - </mri:topicCategory> - </xsl:for-each> + + <xsl:variable name="odsThemes" + select="metas/theme|dataset/metas/default/theme"/> + <xsl:if test="count($odsThemes) > 0"> + <xsl:for-each select="distinct-values($odsThemeToIsoTopic[theme = $odsThemes]/name())"> + <mri:topicCategory> + <mri:MD_TopicCategoryCode> + <xsl:value-of select="."/> + </mri:MD_TopicCategoryCode> + </mri:topicCategory> + </xsl:for-each> + + <mri:descriptiveKeywords> + <mri:MD_Keywords> + <xsl:for-each select="$odsThemes"> + <mri:keyword> + <gco:CharacterString> + <xsl:value-of select="."/> + </gco:CharacterString> + </mri:keyword> + </xsl:for-each> + <mri:type> + <mri:MD_KeywordTypeCode codeListValue="theme" + codeList="./resources/codeList.xml#MD_KeywordTypeCode"/> + </mri:type> + </mri:MD_Keywords> + </mri:descriptiveKeywords> </xsl:if> <!-- ODS keywords copied without type --> @@ -292,6 +315,7 @@ </mri:descriptiveKeywords> </xsl:if> + <!-- license_url: "http://opendatacommons.org/licenses/odbl/", --> diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/odstheme-mapping.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/odstheme-mapping.xsl new file mode 100644 index 00000000000..c5046e099cf --- /dev/null +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/odstheme-mapping.xsl @@ -0,0 +1,43 @@ +<xsl:stylesheet version="2.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + exclude-result-prefixes="#all"> + + <xsl:variable name="odsThemeToIsoTopic" as="node()*"> + <health> + <theme>Santé</theme> + <theme>Health</theme> + </health> + <environment> + <theme>Environnement</theme> + <theme>Environment</theme> + </environment> + <transportation> + <theme>Transports, Déplacements</theme> + <theme>Transport, Movements</theme> + </transportation> + <structure> + <theme>Aménagement du territoire, Urbanisme, Bâtiments, Equipements, Habitat</theme> + <theme>Spatial planning, Town planning, Buildings, Equipment, Housing</theme> + </structure> + <economy> + <theme>Economie, Entreprise, PME, Développement économique, Emploi</theme> + <theme>Economy, Business, SME, Economic development, Employment</theme> + </economy> + <society> + <theme>Patrimoine culturel</theme> + <theme>Culture, Heritage</theme> + <theme>Education, Formation, Recherche, Enseignement</theme> + <theme>Education, Training, Research, Teaching</theme> + <theme>Administration, Gouvernement, Finances publiques, Citoyenneté</theme> + <theme>Administration, Government, Public finances, Citizenship</theme> + <theme>Justice, Sécurité, Police, Criminalité</theme> + <theme>Justice, Safety, Police, Crime</theme> + <theme>Sports, Loisirs</theme> + <theme>Sports, Leisure</theme> + <theme>Hébergement, industrie hôtelière</theme> + <theme>Accommodation, Hospitality Industry</theme> + <theme>Services sociaux</theme> + <theme>Services, Social</theme> + </society> + </xsl:variable> +</xsl:stylesheet> diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/protocol-mapping.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/protocol-mapping.xsl index 81062b32791..a3429faa6dd 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/protocol-mapping.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/convert/protocol-mapping.xsl @@ -1,71 +1,64 @@ <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xmlns:dct="http://purl.org/dc/terms/" - xmlns:dcat="http://www.w3.org/ns/dcat#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" exclude-result-prefixes="#all"> - <xsl:output method="xml" indent="yes"/> - <xsl:strip-space elements="*"/> - - <xsl:variable name="format-protocol-mapping"> - <entry> - <format>csv</format> - <protocol>WWW:DOWNLOAD:text/csv</protocol> - </entry> - <entry> - <format>geojson</format> - <protocol>WWW:DOWNLOAD:application/vnd.geo+json</protocol> - </entry> - <entry> - <format>kml</format> - <protocol>WWW:DOWNLOAD:application/vnd.google-earth.kml+xml</protocol> - </entry> - <entry> - <format>zip</format> - <protocol>WWW:DOWNLOAD:application/zip</protocol> - </entry> - <entry> - <format>shapefile</format> - <format>shp</format> - <protocol>WWW:DOWNLOAD:x-gis/x-shapefile</protocol> - </entry> - <entry> - <format>json</format> - <protocol>WWW:DOWNLOAD:application/json</protocol> - </entry> - <entry> - <format>pdf</format> - <protocol>WWW:DOWNLOAD:application/pdf</protocol> - </entry> - <entry> - <format>xls</format> - <protocol>WWW:DOWNLOAD:application/vnd.ms-excel</protocol> - </entry> - <entry> - <format>xlsx</format> - <format>excel</format> - <protocol>WWW:DOWNLOAD:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</protocol> - </entry> - <entry> - <format>rtf</format> - <protocol>WWW:DOWNLOAD:application/rtf</protocol> - </entry> - <entry> - <format>web page</format> - <format>html</format> - <format>arcgis</format> - <protocol>WWW:LINK-1.0-http--link</protocol> - </entry> - <entry> - <format>wms</format> - <protocol>OGC:WMS</protocol> - </entry> - <entry> - <format>wfs</format> - <protocol>OGC:WFS</protocol> - </entry> - </xsl:variable> + <xsl:variable name="format-protocol-mapping"> + <entry> + <format>csv</format> + <protocol>WWW:DOWNLOAD:text/csv</protocol> + </entry> + <entry> + <format>geojson</format> + <protocol>WWW:DOWNLOAD:application/vnd.geo+json</protocol> + </entry> + <entry> + <format>kml</format> + <protocol>WWW:DOWNLOAD:application/vnd.google-earth.kml+xml</protocol> + </entry> + <entry> + <format>zip</format> + <protocol>WWW:DOWNLOAD:application/zip</protocol> + </entry> + <entry> + <format>shapefile</format> + <format>shp</format> + <protocol>WWW:DOWNLOAD:x-gis/x-shapefile</protocol> + </entry> + <entry> + <format>json</format> + <protocol>WWW:DOWNLOAD:application/json</protocol> + </entry> + <entry> + <format>pdf</format> + <protocol>WWW:DOWNLOAD:application/pdf</protocol> + </entry> + <entry> + <format>xls</format> + <protocol>WWW:DOWNLOAD:application/vnd.ms-excel</protocol> + </entry> + <entry> + <format>xlsx</format> + <format>excel</format> + <protocol>WWW:DOWNLOAD:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet</protocol> + </entry> + <entry> + <format>rtf</format> + <protocol>WWW:DOWNLOAD:application/rtf</protocol> + </entry> + <entry> + <format>web page</format> + <format>html</format> + <format>arcgis</format> + <protocol>WWW:LINK-1.0-http--link</protocol> + </entry> + <entry> + <format>wms</format> + <protocol>OGC:WMS</protocol> + </entry> + <entry> + <format>wfs</format> + <protocol>OGC:WFS</protocol> + </entry> + </xsl:variable> </xsl:stylesheet> From 3f3a196a81cfb30d76620106e59eb726fe9e3d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Fri, 13 Sep 2024 08:10:24 +0200 Subject: [PATCH 65/76] Standard / ISO19115-3 / Label improvement. (#8364) --- .../src/main/plugin/iso19115-3.2018/loc/eng/labels.xml | 4 ++++ .../src/main/plugin/iso19115-3.2018/loc/fre/labels.xml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/eng/labels.xml b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/eng/labels.xml index d24c02eb5c8..d4b329e3acb 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/eng/labels.xml +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/eng/labels.xml @@ -1323,6 +1323,10 @@ <label>Metadata language</label> <description iso="true">Designation of the locale language</description> </element> + <element name="lan:language" id="448.0" context="/mdb:MD_Metadata/mdb:identificationInfo/mri:MD_DataIdentification/mri:defaultLocale/lan:PT_Locale/lan:language"> + <label>Dataset language</label> + <description iso="true">ISO 3 letters code.</description> + </element> <element name="lan:language" id="448.0" context="/mdb:MD_Metadata/mdb:otherLocale/lan:PT_Locale/lan:language"> <label>Other language</label> <description iso="true">Additional metadata language</description> diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/fre/labels.xml b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/fre/labels.xml index b17c9b295b2..1ac962c2539 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/fre/labels.xml +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/loc/fre/labels.xml @@ -1396,6 +1396,10 @@ <label>Langue de la fiche</label> <description iso="true">Langue principale de la fiche. Code ISO de la langue à 3 caractères.</description> </element> + <element name="lan:language" id="448.0" context="/mdb:MD_Metadata/mdb:identificationInfo/mri:MD_DataIdentification/mri:defaultLocale/lan:PT_Locale/lan:language"> + <label>Langue de la donnée</label> + <description iso="true">Langue de la donnée. Code ISO de la langue à 3 caractères.</description> + </element> <element name="lan:language" id="448.0" context="/mdb:MD_Metadata/mdb:otherLocale/lan:PT_Locale/lan:language"> <label>Autres langues</label> <description iso="true">Langue additionnelle. Code ISO de la langue à 3 caractères.</description> From 796385c6e04ba0dbf6b8da1ca0691e2ec420a571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Fri, 13 Sep 2024 08:19:17 +0200 Subject: [PATCH 66/76] Editor / Associated resource / DOI search. (#8363) * Add tooltip with API query to inform user which fields are used for the search. * Ignore https://doi.org prefix as most of the time user search using the DOI URL but the API https://support.datacite.org/docs/api-queries search is only based on prefix/id --- .../edit/onlinesrc/OnlineSrcService.js | 24 ++++++++++++------- .../onlinesrc/partials/doisearchpanel.html | 1 + 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/edit/onlinesrc/OnlineSrcService.js b/web-ui/src/main/resources/catalog/components/edit/onlinesrc/OnlineSrcService.js index 0d9a1fe8329..94ee49c926e 100644 --- a/web-ui/src/main/resources/catalog/components/edit/onlinesrc/OnlineSrcService.js +++ b/web-ui/src/main/resources/catalog/components/edit/onlinesrc/OnlineSrcService.js @@ -265,9 +265,9 @@ linksAndRelatedPromises.push( $http.get( apiPrefix + - "/related?type=" + - relatedTypes.join("&type=") + - (!isApproved ? "&approved=false" : ""), + "/related?type=" + + relatedTypes.join("&type=") + + (!isApproved ? "&approved=false" : ""), { headers: { Accept: "application/json" @@ -281,9 +281,9 @@ linksAndRelatedPromises.push( $http.get( apiPrefix + - "/associated?type=" + - associatedTypes.join(",") + - (!isApproved ? "&approved=false" : ""), + "/associated?type=" + + associatedTypes.join(",") + + (!isApproved ? "&approved=false" : ""), { headers: { Accept: "application/json" @@ -610,8 +610,8 @@ scopedName: params.remote ? "" : params.name === qParams.name - ? "" - : qParams.name, + ? "" + : qParams.name, uuidref: qParams.uuidDS, uuid: qParams.uuidSrv, url: qParams.remote ? qParams.url : "", @@ -932,7 +932,13 @@ function ($http) { return { search: function (url, prefix, query) { - return $http.get(url + "?prefix=" + prefix + "&query=" + query); + return $http.get( + url + + "?prefix=" + + prefix + + "&query=" + + query.replaceAll("https://doi.org/", "") + ); } }; } diff --git a/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/doisearchpanel.html b/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/doisearchpanel.html index 71799b9652e..35e33c568f9 100644 --- a/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/doisearchpanel.html +++ b/web-ui/src/main/resources/catalog/components/edit/onlinesrc/partials/doisearchpanel.html @@ -17,6 +17,7 @@ data-ng-model-options="{ debounce: 1000 }" data-ng-readonly="isSearching" type="text" + title="{{'selectDOIResource' | translate}} - {{doiQueryPattern}}" autocomplete="off" placeholder="{{'anyPlaceHolder' | translate}}" aria-label="{{'anyPlaceHolder' | translate}}" From a66662c17f00b4baaba714c892ba574e12896e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Thu, 25 Jul 2024 17:59:46 +0200 Subject: [PATCH 67/76] Add build profile for MacOS ARM --- pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pom.xml b/pom.xml index 4c491177d3f..8e6770b471a 100644 --- a/pom.xml +++ b/pom.xml @@ -1451,6 +1451,21 @@ <kb.platform>darwin-x86</kb.platform> <kb.installer.extension>tar.gz</kb.installer.extension> </properties> + </profile> + <profile> + <id>macOS_aarch64</id> + <activation> + <os> + <family>mac</family> + <arch>aarch64</arch> + </os> + </activation> + <properties> + <es.platform>darwin-aarch64</es.platform> + <kb.executable>kibana.sh</kb.executable> + <kb.platform>darwin-aarch64</kb.platform> + <kb.installer.extension>tar.gz</kb.installer.extension> + </properties> </profile> <profile> <id>windows</id> From 8e92e94301005e6d420c81f3c37460908dd42376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Tue, 17 Sep 2024 10:02:12 +0200 Subject: [PATCH 68/76] Map viewer / WMS GetFeatureInfo support for application/json info format (#8372) --- .../resources/catalog/components/viewer/gfi/FeaturesLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-ui/src/main/resources/catalog/components/viewer/gfi/FeaturesLoader.js b/web-ui/src/main/resources/catalog/components/viewer/gfi/FeaturesLoader.js index c9ca61926cf..62d6fbdde68 100644 --- a/web-ui/src/main/resources/catalog/components/viewer/gfi/FeaturesLoader.js +++ b/web-ui/src/main/resources/catalog/components/viewer/gfi/FeaturesLoader.js @@ -151,7 +151,7 @@ }) .then( function (response) { - if (infoFormat && infoFormat.match(/application\/(geo|geo\+)json/i) != null) { + if (infoFormat && infoFormat.match(/application\/(geo|geo\+)?json/i) != null) { var jsonf = new ol.format.GeoJSON(); var features = []; response.data.features.forEach(function (f) { From 74cdf4ab36536e052bccff064d2b97915e302f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Tue, 17 Sep 2024 10:31:04 +0200 Subject: [PATCH 69/76] GIT / .gitignore Ignore all schemas plugins. --- .gitignore | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index cbd4fe33eaa..84f282d0af7 100644 --- a/.gitignore +++ b/.gitignore @@ -64,11 +64,7 @@ web/src/main/webapp/META-INF/MANIFEST.MF web/src/main/webapp/WEB-INF/data/0* web/src/main/webapp/WEB-INF/data/config/encryptor.properties web/src/main/webapp/WEB-INF/data/config/index/records.json -web/src/main/webapp/WEB-INF/data/config/schema_plugins/*/schematron/schematron*.xsl -web/src/main/webapp/WEB-INF/data/config/schema_plugins/csw-record -web/src/main/webapp/WEB-INF/data/config/schema_plugins/dublin-core -web/src/main/webapp/WEB-INF/data/config/schema_plugins/iso19* -web/src/main/webapp/WEB-INF/data/config/schema_plugins/schemaplugin-uri-catalog.xml +web/src/main/webapp/WEB-INF/data/config/schema_plugins/* web/src/main/webapp/WEB-INF/data/config/schemaplugin-uri-catalog.xml web/src/main/webapp/WEB-INF/data/data/backup web/src/main/webapp/WEB-INF/data/data/metadata_data From e77f9d61f31affc4badec4c2fff754e3a4305d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Thu, 19 Sep 2024 13:58:50 +0200 Subject: [PATCH 70/76] Xsl utility / Add a function to retrieve thesaurus title with its key (#8378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Xsl utility / Add a function to retrieve thesaurus title with its key Some schema (eg. DCAT) does not contain thesaurus name in the metadata record. While indexing we add the thesaurus title in the index. This utility function allows to retrieve it with the thesaurus key (eg. external.theme.publisher-type). * Update core/src/main/java/org/fao/geonet/util/XslUtil.java Co-authored-by: Jose García <josegar74@gmail.com> --------- Co-authored-by: Jose García <josegar74@gmail.com> --- core/src/main/java/org/fao/geonet/util/XslUtil.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/src/main/java/org/fao/geonet/util/XslUtil.java b/core/src/main/java/org/fao/geonet/util/XslUtil.java index d6514ffd045..34b9ae272ee 100644 --- a/core/src/main/java/org/fao/geonet/util/XslUtil.java +++ b/core/src/main/java/org/fao/geonet/util/XslUtil.java @@ -1440,6 +1440,19 @@ public static String getThesaurusIdByTitle(String title) { return thesaurus == null ? "" : "geonetwork.thesaurus." + thesaurus.getKey(); } + + /** + * Retrieve the thesaurus title using the thesaurus key. + * + * @param id the thesaurus key + * @return the thesaurus title or empty string if the thesaurus doesn't exist. + */ + public static String getThesaurusTitleByKey(String id) { + ApplicationContext applicationContext = ApplicationContextHolder.get(); + ThesaurusManager thesaurusManager = applicationContext.getBean(ThesaurusManager.class); + Thesaurus thesaurus = thesaurusManager.getThesaurusByName(id); + return thesaurus == null ? "" : thesaurus.getTitle(); + } /** From 1a780ac0d09f1a1df0cdbe23e513e8440c994227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Thu, 19 Sep 2024 14:59:23 +0200 Subject: [PATCH 71/76] Indexing / DCAT multilingual support (#8377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Indexing / DCAT multilingual support * Use 3 letter code for field ID like in ISO * Fix position to properly index a set of multilingual elements ```xml <dct:title xml:lang="nl">NL</dct:title> <dct:title xml:lang="en">EN</dct:title> ``` Related to https://github.com/metadata101/dcat-ap.vl/pull/5 * Update web/src/main/webapp/xslt/common/index-utils.xsl Co-authored-by: Jose García <josegar74@gmail.com> --------- Co-authored-by: Jose García <josegar74@gmail.com> --- .../main/webapp/xslt/common/index-utils.xsl | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/web/src/main/webapp/xslt/common/index-utils.xsl b/web/src/main/webapp/xslt/common/index-utils.xsl index 41f73c406f7..100389f8936 100644 --- a/web/src/main/webapp/xslt/common/index-utils.xsl +++ b/web/src/main/webapp/xslt/common/index-utils.xsl @@ -244,24 +244,30 @@ <!--<xsl:message>gn-fn-index:add-field <xsl:value-of select="$fieldName"/></xsl:message> <xsl:message>gn-fn-index:add-field elements <xsl:copy-of select="$elements"/></xsl:message> - <xsl:message>gn-fn-index:add-field languages <xsl:copy-of select="$languages"/></xsl:message>--> + <xsl:message>gn-fn-index:add-field languages <xsl:copy-of select="$languages"/></xsl:message> + <xsl:message>gn-fn-index:add-field mainLanguage <xsl:copy-of select="$mainLanguage"/></xsl:message>--> <xsl:variable name="isArray" select="count($elements[not(@xml:lang)]) > 1"/> - <xsl:for-each select="$elements"> + + + <!-- Select the items to be processed depending on whether they are ISO multilingual or not ISO, but multilingual eg. DC or DCAT --> + <xsl:for-each select="if($languages and count($elements//(*:CharacterString|*:Anchor|*:LocalisedCharacterString)) = 0 ) then $elements[1] else $elements"> <xsl:variable name="element" select="."/> <xsl:variable name="textObject" as="node()*"> <xsl:choose> <!-- Not ISO but multilingual eg. DC or DCAT --> <xsl:when test="$languages and count($element//(*:CharacterString|*:Anchor|*:LocalisedCharacterString)) = 0"> - <xsl:if test="position() = 1"> <value><xsl:value-of select="concat($doubleQuote, 'default', $doubleQuote, ':', $doubleQuote, util:escapeForJson(.), $doubleQuote)"/></value> <xsl:for-each select="$elements"> - <value><xsl:value-of select="concat($doubleQuote, 'lang', @xml:lang, $doubleQuote, ':', + <xsl:variable name="elementLangAttribute" + select="@xml:lang"/> + <xsl:variable name="elementLangCode" + select="$languages/lang[@value = $elementLangAttribute]/@code"/> + <value><xsl:value-of select="concat($doubleQuote, 'lang', $elementLangCode, $doubleQuote, ':', $doubleQuote, util:escapeForJson(.), $doubleQuote)"/></value> </xsl:for-each> - </xsl:if> </xsl:when> <xsl:when test="$languages"> <!-- The default language --> @@ -707,10 +713,10 @@ <xsl:function name="gn-fn-index:json-escape" as="xs:string?"> <!-- This function is deprecated. Please update your code to define the following namespace: xmlns:util="java:org.fao.geonet.util.XslUtil" - - and use util:escapeForJson function + + and use util:escapeForJson function --> - + <xsl:param name="v" as="xs:string?" /> <xsl:choose> <xsl:when test="normalize-space($v) = ''"></xsl:when> From 51ee3df035ff322729eb016a118eecd430f9499c Mon Sep 17 00:00:00 2001 From: Francois Prunayre <fx.prunayre@gmail.com> Date: Wed, 4 Sep 2024 17:43:04 +0200 Subject: [PATCH 72/76] OpenAPI / Operation returning no content should not advertised a schema. --- .../services/inspireatom/AtomDescribe.java | 4 +++- .../geonet/services/inspireatom/AtomGetData.java | 4 +++- .../services/inspireatom/AtomHarvester.java | 4 +++- .../geonet/services/inspireatom/AtomSearch.java | 3 ++- .../inspireatom/AtomServiceDescription.java | 4 +++- .../org/fao/geonet/api/categories/TagsApi.java | 6 ++++-- .../org/fao/geonet/api/groups/GroupsApi.java | 6 ++++-- .../fao/geonet/api/harvesting/HarvestersApi.java | 4 +++- .../geonet/api/identifiers/IdentifiersApi.java | 6 ++++-- .../fao/geonet/api/languages/LanguagesApi.java | 4 +++- .../fao/geonet/api/mapservers/MapServersApi.java | 8 +++++--- .../fao/geonet/api/processing/ProcessApi.java | 4 +++- .../java/org/fao/geonet/api/records/DoiApi.java | 4 +++- .../api/records/MetadataInsertDeleteApi.java | 4 +++- .../geonet/api/records/MetadataSharingApi.java | 16 +++++++++------- .../fao/geonet/api/records/MetadataTagApi.java | 4 +++- .../geonet/api/records/MetadataWorkflowApi.java | 8 +++++--- .../api/records/attachments/AttachmentsApi.java | 4 ++-- .../api/records/editing/MetadataEditingApi.java | 8 +++++--- .../geonet/api/records/formatters/CacheApi.java | 4 +++- .../geonet/api/selections/UserSelectionsApi.java | 8 +++++--- .../java/org/fao/geonet/api/site/LogosApi.java | 4 +++- .../java/org/fao/geonet/api/site/SiteApi.java | 8 +++++--- .../org/fao/geonet/api/sources/SourcesApi.java | 4 ++-- .../org/fao/geonet/api/status/StatusApi.java | 4 +++- .../fao/geonet/api/uisetting/UiSettingApi.java | 6 ++++-- .../geonet/api/userfeedback/UserFeedbackAPI.java | 6 ++++-- .../java/org/fao/geonet/api/users/MeApi.java | 4 +++- .../geonet/api/usersearches/UserSearchesApi.java | 4 +++- 29 files changed, 105 insertions(+), 52 deletions(-) diff --git a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomDescribe.java b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomDescribe.java index 97091e008e1..95871555b1d 100644 --- a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomDescribe.java +++ b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomDescribe.java @@ -24,6 +24,8 @@ import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -107,7 +109,7 @@ public class AtomDescribe { ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Feeds."), - @ApiResponse(responseCode = "204", description = "Not authenticated.") + @ApiResponse(responseCode = "204", description = "Not authenticated.", content = {@Content(schema = @Schema(hidden = true))}) }) @ResponseStatus(OK) @ResponseBody diff --git a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomGetData.java b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomGetData.java index a9133fe38a7..33d0ace6128 100644 --- a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomGetData.java +++ b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomGetData.java @@ -23,6 +23,8 @@ package org.fao.geonet.services.inspireatom; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -84,7 +86,7 @@ public class AtomGetData { ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Get a data file related to dataset"), - @ApiResponse(responseCode = "204", description = "Not authenticated.") + @ApiResponse(responseCode = "204", description = "Not authenticated.", content = {@Content(schema = @Schema(hidden = true))}) }) @ResponseStatus(OK) @ResponseBody diff --git a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomHarvester.java b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomHarvester.java index 94eeb33e4ce..a30dcbb0331 100644 --- a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomHarvester.java +++ b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomHarvester.java @@ -23,6 +23,8 @@ package org.fao.geonet.services.inspireatom; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -61,7 +63,7 @@ public class AtomHarvester { @PreAuthorize("hasAuthority('Administrator')") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Scan completed."), - @ApiResponse(responseCode = "204", description = "Not authenticated.") + @ApiResponse(responseCode = "204", description = "Not authenticated.", content = {@Content(schema = @Schema(hidden = true))}) }) @ResponseStatus(CREATED) @ResponseBody diff --git a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java index 0e27e9c8763..5253d3146ac 100644 --- a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java +++ b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomSearch.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -114,7 +115,7 @@ public class AtomSearch { ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Get a list of feeds."), - @ApiResponse(responseCode = "204", description = "Not authenticated.") + @ApiResponse(responseCode = "204", description = "Not authenticated.", content = {@io.swagger.v3.oas.annotations.media.Content(schema = @Schema(hidden = true))}) }) @ResponseStatus(OK) public Object feeds( diff --git a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomServiceDescription.java b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomServiceDescription.java index 87a255411b2..6c7b99ffbc2 100644 --- a/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomServiceDescription.java +++ b/inspire-atom/src/main/java/org/fao/geonet/services/inspireatom/AtomServiceDescription.java @@ -23,6 +23,8 @@ package org.fao.geonet.services.inspireatom; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -91,7 +93,7 @@ public class AtomServiceDescription { produces = MediaType.APPLICATION_XML_VALUE) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Feeds."), - @ApiResponse(responseCode = "204", description = "Not authenticated.") + @ApiResponse(responseCode = "204", description = "Not authenticated.", content = {@Content(schema = @Schema(hidden = true))}) }) @ResponseStatus(OK) @ResponseBody diff --git a/services/src/main/java/org/fao/geonet/api/categories/TagsApi.java b/services/src/main/java/org/fao/geonet/api/categories/TagsApi.java index d87bab7908a..3447a171fac 100644 --- a/services/src/main/java/org/fao/geonet/api/categories/TagsApi.java +++ b/services/src/main/java/org/fao/geonet/api/categories/TagsApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.categories; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -180,7 +182,7 @@ public org.fao.geonet.domain.MetadataCategory getTag( @PreAuthorize("hasAuthority('UserAdmin')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Tag updated."), + @ApiResponse(responseCode = "204", description = "Tag updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) @ResponseBody @@ -239,7 +241,7 @@ private void updateCategory( method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Tag removed."), + @ApiResponse(responseCode = "204", description = "Tag removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) @PreAuthorize("hasAuthority('UserAdmin')") 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 0b0fb4980d2..12479f28e49 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 @@ -26,6 +26,8 @@ import com.google.common.base.Functions; import com.google.common.collect.Lists; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -430,7 +432,7 @@ public List<User> getGroupUsers( @ResponseStatus(value = HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('UserAdmin')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Group updated."), + @ApiResponse(responseCode = "204", description = "Group updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) @@ -485,7 +487,7 @@ public void updateGroup( @ResponseStatus(value = HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('Administrator')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Group removed."), + @ApiResponse(responseCode = "204", description = "Group removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) 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 ca2b45d30f7..1e6973dde27 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 @@ -24,6 +24,8 @@ package org.fao.geonet.api.harvesting; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -98,7 +100,7 @@ public class HarvestersApi { @ResponseStatus(value = HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('UserAdmin')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Harvester records transfered to new source."), + @ApiResponse(responseCode = "204", description = "Harvester records transfered to new source.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) diff --git a/services/src/main/java/org/fao/geonet/api/identifiers/IdentifiersApi.java b/services/src/main/java/org/fao/geonet/api/identifiers/IdentifiersApi.java index ddf0c3be357..9f9f30e5b8e 100644 --- a/services/src/main/java/org/fao/geonet/api/identifiers/IdentifiersApi.java +++ b/services/src/main/java/org/fao/geonet/api/identifiers/IdentifiersApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.identifiers; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -153,7 +155,7 @@ public ResponseEntity<Integer> addIdentifier( ) @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Identifier template updated."), + @ApiResponse(responseCode = "204", description = "Identifier template updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Resource not found."), @ApiResponse(responseCode = "403", description = "Operation not allowed. Only Editor can access it.") }) @@ -198,7 +200,7 @@ public void updateIdentifier( ) @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Template identifier removed."), + @ApiResponse(responseCode = "204", description = "Template identifier removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Resource not found."), @ApiResponse(responseCode = "403", description = "Operation not allowed. Only Editor can access it.") }) diff --git a/services/src/main/java/org/fao/geonet/api/languages/LanguagesApi.java b/services/src/main/java/org/fao/geonet/api/languages/LanguagesApi.java index e62541f05a4..c9cbcc7d59d 100644 --- a/services/src/main/java/org/fao/geonet/api/languages/LanguagesApi.java +++ b/services/src/main/java/org/fao/geonet/api/languages/LanguagesApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.languages; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -185,7 +187,7 @@ public void addLanguages( @PreAuthorize("hasAuthority('Administrator')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Language translations removed."), + @ApiResponse(responseCode = "204", description = "Language translations removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Resource not found."), @ApiResponse(responseCode = "403", description = "Operation not allowed. Only Administrator can access it.") }) diff --git a/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java b/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java index f6a86262247..db1f0814de1 100644 --- a/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java +++ b/services/src/main/java/org/fao/geonet/api/mapservers/MapServersApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.mapservers; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -209,7 +211,7 @@ public ResponseEntity<Integer> addMapserver( }) @PreAuthorize("hasAuthority('Reviewer')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Mapserver updated."), + @ApiResponse(responseCode = "204", description = "Mapserver updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_REVIEWER) }) @@ -253,7 +255,7 @@ public void updateMapserver( }) @PreAuthorize("hasAuthority('Reviewer')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Mapserver updated."), + @ApiResponse(responseCode = "204", description = "Mapserver updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_REVIEWER) }) @@ -323,7 +325,7 @@ private void updateMapserver( }) @PreAuthorize("hasAuthority('Reviewer')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Mapserver removed."), + @ApiResponse(responseCode = "204", description = "Mapserver removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_REVIEWER) }) diff --git a/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java b/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java index 775874d572d..94a7ed8ca86 100644 --- a/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java +++ b/services/src/main/java/org/fao/geonet/api/processing/ProcessApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.processing; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -99,7 +101,7 @@ public List<ProcessingReport> getProcessReport() throws Exception { MediaType.APPLICATION_JSON_VALUE }) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Report registry cleared."), + @ApiResponse(responseCode = "204", description = "Report registry cleared.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_AUTHENTICATED) }) @ResponseBody diff --git a/services/src/main/java/org/fao/geonet/api/records/DoiApi.java b/services/src/main/java/org/fao/geonet/api/records/DoiApi.java index ce59aa1d8e4..97dbd5b10e5 100644 --- a/services/src/main/java/org/fao/geonet/api/records/DoiApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/DoiApi.java @@ -23,6 +23,8 @@ package org.fao.geonet.api.records; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -182,7 +184,7 @@ ResponseEntity<Map<String, String>> createDoi( ) @PreAuthorize("hasAuthority('Administrator')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "DOI unregistered."), + @ApiResponse(responseCode = "204", description = "DOI unregistered.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Metadata or DOI not found."), @ApiResponse(responseCode = "500", description = "Service unavailable."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java index 0dfb298d2c3..8204995874d 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataInsertDeleteApi.java @@ -27,6 +27,8 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -206,7 +208,7 @@ public class MetadataInsertDeleteApi { + "from the index and then from the database.") @RequestMapping(value = "/{metadataUuid}", method = RequestMethod.DELETE) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Record deleted."), + @ApiResponse(responseCode = "204", description = "Record deleted.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "401", description = "This template is referenced"), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java index e7d9da4e0a9..5967992f740 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataSharingApi.java @@ -25,6 +25,8 @@ import com.google.common.base.Optional; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -215,7 +217,7 @@ public List<PublicationOption> getPublicationOptions() { method = RequestMethod.PUT ) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Settings updated."), + @ApiResponse(responseCode = "204", description = "Settings updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) @PreAuthorize("hasAuthority('Reviewer')") @@ -260,7 +262,7 @@ public void publish( method = RequestMethod.PUT ) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Settings updated."), + @ApiResponse(responseCode = "204", description = "Settings updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) @PreAuthorize("hasAuthority('Reviewer')") @@ -314,7 +316,7 @@ public void unpublish( method = RequestMethod.PUT ) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Settings updated."), + @ApiResponse(responseCode = "204", description = "Settings updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) @PreAuthorize("hasAuthority('Editor')") @@ -775,7 +777,7 @@ public SharingResponse getRecordSharingSettings( method = RequestMethod.PUT ) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Record group updated."), + @ApiResponse(responseCode = "204", description = "Record group updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) @PreAuthorize("hasAuthority('Editor')") @@ -818,9 +820,9 @@ public void setRecordGroup( metadataManager.save(metadata); dataManager.indexMetadata(String.valueOf(metadata.getId()), true); - new RecordGroupOwnerChangeEvent(metadata.getId(), - ApiUtils.getUserSession(request.getSession()).getUserIdAsInt(), - ObjectJSONUtils.convertObjectInJsonObject(oldGroup, RecordGroupOwnerChangeEvent.FIELD), + new RecordGroupOwnerChangeEvent(metadata.getId(), + ApiUtils.getUserSession(request.getSession()).getUserIdAsInt(), + ObjectJSONUtils.convertObjectInJsonObject(oldGroup, RecordGroupOwnerChangeEvent.FIELD), ObjectJSONUtils.convertObjectInJsonObject(group.get(), RecordGroupOwnerChangeEvent.FIELD)).publish(appContext); } diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java index 0e326143429..f13a0b66e3d 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataTagApi.java @@ -25,6 +25,8 @@ import com.google.common.collect.Sets; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -212,7 +214,7 @@ private void indexTags(AbstractMetadata metadata) throws Exception { @DeleteMapping(value = "/{metadataUuid}/tags") @ResponseStatus(value = HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Record tags removed."), + @ApiResponse(responseCode = "204", description = "Record tags removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT) }) @PreAuthorize("hasAuthority('Editor')") diff --git a/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java b/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java index 054a72b0d86..dd72b20e8fe 100644 --- a/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/MetadataWorkflowApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.records; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -576,7 +578,7 @@ public Map<Integer, StatusChangeType> setStatus(@Parameter(description = API_PAR method = RequestMethod.PUT ) @PreAuthorize("hasAuthority('Editor')") - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Task closed."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Task closed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Status not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) @ResponseStatus(HttpStatus.NO_CONTENT) @@ -604,7 +606,7 @@ public void closeTask(@Parameter(description = API_PARAM_RECORD_UUID, required = @io.swagger.v3.oas.annotations.Operation(summary = "Delete a record status", description = "") @RequestMapping(value = "/{metadataUuid}/status/{statusId:[0-9]+}.{userId:[0-9]+}.{changeDate}", method = RequestMethod.DELETE) @PreAuthorize("hasAuthority('Administrator')") - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Status removed."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Status removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Status not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN)}) @ResponseStatus(HttpStatus.NO_CONTENT) @@ -631,7 +633,7 @@ public void deleteRecordStatus( @io.swagger.v3.oas.annotations.Operation(summary = "Delete all record status", description = "") @RequestMapping(value = "/{metadataUuid}/status", method = RequestMethod.DELETE) @PreAuthorize("hasAuthority('Administrator')") - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Status removed."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Status removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Status not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN)}) @ResponseStatus(HttpStatus.NO_CONTENT) diff --git a/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java b/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java index b1b27ecfa30..e30deec309a 100644 --- a/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/attachments/AttachmentsApi.java @@ -178,7 +178,7 @@ public List<MetadataResource> getAllResources( @io.swagger.v3.oas.annotations.Operation(summary = "Delete all uploaded metadata resources") @RequestMapping(method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('Editor')") - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Attachment added."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Attachment added.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) @ResponseStatus(value = HttpStatus.NO_CONTENT) public void delResources( @@ -316,7 +316,7 @@ public MetadataResource patchResource( @io.swagger.v3.oas.annotations.Operation(summary = "Delete a metadata resource") @PreAuthorize("hasAuthority('Editor')") @RequestMapping(value = "/{resourceId:.+}", method = RequestMethod.DELETE) - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Attachment visibility removed."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Attachment visibility removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) @ResponseStatus(value = HttpStatus.NO_CONTENT) public void delResource( diff --git a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java index 3899f9e1967..16aaee8d1d5 100644 --- a/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/editing/MetadataEditingApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.records.editing; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -532,7 +534,7 @@ public void saveEdits( MediaType.ALL_VALUE}, produces = {MediaType.APPLICATION_XML_VALUE}) @PreAuthorize("hasAuthority('Editor')") @ResponseStatus(HttpStatus.NO_CONTENT) - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Editing session cancelled."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Editing session cancelled.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) @ResponseBody public void cancelEdits(@Parameter(description = API_PARAM_RECORD_UUID, required = true) @PathVariable String metadataUuid, @@ -614,7 +616,7 @@ public void reorderElement(@Parameter(description = API_PARAM_RECORD_UUID, requi MediaType.ALL_VALUE}, produces = {MediaType.APPLICATION_XML_VALUE}) @PreAuthorize("hasAuthority('Editor')") @ResponseStatus(HttpStatus.NO_CONTENT) - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Element removed."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Element removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) @ResponseBody public void deleteElement( @@ -638,7 +640,7 @@ public void deleteElement( MediaType.ALL_VALUE}, produces = {MediaType.APPLICATION_XML_VALUE}) @PreAuthorize("hasAuthority('Editor')") @ResponseStatus(HttpStatus.NO_CONTENT) - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Attribute removed."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Attribute removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_CAN_EDIT)}) @ResponseBody public void deleteAttribute( diff --git a/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java b/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java index d90ea166d70..eb9b39200b3 100644 --- a/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java +++ b/services/src/main/java/org/fao/geonet/api/records/formatters/CacheApi.java @@ -23,6 +23,8 @@ package org.fao.geonet.api.records.formatters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import static org.fao.geonet.api.ApiParams.API_CLASS_FORMATTERS_OPS; import static org.fao.geonet.api.ApiParams.API_CLASS_FORMATTERS_TAG; @@ -67,7 +69,7 @@ public class CacheApi { @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('Administrator')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Cache cleared."), + @ApiResponse(responseCode = "204", description = "Cache cleared.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = "Operation not allowed. Only Administrator can access it.") }) public void clearFormatterCache() throws Exception { diff --git a/services/src/main/java/org/fao/geonet/api/selections/UserSelectionsApi.java b/services/src/main/java/org/fao/geonet/api/selections/UserSelectionsApi.java index f4d501f7d90..595ec851698 100644 --- a/services/src/main/java/org/fao/geonet/api/selections/UserSelectionsApi.java +++ b/services/src/main/java/org/fao/geonet/api/selections/UserSelectionsApi.java @@ -23,6 +23,8 @@ package org.fao.geonet.api.selections; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -160,7 +162,7 @@ public ResponseEntity createPersistentSelectionType( method = RequestMethod.PUT) @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Selection updated."), + @ApiResponse(responseCode = "204", description = "Selection updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Selection not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) @@ -209,7 +211,7 @@ public ResponseEntity updateUserSelection( method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Selection removed."), + @ApiResponse(responseCode = "204", description = "Selection removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Selection not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) @@ -348,7 +350,7 @@ ResponseEntity<String> addToUserSelection( public @ResponseBody @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Items removed from a set."), + @ApiResponse(responseCode = "204", description = "Items removed from a set.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Selection or user not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) 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 b448bfe4530..a0dc037a1a3 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 @@ -24,6 +24,8 @@ package org.fao.geonet.api.site; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -240,7 +242,7 @@ public void getLogo( @ResponseStatus(value = HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('UserAdmin')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Logo removed."), + @ApiResponse(responseCode = "204", description = "Logo removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) 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 a2bd724fa59..5668d6429e5 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 @@ -26,6 +26,8 @@ import co.elastic.clients.elasticsearch.core.CountRequest; import co.elastic.clients.elasticsearch.core.CountResponse; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -397,7 +399,7 @@ public List<Setting> getSettingsDetails( @PreAuthorize("hasAuthority('Administrator')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Settings saved."), + @ApiResponse(responseCode = "204", description = "Settings saved.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) }) public void saveSettings( @@ -516,7 +518,7 @@ public boolean isCasEnabled( method = RequestMethod.PUT) @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Staging profile saved."), + @ApiResponse(responseCode = "204", description = "Staging profile saved.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) }) @PreAuthorize("hasAuthority('Administrator')") @@ -761,7 +763,7 @@ public ProxyConfiguration getProxyConfiguration( @PreAuthorize("hasAuthority('Administrator')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Logo set."), + @ApiResponse(responseCode = "204", description = "Logo set.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) public void setLogo( 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 5b50c543e5c..da0f179a4fc 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 @@ -227,7 +227,7 @@ private void copySourceLogo(Source source, HttpServletRequest request) { @PreAuthorize("hasAuthority('UserAdmin')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Source updated."), + @ApiResponse(responseCode = "204", description = "Source updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "Source not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) @@ -278,7 +278,7 @@ public ResponseEntity updateSource( @PreAuthorize("hasAuthority('Administrator')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Source deleted."), + @ApiResponse(responseCode = "204", description = "Source deleted.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) }) @ResponseBody diff --git a/services/src/main/java/org/fao/geonet/api/status/StatusApi.java b/services/src/main/java/org/fao/geonet/api/status/StatusApi.java index df46dfe0bed..fa3715dca08 100644 --- a/services/src/main/java/org/fao/geonet/api/status/StatusApi.java +++ b/services/src/main/java/org/fao/geonet/api/status/StatusApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.status; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -79,7 +81,7 @@ public List<StatusValue> getStatusByType( @RequestMapping(method = RequestMethod.DELETE) @PreAuthorize("hasAuthority('Administrator')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "Status removed."), + @ApiResponse(responseCode = "204", description = "Status removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_ADMIN) }) @ResponseStatus(HttpStatus.NO_CONTENT) diff --git a/services/src/main/java/org/fao/geonet/api/uisetting/UiSettingApi.java b/services/src/main/java/org/fao/geonet/api/uisetting/UiSettingApi.java index 8fa7193367d..1696eca05a9 100644 --- a/services/src/main/java/org/fao/geonet/api/uisetting/UiSettingApi.java +++ b/services/src/main/java/org/fao/geonet/api/uisetting/UiSettingApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.uisetting; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -178,7 +180,7 @@ public UiSetting getUiConfiguration( @PreAuthorize("hasAuthority('UserAdmin')") @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "UI configuration updated."), + @ApiResponse(responseCode = "204", description = "UI configuration updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) @ResponseBody @@ -232,7 +234,7 @@ public ResponseEntity updateUiConfiguration( method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "UI Configuration removed."), + @ApiResponse(responseCode = "204", description = "UI Configuration removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = "UI Configuration not found."), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_USER_ADMIN) }) diff --git a/services/src/main/java/org/fao/geonet/api/userfeedback/UserFeedbackAPI.java b/services/src/main/java/org/fao/geonet/api/userfeedback/UserFeedbackAPI.java index a782fb814f1..9f66a220817 100644 --- a/services/src/main/java/org/fao/geonet/api/userfeedback/UserFeedbackAPI.java +++ b/services/src/main/java/org/fao/geonet/api/userfeedback/UserFeedbackAPI.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.userfeedback; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -146,7 +148,7 @@ public List<RatingCriteria> getRatingCriteria( @RequestMapping(value = "/userfeedback/{uuid}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('Reviewer')") - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "User feedback removed."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "User feedback removed.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_REVIEWER)}) @ResponseBody public ResponseEntity deleteUserFeedback( @@ -719,7 +721,7 @@ private void printOutputMessage(final HttpServletResponse response, final HttpSt @RequestMapping(value = "/userfeedback/{uuid}/publish", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) @ResponseStatus(value = HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('Reviewer')") - @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "User feedback published."), + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "User feedback published.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "403", description = ApiParams.API_RESPONSE_NOT_ALLOWED_ONLY_REVIEWER), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND)}) @ResponseBody diff --git a/services/src/main/java/org/fao/geonet/api/users/MeApi.java b/services/src/main/java/org/fao/geonet/api/users/MeApi.java index 955f9e4f392..ee5e882dc20 100644 --- a/services/src/main/java/org/fao/geonet/api/users/MeApi.java +++ b/services/src/main/java/org/fao/geonet/api/users/MeApi.java @@ -25,6 +25,8 @@ import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -66,7 +68,7 @@ public class MeApi { @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Authenticated. Return user details."), - @ApiResponse(responseCode = "204", description = "Not authenticated.") + @ApiResponse(responseCode = "204", description = "Not authenticated.", content = {@Content(schema = @Schema(hidden = true))}) }) @ResponseStatus(OK) @ResponseBody diff --git a/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java b/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java index 517744f9ce7..23a4a148a85 100644 --- a/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java +++ b/services/src/main/java/org/fao/geonet/api/usersearches/UserSearchesApi.java @@ -24,6 +24,8 @@ package org.fao.geonet.api.usersearches; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -350,7 +352,7 @@ public ResponseEntity<Integer> createUserCustomSearch( @ResponseStatus(value = HttpStatus.NO_CONTENT) @PreAuthorize("hasAuthority('UserAdmin')") @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "User search updated."), + @ApiResponse(responseCode = "204", description = "User search updated.", content = {@Content(schema = @Schema(hidden = true))}), @ApiResponse(responseCode = "404", description = ApiParams.API_RESPONSE_RESOURCE_NOT_FOUND) }) @ResponseBody From aa125f5f1a53f1acf4b80f9f861ae8141cf67f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Prunayre?= <fx.prunayre@gmail.com> Date: Fri, 20 Sep 2024 15:52:15 +0200 Subject: [PATCH 73/76] Aggregations / Temporal range / Avoid browser autocomplete on calendar field When using temporal range for aggregations ``` resourceTemporalDateRange: { gnBuildFilterForRange: { field: "resourceTemporalDateRange", buckets: 2024 - 1970, dateFormat: "YYYY", dateSelectMode: "years", vegaDateFormat: "%Y", from: 1970, to: 2024, mark: "area" }, meta: { vega: "timeline", collapsed: true } }, ``` browser autocomplete may overlap with calendar --- .../elasticsearch/directives/partials/facet-temporalrange.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html b/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html index 8029f53fbf8..0bcc837be43 100644 --- a/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html +++ b/web-ui/src/main/resources/catalog/components/elasticsearch/directives/partials/facet-temporalrange.html @@ -12,6 +12,7 @@ class="input-sm form-control" data-ng-model="range.from" data-ng-model-options="{debounce: 500}" + autocomplete="off" name="start" /> <span class="input-group-addon"> @@ -22,6 +23,7 @@ class="input-sm form-control" data-ng-model="range.to" data-ng-model-options="{debounce: 500}" + autocomplete="off" name="end" /> <span class="input-group-btn"> From a5a1b49ad20a58ebce1c56be2b558fc45fff1ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michel=20Gabri=C3=ABl?= <michel.gabriel@geocat.net> Date: Mon, 23 Sep 2024 16:11:55 +0200 Subject: [PATCH 74/76] Update configuring-faceted-search.md --- .../docs/customizing-application/configuring-faceted-search.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/docs/customizing-application/configuring-faceted-search.md b/docs/manual/docs/customizing-application/configuring-faceted-search.md index 7af888bdc0f..292739175bc 100644 --- a/docs/manual/docs/customizing-application/configuring-faceted-search.md +++ b/docs/manual/docs/customizing-application/configuring-faceted-search.md @@ -467,7 +467,7 @@ A date range field: "resourceTemporalDateRange": { "gnBuildFilterForRange": { "field": "resourceTemporalDateRange", - "buckets": "2021 - 1970", + "buckets": 51, //"2021 - 1970", "dateFormat": "YYYY", "vegaDateFormat": "%Y", "from": "1970", From 94c0586506d0f119e9db238a5a673ccf9de1430d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Tue, 17 Sep 2024 13:41:50 +0200 Subject: [PATCH 75/76] Fix the schema artifact name in add schema script --- add-schema.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/add-schema.sh b/add-schema.sh index 2a268428530..4f1ecc8c92d 100755 --- a/add-schema.sh +++ b/add-schema.sh @@ -83,7 +83,7 @@ then ${insertLine} a\\ \ <dependency>\\ \ <groupId>org.geonetwork-opensource.schemas</groupId>\\ -\ <artifactId>schema-${schema}</artifactId>\\ +\ <artifactId>gn-schema-${schema}</artifactId>\\ \ <version>${gnSchemasVersion}</version>\\ \ </dependency> SED_SCRIPT @@ -103,7 +103,7 @@ SED_SCRIPT \ <dependencies>\\ \ <dependency>\\ \ <groupId>org.geonetwork-opensource.schemas</groupId>\\ -\ <artifactId>schema-${schema}</artifactId>\\ +\ <artifactId>gn-schema-${schema}</artifactId>\\ \ <version>${gnSchemasVersion}</version>\\ \ </dependency>\\ \ </dependencies>\\ @@ -121,7 +121,7 @@ SED_SCRIPT \ <artifactItems>\\ \ <artifactItem>\\ \ <groupId>org.geonetwork-opensource.schemas</groupId>\\ -\ <artifactId>schema-${schema}</artifactId>\\ +\ <artifactId>gn-schema-${schema}</artifactId>\\ \ <type>zip</type>\\ \ <overWrite>false</overWrite>\\ \ <outputDirectory>\$\{schema-plugins.dir\}</outputDirectory>\\ @@ -138,7 +138,7 @@ SED_SCRIPT fi # Add schema resources in service/pom.xml with test scope for unit tests -line=$(grep -n "<artifactId>schema-${schema}</artifactId>" services/pom.xml | cut -d: -f1) +line=$(grep -n "<artifactId>gn-schema-${schema}</artifactId>" services/pom.xml | cut -d: -f1) if [ ! $line ] then @@ -154,7 +154,7 @@ then ${finalLine} a\\ \ <dependency>\\ \ <groupId>${projectGroupId}</groupId>\\ -\ <artifactId>schema-${schema}</artifactId>\\ +\ <artifactId>gn-schema-${schema}</artifactId>\\ \ <version>${gnSchemasVersion}</version>\\ \ <scope>test</scope>\\ \ </dependency> From 8436990a78485522b8712b5aba4b26abb5ca6805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Garc=C3=ADa?= <josegar74@gmail.com> Date: Thu, 12 Sep 2024 14:35:55 +0200 Subject: [PATCH 76/76] ISO19139 / ISO19115.3 / Index resource date fields as defined in the metadata. Previously the values were converted to UTC. If the timezone defined in the server is Europe/Madrid and the metadata has a creation date '2023-01-01T00:00:00', the index field creationYearForResource had the value 2022 --- .../main/plugin/iso19115-3.2018/index-fields/index.xsl | 8 +++++--- .../src/main/plugin/iso19139/index-fields/index.xsl | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl index 35ed82ac5bf..207439102c6 100644 --- a/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl +++ b/schemas/iso19115-3.2018/src/main/plugin/iso19115-3.2018/index-fields/index.xsl @@ -327,14 +327,16 @@ </xsl:variable> <xsl:choose> <xsl:when test="$zuluDateTime != ''"> + <!-- Store original date information for the resource, instead of $zuluDateTime, + to avoid timezone shifts when used for facet filters --> <xsl:element name="{$dateType}DateForResource"> - <xsl:value-of select="$zuluDateTime"/> + <xsl:value-of select="$date"/> </xsl:element> <xsl:element name="{$dateType}YearForResource"> - <xsl:value-of select="substring($zuluDateTime, 0, 5)"/> + <xsl:value-of select="substring($date, 0, 5)"/> </xsl:element> <xsl:element name="{$dateType}MonthForResource"> - <xsl:value-of select="substring($zuluDateTime, 0, 8)"/> + <xsl:value-of select="substring($date, 0, 8)"/> </xsl:element> </xsl:when> <xsl:otherwise> diff --git a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl index c2a7ee33ec8..06c47aa519b 100644 --- a/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl +++ b/schemas/iso19139/src/main/plugin/iso19139/index-fields/index.xsl @@ -287,14 +287,16 @@ <xsl:choose> <xsl:when test="$zuluDateTime != ''"> + <!-- Store original date information for the resource, instead of $zuluDateTime, + to avoid timezone shifts when used for facet filters --> <xsl:element name="{$dateType}DateForResource"> - <xsl:value-of select="$zuluDateTime"/> + <xsl:value-of select="$date"/> </xsl:element> <xsl:element name="{$dateType}YearForResource"> - <xsl:value-of select="substring($zuluDateTime, 0, 5)"/> + <xsl:value-of select="substring($date, 0, 5)"/> </xsl:element> <xsl:element name="{$dateType}MonthForResource"> - <xsl:value-of select="substring($zuluDateTime, 0, 8)"/> + <xsl:value-of select="substring($date, 0, 8)"/> </xsl:element> </xsl:when> <xsl:otherwise>
- +
+
- +
-
- +
+
- +
-
- +
+
- +
-
- +
+
-
- -

feebackSent

diff --git a/web-ui/src/main/resources/catalog/js/CatController.js b/web-ui/src/main/resources/catalog/js/CatController.js index 4d3e704d994..79b66951b47 100644 --- a/web-ui/src/main/resources/catalog/js/CatController.js +++ b/web-ui/src/main/resources/catalog/js/CatController.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2001-2016 Food and Agriculture Organization of the + * Copyright (C) 2001-2024 Food and Agriculture Organization of the * United Nations (FAO-UN), United Nations World Food Programme (WFP) * and United Nations Environment Programme (UNEP) * @@ -1655,6 +1655,10 @@ return gnGlobalSettings.gnCfg.mods.footer.showApplicationInfoAndLinksInFooter; }; + $scope.getContactusVisible = function () { + return gnConfig[gnConfig.key.isFeedbackEnabled]; + }; + function detectNode(detector) { if (detector.regexp) { var res = new RegExp(detector.regexp).exec(location.pathname); diff --git a/web-ui/src/main/resources/catalog/js/ContactUsController.js b/web-ui/src/main/resources/catalog/js/ContactUsController.js index 18876c80ca7..f510439db18 100644 --- a/web-ui/src/main/resources/catalog/js/ContactUsController.js +++ b/web-ui/src/main/resources/catalog/js/ContactUsController.js @@ -26,9 +26,17 @@ goog.require("gn_contactus_directive"); - var module = angular.module("gn_contact_us_controller", ["gn_contactus_directive"]); + var module = angular.module("gn_contact_us_controller", [ + "gn_contactus_directive", + "vcRecaptcha" + ]); - module.constant("$LOCALES", ["core"]); + module.config([ + "$LOCALES", + function ($LOCALES) { + $LOCALES.push("/../api/i18n/packages/search"); + } + ]); /** * diff --git a/web-ui/src/main/resources/catalog/locales/en-admin.json b/web-ui/src/main/resources/catalog/locales/en-admin.json index ec570ee7c95..30282dec251 100644 --- a/web-ui/src/main/resources/catalog/locales/en-admin.json +++ b/web-ui/src/main/resources/catalog/locales/en-admin.json @@ -852,9 +852,9 @@ "userFeedbackList": "Last user feedbacks", "system/userFeedback": "User feedback", "system/userFeedback/enable": "Enable application feedback", - "system/userFeedback/enable-help": "Enabling this option allows to send feedback about the application to the system administrator. It requires the mail server is also configured.", + "system/userFeedback/enable-help": "Enabling this option allows to send feedback about the application to the system administrator. It requires the mail server to be configured.", "system/userFeedback/metadata/enable": "Enable metadata feedback", - "system/userFeedback/metadata/enable-help": "Enabling this option allows to feedback to the metadata owner and system administrator about metadata record. It requires the mail server is also configured.", + "system/userFeedback/metadata/enable-help": "Enabling this option allows to feedback to the metadata owner and system administrator about metadata record. It requires the mail server to be configured.", "system/xlinkResolver": "Metadata XLink", "system/xlinkResolver/enable": "Enable XLink resolution", "system/xlinkResolver/enable-help": "If set, XLinks to metadata fragments in records will be resolved.", diff --git a/web-ui/src/main/resources/catalog/locales/en-core.json b/web-ui/src/main/resources/catalog/locales/en-core.json index e18cb2c1325..cd03dca04eb 100644 --- a/web-ui/src/main/resources/catalog/locales/en-core.json +++ b/web-ui/src/main/resources/catalog/locales/en-core.json @@ -126,6 +126,7 @@ "featureCatalog": "Feature catalog", "frequency": "Frequency", "feebackSent": "Your message has been sent to the catalog manager.", + "feebackSentError": "An error occurred sending your to the catalog manager. Please try again later, contact the service provider, or report this issue.", "feedbackNotEnable": "Feedback is not enabled.", "filter": "Filter", "filterSearch": "Display search options", diff --git a/web-ui/src/main/resources/catalog/views/default/less/gn_contact_us_default.less b/web-ui/src/main/resources/catalog/views/default/less/gn_contact_us_default.less new file mode 100644 index 00000000000..d5c88c43aed --- /dev/null +++ b/web-ui/src/main/resources/catalog/views/default/less/gn_contact_us_default.less @@ -0,0 +1,15 @@ +@import "../../../style/gn_contact_us.less"; + +// nojs styles +.gn-nojs { + .gn-top-search { + padding: 30px 0; + margin-bottom: 30px; + .gn-form-any input.input-lg { + height: 46px; + } + .btn-lg { + padding: 13px 16px; + } + } +} diff --git a/web-ui/src/main/resources/catalog/views/default/templates/footer.html b/web-ui/src/main/resources/catalog/views/default/templates/footer.html index 018b6eedb41..eb006285b99 100644 --- a/web-ui/src/main/resources/catalog/views/default/templates/footer.html +++ b/web-ui/src/main/resources/catalog/views/default/templates/footer.html @@ -12,6 +12,11 @@ about +
  • + + contact + +
  • From e3f92aac98e74614b5b487be1881ed019af95cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Luis=20Rodri=CC=81guez?= Date: Fri, 21 Jun 2024 12:37:00 +0200 Subject: [PATCH 18/76] Automatic formatting --- .../components/userfeedback/partials/mdFeedback.html | 8 ++++++-- .../views/default/templates/recordView/technical.html | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/userfeedback/partials/mdFeedback.html b/web-ui/src/main/resources/catalog/components/userfeedback/partials/mdFeedback.html index 4f01d933713..1489ab400dd 100644 --- a/web-ui/src/main/resources/catalog/components/userfeedback/partials/mdFeedback.html +++ b/web-ui/src/main/resources/catalog/components/userfeedback/partials/mdFeedback.html @@ -143,7 +143,9 @@

    fbFeedback

    fbCatServiceContents - + - +
    diff --git a/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html b/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html index 739c462c2ce..05d5111096d 100644 --- a/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html +++ b/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html @@ -79,7 +79,9 @@

    updateFrequency

    -

    {{(t.multilingualTitle.default || t.title || key) | translate}}

    +

    + {{(t.multilingualTitle.default || t.title || key) | translate}} +

    From 4728e7ddaf839e170ce321f338f0a0a2f2df30d2 Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 24 Jun 2024 10:38:24 -0300 Subject: [PATCH 19/76] Fix canViewRecord function so that it returned the workflow record. (#8152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix canViewRecord function so that it returned the workflow record. Prior to this fix, it would always return working copy record if it exists. Also updated getInternalId to have better error handling * Update services/src/main/java/org/fao/geonet/api/ApiUtils.java Co-authored-by: Jose García * Fix number check based on review - was checking wrong value. --------- Co-authored-by: Jose García --- .../java/org/fao/geonet/api/ApiUtils.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/services/src/main/java/org/fao/geonet/api/ApiUtils.java b/services/src/main/java/org/fao/geonet/api/ApiUtils.java index 1ee51045fa6..1c11fc79ba4 100644 --- a/services/src/main/java/org/fao/geonet/api/ApiUtils.java +++ b/services/src/main/java/org/fao/geonet/api/ApiUtils.java @@ -119,16 +119,19 @@ public static String getInternalId(String uuidOrInternalId, Boolean approved) id = String.valueOf(metadata.getId()); } - if (StringUtils.isEmpty(id)) { - //It wasn't a UUID - id = String.valueOf(metadataUtils.findOne(uuidOrInternalId).getId()); + AbstractMetadata foundMetadata = null; + if (!StringUtils.hasLength(id) && Lib.type.isInteger(uuidOrInternalId)) { + //It wasn't a UUID so assume it is an internalId which should be a number + foundMetadata = metadataUtils.findOne(uuidOrInternalId); } else if (Boolean.TRUE.equals(approved)) { //It was a UUID, check if draft or approved version - id = String.valueOf(ApplicationContextHolder.get().getBean(MetadataRepository.class) - .findOneByUuid(uuidOrInternalId).getId()); + foundMetadata = ApplicationContextHolder.get().getBean(MetadataRepository.class).findOneByUuid(uuidOrInternalId); + } + if (foundMetadata != null) { + id = String.valueOf(foundMetadata.getId()); } - if (StringUtils.isEmpty(id)) { + if (!StringUtils.hasLength(id)) { throw new ResourceNotFoundException(String.format( "Record with UUID '%s' not found in this catalog", uuidOrInternalId)); @@ -295,13 +298,7 @@ public static AbstractMetadata canViewRecord(String metadataUuid, HttpServletReq */ public static AbstractMetadata canViewRecord(String metadataUuid, boolean approved, HttpServletRequest request) throws Exception { String metadataId; - if (!approved) { - // If the record is not approved then we need to get the id of the record. - metadataId = getInternalId(metadataUuid, approved); - } else { - // Otherwise use the uuid or id that was supplied. - metadataId = metadataUuid; - } + metadataId = getInternalId(metadataUuid, approved); AbstractMetadata metadata = getRecord(metadataId); try { From aed2eb1bf0767dd4abe6c2fb0e37e457cb86166d Mon Sep 17 00:00:00 2001 From: joachimnielandt Date: Tue, 25 Jun 2024 08:33:16 +0200 Subject: [PATCH 20/76] Double translation can lead to infinite stack (#8209) * removed double translation * additional fix for double translation --- .../components/admin/harvester/partials/extras.html | 8 ++++---- .../partials/associatedResourcesContainer.html | 2 +- .../partials/distributionResourcesContainer.html | 2 +- .../views/default/directives/partials/mdactionmenu.html | 4 ++-- .../views/default/templates/recordView/technical.html | 6 ++---- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/web-ui/src/main/resources/catalog/components/admin/harvester/partials/extras.html b/web-ui/src/main/resources/catalog/components/admin/harvester/partials/extras.html index 7972afea9cc..4a29f33cee8 100644 --- a/web-ui/src/main/resources/catalog/components/admin/harvester/partials/extras.html +++ b/web-ui/src/main/resources/catalog/components/admin/harvester/partials/extras.html @@ -8,17 +8,17 @@ || harvester['@type'] == 'filesystem'" > harvesterOverrideUUIDHelp diff --git a/web-ui/src/main/resources/catalog/components/metadataactions/partials/associatedResourcesContainer.html b/web-ui/src/main/resources/catalog/components/metadataactions/partials/associatedResourcesContainer.html index 8c113158d52..5f6b0470977 100644 --- a/web-ui/src/main/resources/catalog/components/metadataactions/partials/associatedResourcesContainer.html +++ b/web-ui/src/main/resources/catalog/components/metadataactions/partials/associatedResourcesContainer.html @@ -14,7 +14,7 @@

    data-ng-click="onlinesrcService.onOpenPopup(config.type, config.config)" > - {{ config.label | translate}} + {{ config.label | translate}}

    data-ng-click="onlinesrcService.onOpenPopup('onlinesrc', action)" > - {{ action | translate}} + {{ action | translate}}
    diff --git a/web-ui/src/main/resources/catalog/views/default/directives/partials/mdactionmenu.html b/web-ui/src/main/resources/catalog/views/default/directives/partials/mdactionmenu.html index d39dce2594e..cac4c61d5a1 100644 --- a/web-ui/src/main/resources/catalog/views/default/directives/partials/mdactionmenu.html +++ b/web-ui/src/main/resources/catalog/views/default/directives/partials/mdactionmenu.html @@ -229,14 +229,14 @@ target="_blank" >   - {{f.label | translate}} + {{f.label | translate}}   - {{f.label | translate}} + {{f.label | translate}} diff --git a/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html b/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html index 05d5111096d..a6bfc157cba 100644 --- a/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html +++ b/web-ui/src/main/resources/catalog/views/default/templates/recordView/technical.html @@ -15,7 +15,7 @@ >