sharing: extend DB with Conversion and Actions(reserved)

This commit is contained in:
Peter Bieringer
2026-03-22 21:32:12 +01:00
parent 62a8189917
commit f6d0b0b07c
5 changed files with 122 additions and 33 deletions

View File

@@ -31,7 +31,7 @@ from radicale.log import logger
def propose_filename(collection: storage.BaseCollection, share: Union[dict, None] = None) -> str:
"""Propose a filename for a collection."""
share_bday_automap = False
if share and share['ShareType'] == "bday":
if share and share['Conversion'] == "bday":
share_bday_automap = True
if collection.tag == "VADDRESSBOOK" and not share_bday_automap:
fallback_title = "Address book"
@@ -112,7 +112,7 @@ class ApplicationPartGet(ApplicationBase):
if not item.tag:
return (httputils.NOT_ALLOWED if limited_access else
httputils.DIRECTORY_LISTING)
if share and share['ShareType'] == "bday":
if share and share['Conversion'] == "bday":
content_type = xmlutils.MIMETYPES["VCALENDAR"]
else:
content_type = xmlutils.MIMETYPES[item.tag]
@@ -130,7 +130,7 @@ class ApplicationPartGet(ApplicationBase):
"ETag": item.etag}
if content_disposition:
headers["Content-Disposition"] = content_disposition
if isinstance(item, storage.BaseCollection) and self._sharing._enabled and share and share['ShareType'] == "bday":
if isinstance(item, storage.BaseCollection) and share and share['Conversion'] == "bday":
# convert VCF to ICS
answer = item.serialize(vcf_to_ics=True)
else:

View File

@@ -124,7 +124,7 @@ def xml_propfind_response(
break
share_bday_automap = False
if share and share['ShareType'] == "bday":
if share and share['Conversion'] == "bday":
share_bday_automap = True
if share:
@@ -546,7 +546,7 @@ class ApplicationPartPropfind(ApplicationBase):
items_iter = itertools.chain([item], items_iter)
for item, permission in list(self._collect_allowed_items(items_iter, user)):
if self._sharing._enabled and share:
if share['ShareType'] == "bday" and not isinstance(item, storage.BaseCollection):
if share['Conversion'] == "bday" and not isinstance(item, storage.BaseCollection):
if not item.convert_vcf_to_ics():
continue
allowed_items.append((item, permission, share['ShareType']))

View File

@@ -34,7 +34,7 @@ from radicale.log import logger
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Owner', 'User', 'Permissions', 'EnabledByOwner', 'EnabledByUser', 'HiddenByOwner', 'HiddenByUser', 'TimestampCreated', 'TimestampUpdated', 'Properties')
DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Owner', 'User', 'Permissions', 'EnabledByOwner', 'EnabledByUser', 'HiddenByOwner', 'HiddenByUser', 'TimestampCreated', 'TimestampUpdated', 'Properties', 'Conversion', 'Actions')
# ShareType: <token|map>
# PathOrToken: <path|token> [PrimaryKey]
# PathMapped: <path>
@@ -48,6 +48,10 @@ DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Owner'
# TimestampCreated: <unixtime> (when created)
# TimestampUpdated: <unixtime> (last update)
# Properties: Overlay of collection properties in JSON
# Conversion: None|bday
# bday: check VCARD(vcf) for BDAY and convert to reoccuring VEVENT(ics)
# Actions: Actions structure in JSON
# (future reserved for e.g. "filter", "filter_pre", "filter_post" or anything else, implemented on request)
DB_TYPES_V1: dict[str, type] = {
"ShareType": str,
@@ -62,7 +66,9 @@ DB_TYPES_V1: dict[str, type] = {
"HiddenByUser": bool,
"TimestampCreated": int,
"TimestampUpdated": int,
"Properties": dict
"Properties": dict,
"Conversion": str,
"Actions": dict,
}
DB_FIELDS_V1_USER_PERMITTED: Sequence[str] = ('EnabledByUser', 'HiddenByUser', 'Properties')
@@ -106,7 +112,11 @@ API_TYPES_V1: dict[str, type] = {
"Permissions": str,
"Enabled": bool,
"Hidden": bool,
"Properties": dict}
"Properties": dict,
"Conversion": str,
"Actions": dict,
}
TOKEN_PATTERN_V1: str = "v1/[a-zA-Z0-9_\\-]{44}"
@@ -116,6 +126,8 @@ USER_PATTERN: str = "([a-zA-Z0-9@]+)" # TODO: extend or find better source
OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname")
CONVERSIONS_WHITELIST: Sequence[str] = ("bday")
def load(configuration: "config.Configuration") -> "BaseSharing":
"""Load the sharing database module chosen in configuration."""
@@ -216,9 +228,11 @@ class BaseSharing:
PathMapped: Union[str, None] = None,
User: Union[str, None] = None,
EnabledByOwner: Union[bool, None] = None,
EnabledByUser: Union[bool, None] = None,
HiddenByOwner: Union[bool, None] = None,
HiddenByUser: Union[bool, None] = None) -> list[dict]:
EnabledByUser: Union[bool, None] = None,
HiddenByOwner: Union[bool, None] = None,
HiddenByUser: Union[bool, None] = None,
Conversion: Union[str, None] = None,
) -> list[dict]:
""" retrieve sharing """
return []
@@ -238,7 +252,10 @@ class BaseSharing:
EnabledByOwner: bool = False, EnabledByUser: bool = False,
HiddenByOwner: bool = True, HiddenByUser: bool = True,
Timestamp: int = 0,
Properties: Union[dict, None] = None) -> dict:
Properties: Union[dict, None] = None,
Conversion: Union[str, None] = None,
Actions: Union[dict, None] = None,
) -> dict:
""" create sharing """
return {"status": "not-implemented"}
@@ -254,7 +271,10 @@ class BaseSharing:
HiddenByOwner: Union[bool, None] = None,
HiddenByUser: Union[bool, None] = None,
Timestamp: int = 0,
Properties: Union[dict, None] = None) -> dict:
Properties: Union[dict, None] = None,
Conversion: Union[str, None] = None,
Actions: Union[dict, None] = None,
) -> dict:
""" update sharing """
return {"status": "not-implemented"}
@@ -446,7 +466,7 @@ class BaseSharing:
ShareType="token",
PathOrToken=match[1])
if result is not None:
logger.info("Sharing/%s: resolved %r->%r, user ->%r, permissions %r", "token", path, result['PathMapped'], result['Owner'], result['Permissions'])
logger.info("Sharing/%s: resolved %r->%r, User=%r, Permissions=%r Conversion=%r", "token", path, result['PathMapped'], result['Owner'], result['Permissions'], result['Conversion'])
return result
else:
if logger.isEnabledFor(logging.DEBUG):
@@ -487,7 +507,7 @@ class BaseSharing:
logger.debug("TRACE/sharing/map: not found")
return None
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r, permissions %r", "map", path, result['PathMapped'], user, result['Owner'], result['Permissions'])
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r", "map", path, result['PathMapped'], user, result['Owner'], result['Permissions'], result['Conversion'])
return result
else:
if logger.isEnabledFor(logging.DEBUG):
@@ -524,7 +544,9 @@ class BaseSharing:
logger.debug("TRACE/sharing/bday: not found")
return None
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r, permissions %r", "bday", path, result['PathMapped'], user, result['Owner'], result['Permissions'])
if not result['Conversion']:
result['Conversion'] = "bday"
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r", "bday", path, result['PathMapped'], user, result['Owner'], result['Permissions'], result['Conversion'])
return result
else:
if logger.isEnabledFor(logging.DEBUG):
@@ -738,12 +760,14 @@ class BaseSharing:
# parameters default
PathOrToken: Union[str, None] = None
PathMapped: Union[str, None] = None
User: Union[str, None] = None
PathMapped: Union[str, None] = None
User: Union[str, None] = None
Permissions: Union[str, None] = None # no permissions by default
Enabled: Union[bool, None] = None
Hidden: Union[bool, None] = None
Properties: Union[dict, None] = None
Enabled: Union[bool, None] = None
Hidden: Union[bool, None] = None
Properties: Union[dict, None] = None
Conversion: Union[str, None] = None
Actions: Union[dict, None] = None # reserved so far
# parameters sanity check
for key in request_data:
@@ -805,6 +829,16 @@ class BaseSharing:
return httputils.bad_request("Property not supported to overlay: %r" % entry)
Properties = request_data['Properties']
if 'Conversion' in request_data:
# verify against whitelist
for entry in request_data['Conversion']:
if entry not in CONVERSIONS_WHITELIST:
return httputils.bad_request("Conversion not supported: %r" % entry)
Conversion = request_data['Conversion']
if 'Actions' in request_data:
return httputils.bad_request("Actions currently not supported (reserved for future needs)")
if 'Enabled' in request_data:
Enabled = request_data['Enabled']
else:
@@ -946,7 +980,10 @@ class BaseSharing:
HiddenByOwner=Hidden,
HiddenByUser=HiddenByUser,
Timestamp=Timestamp,
Properties=Properties)
Properties=Properties,
Conversion=Conversion,
Actions=Actions,
)
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/" + api_info + ": result=%r", result)
@@ -992,6 +1029,7 @@ class BaseSharing:
return httputils.NOT_ALLOWED
elif ShareType == "bday":
Conversion = "bday"
if self.permit_create_bday is False:
if "b" not in access.permissions:
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=False but explicit grant misses 'b')", PathMapped, user)
@@ -1030,7 +1068,10 @@ class BaseSharing:
HiddenByOwner=Hidden,
HiddenByUser=HiddenByUser,
Timestamp=Timestamp,
Properties=Properties)
Properties=Properties,
Conversion=Conversion,
Actions=Actions,
)
else:
logger.warning(api_info + ": unsupported for ShareType=%r", ShareType)

View File

@@ -131,8 +131,14 @@ class Sharing(sharing.BaseSharing):
Permissions = row['Permissions']
Hidden: bool = (row['HiddenByOwner'] or row['HiddenByUser'])
Properties: Union[dict, None] = None
Conversion: Union[str, None] = None
Actions: Union[dict, None] = None
if 'Properties' in row:
Properties = row['Properties']
if 'Conversion' in row:
Conversion = row['Conversion']
if 'Actions' in row:
Actions = row['Actions']
return {
"mapped": True,
"ShareType": ShareType,
@@ -142,7 +148,10 @@ class Sharing(sharing.BaseSharing):
"User": UserShare,
"Hidden": Hidden,
"Permissions": Permissions,
"Properties": Properties}
"Properties": Properties,
"Conversion": Conversion,
"Actions": Actions,
}
return None
def database_list_sharing(self,
@@ -154,7 +163,9 @@ class Sharing(sharing.BaseSharing):
EnabledByOwner: Union[bool, None] = None,
EnabledByUser: Union[bool, None] = None,
HiddenByOwner: Union[bool, None] = None,
HiddenByUser: Union[bool, None] = None) -> list[dict]:
HiddenByUser: Union[bool, None] = None,
Conversion: Union[str, None] = None,
) -> list[dict]:
""" retrieve sharing """
row: dict
index = 0
@@ -212,7 +223,10 @@ class Sharing(sharing.BaseSharing):
EnabledByOwner: bool = False, EnabledByUser: bool = False,
HiddenByOwner: bool = True, HiddenByUser: bool = True,
Timestamp: int = 0,
Properties: Union[dict, None] = None) -> dict:
Properties: Union[dict, None] = None,
Conversion: Union[str, None] = None,
Actions: Union[dict, None] = None,
) -> dict:
""" create sharing """
row: dict
@@ -267,7 +281,10 @@ class Sharing(sharing.BaseSharing):
"HiddenByUser": HiddenByUser,
"TimestampCreated": Timestamp,
"TimestampUpdated": Timestamp,
"Properties": Properties}
"Properties": Properties,
"Conversion": Conversion,
"Actions": Actions,
}
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/sharing/*/create: add row: %r", row)
@@ -293,7 +310,10 @@ class Sharing(sharing.BaseSharing):
HiddenByOwner: Union[bool, None] = None,
HiddenByUser: Union[bool, None] = None,
Timestamp: int = 0,
Properties: Union[dict, None] = None) -> dict:
Properties: Union[dict, None] = None,
Conversion: Union[str, None] = None,
Actions: Union[dict, None] = None,
) -> dict:
""" update sharing """
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/sharing/%s/update: PathOrToken=%r OwnerOrUser=%r PathMapped=%r Properties=%r EnabledByOwner=%s EnabledByUser=%s HiddenByOwner=%s HiddenByUser=%s", ShareType, PathOrToken, OwnerOrUser, PathMapped, Properties, EnabledByOwner, EnabledByUser, HiddenByOwner, HiddenByUser)
@@ -336,6 +356,10 @@ class Sharing(sharing.BaseSharing):
self._sharing_cache[index]["HiddenByUser"] = HiddenByUser
if Properties is not None:
self._sharing_cache[index]["Properties"] = Properties
if Conversion is not None:
self._sharing_cache[index]["Conversion"] = Conversion
if Actions is not None:
self._sharing_cache[index]["Actions"] = Actions
# update timestamp
self._sharing_cache[index]["TimestampUpdated"] = Timestamp

View File

@@ -124,8 +124,14 @@ class Sharing(sharing.BaseSharing):
Permissions = row['Permissions']
Hidden: bool = (row['HiddenByOwner'] or row['HiddenByUser'])
Properties: Union[dict, None] = None
Conversion: Union[str, None] = None
Actions: Union[dict, None] = None
if 'Properties' in row:
Properties = row['Properties']
if 'Conversion' in row:
Conversion = row['Conversion']
if 'Actions' in row:
Actions = row['Actions']
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/sharing: map %r to %r (Owner=%r User=%r Permissions=%r Hidden=%s Properties=%r)", PathOrToken, PathMapped, Owner, UserShare, Permissions, Hidden, Properties)
return {
@@ -137,7 +143,10 @@ class Sharing(sharing.BaseSharing):
"User": UserShare,
"Hidden": Hidden,
"Permissions": Permissions,
"Properties": Properties}
"Properties": Properties,
"Conversion": Conversion,
"Actions": Actions,
}
return None
@@ -150,7 +159,9 @@ class Sharing(sharing.BaseSharing):
EnabledByOwner: Union[bool, None] = None,
EnabledByUser: Union[bool, None] = None,
HiddenByOwner: Union[bool, None] = None,
HiddenByUser: Union[bool, None] = None) -> list[dict]:
HiddenByUser: Union[bool, None] = None,
Conversion: Union[str, None] = None,
) -> list[dict]:
""" retrieve sharing """
result = []
@@ -221,7 +232,10 @@ class Sharing(sharing.BaseSharing):
EnabledByOwner: bool = False, EnabledByUser: bool = False,
HiddenByOwner: bool = True, HiddenByUser: bool = True,
Timestamp: int = 0,
Properties: Union[dict, None] = None) -> dict:
Properties: Union[dict, None] = None,
Conversion: Union[str, None] = None,
Actions: Union[dict, None] = None,
) -> dict:
""" create sharing """
row: dict
@@ -245,7 +259,10 @@ class Sharing(sharing.BaseSharing):
"HiddenByUser": HiddenByUser,
"TimestampCreated": Timestamp,
"TimestampUpdated": Timestamp,
"Properties": Properties}
"Properties": Properties,
"Conversion": Conversion,
"Actions": Actions,
}
version = DB_VERSION
@@ -275,7 +292,10 @@ class Sharing(sharing.BaseSharing):
HiddenByOwner: Union[bool, None] = None,
HiddenByUser: Union[bool, None] = None,
Timestamp: int = 0,
Properties: Union[dict, None] = None) -> dict:
Properties: Union[dict, None] = None,
Conversion: Union[str, None] = None,
Actions: Union[dict, None] = None,
) -> dict:
""" update sharing """
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/sharing/%s/update: PathOrToken=%r OwnerOrUser=%r User=%r Properties=%r", ShareType, PathOrToken, OwnerOrUser, User, Properties)
@@ -316,6 +336,10 @@ class Sharing(sharing.BaseSharing):
row["HiddenByUser"] = HiddenByUser
if Properties is not None:
row["Properties"] = Properties
if Conversion is not None:
row["Conversion"] = Conversion
if Actions is not None:
row["Actions"] = Actions
# update timestamp
row["TimestampUpdated"] = Timestamp