From 2c4ce82de5b32cadfd4a1adc3727fde0454024dd Mon Sep 17 00:00:00 2001 From: Ali Hamdi Date: Thu, 3 Sep 2026 13:45:21 +0100 Subject: [PATCH] fix: mount and browse space drives whose display name contains a slash - EXO-89613 A Space display name holding a '/' was URL-encoded into the WebDAV drive segment as %2F, which StrictHttpFirewall rejects before the request reaches any handler and which, once decoded, split the drive into two path segments so that the identity id could no longer be read back from the path. A drive is now addressed by its Space pretty name - the very name its JCR drive is created under - while staying presented under its display name in the WebDAV displayname property. Personal drives keep the user full name. toWebDavSegment guards both against any character that cannot survive a single path segment. The legacy fallback in resolveToJcrPath now compares identity-relative paths only, so a client holding an older spelling of the drive name still resolves. The 'Map eXo Drives' drawer built the space segment client-side from the display name and is aligned on the same rule. Co-Authored-By: Claude Opus 5 (1M context) --- .../jcr/webdav/plugin/PathCommandHandler.java | 97 ++++++++++++++----- .../plugin/WebdavReadCommandHandler.java | 39 ++++---- .../webdav/plugin/PathCommandHandlerTest.java | 62 +++++++++++- .../plugin/WebdavReadCommandHandlerTest.java | 55 +++++++++++ .../DocumentsWebdavMapDrivesDrawer.vue | 8 +- 5 files changed, 220 insertions(+), 41 deletions(-) diff --git a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java index 9c0f36d72f..31b7cebcc7 100644 --- a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java +++ b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandler.java @@ -23,8 +23,8 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.Optional; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.jcr.*; @@ -95,6 +95,15 @@ public class PathCommandHandler { public static final String PATHS_CONCAT_FORMAT = "%s/%s"; + public static final String SEGMENT_CHAR_REPLACEMENT = "_"; + + /** + * Characters a name cannot carry into a WebDAV path segment: the two path + * separators, and the characters whose percent-encoded form is in + * {@code StrictHttpFirewall}'s blocklist. + */ + private static final Pattern UNSAFE_SEGMENT_CHARS = Pattern.compile("[/\\\\%;\\p{Cntrl}]"); + protected static final Log LOG = ExoLogger.getLogger(PathCommandHandler.class); private static final String WEBDAV_IDENTITY_JCR_PATH_CACHE_NAME = "webdav.identityJcrBasePath"; @@ -146,6 +155,23 @@ public void init() { addMappingEventListener(); } + /** + * Last-resort guard keeping a drive name inside a single WebDAV path segment. + * The names fed to it — a Space pretty name, a username — are already URL-safe + * by construction (see {@link #getIdentitySegmentName(Identity)}); this only + * makes sure an identity store that yields something unexpected cannot emit a + * '%2F', which is rejected before the request reaches any handler and which, + * once decoded, would split the drive into two segments so that the identity + * id can no longer be read back from the path. + * + * @param segmentName drive name, may be null + * @return the name with every character unusable in a path segment replaced + * by {@link #SEGMENT_CHAR_REPLACEMENT} + */ + public static String toWebDavSegment(String segmentName) { + return UNSAFE_SEGMENT_CHARS.matcher(StringUtils.defaultString(segmentName)).replaceAll(SEGMENT_CHAR_REPLACEMENT); + } + @SneakyThrows @Cacheable(WEBDAV_IDENTITY_JCR_PATH_CACHE_NAME) public String getIdentityBaseJcrPath(String webDavPath) { @@ -264,11 +290,13 @@ public String resolveToJcrPath(Session session, String webDavPath) throws WebDav if (session.itemExists(legacyChildJcrPath)) { Item existingItem = session.getItem(legacyChildJcrPath); if (existingItem instanceof Node existingNode) { - String existingWebDavPath = getOrCreateWebDavPath(existingNode); - String decodedExistingWebDavPath = decodeUrlString(existingWebDavPath); - String decodedExistingWebDavPathPrefix = StringUtils.removeEnd(decodedExistingWebDavPath, "/") + "/"; // NOSONAR - if (webDavPath.startsWith(decodedExistingWebDavPathPrefix) - || webDavPath.equals(decodedExistingWebDavPath)) { + // Compare the identity-relative parts only: the drive segment is + // addressed by its id, so a client may legitimately hold an older + // spelling of the drive name in the path it sends + String existingRelativePath = getIdentityRelativeDecodedWebDavPath(getOrCreateWebDavPath(existingNode)); + if (StringUtils.isBlank(existingRelativePath) + || StringUtils.equals(identityRelativeWebDavPath, existingRelativePath) + || StringUtils.startsWith(identityRelativeWebDavPath, existingRelativePath + "/")) { currentParentJcrPath = legacyChildJcrPath; continue; } @@ -307,7 +335,7 @@ public String getOrCreateWebDavPath(Node node) { return null; } String identityBaseJcrPath = getIdentityBaseJcrPath(identityId); - String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, getIdentityDisplayName(identity)); + String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, getIdentitySegmentName(identity)); return getOrCreateWebDavPath(String.valueOf(identityId), identityBaseJcrPath, identityRootWebDavPath, @@ -486,6 +514,19 @@ private String getIdentityRelativeWebDavPath(String webDavPath) { .collect(Collectors.joining("/")); } + /** + * @param encodedWebDavPath a WebDAV path as stored/emitted, percent-encoded + * @return the same path without its drive segment, each remaining segment + * decoded — comparable with the decoded path a client sends + */ + private String getIdentityRelativeDecodedWebDavPath(String encodedWebDavPath) { + return Arrays.stream(StringUtils.defaultString(encodedWebDavPath).split("/")) + .filter(StringUtils::isNotBlank) + .skip(1) + .map(this::decodeUrlString) + .collect(Collectors.joining("/")); + } + private List splitDecodedSegments(String relativeWebDavPath) { return Arrays.stream(relativeWebDavPath.split("/")) .filter(StringUtils::isNotBlank) @@ -734,30 +775,42 @@ private Long getIdentityIdFromJcrPath(String jcrPath) { } private String getIdentityRootWebDavPath(long identityId) { - return getIdentityRootWebDavPath(identityId, getIdentityDisplayName(identityId)); + return getIdentityRootWebDavPath(identityId, getIdentitySegmentName(identityManager.getIdentity(identityId))); } - private String getIdentityRootWebDavPath(long identityId, String displayName) { + private String getIdentityRootWebDavPath(long identityId, String segmentName) { return String.format("/%s%s%s%s", - encodeUrlString(displayName), + encodeUrlString(toWebDavSegment(segmentName)), IDENTITY_ID_PREFIX, identityId, IDENTITY_ID_SUFFIX); } - private String getIdentityDisplayName(long identityId) { - Identity identity = identityManager.getIdentity(identityId); - return Objects.requireNonNullElseGet(getIdentityDisplayName(identity), () -> String.valueOf(identityId)); - } - - private String getIdentityDisplayName(Identity identity) { - if (identity != null && identity.isSpace()) { - Space space = spaceService.getSpaceByPrettyName(identity.getRemoteId()); - if (space != null) { - return space.getDisplayName(); - } + /** + * Returns the name an identity contributes to its WebDAV drive segment: + *
    + *
  • a Space is addressed by its pretty name, the very name its drive + * is created under in JCR + * (/groups/spaces/<prettyName>/Documents): URL-safe by + * construction and frozen at creation, unlike the Space display name, which a + * rename can change and which may carry a '/';
  • + *
  • a personal drive keeps the user full name, which reads far better + * than a username when the drive is mounted, and which + * {@link #toWebDavSegment(String)} keeps inside a single path segment.
  • + *
+ * + * @param identity {@link Identity} of the drive owner, may be null + * @return the name the drive is addressed by, never null + */ + public static String getIdentitySegmentName(Identity identity) { + if (identity == null) { + return ""; + } + if (identity.isSpace()) { + return StringUtils.defaultIfBlank(identity.getRemoteId(), identity.getId()); } - return identity == null ? null : identity.getProfile().getFullName(); + String fullName = identity.getProfile() == null ? null : identity.getProfile().getFullName(); + return StringUtils.firstNonBlank(fullName, identity.getRemoteId(), identity.getId(), ""); } @SneakyThrows diff --git a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java index 6ce7e22363..c79653efc5 100644 --- a/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java +++ b/documents-storage-jcr/src/main/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandler.java @@ -38,6 +38,8 @@ import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.IDENTITY_PATHS_FORMAT; import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.LOG; import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.PROPERTY_NAMES; +import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.getIdentitySegmentName; +import static org.exoplatform.documents.storage.jcr.webdav.plugin.PathCommandHandler.toWebDavSegment; import static org.exoplatform.documents.webdav.model.constant.PropertyConstants.CHECKEDIN; import static org.exoplatform.documents.webdav.model.constant.PropertyConstants.CHECKEDOUT; import static org.exoplatform.documents.webdav.model.constant.PropertyConstants.CHILDCOUNT; @@ -190,7 +192,7 @@ public WebDavItem get(Session session, // NOSONAR return get(getNode(session, pathCommandHandler.resolveToJcrPath(session, webDavPath)), pathCommandHandler.getIdentityBaseJcrPath(webDavPath), identity.getIdentityId(), - identity.getProfile().getFullName(), + getIdentitySegmentName(identity), requestedPropertyNames, requestPropertyNamesOnly, depth, @@ -261,7 +263,7 @@ public List getVersions(Session session, String webDavPath, Set get(getVersionNode(version), identityBaseJcrPath, identity.getIdentityId(), - identity.getProfile().getFullName(), + getIdentitySegmentName(identity), requestedPropertyNames, false, 0, @@ -365,7 +367,7 @@ private WebDavItem get(Node node, return get(node, pathCommandHandler.getIdentityBaseJcrPath(identityId), identity.getIdentityId(), - identity.getProfile().getFullName(), + getIdentitySegmentName(identity), requestedPropertyNames, false, 0, @@ -377,7 +379,7 @@ private WebDavItem get(Node node, private WebDavItem get(Node node, // NOSONAR String identityBaseJcrPath, long identityId, - String displayName, + String segmentName, Set requestedPropertyNames, boolean requestPropertyNamesOnly, int depth, @@ -387,8 +389,8 @@ private WebDavItem get(Node node, // NOSONAR } WebDavItem result = new WebDavItem(); result.setFile(isFile(node)); - String webDavPath = getMappedWebDavPath(node, identityBaseJcrPath, identityId, displayName); - String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, displayName); + String webDavPath = getMappedWebDavPath(node, identityBaseJcrPath, identityId, segmentName); + String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, segmentName); String identifier = identityBaseUri; if (!StringUtils.equals(webDavPath, identityRootWebDavPath)) { identifier = identityBaseUri + webDavPath.substring(identityRootWebDavPath.length()); @@ -400,7 +402,7 @@ private WebDavItem get(Node node, // NOSONAR node, identityBaseJcrPath, identityId, - displayName, + segmentName, requestedPropertyNames, requestPropertyNamesOnly, depth, @@ -418,7 +420,7 @@ private void addChildren(WebDavItem webDavItem, // NOSONAR Node node, String identityBaseJcrPath, long identityId, - String displayName, + String segmentName, Set requestedPropertyNames, boolean requestPropertyNamesOnly, int depth, @@ -431,7 +433,7 @@ private void addChildren(WebDavItem webDavItem, // NOSONAR .map(childNode -> get(childNode, identityBaseJcrPath, identityId, - displayName, + segmentName, requestedPropertyNames, requestPropertyNamesOnly, depth - 1, @@ -706,9 +708,12 @@ private WebDavItem getWebDavIdentityItem(Session session, // NOSONAR displayName = getDisplayName(identityId); } WebDavItem identityWebDavItem = new WebDavItem(); + // The drive is addressed by its segment name (Space pretty name / username) + // and only presented under its display name + String segmentName = getIdentitySegmentName(identityManager.getIdentity(identityId)); String identityBaseUri = String.format(IDENTITY_PATHS_FORMAT, baseUri, - encodeUrlString(displayName), + encodeUrlString(toWebDavSegment(segmentName)), IDENTITY_ID_PREFIX, identityId, IDENTITY_ID_SUFFIX); @@ -723,7 +728,7 @@ private WebDavItem getWebDavIdentityItem(Session session, // NOSONAR identityWebDavItem.setWebDavPath(getMappedWebDavPath(identityParentNode, identityBaseJcrPath, identityId, - displayName)); + segmentName)); addProperties(identityWebDavItem, identityParentNode, requestedPropertyNames, @@ -733,7 +738,7 @@ private WebDavItem getWebDavIdentityItem(Session session, // NOSONAR identityParentNode, identityBaseJcrPath, identityId, - displayName, + segmentName, requestedPropertyNames, requestPropertyNamesOnly, depth, @@ -765,9 +770,9 @@ private void addProperties(WebDavItem result, } } - private String getIdentityRootWebDavPath(long identityId, String displayName) { + private String getIdentityRootWebDavPath(long identityId, String segmentName) { return String.format("/%s%s%s%s", - encodeUrlString(displayName), + encodeUrlString(toWebDavSegment(segmentName)), IDENTITY_ID_PREFIX, identityId, IDENTITY_ID_SUFFIX); @@ -777,8 +782,8 @@ private String getIdentityRootWebDavPath(long identityId, String displayName) { private String getMappedWebDavPath(Node node, String identityBaseJcrPath, long identityId, - String displayName) { - String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, displayName); + String segmentName) { + String identityRootWebDavPath = getIdentityRootWebDavPath(identityId, segmentName); if (StringUtils.equals(node.getPath(), identityBaseJcrPath)) { return identityRootWebDavPath; } else { @@ -804,7 +809,7 @@ private String getIdentityBaseUri(String baseUri, long identityId) { private String getIdentityBaseUri(String baseUri, Identity identity) { return String.format(IDENTITY_PATHS_FORMAT, baseUri, - encodeUrlString(identity.getProfile().getFullName()), + encodeUrlString(toWebDavSegment(getIdentitySegmentName(identity))), IDENTITY_ID_PREFIX, identity.getId(), IDENTITY_ID_SUFFIX); diff --git a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java index 1bd81dbb8a..e2bdddbeec 100644 --- a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java +++ b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/PathCommandHandlerTest.java @@ -493,7 +493,7 @@ public void testRefreshMappingOrDeleteRefreshesExistingMappingAfterCrossIdentity String newIdentityBaseJcrPath = "/groups/spaces/marketing/Documents"; // NOSONAR String newParentJcrPath = newIdentityBaseJcrPath + "/Folder"; String newJcrPath = newParentJcrPath + "/rapport_equipe.docx"; - String newIdentityRootWebDavPath = "/Marketing%20Space%20%2842%29"; // NOSONAR + String newIdentityRootWebDavPath = "/marketing%20%2842%29"; // NOSONAR NodeImpl movedNode = mock(NodeImpl.class); NodeImpl movedParent = mock(NodeImpl.class); @@ -569,6 +569,66 @@ public void testRefreshMappingOrDeleteRefreshesExistingMappingAfterCrossIdentity assertEquals(newIdentityRootWebDavPath + "/Folder/Rapport%20%C3%89quipe.docx", refreshed.getWebDavPath()); } + @Test + public void testToWebDavSegmentReplacesCharactersUnusableInAPathSegment() { + assertEquals("R&D _ Ops", PathCommandHandler.toWebDavSegment("R&D / Ops")); + assertEquals("a_b", PathCommandHandler.toWebDavSegment("a\\b")); + assertEquals("50_ Club", PathCommandHandler.toWebDavSegment("50% Club")); + assertEquals("a_b", PathCommandHandler.toWebDavSegment("a;b")); + assertEquals("a_b", PathCommandHandler.toWebDavSegment("a\nb")); + assertEquals("Marketing Équipe", PathCommandHandler.toWebDavSegment("Marketing Équipe")); + assertEquals("", PathCommandHandler.toWebDavSegment(null)); + } + + @Test + @SneakyThrows + public void testGetOrCreateWebDavPathAddressesSpaceDriveByPrettyNameWhenDisplayNameHasSlash() { + String spaceBaseJcrPath = "/groups/spaces/marketing/Documents"; // NOSONAR + String spaceFileJcrPath = spaceBaseJcrPath + "/rapport.docx"; + + NodeImpl spaceFileNode = mock(NodeImpl.class); + NodeImpl spaceRootNode = mock(NodeImpl.class); + SessionImpl spaceSession = mock(SessionImpl.class); + Identity spaceIdentity = mock(Identity.class); + Space marketingSpace = mock(Space.class); + + when(spaceFileNode.getPath()).thenReturn(spaceFileJcrPath); + when(spaceFileNode.getName()).thenReturn("rapport.docx"); + when(spaceFileNode.getIdentifier()).thenReturn("space-file-id"); + when(spaceFileNode.getParent()).thenReturn(spaceRootNode); + when(spaceFileNode.getSession()).thenReturn(spaceSession); + when(spaceSession.getUserID()).thenReturn(USER1); + when(spaceRootNode.getPath()).thenReturn(spaceBaseJcrPath); + + when(spaceService.getSpaceByGroupId("/spaces/marketing")).thenReturn(marketingSpace); + when(spaceService.getSpaceByPrettyName(SPACE_NAME)).thenReturn(marketingSpace); + when(marketingSpace.getPrettyName()).thenReturn(SPACE_NAME); + when(marketingSpace.getGroupId()).thenReturn("/spaces/marketing"); + when(marketingSpace.getDisplayName()).thenReturn("R&D / Ops"); + when(identityManager.getOrCreateSpaceIdentity(SPACE_NAME)).thenReturn(spaceIdentity); + when(identityManager.getIdentity(42L)).thenReturn(spaceIdentity); + when(spaceIdentity.getIdentityId()).thenReturn(42L); + when(spaceIdentity.isSpace()).thenReturn(true); + when(spaceIdentity.getRemoteId()).thenReturn(SPACE_NAME); + + when(webDavPathMappingStorage.findByNodeIdentifier(anyString())).thenReturn(Optional.empty()); + when(webDavPathMappingStorage.findByJcrPath(anyString())).thenReturn(Optional.empty()); + when(webDavPathMappingStorage.findByParentJcrPathAndNormalizedVisibleName(anyString(), + anyString())).thenReturn(Optional.empty()); + + String result = handler.getOrCreateWebDavPath(spaceFileNode); + + // the drive is addressed by the Space pretty name — the name its JCR drive + // is created under — so the '/' of the display name never reaches the path: + // %2F is rejected before any handler runs, and once decoded it would split + // the drive into two segments + assertEquals("/marketing%20%2842%29/rapport.docx", result); + assertFalse(result.contains("%2F")); + + // the identity id is still readable back from the path the client sends + assertEquals(Long.valueOf(42L), handler.getIdentityIdFromWebDavPath(handler.decodeUrlString(result))); + } + @Test public void testIsTitlePropertyPath() { assertTrue(handler.isTitlePropertyPath(JCR_PATH + "/exo:title")); diff --git a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java index 6d78ac6cad..496658ef53 100644 --- a/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java +++ b/documents-storage-jcr/src/test/java/org/exoplatform/documents/storage/jcr/webdav/plugin/WebdavReadCommandHandlerTest.java @@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; @@ -189,6 +190,60 @@ public void testGetRootPath() { assertNotNull(webDavItem.getProperties()); } + @Test + @SneakyThrows + public void testGetRootPathAddressesSpaceByPrettyNameAndPersonalDriveByFullName() { + String spaceBaseJcrPath = "/groups/spaces/rd-ops/Documents"; // NOSONAR + Space space = mock(Space.class); + Identity spaceIdentity = mock(Identity.class); + Profile spaceProfile = mock(Profile.class); + + when(memberSpacesListAccess.getSize()).thenReturn(1); + when(spaceService.getMemberSpacesIds(USERNAME, 0, 1)).thenReturn(List.of("s1")); + when(spaceService.getSpaceById("s1")).thenReturn(space); + when(space.getPrettyName()).thenReturn("rd-ops"); + when(space.getDisplayName()).thenReturn("R&D / Ops"); + when(identityManager.getOrCreateSpaceIdentity("rd-ops")).thenReturn(spaceIdentity); + when(identityManager.getIdentity(42L)).thenReturn(spaceIdentity); + when(spaceIdentity.getProfile()).thenReturn(spaceProfile); + when(spaceProfile.getFullName()).thenReturn("R&D / Ops"); + when(spaceIdentity.getIdentityId()).thenReturn(42L); + when(spaceIdentity.isSpace()).thenReturn(true); + when(spaceIdentity.getRemoteId()).thenReturn("rd-ops"); + when(pathCommandHandler.getIdentityBaseJcrPath(42L)).thenReturn(spaceBaseJcrPath); + when(session.getItem(spaceBaseJcrPath)).thenReturn(node); + + WebDavItem webDavItem = handler.get(session, + "/", + REQUESTED_PROPERTY_NAMES, + false, + 1, + BASE_URI, + USERNAME); + + assertNotNull(webDavItem.getChildren()); + WebDavItem spaceItem = webDavItem.getChildren() + .stream() + .filter(child -> child.getIdentifier().toString().contains("%2842%29")) + .findFirst() + .orElse(null); + assertNotNull(spaceItem); + // the drive is addressed by the Space pretty name, so the '/' of the + // display name never reaches the href + assertEquals(BASE_URI + "/rd-ops%20%2842%29", spaceItem.getIdentifier().toString()); + // while it stays presented under its real display name + assertEquals("R&D / Ops", spaceItem.getProperty(DISPLAYNAME).getValue()); + + // the personal drive keeps the user full name, it is not slugified + WebDavItem userItem = webDavItem.getChildren() + .stream() + .filter(child -> child.getIdentifier().toString().contains("%281%29")) + .findFirst() + .orElse(null); + assertNotNull(userItem); + assertEquals(BASE_URI + "/John%20Doe%20%281%29", userItem.getIdentifier().toString()); + } + @Test @SneakyThrows public void testGetWithNodePathUsesMappedWebDavPath() { diff --git a/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue b/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue index 1b35b49653..a08f38aecb 100644 --- a/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue +++ b/documents-webapp/src/main/webapp/vue-app/documents-user-setting/components/drawers/DocumentsWebdavMapDrivesDrawer.vue @@ -188,6 +188,7 @@ export default { userIdentity: null, spaceIdentity: null, spaceIdentityId: null, + spaceIdentityRemoteId: null, hrefCopied: false, canCopy: false, tipsByOs: { @@ -269,12 +270,15 @@ export default { }; }, href() { + // The personal drive is addressed by the user full name, a Space by its + // pretty name — never by the Space display name, which may hold a '/' + // and would split the drive into two path segments if (this.driveType === 'ALL') { return `${window.location.origin}/webdav/drives`; } else if (this.driveType === 'PERSONAL') { return `${window.location.origin}/webdav/drives/d/${this.userFullName} (${eXo.env.portal.userIdentityId})`; } else if (this.driveType === 'SPACE' && this.spaceIdentityId) { - return `${window.location.origin}/webdav/drives/d/${this.spaceIdentity.displayName} (${this.spaceIdentityId})`; + return `${window.location.origin}/webdav/drives/d/${this.spaceIdentityRemoteId} (${this.spaceIdentityId})`; } else { return null; } @@ -289,9 +293,11 @@ export default { }, async spaceIdentity() { this.spaceIdentityId = null; + this.spaceIdentityRemoteId = null; if (this.spaceIdentity) { const identity = await this.$identityService.getIdentityByProviderIdAndRemoteId(this.spaceIdentity.providerId, this.spaceIdentity.remoteId); this.spaceIdentityId = identity?.id; + this.spaceIdentityRemoteId = identity?.remoteId || this.spaceIdentity.remoteId; } }, },