From c65b8fee094119db15fe62671b4803cb4f4c5bae Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 1 Aug 2026 08:45:38 +0200 Subject: [PATCH 1/4] sharing: extend comments --- radicale/sharing/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 3cf8bdfd..0d0ebe8a 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -628,6 +628,7 @@ class BaseSharing: """ returning dict with PathMapped, Owner, Permissions or None if invalid""" if self.sharing_collection_by_map: logger.trace("sharing/map/resolver: check path: %r", path) + # check collection path result = self.database_get_sharing( ShareType="map", PathOrToken=path, @@ -635,7 +636,7 @@ class BaseSharing: User=user) if not result: - # fallback to parent path + # assume item path, fallback to parent path parent_path = pathutils.parent_path(path) logger.trace("sharing/map/resolver: check parent path: %r", parent_path) result = self.database_get_sharing( From 9f2d193fa5cdfe512447240ed684992610d60b78 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 1 Aug 2026 08:46:05 +0200 Subject: [PATCH 2/4] sharing/map: catch collection path with missing trailing / --- radicale/sharing/__init__.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 0d0ebe8a..88874d31 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -647,9 +647,19 @@ class BaseSharing: if result: result['PathMapped'] = path.replace(parent_path, result['PathMapped']) logger.trace("sharing/map/resolver: PathMapped=%r Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], parent_path) - else: - logger.trace("sharing/map/resolver: not found") - return None + + if not result and not path.endswith("/"): + # assume collection path with missing trailing / + path_with_trailing_slash = path + "/" + logger.trace("sharing/map/resolver: check path having '/' appended: %r", path_with_trailing_slash) + result = self.database_get_sharing( + ShareType="map", + PathOrToken=path_with_trailing_slash, + OnlyEnabled=False, + User=user) + if result: + path = path_with_trailing_slash + logger.trace("sharing/map/resolver: PathMapped=%r Permissions=%r by path_with_trailing_slash=%r", result['PathMapped'], result['Permissions'], path_with_trailing_slash) if result: if result['EnabledByOwner'] is not True: @@ -667,6 +677,7 @@ class BaseSharing: logger.info("sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r", "map", result['PathOrToken'], result['PathMapped'], user, result['Owner'], result['Permissions'], result['Conversion']) return result + logger.trace("sharing/map/resolver: not found") return None else: logger.trace("sharing/map: not active") From 4952d2c3d9575f7c943f46214f35dc27aff844e5 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 1 Aug 2026 08:47:01 +0200 Subject: [PATCH 3/4] sharing/map: add test cases for collections without trailing / --- radicale/tests/test_sharing.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index cfa85815..be26c11f 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -1988,10 +1988,34 @@ class TestSharingApiSanity(BaseTest): json_dict['PathOrToken'] = path_shared _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict) + # check GET as user + logging.info("\n*** GET collection user -> ok") + _, headers, answer = self.request("GET", path_shared, check=200, login="user:userpw") + # check REPORT as user logging.info("\n*** REPORT collection user -> ok") _, responses = self.report(path_shared, """\ + + + + +""", login="user:userpw") + assert len(responses) == 1 + logging.info("response: %r", responses) + response = responses[path_shared_item] + assert isinstance(response, dict) + status, prop = response["D:getetag"] + assert status == 200 and prop.text + + # check GET as user without trailing / + logging.info("\n*** GET collection user (without trailing /) -> ok") + _, headers, answer = self.request("GET", path_shared.removesuffix("/"), check=200, login="user:userpw") + + # check REPORT as user without trailing / + logging.info("\n*** REPORT collection user (without trailing /) -> ok") + _, responses = self.report(path_shared.removesuffix("/"), """\ + From 6cdf0e75b4dafa71bbf04aaf56a14cccb21a286a Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 1 Aug 2026 08:48:55 +0200 Subject: [PATCH 4/4] sharing/map: collection missing trailing / - changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45f1874f..2f3351f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 3.7.8.dev * Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions +* Fix: sharing/by-map: catch collection path without trailing / (supporting "pimsync") ## 3.7.7 * Fix: web plugin helpers httputils.serve_resource/serve_folder ignored their mimetypes and fallback_mimetype parameters and always used the built-in mapping, so custom web plugins could not serve additional file types with a correct Content-Type