diff --git a/engine/src/main/java/org/entando/entando/aps/system/services/page/IPageService.java b/engine/src/main/java/org/entando/entando/aps/system/services/page/IPageService.java index 2ab86a1019..bced944cac 100644 --- a/engine/src/main/java/org/entando/entando/aps/system/services/page/IPageService.java +++ b/engine/src/main/java/org/entando/entando/aps/system/services/page/IPageService.java @@ -48,6 +48,12 @@ public interface IPageService extends IComponentUsageService { PageDto getPage(String pageCode, String status); + PageDto getRootPage(String status, UserDetails user); + + PageDto getRootPage(String status); + + String getRootPageCode(); + PageDto addPage(PageRequest pageRequest); void removePage(String pageName); diff --git a/engine/src/main/java/org/entando/entando/aps/system/services/page/PageService.java b/engine/src/main/java/org/entando/entando/aps/system/services/page/PageService.java index 29afe8e446..267488f90d 100644 --- a/engine/src/main/java/org/entando/entando/aps/system/services/page/PageService.java +++ b/engine/src/main/java/org/entando/entando/aps/system/services/page/PageService.java @@ -241,6 +241,44 @@ public PageDto getPage(String pageCode, String status) { return pageDto; } + @Override + public PageDto getRootPage(String status, UserDetails user) { + PageDto pageDto = this.getRootPage(status); + return this.loadVirtualChildren(pageDto, user); + } + + @Override + public PageDto getRootPage(String status) { + IPage page; + switch (status) { + case STATUS_ONLINE: + page = this.getPageManager().getOnlineRoot(); + break; + case STATUS_DRAFT: + default: + page = this.getPageManager().getDraftRoot(); + break; + } + if (null == page) { + throw new RestServerError("Root page not found"); + } + PageDto pageDto = this.getDtoBuilder().convert(page); + String token = this.getPageTokenManager().encrypt(page.getCode()); + String urlToken = getUrlToken(token); + pageDto.setToken(urlToken); + pageDto.setReferences(this.getReferencesInfo(page)); + return pageDto; + } + + @Override + public String getRootPageCode() { + IPage root = this.getPageManager().getDraftRoot(); + if (root == null) { + throw new ResourceNotFoundException(ERRCODE_PAGE_NOT_FOUND, "page", "RootPage"); + } + return root.getCode(); + } + private String getUrlToken(String token) { try { return URLEncoder.encode(token, "UTF-8"); @@ -377,6 +415,12 @@ public PageDto updatePageStatus(String pageCode, String status) { this.getPageManager().setPageOnline(pageCode); newPage = this.getPageManager().getOnlinePage(pageCode); } else if (status.equals(STATUS_DRAFT)) { + IPage rootPage = this.getPageManager().getOnlineRoot(); + if (rootPage != null && rootPage.getCode().equals(pageCode)) { + bindingResult.reject(PageValidator.ERRCODE_ROOT_PAGE, + new String[]{pageCode}, "page.status.root.unpublish"); + throw new ValidationGenericException(bindingResult); + } String[] childCodes = currentPage.getChildrenCodes(); for (String childCode : childCodes) { IPage publicChild = this.getPageManager().getOnlinePage(childCode); diff --git a/engine/src/main/java/org/entando/entando/web/page/PageController.java b/engine/src/main/java/org/entando/entando/web/page/PageController.java index 0e90c853b8..b02b359b11 100644 --- a/engine/src/main/java/org/entando/entando/web/page/PageController.java +++ b/engine/src/main/java/org/entando/entando/web/page/PageController.java @@ -112,14 +112,18 @@ public void setAuthorizationService(IPageAuthorizationService authorizationServi @RequestMapping(value = "/pages", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity, Map>> getPages( @RequestAttribute("user") UserDetails user, - @RequestParam(value = "parentCode", required = false, defaultValue = "homepage") String parentCode, + @RequestParam(value = "parentCode", required = false) String parentCode, @RequestParam(value = "forLinkingToOwnerGroup", required = false) String forLinkingToOwnerGroup, @RequestParam(value = "forLinkingToExtraGroups", required = false) String forLinkingToExtraGroups) { + String rootPageCode = this.getPageService().getRootPageCode(); + if (parentCode == null || parentCode.isEmpty()) { + parentCode = rootPageCode; + } logger.debug("getting page tree for parent {} ({}|{})", parentCode, forLinkingToOwnerGroup, forLinkingToExtraGroups); boolean editableParent = this.getAuthorizationService().canEdit(user, parentCode); - if (!editableParent && !parentCode.equals("homepage")) { + if (!editableParent && !parentCode.equals(rootPageCode)) { throw new ResourcePermissionsException(user.getUsername(), parentCode); } @@ -145,7 +149,7 @@ public ResponseEntity, Map>> getPages } @RestAccessControl(permission = Permission.MANAGE_PAGES) - @RequestMapping(value = "/pages/search", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(value = "/pages/utils/search", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getPages(@RequestAttribute("user") UserDetails user, PageSearchRequest searchRequest) { logger.debug("getting page list with request {}", searchRequest); this.getPageValidator().validateRestListRequest(searchRequest, PageDto.class); @@ -155,7 +159,7 @@ public ResponseEntity> getPages(@RequestAttribute("us } @RestAccessControl(permission = Permission.MANAGE_PAGES) - @RequestMapping(value = "/pages/search/group/free", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(value = "/pages/utils/search/group/free", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getFreeOnlinePages(@RequestAttribute("user") UserDetails user, RestListRequest restListRequest) { logger.debug("getting free pages list with request {}", restListRequest); this.getPageValidator().validateRestListRequest(restListRequest, PageDto.class); @@ -164,7 +168,7 @@ public ResponseEntity> getFreeOnlinePages(@RequestAtt } @RestAccessControl(permission = Permission.MANAGE_PAGES) - @RequestMapping(value = "/pages/viewpages", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(value = "/pages/utils/viewpages", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity>> listViewPages() { logger.debug("REST request - content type list view pages"); @@ -172,6 +176,18 @@ public ResponseEntity>> listViewPages() { new SimpleRestResponse<>(pageService.listViewPages())); } + @RestAccessControl(permission = Permission.MANAGE_PAGES) + @RequestMapping(value = "/pages/utils/root", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity>> getRootPage( + @RequestAttribute("user") UserDetails user, + @RequestParam(value = "status", required = false, defaultValue = IPageService.STATUS_DRAFT) String status) { + logger.debug("getting root page"); + PageDto page = this.getPageService().getRootPage(status, user); + Map metadata = new HashMap<>(); + metadata.put("status", status); + return new ResponseEntity<>(new RestResponse<>(page, metadata), HttpStatus.OK); + } + @RestAccessControl(permission = Permission.MANAGE_PAGES) @RequestMapping(value = "/pages/{pageCode}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity>> getPage(@RequestAttribute("user") UserDetails user, @PathVariable String pageCode, @@ -348,6 +364,10 @@ public ResponseEntity> deletePage( throw new ValidationGenericException(bindingResult); } //business validations + getPageValidator().validateRootPage(pageCode, bindingResult); + if (bindingResult.hasErrors()) { + throw new ValidationGenericException(bindingResult); + } getPageValidator().validateOnlinePage(pageCode, bindingResult); if (bindingResult.hasErrors()) { throw new ValidationGenericException(bindingResult); diff --git a/engine/src/main/java/org/entando/entando/web/page/validator/PageValidator.java b/engine/src/main/java/org/entando/entando/web/page/validator/PageValidator.java index 7a36a6274f..d8061e3c19 100644 --- a/engine/src/main/java/org/entando/entando/web/page/validator/PageValidator.java +++ b/engine/src/main/java/org/entando/entando/web/page/validator/PageValidator.java @@ -56,6 +56,7 @@ public class PageValidator extends AbstractPaginationValidator { public static final String ERRCODE_PAGE_WITH_PUBLIC_CHILD = "8"; public static final String ERRCODE_PAGE_WITH_NO_PUBLIC_PARENT = "9"; public static final String ERRCODE_PAGE_INVALID_TITLE = "12"; + public static final String ERRCODE_ROOT_PAGE = "13"; private final org.slf4j.Logger logger = EntLogFactory.getSanitizedLogger(getClass()); @@ -123,6 +124,13 @@ public void validateChildren(String pageCode, Errors errors) { } } + public void validateRootPage(String pageCode, Errors errors) { + IPage root = this.getPageManager().getDraftRoot(); + if (root != null && root.getCode().equals(pageCode)) { + errors.reject(ERRCODE_ROOT_PAGE, new String[]{pageCode}, "page.delete.root"); + } + } + public void validateChangePositionRequest(String pageCode, PagePositionRequest pageRequest, Errors errors) { if (!StringUtils.equals(pageCode, pageRequest.getCode())) { errors.rejectValue("code", ERRCODE_URINAME_MISMATCH, new String[]{pageCode, pageRequest.getCode()}, "page.code.mismatch"); diff --git a/engine/src/main/resources/rest/messages.properties b/engine/src/main/resources/rest/messages.properties index 7eda70d3d0..e14807106d 100644 --- a/engine/src/main/resources/rest/messages.properties +++ b/engine/src/main/resources/rest/messages.properties @@ -86,6 +86,8 @@ page.status.invalid.draft.ref=draft page has references page.status.parent.unpublished=The page ''{0}'' can''t be published because the parent page ''{1}'' is not public page.status.publicChild=The page ''{0}'' can''t be unpublished because it has public children page.widgetconfig.notoverridable=The widget configuration is not overridable +page.delete.root=The root page ''{0}'' cannot be deleted +page.status.root.unpublish=The root page ''{0}'' cannot be unpublished page.title.notBlank=Page title for language ''{0}'' is required invalidParameter.framedId=the URI parameter ''{0}'' is not valid diff --git a/engine/src/test/java/org/entando/entando/web/page/PageControllerIntegrationTest.java b/engine/src/test/java/org/entando/entando/web/page/PageControllerIntegrationTest.java index e18fca611f..7d5bba56ab 100644 --- a/engine/src/test/java/org/entando/entando/web/page/PageControllerIntegrationTest.java +++ b/engine/src/test/java/org/entando/entando/web/page/PageControllerIntegrationTest.java @@ -204,7 +204,7 @@ void testPageSearchByCode() throws Exception { .build(); String accessToken = mockOAuthInterceptor(user); ResultActions result = mockMvc - .perform(get("/pages/search") + .perform(get("/pages/utils/search") .param("pageSize", "5") .param("pageCodeToken", "pagin") .header("Authorization", "Bearer " + accessToken)); @@ -220,7 +220,7 @@ void testPageSearchByTitle() throws Exception { .build(); String accessToken = mockOAuthInterceptor(user); ResultActions result = mockMvc - .perform(get("/pages/search") + .perform(get("/pages/utils/search") .param("pageSize", "20") .param("title", "errore") .header("Authorization", "Bearer " + accessToken)); @@ -230,7 +230,7 @@ void testPageSearchByTitle() throws Exception { result.andExpect(jsonPath("$.payload[0].code", is("errorpage"))); result = mockMvc - .perform(get("/pages/search") + .perform(get("/pages/utils/search") .param("pageSize", "20") .param("title", "iniziale") .header("Authorization", "Bearer " + accessToken)); @@ -240,7 +240,7 @@ void testPageSearchByTitle() throws Exception { result.andExpect(jsonPath("$.payload[0].code", is("homepage"))); result = mockMvc - .perform(get("/pages/search") + .perform(get("/pages/utils/search") .param("pageSize", "20") .param("title", "di") .header("Authorization", "Bearer " + accessToken)); @@ -250,7 +250,7 @@ void testPageSearchByTitle() throws Exception { result.andExpect(jsonPath("$.payload[0].code", is("errorpage"))); result = mockMvc - .perform(get("/pages/search") + .perform(get("/pages/utils/search") .param("pageSize", "20") .param("title", "start") .header("Authorization", "Bearer " + accessToken)); @@ -421,7 +421,7 @@ void testPageSearchFreeOnlinePages() throws Exception { .build(); String accessToken = mockOAuthInterceptor(user); ResultActions result = mockMvc - .perform(get("/pages/search/group/free") + .perform(get("/pages/utils/search/group/free") .param("pageSize", "50") .header("Authorization", "Bearer " + accessToken)); result.andExpect(status().isOk()); @@ -1872,7 +1872,7 @@ void testCloneValidations() throws Exception { private ResultActions performListViewPages(String accessToken) throws Exception { - return mockMvc.perform(get("/pages/viewpages") + return mockMvc.perform(get("/pages/utils/viewpages") .header("Authorization", "Bearer " + accessToken)); } @@ -2246,7 +2246,7 @@ void testRootIsVisibleAlsoToNonAdminUser() throws Exception { .andExpect(status().isOk()); // search - mockMvc.perform(get("/pages/search") + mockMvc.perform(get("/pages/utils/search") .param("sort", "code") .param("direction", "ASC") .param("pageCodeToken", "homepage") diff --git a/engine/src/test/java/org/entando/entando/web/page/PageControllerTest.java b/engine/src/test/java/org/entando/entando/web/page/PageControllerTest.java index 0e5f8789e7..3818fc1f28 100644 --- a/engine/src/test/java/org/entando/entando/web/page/PageControllerTest.java +++ b/engine/src/test/java/org/entando/entando/web/page/PageControllerTest.java @@ -43,6 +43,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import org.entando.entando.aps.system.services.page.IPageService; import org.entando.entando.aps.system.services.page.PageAuthorizationService; import org.entando.entando.aps.system.services.page.PageService; import org.entando.entando.aps.system.services.page.model.PageDto; @@ -219,6 +220,7 @@ void usersWithoutAdminOrFreeGroupAccessShouldSeeVirtualRoot() throws Exception { List mockResult = List.of(page1); Mockito.when(pageService.getPagesTree(eq("homepage"), any())).thenReturn(mockResult); + Mockito.when(pageService.getRootPageCode()).thenReturn("homepage"); Mockito.when(authorizationService.canEdit(any(UserDetails.class), eq("homepage"))).thenReturn(false); ResultActions result = mockMvc.perform( @@ -912,6 +914,7 @@ void shouldAllowRetrievingTreeFromUnauthorizedHomepage() throws Exception { UserDetails user = new OAuth2TestUtils.UserBuilder("jack_bauer", "0x24").grantedToRoleAdmin().build(); String accessToken = mockOAuthInterceptor(user); + when(pageService.getRootPageCode()).thenReturn("homepage"); when(pageService.getPagesTree(eq("homepage"), any())).thenReturn(List.of()); mockMvc.perform(get("/pages") @@ -928,6 +931,44 @@ private List createMetadataList(String json) throws IOException, JsonPa return result; } + @Test + void shouldGetRootPage() throws Exception { + UserDetails user = new OAuth2TestUtils.UserBuilder("jack_bauer", "0x24") + .withAuthorization(Group.FREE_GROUP_NAME, "managePages", Permission.MANAGE_PAGES) + .build(); + String accessToken = mockOAuthInterceptor(user); + + PageDto rootPageDto = new PageDto(); + rootPageDto.setCode("home"); + + when(pageService.getRootPage(eq(IPageService.STATUS_DRAFT), any(UserDetails.class))).thenReturn(rootPageDto); + + mockMvc.perform(get("/pages/utils/root") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.payload.code", is("home"))) + .andExpect(jsonPath("$.metaData.status", is("draft"))); + } + + @Test + void shouldValidateDeleteRootPage() throws Exception { + UserDetails user = new OAuth2TestUtils.UserBuilder("jack_bauer", "0x24").grantedToRoleAdmin().build(); + String accessToken = mockOAuthInterceptor(user); + + Page rootPage = new Page(); + rootPage.setCode("homepage"); + when(authorizationService.canEdit(any(UserDetails.class), any(String.class))).thenReturn(true); + when(this.controller.getPageValidator().getPageManager().getDraftRoot()).thenReturn(rootPage); + + ResultActions result = mockMvc.perform( + delete("/pages/{pageCode}", "homepage") + .header("Authorization", "Bearer " + accessToken)); + + result.andExpect(status().isBadRequest()); + result.andExpect(jsonPath("$.errors", hasSize(1))); + result.andExpect(jsonPath("$.errors[0].code", is(PageValidator.ERRCODE_ROOT_PAGE))); + } + private class PageM extends Page { public PageM(boolean isOnline) { diff --git a/engine/src/test/java/org/entando/entando/web/page/validator/PageValidatorTest.java b/engine/src/test/java/org/entando/entando/web/page/validator/PageValidatorTest.java index 1c6459ac14..7e57374dd7 100644 --- a/engine/src/test/java/org/entando/entando/web/page/validator/PageValidatorTest.java +++ b/engine/src/test/java/org/entando/entando/web/page/validator/PageValidatorTest.java @@ -88,6 +88,28 @@ void validateMovePagePermissionsShouldReturnExceptionIfYouCannotWriteOldParent() Assertions.assertTrue(errors.hasErrors()); } + @Test + void validateRootPageShouldRejectRootPageDeletion() { + IPage rootPage = Mockito.mock(IPage.class); + Mockito.when(rootPage.getCode()).thenReturn("homepage"); + Mockito.when(pageManager.getDraftRoot()).thenReturn(rootPage); + + BindingResult errors = (new DataBinder(new Object())).getBindingResult(); + validator.validateRootPage("homepage", errors); + Assertions.assertTrue(errors.hasErrors()); + } + + @Test + void validateRootPageShouldAllowNonRootPageDeletion() { + IPage rootPage = Mockito.mock(IPage.class); + Mockito.when(rootPage.getCode()).thenReturn("homepage"); + Mockito.when(pageManager.getDraftRoot()).thenReturn(rootPage); + + BindingResult errors = (new DataBinder(new Object())).getBindingResult(); + validator.validateRootPage("some_other_page", errors); + Assertions.assertFalse(errors.hasErrors()); + } + private UserDetails getMockUser(){ User u = new User(); u.setUsername("mockUser");