sharing/bday: parameter validation
This commit is contained in:
@@ -597,15 +597,15 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
||||
("conversion_bday_summary_template", {
|
||||
"value": "[{n:f} {n:g} {n:a}|{fn}|{nickname}] (BDAY)",
|
||||
"help": "conversion bday summary template",
|
||||
"type": str}),
|
||||
"type": sharing.check_template}),
|
||||
("conversion_bday_description_template", {
|
||||
"value": "BDAY={year}-{month}-{day}",
|
||||
"help": "conversion bday description template",
|
||||
"type": str}),
|
||||
"type": sharing.check_template}),
|
||||
("conversion_bday_alarm_trigger_template", {
|
||||
"value": "",
|
||||
"help": "conversion bday alarm trigger template",
|
||||
"type": str}),
|
||||
"type": sharing.check_template_alarm_trigger}),
|
||||
("conversion_bday_age_max", {
|
||||
"value": "99",
|
||||
"help": "conversion bday age max",
|
||||
|
||||
@@ -47,6 +47,8 @@ PRODID_CONVERTED = u"-//Radicale//NONSGML " + utils.package_version("radicale")
|
||||
PRODID_SUFFIX = " (auto-converted by Radicale " + utils.package_version("radicale") + ")"
|
||||
UID_SUFFIX = "-auto-converted-by-Radicale"
|
||||
|
||||
VCF_TO_ICS_SUPPORTED_PLACEHOLDERS: list = ["fn", "n:f", "n:g", "n:a", "age", "nickname", "year", "month", "day"]
|
||||
|
||||
|
||||
def read_components(s: str) -> List[vobject.base.Component]:
|
||||
"""Wrapper for vobject.readComponents"""
|
||||
@@ -362,6 +364,65 @@ def verify(file: str, encoding: str):
|
||||
return True
|
||||
|
||||
|
||||
def replace_placeholders(text: str, placeholder_mapping: dict) -> str:
|
||||
for placeholder in placeholder_mapping:
|
||||
text = text.replace(placeholder, placeholder_mapping[placeholder])
|
||||
|
||||
# resolve {..|..} recursive
|
||||
pattern = re.compile('(.*)(\\[)([^|]+)\\|(.+)(\\])(.*)')
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] starting with: %r", text)
|
||||
while True:
|
||||
match = pattern.match(text)
|
||||
if not match:
|
||||
# nothing more todo
|
||||
break
|
||||
else:
|
||||
if match[3].startswith('!') and match[3].endswith('!'):
|
||||
# not resolved variable
|
||||
if '|' in match[4]:
|
||||
# further recursion required
|
||||
text = match[1] + match[2] + match[4] + match[5] + match[6]
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace/continue result: %r", text)
|
||||
else:
|
||||
text = match[1] + match[4] + match[6]
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace/final result: %r", text)
|
||||
break
|
||||
else:
|
||||
# resolved variable
|
||||
text = match[1] + match[3] + match[6]
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace(resolved) result: %r", text)
|
||||
return text
|
||||
|
||||
|
||||
def trigger_to_timedelta(trigger) -> Union[datetime.timedelta, None]:
|
||||
# workaround as vobject is not supporting direct set of value
|
||||
# limited implementatino of reverse function of timedeltaToString in vobject/icalendar.py
|
||||
pattern = re.compile('([+-])?([0-9]+)([WDHM])$')
|
||||
match = pattern.match(trigger)
|
||||
if not match:
|
||||
logger.error("item/convert_vcf_to_ics: trigger time value not valid: %r", trigger)
|
||||
return None
|
||||
|
||||
sign = 1
|
||||
if match[1] == "-":
|
||||
sign = -1
|
||||
|
||||
value = int(match[2]) * sign
|
||||
|
||||
td: Union[datetime.timedelta, None] = None
|
||||
|
||||
if match[3] == "D":
|
||||
td = datetime.timedelta(days=value)
|
||||
elif match[3] == "M":
|
||||
td = datetime.timedelta(minutes=value)
|
||||
elif match[3] == "H":
|
||||
td = datetime.timedelta(hours=value)
|
||||
elif match[3] == "W":
|
||||
td = datetime.timedelta(weeks=value)
|
||||
|
||||
return td
|
||||
|
||||
|
||||
class Item:
|
||||
"""Class for address book and calendar entries."""
|
||||
|
||||
@@ -504,63 +565,6 @@ class Item:
|
||||
self.component_name
|
||||
self._vobject_item = orig_vobject_item
|
||||
|
||||
def replace_placeholders(self, text: str, placeholder_mapping: dict) -> str:
|
||||
for placeholder in placeholder_mapping:
|
||||
text = text.replace(placeholder, placeholder_mapping[placeholder])
|
||||
|
||||
# resolve {..|..} recursive
|
||||
pattern = re.compile('(.*)(\\[)([^|]+)\\|(.+)(\\])(.*)')
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] starting with: %r", text)
|
||||
while True:
|
||||
match = pattern.match(text)
|
||||
if not match:
|
||||
# nothing more todo
|
||||
break
|
||||
else:
|
||||
if match[3].startswith('!') and match[3].endswith('!'):
|
||||
# not resolved variable
|
||||
if '|' in match[4]:
|
||||
# further recursion required
|
||||
text = match[1] + match[2] + match[4] + match[5] + match[6]
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace/continue result: %r", text)
|
||||
else:
|
||||
text = match[1] + match[4] + match[6]
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace/final result: %r", text)
|
||||
break
|
||||
else:
|
||||
# resolved variable
|
||||
text = match[1] + match[3] + match[6]
|
||||
logger.trace("item/convert_vcf_to_ics: resolve [..|..] match/replace(resolved) result: %r", text)
|
||||
return text
|
||||
|
||||
def trigger_to_timedelta(self, trigger) -> Union[datetime.timedelta, None]:
|
||||
# workaround as vobject is not supporting direct set of value
|
||||
# limited implementatino of reverse function of timedeltaToString in vobject/icalendar.py
|
||||
pattern = re.compile('([+-])?([0-9]+)([WDHM])$')
|
||||
match = pattern.match(trigger)
|
||||
if not match:
|
||||
logger.error("item/convert_vcf_to_ics: trigger time value not valid: %r", trigger)
|
||||
return None
|
||||
|
||||
sign = 1
|
||||
if match[1] == "-":
|
||||
sign = -1
|
||||
|
||||
value = int(match[2]) * sign
|
||||
|
||||
td: Union[datetime.timedelta, None] = None
|
||||
|
||||
if match[3] == "D":
|
||||
td = datetime.timedelta(days=value)
|
||||
elif match[3] == "M":
|
||||
td = datetime.timedelta(minutes=value)
|
||||
elif match[3] == "H":
|
||||
td = datetime.timedelta(hours=value)
|
||||
elif match[3] == "W":
|
||||
td = datetime.timedelta(weeks=value)
|
||||
|
||||
return td
|
||||
|
||||
def convert_vcf_to_ics(self, ShareActions: dict = {}) -> Union["Item", None]:
|
||||
logger.trace("item/convert_vcf_to_ics: ShareActions: %r", ShareActions)
|
||||
logger.trace("item/convert_vcf_to_ics: convert VCF to ICS (href): %r", self.href)
|
||||
@@ -650,14 +654,14 @@ class Item:
|
||||
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 = self.replace_placeholders(summary, placeholder_mapping)
|
||||
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 = self.replace_placeholders(description, placeholder_mapping)
|
||||
description = replace_placeholders(description, placeholder_mapping)
|
||||
|
||||
# check ALARM
|
||||
alarm_trigger = "" # default
|
||||
@@ -719,9 +723,9 @@ class Item:
|
||||
for entry in alarm_trigger.split('|'):
|
||||
(trigger, alarm_description) = entry.split(';')
|
||||
logger.trace("item/convert_vcf_to_ics: alarm trigger entry: %r (trigger=%r description=%r)", entry, trigger, description)
|
||||
td = self.trigger_to_timedelta(trigger)
|
||||
td = trigger_to_timedelta(trigger)
|
||||
if td is not None:
|
||||
alarm_description = self.replace_placeholders(alarm_description, placeholder_mapping)
|
||||
alarm_description = replace_placeholders(alarm_description, placeholder_mapping)
|
||||
alarm_description_value = alarm_description.replace("{age}", str(age))
|
||||
valarm = vevent.add('valarm')
|
||||
valarm.add('action').value = "DISPLAY"
|
||||
|
||||
@@ -27,8 +27,8 @@ from http import client
|
||||
from typing import Any, Sequence, Union
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from radicale import (config, httputils, pathutils, rights, storage, types,
|
||||
utils)
|
||||
from radicale import (config, httputils, item, pathutils, rights, storage,
|
||||
types, utils)
|
||||
from radicale.log import logger
|
||||
|
||||
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
|
||||
@@ -135,11 +135,49 @@ def check_bday_max_age(data: Any) -> int:
|
||||
return value
|
||||
|
||||
|
||||
def check_template(data: Any) -> str:
|
||||
placeholder_mapping: dict = {}
|
||||
for placeholder in item.VCF_TO_ICS_SUPPORTED_PLACEHOLDERS:
|
||||
placeholder_mapping["{" + placeholder + "}"] = '!' + placeholder + '!'
|
||||
|
||||
result = item.replace_placeholders(data, placeholder_mapping)
|
||||
logger.trace("replace placeholders: %r -> %r", data, result)
|
||||
pattern = re.compile('.*{.*}.*')
|
||||
if pattern.search(result):
|
||||
raise ValueError("template contains unsupported placeholder {..}: %r" % result)
|
||||
return data
|
||||
|
||||
|
||||
def check_template_alarm_trigger(data: Any) -> str:
|
||||
if data is not None and data != '':
|
||||
for entry in data.split('|'):
|
||||
try:
|
||||
(trigger, alarm_description) = entry.split(';')
|
||||
except ValueError:
|
||||
raise ValueError("alarm trigger template misses <trigger>;<alarm description>")
|
||||
|
||||
if trigger is not None and trigger != '':
|
||||
td = item.trigger_to_timedelta(trigger)
|
||||
if td is None:
|
||||
raise ValueError("alarm trigger template contains unsupported trigger: %r" % trigger)
|
||||
else:
|
||||
raise ValueError("alarm trigger template misses trigger")
|
||||
|
||||
if alarm_description is not None and alarm_description != '':
|
||||
try:
|
||||
check_template(alarm_description)
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
raise ValueError("alarm trigger template misses description")
|
||||
return data
|
||||
|
||||
|
||||
ACTIONS_WHITELIST: dict = {
|
||||
'config': {
|
||||
'conversion_bday_summary_template': str,
|
||||
'conversion_bday_description_template': str,
|
||||
'conversion_bday_alarm_trigger_template': str,
|
||||
'conversion_bday_summary_template': check_template,
|
||||
'conversion_bday_description_template': check_template,
|
||||
'conversion_bday_alarm_trigger_template': check_template_alarm_trigger,
|
||||
'conversion_bday_age_max': check_bday_max_age,
|
||||
},
|
||||
}
|
||||
@@ -872,12 +910,11 @@ class BaseSharing:
|
||||
if level1 in ACTIONS_WHITELIST:
|
||||
for level2 in request_data['Actions'][level1]:
|
||||
if level2 in ACTIONS_WHITELIST[level1]:
|
||||
logger.trace(api_info + ": Actions validation: type='%r'", type(ACTIONS_WHITELIST[level1][level2]))
|
||||
if callable(ACTIONS_WHITELIST[level1][level2]):
|
||||
try:
|
||||
value = ACTIONS_WHITELIST[level1][level2](request_data['Actions'][level1][level2])
|
||||
except ValueError:
|
||||
hint = "'" + level1 + "': {'" + level2 + "'} is out-of-range"
|
||||
hint = "'" + level1 + "': {'" + level2 + "'} is not supported"
|
||||
valid = False
|
||||
break
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user