diff --git a/radicale/app/get.py b/radicale/app/get.py index 9accb8ce..b1e46540 100644 --- a/radicale/app/get.py +++ b/radicale/app/get.py @@ -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: diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 66edf99a..90c89a83 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -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'])) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 068a336b..8ed49526 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -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: # PathOrToken: [PrimaryKey] # PathMapped: @@ -48,6 +48,10 @@ DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Owner' # TimestampCreated: (when created) # TimestampUpdated: (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) diff --git a/radicale/sharing/csv.py b/radicale/sharing/csv.py index 05d2d9ae..adb1ca1e 100644 --- a/radicale/sharing/csv.py +++ b/radicale/sharing/csv.py @@ -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 diff --git a/radicale/sharing/files.py b/radicale/sharing/files.py index a9c1413f..50fee2e7 100644 --- a/radicale/sharing/files.py +++ b/radicale/sharing/files.py @@ -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