From dfd9eea18c318e1a61b0b63230a08b3b494b33a2 Mon Sep 17 00:00:00 2001 From: Ali HAMDI Date: Thu, 28 Sep 2023 14:07:15 +0100 Subject: [PATCH] fix: store files under space root folder - EXO-64344 (#2183) By default images uploaded with Ckeditor image plugin are stored under the folder Documents of the space. This fix allows to choose to store images under a folder outside the default Documents folder. --- apps/portlet-clouddrives/package-lock.json | 4 +- apps/portlet-editors/package-lock.json | 4 +- core/connector/pom.xml | 14 +--- .../platform/ManageDocumentService.java | 54 ++++++++++++++- .../exoplatform/BaseConnectorTestSuite.java | 4 +- .../platform/ManageDocumentServiceTest.java | 66 +++++++++++-------- .../drives/impl/ManageDriveServiceImpl.java | 2 +- .../exoplatform/services/cms/impl/Utils.java | 2 +- 8 files changed, 99 insertions(+), 51 deletions(-) diff --git a/apps/portlet-clouddrives/package-lock.json b/apps/portlet-clouddrives/package-lock.json index 1dfb303d152..775744306de 100644 --- a/apps/portlet-clouddrives/package-lock.json +++ b/apps/portlet-clouddrives/package-lock.json @@ -5224,9 +5224,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "requires": { - "ajv": "^8.0.0" - } + "requires": {} }, "ajv-keywords": { "version": "5.1.0", diff --git a/apps/portlet-editors/package-lock.json b/apps/portlet-editors/package-lock.json index 8cb22084ac1..dfaa3199f75 100644 --- a/apps/portlet-editors/package-lock.json +++ b/apps/portlet-editors/package-lock.json @@ -5224,9 +5224,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "requires": { - "ajv": "^8.0.0" - } + "requires": {} }, "ajv-keywords": { "version": "5.1.0", diff --git a/core/connector/pom.xml b/core/connector/pom.xml index 5b902b90fdb..2dd0e96d3a9 100644 --- a/core/connector/pom.xml +++ b/core/connector/pom.xml @@ -12,7 +12,7 @@ eXo PLF:: ECMS Connector eXo ECMS REST Services - 0.0 + 0.10 @@ -204,17 +204,7 @@ org.mockito - mockito-core - test - - - org.powermock - powermock-api-mockito2 - test - - - org.powermock - powermock-module-junit4 + mockito-inline test diff --git a/core/connector/src/main/java/org/exoplatform/ecm/connector/platform/ManageDocumentService.java b/core/connector/src/main/java/org/exoplatform/ecm/connector/platform/ManageDocumentService.java index 8cc1c4ee580..8d4f1e918fd 100644 --- a/core/connector/src/main/java/org/exoplatform/ecm/connector/platform/ManageDocumentService.java +++ b/core/connector/src/main/java/org/exoplatform/ecm/connector/platform/ManageDocumentService.java @@ -44,6 +44,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.apache.commons.lang.StringUtils; +import org.exoplatform.services.cms.drives.impl.ManageDriveServiceImpl; +import org.exoplatform.services.jcr.core.ExtendedNode; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -650,10 +652,31 @@ private Element createFileElement(Document document, return file; } + /** + * This function gets or creates the folder where the files will be stored + * @param driveName the name of the selected JCR drive + * @param workspaceName The name of the JCR workspace + * @param currentFolder The folder where the files will be stored + * for spaces, the currentFolder will be created directly under the Documents folder of the space. + * If the currentFolder param is preceded with 'DRIVE_ROOT_NODE' then currentFolder will be created under the root folder of the space + * @return Node object representing the parent folder where the files will be uploaded + * @throws Exception + */ private Node getNode(String driveName, String workspaceName, String currentFolder) throws Exception { return getNode(driveName, workspaceName, currentFolder, false); } - + + /** + * This function gets or creates the folder where the files will be stored + * @param driveName the name of the selected JCR drive + * @param workspaceName The name of the JCR workspace + * @param currentFolder The folder where the files will be stored + * for spaces, the currentFolder will be created directly under the Documents folder of the space. + * If the currentFolder param is preceded with 'DRIVE_ROOT_NODE' then currentFolder will be created under the root folder of the space + * @param isSystem use system session if true + * @return Node object representing the parent folder where the files will be uploaded + * @throws Exception + */ private Node getNode(String driveName, String workspaceName, String currentFolder, boolean isSystem) throws Exception { Session session; if (isSystem) { @@ -661,14 +684,29 @@ private Node getNode(String driveName, String workspaceName, String currentFolde } else { session = getSession(workspaceName); } - String driveHomePath = manageDriveService.getDriveByName(Text.escapeIllegalJcrChars(driveName)).getHomePath(); + DriveData driveData = manageDriveService.getDriveByName(Text.escapeIllegalJcrChars(driveName)); + String driveHomePath = driveData.getHomePath(); String userId = ConversationState.getCurrent().getIdentity().getUserId(); String drivePath = Utils.getPersonalDrivePath(driveHomePath, userId); Node node = (Node) session.getItem(Text.escapeIllegalJcrChars(drivePath)); if (StringUtils.isEmpty(currentFolder)) { return node; } - for (String folder : currentFolder.split("/")) { + // Check if we store the file under Documents folder or under the root folder of the space + if(driveName.startsWith(".spaces.") && currentFolder.startsWith("DRIVE_ROOT_NODE")) { + String driveRootPath = driveHomePath; + if(driveHomePath.contains("/Documents")) { + driveRootPath = driveHomePath.substring(0, driveHomePath.indexOf("/Documents")); + } + // Need system session to create the folder if it does not exist + SessionProvider sessionProvider = SessionProvider.createSystemProvider(); + Session systemSession = sessionProvider.getSession(workspaceName, getCurrentRepository()); + node = (Node) systemSession.getItem(Text.escapeIllegalJcrChars(driveRootPath)); + currentFolder = currentFolder.substring("DRIVE_ROOT_NODE/".length()); + } + + List foldersNames = Arrays.stream(currentFolder.split("/")).filter(item -> !item.isEmpty()).toList().stream().toList(); + for (String folder : foldersNames) { String cleanFolderName = org.exoplatform.services.jcr.util.Text.escapeIllegalJcrChars(org.exoplatform.services.cms.impl.Utils.cleanString(folder)); if (node.hasNode(folder)) { node = node.getNode(folder); @@ -683,6 +721,16 @@ private Node getNode(String driveName, String workspaceName, String currentFolde newNode.addMixin(NodetypeConstant.EXO_RSS_ENABLE); } newNode.setProperty(NodetypeConstant.EXO_TITLE, org.exoplatform.services.cms.impl.Utils.cleanDocumentTitle(folder)); + + // Update permissions + if (newNode.canAddMixin("exo:privilegeable")) { + newNode.addMixin("exo:privilegeable"); + } + String groupId = driveData.getParameters().get(ManageDriveServiceImpl.DRIVE_PARAMATER_GROUP_ID); + if(StringUtils.isNotBlank(groupId) && groupId.startsWith("/spaces/")) { + ((ExtendedNode) newNode).setPermission(groupId, new String[]{PermissionType.READ, PermissionType.ADD_NODE, PermissionType.SET_PROPERTY}); + } + node.save(); node = newNode; } diff --git a/core/connector/src/test/java/org/exoplatform/BaseConnectorTestSuite.java b/core/connector/src/test/java/org/exoplatform/BaseConnectorTestSuite.java index 178a9dc1acc..2ead9336724 100644 --- a/core/connector/src/test/java/org/exoplatform/BaseConnectorTestSuite.java +++ b/core/connector/src/test/java/org/exoplatform/BaseConnectorTestSuite.java @@ -20,6 +20,7 @@ import org.exoplatform.commons.testing.ConfigTestCase; //import org.exoplatform.wcm.connector.authoring.TestCopyContentFile; //import org.exoplatform.wcm.connector.authoring.TestLifecycleConnector; +import org.exoplatform.ecm.connector.platform.ManageDocumentServiceTest; import org.exoplatform.wcm.connector.collaboration.TestDownloadConnector; import org.exoplatform.wcm.connector.collaboration.TestFavoriteRESTService; import org.exoplatform.wcm.connector.collaboration.TestOpenInOfficeConnector; @@ -47,7 +48,8 @@ TestDownloadConnector.class, TestOpenInOfficeConnector.class, TestThumbnailRESTService.class, - TestFavoriteRESTService.class + TestFavoriteRESTService.class, + ManageDocumentServiceTest.class }) @ConfigTestCase(BaseConnectorTestCase.class) public class BaseConnectorTestSuite extends BaseExoContainerTestSuite { diff --git a/core/connector/src/test/java/org/exoplatform/ecm/connector/platform/ManageDocumentServiceTest.java b/core/connector/src/test/java/org/exoplatform/ecm/connector/platform/ManageDocumentServiceTest.java index f91cf4d6949..f2911be8fda 100644 --- a/core/connector/src/test/java/org/exoplatform/ecm/connector/platform/ManageDocumentServiceTest.java +++ b/core/connector/src/test/java/org/exoplatform/ecm/connector/platform/ManageDocumentServiceTest.java @@ -19,10 +19,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnitRunner; import javax.jcr.Node; import javax.jcr.Session; @@ -30,12 +28,9 @@ import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.anyString; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.when; +import static org.mockito.Mockito.*; -@RunWith(PowerMockRunner.class) -@PowerMockIgnore({ "javax.management.*" }) -@PrepareForTest({ WCMCoreUtils.class, ConversationState.class, Utils.class}) +@RunWith(MockitoJUnitRunner.class) public class ManageDocumentServiceTest { @Mock @@ -58,9 +53,12 @@ public class ManageDocumentServiceTest { private ManageDocumentService manageDocumentService; + MockedStatic UTILS; + MockedStatic SESSION_PROVIDER; + @Before public void setUp() throws Exception { - PowerMockito.mockStatic(WCMCoreUtils.class); + MockedStatic WCM_CORE_UTILS = mockStatic(WCMCoreUtils.class); ValueParam valueParam = mock(ValueParam.class); when(valueParam.getValue()).thenReturn("20"); when(initParams.getValueParam("upload.limit.size")).thenReturn(valueParam); @@ -68,21 +66,33 @@ public void setUp() throws Exception { SessionProvider userSessionProvider = mock(SessionProvider.class); SessionProvider systemSessionProvider = mock(SessionProvider.class); - when(WCMCoreUtils.getUserSessionProvider()).thenReturn(userSessionProvider); - when(WCMCoreUtils.getSystemSessionProvider()).thenReturn(systemSessionProvider); + WCM_CORE_UTILS.when(WCMCoreUtils::getUserSessionProvider).thenReturn(userSessionProvider); + WCM_CORE_UTILS.when(WCMCoreUtils::getSystemSessionProvider).thenReturn(systemSessionProvider); RepositoryService repositoryService = mock(RepositoryService.class); - when(WCMCoreUtils.getService(RepositoryService.class)).thenReturn(repositoryService); + WCM_CORE_UTILS.when(() -> WCMCoreUtils.getService(RepositoryService.class)).thenReturn(repositoryService); ManageableRepository manageableRepository = mock(ManageableRepository.class); when(repositoryService.getCurrentRepository()).thenReturn(manageableRepository); - when(systemSessionProvider.getSession("collaboration", manageableRepository)).thenReturn(session); + lenient().when(systemSessionProvider.getSession("collaboration", manageableRepository)).thenReturn(session); when(userSessionProvider.getSession("collaboration", manageableRepository)).thenReturn(session); - PowerMockito.mockStatic(ConversationState.class); + MockedStatic CONVERSATION_STATE = mockStatic(ConversationState.class); ConversationState conversationState = mock(ConversationState.class); - when(ConversationState.getCurrent()).thenReturn(conversationState); + CONVERSATION_STATE.when(ConversationState::getCurrent).thenReturn(conversationState); Identity identity = mock(Identity.class); when(conversationState.getIdentity()).thenReturn(identity); when(identity.getUserId()).thenReturn("user"); - PowerMockito.mockStatic(Utils.class); + DriveData driveData = mock(DriveData.class); + when(manageDriveService.getDriveByName(anyString())).thenReturn(driveData); + when(driveData.getHomePath()).thenReturn("path"); + UTILS = mockStatic(Utils.class); + UTILS.when(() -> Utils.getPersonalDrivePath("path", "user")).thenReturn("personalDrivePath"); + UTILS.when(() -> Utils.cleanString(anyString())).thenCallRealMethod(); + UTILS.when(() -> Utils.cleanName(anyString())).thenCallRealMethod(); + UTILS.when(() -> Utils.cleanName(anyString(), anyString())).thenCallRealMethod(); + UTILS.when(() -> Utils.cleanNameWithAccents(anyString())).thenCallRealMethod(); + UTILS.when(() -> Utils.replaceSpecialChars(anyString(), anyString())).thenCallRealMethod(); + UTILS.when(() -> Utils.replaceSpecialChars(anyString(), anyString(), anyString())).thenCallRealMethod(); + SESSION_PROVIDER = mockStatic(SessionProvider.class); + SESSION_PROVIDER.when(SessionProvider::createSystemProvider).thenReturn(systemSessionProvider); } @Test @@ -96,19 +106,21 @@ public void checkFileExistence() throws Exception { response = this.manageDocumentService.checkFileExistence("collaboration", "testspace", "/documents", null); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - response = this.manageDocumentService.checkFileExistence("collaboration", "testspace", "/documents", "test.docx"); - DriveData driveData = mock(DriveData.class); - when(manageDriveService.getDriveByName(anyString())).thenReturn(driveData); - when(driveData.getHomePath()).thenReturn("path"); - when(Utils.getPersonalDrivePath("path", "user")).thenReturn("personalDrivePath"); Node node = mock(Node.class); - when(session.getItem("personalDrivePath")).thenReturn(node); - when(node.hasNode("Documents")).thenReturn(true); + when(session.getItem(anyString())).thenReturn(node); + lenient().when(node.hasNode("Documents")).thenReturn(true); Node targetNode = mock(Node.class); - when(node.getNode("Documents")).thenReturn(targetNode); - when(node.isNodeType(NodetypeConstant.EXO_SYMLINK)).thenReturn(false); - when(fileUploadHandler.checkExistence(targetNode, "test.docx")).thenReturn(Response.ok().build()); + lenient().when(node.getNode("Documents")).thenReturn(targetNode); + lenient().when(node.isNodeType(NodetypeConstant.EXO_SYMLINK)).thenReturn(false); + Node folderNode1 = mock(Node.class); + when(node.addNode(anyString(), eq(NodetypeConstant.NT_FOLDER))).thenReturn(folderNode1); + lenient().when(fileUploadHandler.checkExistence(targetNode, "test.docx")).thenReturn(Response.ok().build()); + + response = this.manageDocumentService.checkFileExistence("collaboration", "testspace", "/documents", "test.docx"); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + + response = this.manageDocumentService.checkFileExistence("collaboration", ".spaces.space_one", "DRIVE_ROOT_NODE/Documents", "test.docx"); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); } } diff --git a/core/services/src/main/java/org/exoplatform/services/cms/drives/impl/ManageDriveServiceImpl.java b/core/services/src/main/java/org/exoplatform/services/cms/drives/impl/ManageDriveServiceImpl.java index e583dc209fc..844f0cc853e 100644 --- a/core/services/src/main/java/org/exoplatform/services/cms/drives/impl/ManageDriveServiceImpl.java +++ b/core/services/src/main/java/org/exoplatform/services/cms/drives/impl/ManageDriveServiceImpl.java @@ -332,7 +332,7 @@ public DriveData getDriveByName(String name) throws Exception{ DriveData drive = groupDriveTemplate.clone(); drive.setHomePath(groupDriveTemplate.getHomePath().replace("${groupId}", groupName)); drive.setName(name); - drive.getParameters().put(ManageDriveServiceImpl.DRIVE_PARAMATER_GROUP_ID, name); + drive.getParameters().put(ManageDriveServiceImpl.DRIVE_PARAMATER_GROUP_ID, groupName); drive.setPermissions("*:" + groupName); return drive; } diff --git a/core/services/src/main/java/org/exoplatform/services/cms/impl/Utils.java b/core/services/src/main/java/org/exoplatform/services/cms/impl/Utils.java index 6cfe1806a4c..d2bb10d9f3f 100644 --- a/core/services/src/main/java/org/exoplatform/services/cms/impl/Utils.java +++ b/core/services/src/main/java/org/exoplatform/services/cms/impl/Utils.java @@ -704,7 +704,7 @@ public static String cleanDocumentTitle(String title) { return replaceSpecialChars(title, "[<\\>:\"/|?*]"); } - private static String replaceSpecialChars(String name, String specialChars) { + public static String replaceSpecialChars(String name, String specialChars) { return replaceSpecialChars(name, specialChars, NodetypeConstant.NT_FILE); } public static String replaceSpecialChars(String name, String specialChars, String nodeType) {