From 07c5873bdd350d1c575295adfcdb793e1f140272 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 31 May 2026 07:06:35 +0200 Subject: [PATCH] sharing/bday/templating: default config rework --- radicale/config.py | 8 ++--- radicale/item/__init__.py | 57 ++++++++++++++++++++-------------- radicale/sharing/__init__.py | 32 +++++++++++++------ radicale/tests/test_sharing.py | 12 +++---- 4 files changed, 67 insertions(+), 42 deletions(-) diff --git a/radicale/config.py b/radicale/config.py index 27bba0d2..a1688305 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -595,11 +595,11 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "help": "default permissions for map-based sharing", "type": rights_permission}), ("conversion_bday_summary_template", { - "value": "[{n:f} {n:g} {n:a}|{fn}|{nickname}] (BDAY)", + "value": sharing.SHARING_BDAY_SUMMARY_TEMPLATE_DEFAULT, "help": "conversion bday summary template", "type": sharing.check_template}), ("conversion_bday_description_template", { - "value": "BDAY={year}-{month}-{day}", + "value": sharing.SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT, "help": "conversion bday description template", "type": sharing.check_template}), ("conversion_bday_alarm_trigger_template", { @@ -607,11 +607,11 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "help": "conversion bday alarm trigger template", "type": sharing.check_template_alarm_trigger}), ("conversion_bday_categories", { - "value": "Birthday", + "value": sharing.SHARING_BDAY_CATEGORIES_DEFAULT, "help": "conversion bday categories", "type": str}), ("conversion_bday_age_max", { - "value": "99", + "value": str(sharing.SHARING_BDAY_AGE_MAX_DEFAULT), "help": "conversion bday age max", "type": sharing.check_bday_max_age}), ])), diff --git a/radicale/item/__init__.py b/radicale/item/__init__.py index 3e0fb599..07e0f7b3 100644 --- a/radicale/item/__init__.py +++ b/radicale/item/__init__.py @@ -38,7 +38,7 @@ from typing import (Any, Callable, List, MutableMapping, Optional, Sequence, import vobject from radicale import storage # noqa:F401 -from radicale import pathutils, utils +from radicale import pathutils, sharing, utils from radicale.item import filter as radicale_filter from radicale.log import logger @@ -594,7 +594,6 @@ class Item: placeholder_mapping: dict = {} bdayS = match[1] + match[2] + match[3] - bdaySdesc = match[1] + "-" + match[2] + "-" + match[3] bdayY = int(match[1]) bdayM = int(match[2]) bdayD = int(match[3]) @@ -649,37 +648,49 @@ class Item: else: item_ics.add('prodid').value = PRODID_CONVERTED - # create SUMMARY - summary = name + " (BDAY)" # default - if ShareActions is not None and 'config' in ShareActions: - if 'conversion_bday_summary_template' in ShareActions['config']: - summary = ShareActions['config']['conversion_bday_summary_template'] - summary = replace_placeholders(summary, placeholder_mapping) + # prepare SUMMARY + if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_summary_template' in ShareActions['config']: + summary = ShareActions['config']['conversion_bday_summary_template'] + elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_summary_template' in ShareActions['config_default']: + summary = ShareActions['config_default']['conversion_bday_summary_template'] + else: + summary = sharing.SHARING_BDAY_SUMMARY_TEMPLATE_DEFAULT # fallback + summary = replace_placeholders(summary, placeholder_mapping) - # create DESCRIPTION - description = "BDAY=" + bdaySdesc # default - if ShareActions is not None and 'config' in ShareActions: - if 'conversion_bday_description_template' in ShareActions['config']: - description = ShareActions['config']['conversion_bday_description_template'] - description = replace_placeholders(description, placeholder_mapping) + # prepare DESCRIPTION + if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_description_template' in ShareActions['config']: + description = ShareActions['config']['conversion_bday_description_template'] + elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_description_template' in ShareActions['config_default']: + description = ShareActions['config_default']['conversion_bday_description_template'] + else: + description = sharing.SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT # fallback + description = replace_placeholders(description, placeholder_mapping) # create CATEGORIES - categories: list = ["Birthday"] # default - if ShareActions is not None and 'config' in ShareActions: - if 'conversion_bday_categories' in ShareActions['config']: - categories = ShareActions['config']['conversion_bday_categories'].split(',') + if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_categories' in ShareActions['config']: + categories = ShareActions['config']['conversion_bday_categories'].split(',') + elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_categories' in ShareActions['config_default']: + categories = ShareActions['config_default']['conversion_bday_categories'].split(',') + else: + categories = sharing.SHARING_BDAY_CATEGORIES_DEFAULT.split(',') # fallback # check ALARM - alarm_trigger = "" # default - if ShareActions is not None and 'config' in ShareActions: + if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_alarm_trigger_template' in ShareActions['config']: alarm_trigger = ShareActions['config']['conversion_bday_alarm_trigger_template'] + elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_alarm_trigger_template' in ShareActions['config_default']: + alarm_trigger = ShareActions['config_default']['conversion_bday_alarm_trigger_template'] + else: + alarm_trigger = "" # default vevent_enable_age = False age_max = 0 if "{age}" in summary or "{age}" in description or "age" in alarm_trigger: - if ShareActions is not None and 'config' in ShareActions: - if 'conversion_bday_age_max' in ShareActions['config']: - age_max = ShareActions['config']['conversion_bday_age_max'] + if ShareActions is not None and 'config' in ShareActions and 'conversion_bday_age_max' in ShareActions['config']: + age_max = ShareActions['config']['conversion_bday_age_max'] + elif ShareActions is not None and 'config_default' in ShareActions and 'conversion_bday_age_max' in ShareActions['config_default']: + age_max = ShareActions['config_default']['conversion_bday_age_max'] + else: + age_max = sharing.SHARING_BDAY_AGE_MAX_DEFAULT # fallback vevent_enable_age = True # create UID diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 6356fd86..d2eb90dc 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -123,15 +123,19 @@ TOKEN_PATTERN_V1: str = "v1/[a-zA-Z0-9_\\-]{44}" OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname", "ICAL:calendar-order") -SHARING_BDAY_AGE_MAX: int = 199 # maximum age to prevent unexpected DoS by config +SHARING_BDAY_AGE_MAX_LIMIT: int = 199 # maximum age to prevent unexpected DoS by config +SHARING_BDAY_AGE_MAX_DEFAULT: int = 99 +SHARING_BDAY_SUMMARY_TEMPLATE_DEFAULT: str = "[{n:f} {n:g} {n:a}|{fn}|{nickname}] ({year}) (BDAY)" +SHARING_BDAY_DESCRIPTION_TEMPLATE_DEFAULT: str = "BDAY={year}-{month}-{day}" +SHARING_BDAY_CATEGORIES_DEFAULT: str = 'Birthday' def check_bday_max_age(data: Any) -> int: value = int(data) if value < 0: raise ValueError("value is negative: %d" % value) - if value > SHARING_BDAY_AGE_MAX: - raise ValueError("value exceeds maximum (%d): %d" % (SHARING_BDAY_AGE_MAX, value)) + if value > SHARING_BDAY_AGE_MAX_LIMIT: + raise ValueError("value exceeds maximum (%d): %d" % (SHARING_BDAY_AGE_MAX_LIMIT, value)) return value @@ -478,7 +482,7 @@ class BaseSharing: # autogenerate Actions if not existing if share['Actions'] is None or 'config' not in share['Actions']: share['Actions'] = { - 'config': { + 'config_default': { 'conversion_bday_summary_template': self.conversion_bday_summary_template, 'conversion_bday_description_template': self.conversion_bday_description_template, 'conversion_bday_alarm_trigger_template': self.conversion_bday_alarm_trigger_template, @@ -492,7 +496,9 @@ class BaseSharing: # nothing to do pass else: - share['Actions']['config'].update( + if 'config_default' not in share['Actions']: + share['Actions'].update({'config_default': {}}) + share['Actions']['config_default'].update( {'conversion_bday_summary_template': self.conversion_bday_summary_template} ) @@ -500,7 +506,9 @@ class BaseSharing: # nothing to do pass else: - share['Actions']['config'].update( + if 'config_default' not in share['Actions']: + share['Actions'].update({'config_default': {}}) + share['Actions']['config_default'].update( {'conversion_bday_description_template': self.conversion_bday_description_template} ) @@ -508,7 +516,9 @@ class BaseSharing: # nothing to do pass else: - share['Actions']['config'].update( + if 'config_default' not in share['Actions']: + share['Actions'].update({'config_default': {}}) + share['Actions']['config_default'].update( {'conversion_bday_alarm_trigger_template': self.conversion_bday_alarm_trigger_template} ) @@ -516,7 +526,9 @@ class BaseSharing: # nothing to do pass else: - share['Actions']['config'].update( + if 'config_default' not in share['Actions']: + share['Actions'].update({'config_default': {}}) + share['Actions']['config_default'].update( {'conversion_bday_categories': self.conversion_bday_categories} ) @@ -524,7 +536,9 @@ class BaseSharing: # nothing to do pass else: - share['Actions']['config'].update( + if 'config_default' not in share['Actions']: + share['Actions'].update({'config_default': {}}) + share['Actions']['config_default'].update( {'conversion_bday_age_max': self.conversion_bday_age_max} ) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index be44d0e8..8d1ee198 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -5008,7 +5008,7 @@ permissions: RrWw""") assert "SUMMARY:Test-FN (BDAY)" in answer self.configure({"sharing": {"conversion_bday_summary_template": "[{fn}|{n:f} {n:g} {n:a}|{nickname}] (Birthday)"}}) - logging.info("\n*** GET collection user format:text -> ok") + logging.info("\n*** GET collection user format with fn+Birthday -> ok") _, headers, answer = self.request("GET", path_shared_2, login="user:userpw") assert "SUMMARY:Test-FN (Birthday)" in answer @@ -5244,7 +5244,7 @@ permissions: RrWw""") logging.info("\n*** configuration test: conversion_bday_age_max > MAX") try: self.configure({"sharing": { - "conversion_bday_age_max": (sharing.SHARING_BDAY_AGE_MAX + 1), + "conversion_bday_age_max": (sharing.SHARING_BDAY_AGE_MAX_LIMIT + 1), }}) except RuntimeError: pass @@ -5349,7 +5349,7 @@ permissions: RrWw""") json_dict['PathMapped'] = path_mapped json_dict['PathOrToken'] = path_shared_r json_dict['Actions'] = {"config": { - "conversion_bday_age_max": (sharing.SHARING_BDAY_AGE_MAX + 1), + "conversion_bday_age_max": (sharing.SHARING_BDAY_AGE_MAX_LIMIT + 1), }} _, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict) @@ -5371,7 +5371,7 @@ permissions: RrWw""") json_dict['PathMapped'] = path_mapped json_dict['PathOrToken'] = path_shared_r json_dict['Actions'] = {"config": { - "conversion_bday_age_max": sharing.SHARING_BDAY_AGE_MAX, + "conversion_bday_age_max": sharing.SHARING_BDAY_AGE_MAX_LIMIT, }} _, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict) @@ -5952,8 +5952,8 @@ permissions: RrWw""") logging.info("\n*** GET bday with token") _, answer = self.get(path_shared) assert "VCARD" not in answer - assert "Test-FN-C3 (BDAY)" in answer - assert "Test-FN (BDAY)" in answer + assert "Test-FN-C3 (BDAY)" in answer # contact2 + assert "Test-FN (BDAY)" in answer # contact1 # verify content as owner logging.info("\n*** GET collection owner -> ok")