Skip to content

Commit 1907456

Browse files
Static pages / add support for mail links (#8605)
* Static pages / add support for mail links * Static pages / update documentation page * Update common/src/main/java/org/fao/geonet/utils/EmailUtil.java Co-authored-by: Juan Luis Rodríguez Ponce <juanluisrp@gmail.com> * Update common/src/test/java/org/fao/geonet/utils/EmailUtilTest.java Co-authored-by: Juan Luis Rodríguez Ponce <juanluisrp@gmail.com> * Update common/src/test/java/org/fao/geonet/utils/EmailUtilTest.java --------- Co-authored-by: Juan Luis Rodríguez Ponce <juanluisrp@gmail.com>
1 parent a4b92d6 commit 1907456

File tree

14 files changed

+173
-92
lines changed

14 files changed

+173
-92
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//=============================================================================
2+
//=== Copyright (C) 2001-2025 Food and Agriculture Organization of the
3+
//=== United Nations (FAO-UN), United Nations World Food Programme (WFP)
4+
//=== and United Nations Environment Programme (UNEP)
5+
//===
6+
//=== This library is free software; you can redistribute it and/or
7+
//=== modify it under the terms of the GNU Lesser General Public
8+
//=== License as published by the Free Software Foundation; either
9+
//=== version 2.1 of the License, or (at your option) any later version.
10+
//===
11+
//=== This library is distributed in the hope that it will be useful,
12+
//=== but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
//=== MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
//=== Lesser General Public License for more details.
15+
//===
16+
//=== You should have received a copy of the GNU Lesser General Public
17+
//=== License along with this library; if not, write to the Free Software
18+
//=== Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19+
//===
20+
//=== Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
21+
//=== Rome - Italy. email: geonetwork@osgeo.org
22+
//==============================================================================
23+
24+
package org.fao.geonet.utils;
25+
26+
public class EmailUtil {
27+
private static final String OWASP_EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$";
28+
private static final java.util.regex.Pattern OWASP_EMAIL_PATTERN = java.util.regex.Pattern.compile(OWASP_EMAIL_REGEX);
29+
30+
private EmailUtil() {
31+
throw new UnsupportedOperationException();
32+
}
33+
34+
/**
35+
* Checks if a string contains a valid email address format.
36+
*
37+
* @param emailAddress Value to validate.
38+
* @return true if the value contains a valid email address format, otherwise false.
39+
*/
40+
public static boolean isValidEmailAddress(String emailAddress) {
41+
return OWASP_EMAIL_PATTERN.matcher(emailAddress).matches();
42+
}
43+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//=============================================================================
2+
//=== Copyright (C) 2001-2025 Food and Agriculture Organization of the
3+
//=== United Nations (FAO-UN), United Nations World Food Programme (WFP)
4+
//=== and United Nations Environment Programme (UNEP)
5+
//===
6+
//=== This library is free software; you can redistribute it and/or
7+
//=== modify it under the terms of the GNU Lesser General Public
8+
//=== License as published by the Free Software Foundation; either
9+
//=== version 2.1 of the License, or (at your option) any later version.
10+
//===
11+
//=== This library is distributed in the hope that it will be useful,
12+
//=== but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
//=== MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
//=== Lesser General Public License for more details.
15+
//===
16+
//=== You should have received a copy of the GNU Lesser General Public
17+
//=== License along with this library; if not, write to the Free Software
18+
//=== Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19+
//===
20+
//=== Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
21+
//=== Rome - Italy. email: geonetwork@osgeo.org
22+
//==============================================================================
23+
24+
package org.fao.geonet.utils;
25+
26+
import org.junit.Test;
27+
28+
import static org.junit.Assert.assertEquals;
29+
30+
public class EmailUtilTest {
31+
@Test
32+
public void testEmailAddress() {
33+
assertEquals(true, EmailUtil.isValidEmailAddress("test@domain.com"));
34+
35+
assertEquals(true, EmailUtil.isValidEmailAddress("test@example.international"));
36+
37+
assertEquals(true, EmailUtil.isValidEmailAddress("test.user@domain.com"));
38+
39+
assertEquals(true, EmailUtil.isValidEmailAddress("test.user@domain.subdomain.com"));
40+
41+
assertEquals(false, EmailUtil.isValidEmailAddress("test.user"));
42+
43+
assertEquals(false, EmailUtil.isValidEmailAddress("test.user@domain"));
44+
}
45+
}
Lines changed: 37 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,55 @@
11
# Adding static pages
22

3-
This feature allows to store the HTML content for static pages and show the links to these pages in specific sections of the user interface:
3+
This function allows storing the HTML content of static pages and displaying links to these pages in specific sections of the user interface.
44

5-
- The HTML content is stored in a new table of the GN's database.
5+
To add new static pages go to **Admin Console** --> **Settings** --> **Static Pages** and select the **+ New Static Page** option, providing the following information:
66

7-
- The link to pages can be showed in different points of the GN's GUI according to a list of "sections" associated to each page. In this PR is introduced the support to show the links for the top toolbar and the footer.
7+
- **Language**: User interface language in which the static page will be displayed.
8+
- **Page identifier**: A unique text identifier for the page.
9+
- **Page label**: Label to display on the link.
10+
- **Page icon**: (Optional) Icon to display next to the link label.
11+
- **Format**: Format of the static page:
812

9-
- Each page can be in 3 states:
13+
- **Web link**: Link to a web page.
14+
- **HTML content displayed embedded in the app**
15+
- **HTML content displayed in a new browser tab**
16+
- **Plain text content**
17+
- **Email link**: Email address. Opens the system's mail client to send mail to the configured address.
1018

11-
- `HIDDEN`: visible to administrator.
12-
- `PRIVATE`: visible to logged users.
13-
- `PUBLIC`: visible to everyone.
19+
- **Link**: Available for **Web link** / **Email link** formats.
1420

15-
- Pages can be added to different page sections. Currently the sections implemented are `TOP` (top menu of the main page) and `FOOTER` (footer of the main page).
21+
- For **Web link** the link to a web page.
22+
- For **Email link**, the email address to which the email will be sent.
1623

17-
- Only the administrator can edit the pages and see the pages in `HIDDEN` status.
24+
- **Page content file**: For formats other than **Web link** or **Email link**, allows uploading a file with the HTML content / text to be displayed.
1825

19-
- The creation and the management of the content is done via the API.
26+
- **Page content**: For formats other than **Web link** or **Email link**, allows editing the HTML / text content to display.
2027

21-
![](img/pages-api.png)
28+
- **Page section**: Section of the page to display the link. Currently implemented sections are `TOP` (top menu of the main page) and `FOOTER` (footer of the main page).
2229

23-
Some restrictions:
24-
25-
- It is not possible to apply custom CSS to the page.
26-
- Any external image must be loaded externally.
27-
28-
## Examples of API usage
29-
30-
Before executing the following examples, see [Example of CSRF call using curl](misc.md#example-csrf-curl) for details on the usage of the CSRF token (instead of the value `"X-XSRF-TOKEN: e934f557-17a3-47f2-8e6b-bdf1a3c90a97"` used in the examples) and cookies in the requests.
31-
32-
### Load a page in the top menu bar
33-
34-
In this example we're going to upload a file ``contactus.html`` and link it in the top menu:
35-
36-
1. Load the content by using the method POST `/api/pages/`, the mandatory fields are:
37-
38-
- language (3 letters like 'eng', 'ita', 'fra' \...)
39-
- pageId (the identifier/link description of the page)
40-
- format (must be LINK if a link is associated to the page)
41-
- the content: data (a file with the page content) or a link (URL to another page). Define both is not possible.
42-
43-
``` bash
44-
$ curl -X POST "http://localhost:8080/geonetwork/srv/api/pages/?language=eng&pageId=contactus&format=HTML" -H "accept: */*" -H "Content-Type: multipart/form-data" -H "X-XSRF-TOKEN: e934f557-17a3-47f2-8e6b-bdf1a3c90a97" -d contactus.html
45-
```
46-
47-
At this point the page is created but not visible because is in status HIDDEN and is not loaded explicitly in any section of the page, except DRAFT that is not visible (in the future could be added to a page with an editor interface). Similar requests should be done for each UI language supported.
48-
49-
2. To associate the link to the top bar is necessary to use the method POST `/api/pages/{language}/{pageId}/{section}` with the `TOP` value for the section.
30+
- **Status**: Defines which users can see the link.
5031

51-
``` bash
52-
$ curl -X POST "http://localhost:8080/geonetwork/srv/api/pages/eng/contactus/TOP" -H "accept: */*" -H "X-XSRF-TOKEN: 7cfa1a0d-3335-4846-8061-a5bf176687b5" --user admin:admin -b /tmp/cookie
53-
```
32+
- **Visible only to the administrator**
33+
- **Visible to logged users**
34+
- **Visible to users belonging to groups**
35+
- **Visible to every one**
5436

55-
### Load a link in the footer bar
37+
![](img/add-static-page.png)
5638

57-
In this example we're going to add a link to an external resource <http://myorganisation/contactus.html> and link it in the footer:
5839

59-
1. Add the link by using the method POST `/api/pages/` with the `link` parameter in the request:
40+
![](img/static-page-footer.png)
6041

61-
``` bash
62-
$ curl -X POST "http://localhost:8080/geonetwork/srv/api/pages/?language=eng&pageId=contactus&format=LINK&link=http://myorganisation/contactus.html" -H "accept: */*" -H "X-XSRF-TOKEN: e934f557-17a3-47f2-8e6b-bdf1a3c90a97"
63-
```
6442

65-
2. To associate the link to the footer is necessary to use the method POST `/api/pages/{language}/{pageId}/{section}` with the `FOOTER` value for the section.
66-
67-
``` bash
68-
$ curl -X POST "http://localhost:8080/geonetwork/srv/api/pages/eng/contactus/FOOTER" -H "accept: */*" -H "X-XSRF-TOKEN: 7cfa1a0d-3335-4846-8061-a5bf176687b5" --user admin:admin -b /tmp/cookie
69-
```
70-
71-
### Remove a page from a section
72-
73-
To remove a page from a section DELETE `/api/pages/{language}/{pageId}/{section}`
74-
75-
``` bash
76-
curl -X DELETE "http://localhost:8080/geonetwork/srv/api/pages/eng/contactus?format=LINK" -H "accept: */*" -H "X-XSRF-TOKEN: 7cfa1a0d-3335-4846-8061-a5bf176687b5" --user admin:admin -b /tmp/cookie
77-
```
78-
79-
### Change the page status
80-
81-
The status of the page can be changed with the method PUT `/api/pages/{language}/{pageId}/{status}` where status could assume these values:
43+
Some restrictions:
8244

83-
- `PUBLIC` - Visible to every user
84-
- `PUBLIC_ONLY` - Visible to not logged users
85-
- `PRIVATE` - Visible to logged users
86-
- `HIDDEN` - Hidden to anyone
45+
- It is not possible to apply custom CSS to the page.
46+
- Any external image must be loaded externally.
8747

88-
Other methods in the API are to change/delete a page and to GET the list of the pages or the info of a specific one.
48+
## Change the menu order in the top toolbar
8949

90-
### Change the menu order in the top toolbar
50+
In the top bar, pages can be inserted between the default menu of the catalogue. This can be configured in **Admin Console** --> **Settings** --> **User interface**, in the **Header custom menu items** section.
9151

92-
Pages can be inserted in between catalogue default menu which are:
52+
By default, the order of the items in the top bar is as follows:
9353

9454
``` json
9555
["gn-site-name-menu",
@@ -100,7 +60,7 @@ Pages can be inserted in between catalogue default menu which are:
10060
"gn-admin-menu"]
10161
```
10262

103-
Insert a page as a simple menu using its id or as a submenu using an object:
63+
Insert a page as a simple menu using the page identifier or as a submenu using an object:
10464

10565
``` json
10666
["gn-site-name-menu",
@@ -115,3 +75,8 @@ Insert a page as a simple menu using its id or as a submenu using an object:
11575
"gn-admin-menu",
11676
"documentation"]
11777
```
78+
79+
## Change the static pages order in the footer
80+
81+
The order of the footer pages can be configured in **Admin Console** --> **Settings** --> **User interface**, in the **Footer custom menu items** section.
82+
64 KB
Loading
-105 KB
Binary file not shown.
12.9 KB
Loading

domain/src/main/java/org/fao/geonet/domain/page/Page.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public enum PageStatus {
9090
}
9191

9292
public enum PageFormat {
93-
LINK, HTML, HTMLPAGE, TEXT;
93+
LINK, HTML, HTMLPAGE, TEXT, EMAILLINK;
9494
}
9595

9696
// These are the sections where is shown the link to the Page object

services/src/main/java/org/fao/geonet/api/pages/PagesAPI.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.fao.geonet.repository.UserGroupRepository;
4646
import org.fao.geonet.repository.page.PageRepository;
4747
import org.fao.geonet.repository.specification.UserGroupSpecs;
48+
import org.fao.geonet.utils.EmailUtil;
4849
import org.springframework.beans.factory.annotation.Autowired;
4950
import org.springframework.http.HttpStatus;
5051
import org.springframework.http.MediaType;
@@ -58,6 +59,7 @@
5859

5960
import javax.servlet.http.HttpServletResponse;
6061
import javax.servlet.http.HttpSession;
62+
import javax.validation.constraints.Email;
6163
import javax.validation.constraints.NotNull;
6264
import java.nio.charset.StandardCharsets;
6365
import java.util.ArrayList;
@@ -178,7 +180,11 @@ private ResponseEntity<String> createPage(PageProperties pageProperties,
178180
}
179181

180182
if (!StringUtils.isBlank(link)) {
181-
format = Page.PageFormat.LINK;
183+
if (EmailUtil.isValidEmailAddress(link)) {
184+
format = Page.PageFormat.EMAILLINK;
185+
} else {
186+
format = Page.PageFormat.LINK;
187+
}
182188
}
183189

184190
checkMandatoryContent(data, link, content);
@@ -499,8 +505,8 @@ public Page.PageStatus[] getPageStatus() {
499505
}
500506

501507
private void checkCorrectFormat(final MultipartFile data, final Page.PageFormat format) {
502-
if (Page.PageFormat.LINK.equals(format) && data != null && !data.isEmpty()) {
503-
throw new IllegalArgumentException("Wrong format. Cannot set format to LINK and upload a file.");
508+
if ((Page.PageFormat.LINK.equals(format) || Page.PageFormat.EMAILLINK.equals(format)) && data != null && !data.isEmpty()) {
509+
throw new IllegalArgumentException("Wrong format. Cannot set format to LINK or EMAILLINK and upload a file.");
504510
}
505511
}
506512

@@ -619,15 +625,24 @@ private void fillContent(final MultipartFile data,
619625
page.setData(content.getBytes());
620626
} else if (page.getData() == null) {
621627
// Check the link, unless it refers to a file uploaded to the page, that contains the original file name.
622-
if (StringUtils.isNotBlank(link) && !UrlUtils.isValidRedirectUrl(link)) {
623-
throw new IllegalArgumentException("The link provided is not valid");
624-
} else {
625-
page.setLink(link);
628+
// The link should be a valid URL or mailto address
629+
if (page.getFormat() == Page.PageFormat.LINK) {
630+
if (StringUtils.isNotBlank(link) && !UrlUtils.isValidRedirectUrl(link)) {
631+
throw new IllegalArgumentException("The link provided is not valid");
632+
} else {
633+
page.setLink(link);
634+
}
635+
} else if (page.getFormat() == Page.PageFormat.EMAILLINK) {
636+
if (StringUtils.isNotBlank(link) && !EmailUtil.isValidEmailAddress(link)) {
637+
throw new IllegalArgumentException("The link provided is not valid");
638+
} else {
639+
page.setLink(link);
640+
}
626641
}
627642
}
628-
629643
}
630644

645+
631646
/**
632647
* Check is the user is in designated group to access the static page when page permission level is set to GROUP
633648
* @param us Current User Session

services/src/main/java/org/fao/geonet/api/users/validation/UserRegisterDtoValidator.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2001-2024 Food and Agriculture Organization of the
2+
* Copyright (C) 2001-2025 Food and Agriculture Organization of the
33
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
44
* and United Nations Environment Programme (UNEP)
55
*
@@ -27,6 +27,7 @@
2727
import org.fao.geonet.api.users.model.UserRegisterDto;
2828
import org.fao.geonet.constants.Params;
2929
import org.fao.geonet.repository.UserRepository;
30+
import org.fao.geonet.utils.EmailUtil;
3031
import org.springframework.util.StringUtils;
3132
import org.springframework.validation.Errors;
3233
import org.springframework.validation.ValidationUtils;
@@ -37,8 +38,6 @@
3738
*
3839
*/
3940
public class UserRegisterDtoValidator implements Validator {
40-
private static final String OWASP_EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
41-
private static final java.util.regex.Pattern OWASP_EMAIL_PATTERN = java.util.regex.Pattern.compile(OWASP_EMAIL_REGEX);
4241

4342
@Override
4443
public boolean supports(Class<?> clazz) {
@@ -55,7 +54,7 @@ public void validate(Object target, Errors errors) {
5554
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "field.required", Params.EMAIL
5655
+ " is required");
5756

58-
if (StringUtils.hasLength(userRegisterDto.getEmail()) && !OWASP_EMAIL_PATTERN.matcher(userRegisterDto.getEmail()).matches()) {
57+
if (StringUtils.hasLength(userRegisterDto.getEmail()) && !EmailUtil.isValidEmailAddress(userRegisterDto.getEmail())) {
5958
errors.rejectValue("email", "field.notvalid", "Email address is not valid");
6059
}
6160

web-ui/src/main/resources/catalog/components/pages/GnStaticPagesDirective.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,13 @@
153153
$scope.page = $scope.pagesMenu[0];
154154
$scope.isSubmenu = $scope.page.type === "submenu";
155155
$scope.isExternalLink =
156-
$scope.page.format == "LINK" || $scope.page.format == "HTMLPAGE";
156+
$scope.page.format === "LINK" ||
157+
$scope.page.format === "EMAILLINK" ||
158+
$scope.page.format === "HTMLPAGE";
159+
160+
if ($scope.page.format === "EMAILLINK") {
161+
$scope.page.link = "mailto:" + $scope.page.link;
162+
}
157163
}
158164
},
159165
function (response) {

0 commit comments

Comments
 (0)