From 6a162e3882b86532ee076a4b29b0b6d1dee5d0d6 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Tue, 24 Mar 2026 12:23:48 +0100 Subject: [PATCH] sharing: replace type bday by conversion --- radicale/sharing/__init__.py | 275 +++++++++++++++++------------------ 1 file changed, 134 insertions(+), 141 deletions(-) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 8ed49526..25db31d8 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -19,6 +19,7 @@ import base64 import io import json import logging +import os # TODO: remove/3.7.0-final import re import socket import uuid @@ -34,7 +35,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', 'Conversion', 'Actions') +DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Conversion', 'Owner', 'User', 'Permissions', 'EnabledByOwner', 'EnabledByUser', 'HiddenByOwner', 'HiddenByUser', 'TimestampCreated', 'TimestampUpdated', 'Properties', 'Actions') # ShareType: # PathOrToken: [PrimaryKey] # PathMapped: @@ -48,8 +49,8 @@ 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) +# Conversion: none|bday +# bday: check VADDRESSBOOK VCARD(vcf) entries for BDAY and convert to VCALENDAR reoccuring VEVENT(ics) # Actions: Actions structure in JSON # (future reserved for e.g. "filter", "filter_pre", "filter_post" or anything else, implemented on request) @@ -73,14 +74,17 @@ DB_TYPES_V1: dict[str, type] = { DB_FIELDS_V1_USER_PERMITTED: Sequence[str] = ('EnabledByUser', 'HiddenByUser', 'Properties') -SHARE_TYPES: Sequence[str] = ('token', 'map', 'bday', 'all') - -SHARE_TYPES_V1: Sequence[str] = ('token', 'map', 'bday') +SHARE_TYPES: Sequence[str] = ('token', 'map', 'all') # token: share by secret token (does not require authentication) # map : share by mapping collection of one user to another as virtual -# bday : share by mapping addressbook-collection of one user to another as virtual calendar-collection # all : only supported for "list" and "info" +SHARE_TYPES_V1: Sequence[str] = ('token', 'map') + +if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final + SHARE_TYPES: Sequence[str] = ('token', 'map', 'bday', 'all') # type: ignore[no-redef] # TODO: remove/3.7.0-final + SHARE_TYPES_V1: Sequence[str] = ('token', 'map', 'bday') # type: ignore[no-redef] # TODO: remove/3.7.0-final + API_HOOKS_V1: Sequence[str] = ('list', 'create', 'delete', 'update', 'hide', 'unhide', 'enable', 'disable', 'info') # list : list sharings (optional filtered) # create : create share by token or map @@ -115,6 +119,7 @@ API_TYPES_V1: dict[str, type] = { "Properties": dict, "Conversion": str, "Actions": dict, + "SupportedConversions": list, } @@ -126,7 +131,7 @@ 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") +CONVERSIONS_WHITELIST: Sequence[str] = ("bday", "none") def load(configuration: "config.Configuration") -> "BaseSharing": @@ -157,20 +162,21 @@ class BaseSharing: # Sharing self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map") self.sharing_collection_by_token = configuration.get("sharing", "collection_by_token") - self.sharing_collection_by_bday = configuration.get("sharing", "collection_by_bday") + if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final + self.sharing_collection_by_map = self.sharing_collection_by_map or configuration.get("sharing", "collection_by_bday") # TODO: remove/3.7.0-final self.permit_create_token = configuration.get("sharing", "permit_create_token") self.permit_create_map = configuration.get("sharing", "permit_create_map") - self.permit_create_bday = configuration.get("sharing", "permit_create_bday") + if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final + self.permit_create_map = self.permit_create_map or configuration.get("sharing", "permit_create_bday") # TODO: remove/3.7.0-final self.default_permissions_create_token = configuration.get("sharing", "default_permissions_create_token") self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map") self.permit_properties_overlay = configuration.get("sharing", "permit_properties_overlay") self.enforce_properties_overlay = configuration.get("sharing", "enforce_properties_overlay") + logger.info("sharing.collection_by_map : %s", self.sharing_collection_by_map) logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token) - logger.info("sharing.collection_by_bday : %s", self.sharing_collection_by_bday) logger.info("sharing.permit_create_token: %s", self.permit_create_token) logger.info("sharing.permit_create_map : %s", self.permit_create_map) - logger.info("sharing.permit_create_bday : %s", self.permit_create_bday) logger.info("sharing.default_permissions_create_token: %r", self.default_permissions_create_token) logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map) logger.info("sharing.permit_properties_overlay: %s", self.permit_properties_overlay) @@ -180,7 +186,7 @@ class BaseSharing: self.sharing_db_type = configuration.get("sharing", "type") logger.info("sharing.database_type: %s", self.sharing_db_type) - if ((self.sharing_collection_by_map is False) and (self.sharing_collection_by_token is False) and (self.sharing_collection_by_bday is False)): + if ((self.sharing_collection_by_map is False) and (self.sharing_collection_by_token is False)): logger.info("sharing disabled as no feature is enabled") self._enabled = False return @@ -195,7 +201,7 @@ class BaseSharing: """ try: if self.database_init() is False: - logger.info("sharing disabled as no database is active") + logger.warning("sharing disabled as no database is active") self._enabled = False return False except Exception as e: @@ -247,13 +253,13 @@ class BaseSharing: def database_create_sharing(self, ShareType: str, PathOrToken: str, PathMapped: str, + Conversion: str, Owner: str, User: str, Permissions: str = "r", EnabledByOwner: bool = False, EnabledByUser: bool = False, HiddenByOwner: bool = True, HiddenByUser: bool = True, Timestamp: int = 0, Properties: Union[dict, None] = None, - Conversion: Union[str, None] = None, Actions: Union[dict, None] = None, ) -> dict: """ create sharing """ @@ -350,7 +356,12 @@ class BaseSharing: # *** sharing functions called by request methods *** # list sharings - def sharing_collection_list(self, User: Union[str, None] = None, Enabled: Union[bool, None] = None, Hidden: Union[bool, None] = None) -> list[dict]: + def sharing_collection_list(self, + User: Union[str, None] = None, + Enabled: Union[bool, None] = None, + Hidden: Union[bool, None] = None, + Conversion: Union[str, None] = None, + ) -> list[dict]: """ returning dict with shared collections by filter(User/Enabled/Hidden) or None if not found""" sharing_collection_list = [] @@ -366,27 +377,16 @@ class BaseSharing: EnabledByOwner=Enabled, EnabledByUser=Enabled, HiddenByOwner=Hidden, - HiddenByUser=Hidden) - - if not self.sharing_collection_by_bday: - if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/bday: not active") - else: - # retrieve collections depending on filter - sharing_collection_list += self.database_list_sharing( - ShareType="bday", - OwnerOrUser=User, - User=User, - EnabledByOwner=Enabled, - EnabledByUser=Enabled, - HiddenByOwner=Hidden, - HiddenByUser=Hidden) + HiddenByUser=Hidden, + Conversion=Conversion, + ) return sharing_collection_list # resolves a path to a share def sharing_collection_resolver(self, path: str, user: str) -> Union[dict, None]: """ returning dict with PathMapped, Owner, Permissions or None if not found""" + logger.debug("TRACE/sharing/resolver: lookup path=%r user=%r", path, user) share = None if path == "/": @@ -396,6 +396,8 @@ class BaseSharing: if self.sharing_collection_by_token: if share is None: share = self.sharing_collection_by_token_resolver(path) + if share is not None and 'error' in share: + return None else: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/token: not active") @@ -403,17 +405,12 @@ class BaseSharing: if self.sharing_collection_by_map: if share is None: share = self.sharing_collection_by_map_resolver(path, user) + if share is not None and 'error' in share: + return None else: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/map: not active") - if self.sharing_collection_by_bday: - if share is None: - share = self.sharing_collection_by_bday_resolver(path, user) - else: - if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/bday: not active") - if share is not None: if self.permit_properties_overlay: if share['Permissions'] and "p" not in share['Permissions']: @@ -446,31 +443,44 @@ class BaseSharing: # *** internal sharing functions *** # resolves a token "path" to a share + # dict: share + # None: not supported + # False: supported but not found def sharing_collection_by_token_resolver(self, path) -> Union[dict, None]: """ returning dict with PathMapped, Owner, Permissions or None if invalid""" if self.sharing_collection_by_token: if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/token: check path: %r", path) + logger.debug("TRACE/sharing/token/resolver: check path: %r", path) if path.startswith("/.token/"): pattern = re.compile('^(/\\.token/' + TOKEN_PATTERN_V1 + '/)$') match = pattern.match(path) if not match: if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/token: unsupported token: %r", path) - return None + logger.debug("TRACE/sharing/token/resolver: unsupported token: %r", path) + return {'error': 'token-not-supported'} else: # TODO add token validity checks if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/token: supported token found in path: %r (token=%r)", path, match[1]) + logger.debug("TRACE/sharing/token/resolver: supported token: %r", path) result = self.database_get_sharing( ShareType="token", + OnlyEnabled=False, PathOrToken=match[1]) - if result is not None: - logger.info("Sharing/%s: resolved %r->%r, User=%r, Permissions=%r Conversion=%r", "token", path, result['PathMapped'], result['Owner'], result['Permissions'], result['Conversion']) + + if result is None: + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/sharing/token/resolver: supported token not found: %r", path) + return {'error': 'token-not-found'} + + if result['EnabledByOwner'] is not True: + logger.info("Sharing/%s: resolved path %r->%r, User=%r not enabled by owner", "token", path, result['PathMapped'], result['Owner']) + return {'error': 'token-not-enabled'} + + 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): - logger.debug("TRACE/sharing/token: no supported prefix found in path: %r", path) + logger.debug("TRACE/sharing/token/resolver: no supported prefix found in path: %r", path) return None else: if logger.isEnabledFor(logging.DEBUG): @@ -483,13 +493,14 @@ class BaseSharing: if self.sharing_collection_by_map: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/map/resolver: check path: %r", path) + result = self.database_get_sharing( - ShareType="map", - PathOrToken=path, - User=user) - if result: - pass - else: + ShareType="map", + PathOrToken=path, + OnlyEnabled=False, + User=user) + + if not result: # fallback to parent path parent_path = pathutils.parent_path(path) if logger.isEnabledFor(logging.DEBUG): @@ -497,6 +508,7 @@ class BaseSharing: result = self.database_get_sharing( ShareType="map", PathOrToken=parent_path, + OnlyEnabled=False, User=user) if result: result['PathMapped'] = path.replace(parent_path, result['PathMapped']) @@ -504,55 +516,26 @@ class BaseSharing: logger.debug("TRACE/sharing/map/resolver: PathMapped=%r Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], parent_path) else: if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/map: not found") + logger.debug("TRACE/sharing/map/resolver: not found") return None - 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 + if result: + if result['EnabledByOwner'] is not True: + logger.info("Sharing/%s: resolved path %r->%r, user %r->%r not enabled by owner", "map", path, result['PathMapped'], user, result['Owner']) + return {'error': 'map-not-enabled'} + if result['EnabledByUser'] is not True: + logger.info("Sharing/%s: resolved path %r->%r, user %r->%r not enabled by user", "map", path, result['PathMapped'], user, result['Owner']) + return {'error': 'map-not-enabled'} + + 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 + + return None else: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/map: not active") return None - # resolves a bday "path" to a share - def sharing_collection_by_bday_resolver(self, path: str, user: str) -> Union[dict, None]: - """ returning dict with PathMapped, Owner, Permissions or None if invalid""" - if self.sharing_collection_by_bday: - if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/bday/resolver: check path: %r", path) - result = self.database_get_sharing( - ShareType="bday", - PathOrToken=path, - User=user) - if result: - pass - else: - # fallback to parent path - parent_path = pathutils.parent_path(path) - if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/bday/resolver: check parent path: %r", parent_path) - result = self.database_get_sharing( - ShareType="bday", - PathOrToken=parent_path, - User=user) - if result: - result['PathMapped'] = path.replace(parent_path, result['PathMapped']) - if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/bday/resolver: PathMapped=%r Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], parent_path) - else: - if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/sharing/bday: not found") - return None - - 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): - logger.debug("TRACE/sharing/bday: not active") - return None - # *** POST API *** def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str) -> types.WSGIResponse: # Late import to avoid circular dependency in config @@ -567,27 +550,28 @@ class BaseSharing: ``user`` is empty for anonymous users. Request: - action: (token|map|bday)/list + action: (token|map)/list PathOrToken: (optional for filter) - action: (token|map|bday)/create + action: (token|map)/create PathMapped: (mandatory) Permissions: (default: r) token -> returns - map|bday + map PathOrToken: (mandatory) User: (mandatory) + Conversion: None|bday (optional) - action: (token|map|bday)/update + action: (token|map)/update - action: (token|map|bday)/(delete|disable|enable|hide|unhide) + action: (token|map)/(delete|disable|enable|hide|unhide) PathOrToken: (mandatory) token - map|bday + map PathMapped: (mandatory) User: @@ -648,11 +632,6 @@ class BaseSharing: logger.warning(api_info + ": not enabled by config (collection_by_map)") return httputils.NOT_FOUND - if not self.sharing_collection_by_bday and ShareType == "bday": - # API "token" is not enabled - logger.warning(api_info + ": not enabled by config (collection_by_bday)") - return httputils.NOT_FOUND - # check for valid API hooks if action not in API_HOOKS_V1: if logger.isEnabledFor(logging.DEBUG): @@ -830,11 +809,10 @@ class BaseSharing: 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'] + # verify against whitelist + if Conversion not in CONVERSIONS_WHITELIST: + return httputils.bad_request("Conversion not supported: %r" % Conversion) if 'Actions' in request_data: return httputils.bad_request("Actions currently not supported (reserved for future needs)") @@ -860,7 +838,12 @@ class BaseSharing: answer['ApiVersion'] = 1 Timestamp = int((datetime.now() - datetime(1970, 1, 1)).total_seconds()) - if not self.sharing_collection_by_map and not self.sharing_collection_by_token and not self.sharing_collection_by_bday: + if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final + if ShareType == "bday": # TODO: remove/3.7.0-final + ShareType = "map" # TODO: remove/3.7.0-final + Conversion = "bday" # TODO: remove/3.7.0-final + + if not self.sharing_collection_by_map and not self.sharing_collection_by_token: if not action == 'info': # API is not enabled logger.warning(api_info + ": API is not enabled") @@ -880,12 +863,16 @@ class BaseSharing: ShareType=ShareType, OwnerOrUser=user, PathMapped=PathMapped, - PathOrToken=PathOrToken) + PathOrToken=PathOrToken, + Conversion=Conversion, + ) else: result_array = self.database_list_sharing( OwnerOrUser=user, PathMapped=PathMapped, - PathOrToken=PathOrToken) + PathOrToken=PathOrToken, + Conversion=Conversion, + ) answer['Lines'] = len(result_array) if len(result_array) == 0: @@ -905,6 +892,9 @@ class BaseSharing: logger.warning(api_info + ": missing PathMapped") return httputils.bad_request("Missing PathMapped") + if Conversion is None: + Conversion = "none" + # check whether collection exists with self._storage.acquire_lock("r", user, path=PathMapped): item = next(iter(self._storage.discover(PathMapped)), None) @@ -912,7 +902,12 @@ class BaseSharing: logger.warning(api_info + ": cannot find PathMapped=%r", PathMapped) return httputils.NOT_FOUND if not isinstance(item, storage.BaseCollection): + logger.warning(api_info + ": PathMapped=%r is not a collection", PathMapped) return httputils.METHOD_NOT_ALLOWED + if Conversion == "bday": + if item.tag != "VADDRESSBOOK": + logger.warning(api_info + ": PathMapped=%r is not a VADDRESSBOOK collection (mandatory for Conversion=%r)", PathMapped, Conversion) + return httputils.METHOD_NOT_ALLOWED if Permissions is None: if ShareType == "token": @@ -924,6 +919,12 @@ class BaseSharing: Permissions = "r" else: Permissions = str(Permissions) + if Conversion == "bday": + # bday is read-only + for permission in Permissions: + if permission not in "r": + logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion) + return httputils.METHOD_NOT_ALLOWED if Enabled is None: Enabled = False # security by default @@ -988,7 +989,7 @@ class BaseSharing: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/" + api_info + ": result=%r", result) - elif ShareType in ["map", "bday"]: + elif ShareType == "map": # check preconditions if PathOrToken is None: return httputils.bad_request("Missing PathOrToken") @@ -1006,10 +1007,10 @@ class BaseSharing: else: User = str(User) - # lookup existing shares with requested PathMapped for same User - shares = self.database_list_sharing(ShareType=ShareType, PathMapped=PathMapped, User=User) + # lookup existing shares with requested PathMapped for same User and same Conversion + shares = self.database_list_sharing(ShareType=ShareType, PathMapped=PathMapped, User=User, Conversion=Conversion) if len(shares) > 0: - logger.warning(api_info + ": share already exists with PathMapped=%r User=%r", PathMapped, User) + logger.warning(api_info + ": share already exists with PathMapped=%r User=%r Conversion=%r", PathMapped, User, Conversion) return httputils.CONFLICT # check access Permissions @@ -1018,26 +1019,14 @@ class BaseSharing: logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r", PathMapped, user) return httputils.NOT_ALLOWED - if ShareType == "map": - if self.permit_create_map is False: - if "m" not in access.permissions: - logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=False but explicit grant misses 'm')", PathMapped, user) - return httputils.NOT_ALLOWED - else: - if "M" in access.permissions: - logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=True but denied by 'M')", PathMapped, user) - 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) - return httputils.NOT_ALLOWED - else: - if "B" in access.permissions: - logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=True but denied by 'B')", PathMapped, user) - return httputils.NOT_ALLOWED + if self.permit_create_map is False: + if "m" not in access.permissions: + logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=False but explicit grant misses 'm')", PathMapped, user) + return httputils.NOT_ALLOWED + else: + if "M" in access.permissions: + logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=True but denied by 'M')", PathMapped, user) + return httputils.NOT_ALLOWED access = Access(self._rights, User, PathOrToken) if not access.check("r"): @@ -1100,7 +1089,7 @@ class BaseSharing: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/" + api_info + ": start") - if ShareType not in ["token", "map", "bday"]: + if ShareType not in SHARE_TYPES_V1: logger.warning(api_info + ": unsupported for ShareType=%r", ShareType) return httputils.bad_request("Invalid share type") @@ -1226,7 +1215,7 @@ class BaseSharing: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/" + api_info + ": start") - if ShareType not in ["token", "map", "bday"]: + if ShareType not in SHARE_TYPES_V1: logger.warning(api_info + ": unsupported for ShareType=%r", ShareType) return httputils.bad_request("Invalid share type") @@ -1268,19 +1257,21 @@ class BaseSharing: if ShareType in ["all", "map"]: answer['FeatureEnabledCollectionByMap'] = self.sharing_collection_by_map answer['PermittedCreateCollectionByMap'] = self.permit_create_map + if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final + answer['FeatureEnabledCollectionByBday'] = self.sharing_collection_by_map # TODO: remove/3.7.0-final + answer['PermittedCreateCollectionByBday'] = self.permit_create_map # TODO: remove/3.7.0-final if ShareType in ["all", "token"]: answer['FeatureEnabledCollectionByToken'] = self.sharing_collection_by_token answer['PermittedCreateCollectionByToken'] = self.permit_create_token - if ShareType in ["all", "bday"]: - answer['FeatureEnabledCollectionByBday'] = self.sharing_collection_by_bday - answer['PermittedCreateCollectionByBday'] = self.permit_create_bday + if ShareType in ["all", "map", "token"]: + answer['SupportedConversions'] = CONVERSIONS_WHITELIST # action: TOGGLE elif action in API_SHARE_TOGGLES_V1: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/API/POST/" + action) - if ShareType not in ["token", "map", "bday"]: + if ShareType not in SHARE_TYPES_V1: logger.warning(api_info + ": unsupported for ShareType=%r", ShareType) return httputils.bad_request("Invalid share type") @@ -1365,6 +1356,8 @@ class BaseSharing: if key != 'Content': if API_TYPES_V1[key] is bool or API_TYPES_V1[key] is int: answer_array.append(key + '=' + str(answer[key])) + elif API_TYPES_V1[key] is list: + answer_array.append(key + '=(' + str(" ".join(answer[key])) + ')') else: answer_array.append(key + "='" + str(answer[key]) + "'") if 'Content' in answer and answer['Content'] is not None: