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
diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py
index 3cf8bdfd..88874d31 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(
@@ -646,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:
@@ -666,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")
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("/"), """\
+