sharing/bday: parameter validation
This commit is contained in:
@@ -597,15 +597,15 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
|||||||
("conversion_bday_summary_template", {
|
("conversion_bday_summary_template", {
|
||||||
"value": "[{n:f} {n:g} {n:a}|{fn}|{nickname}] (BDAY)",
|
"value": "[{n:f} {n:g} {n:a}|{fn}|{nickname}] (BDAY)",
|
||||||
"help": "conversion bday summary template",
|
"help": "conversion bday summary template",
|
||||||
"type": str}),
|
"type": sharing.check_template}),
|
||||||
("conversion_bday_description_template", {
|
("conversion_bday_description_template", {
|
||||||
"value": "BDAY={year}-{month}-{day}",
|
"value": "BDAY={year}-{month}-{day}",
|
||||||
"help": "conversion bday description template",
|
"help": "conversion bday description template",
|
||||||
"type": str}),
|
"type": sharing.check_template}),
|
||||||
("conversion_bday_alarm_trigger_template", {
|
("conversion_bday_alarm_trigger_template", {
|
||||||
"value": "",
|
"value": "",
|
||||||
"help": "conversion bday alarm trigger template",
|
"help": "conversion bday alarm trigger template",
|
||||||
"type": str}),
|
"type": sharing.check_template_alarm_trigger}),
|
||||||
("conversion_bday_age_max", {
|
("conversion_bday_age_max", {
|
||||||
"value": "99",
|
"value": "99",
|
||||||
"help": "conversion bday age max",
|
"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") + ")"
|
PRODID_SUFFIX = " (auto-converted by Radicale " + utils.package_version("radicale") + ")"
|
||||||
UID_SUFFIX = "-auto-converted-by-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]:
|
def read_components(s: str) -> List[vobject.base.Component]:
|
||||||
"""Wrapper for vobject.readComponents"""
|
"""Wrapper for vobject.readComponents"""
|
||||||
@@ -362,6 +364,65 @@ def verify(file: str, encoding: str):
|
|||||||
return True
|
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 Item:
|
||||||
"""Class for address book and calendar entries."""
|
"""Class for address book and calendar entries."""
|
||||||
|
|
||||||
@@ -504,63 +565,6 @@ class Item:
|
|||||||
self.component_name
|
self.component_name
|
||||||
self._vobject_item = orig_vobject_item
|
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]:
|
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: ShareActions: %r", ShareActions)
|
||||||
logger.trace("item/convert_vcf_to_ics: convert VCF to ICS (href): %r", self.href)
|
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 ShareActions is not None and 'config' in ShareActions:
|
||||||
if 'conversion_bday_summary_template' in ShareActions['config']:
|
if 'conversion_bday_summary_template' in ShareActions['config']:
|
||||||
summary = ShareActions['config']['conversion_bday_summary_template']
|
summary = ShareActions['config']['conversion_bday_summary_template']
|
||||||
summary = self.replace_placeholders(summary, placeholder_mapping)
|
summary = replace_placeholders(summary, placeholder_mapping)
|
||||||
|
|
||||||
# create DESCRIPTION
|
# create DESCRIPTION
|
||||||
description = "BDAY=" + bdaySdesc # default
|
description = "BDAY=" + bdaySdesc # default
|
||||||
if ShareActions is not None and 'config' in ShareActions:
|
if ShareActions is not None and 'config' in ShareActions:
|
||||||
if 'conversion_bday_description_template' in ShareActions['config']:
|
if 'conversion_bday_description_template' in ShareActions['config']:
|
||||||
description = ShareActions['config']['conversion_bday_description_template']
|
description = ShareActions['config']['conversion_bday_description_template']
|
||||||
description = self.replace_placeholders(description, placeholder_mapping)
|
description = replace_placeholders(description, placeholder_mapping)
|
||||||
|
|
||||||
# check ALARM
|
# check ALARM
|
||||||
alarm_trigger = "" # default
|
alarm_trigger = "" # default
|
||||||
@@ -719,9 +723,9 @@ class Item:
|
|||||||
for entry in alarm_trigger.split('|'):
|
for entry in alarm_trigger.split('|'):
|
||||||
(trigger, alarm_description) = entry.split(';')
|
(trigger, alarm_description) = entry.split(';')
|
||||||
logger.trace("item/convert_vcf_to_ics: alarm trigger entry: %r (trigger=%r description=%r)", entry, trigger, description)
|
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:
|
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))
|
alarm_description_value = alarm_description.replace("{age}", str(age))
|
||||||
valarm = vevent.add('valarm')
|
valarm = vevent.add('valarm')
|
||||||
valarm.add('action').value = "DISPLAY"
|
valarm.add('action').value = "DISPLAY"
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ from http import client
|
|||||||
from typing import Any, Sequence, Union
|
from typing import Any, Sequence, Union
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
from radicale import (config, httputils, pathutils, rights, storage, types,
|
from radicale import (config, httputils, item, pathutils, rights, storage,
|
||||||
utils)
|
types, utils)
|
||||||
from radicale.log import logger
|
from radicale.log import logger
|
||||||
|
|
||||||
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
|
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
|
||||||
@@ -135,11 +135,49 @@ def check_bday_max_age(data: Any) -> int:
|
|||||||
return value
|
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 = {
|
ACTIONS_WHITELIST: dict = {
|
||||||
'config': {
|
'config': {
|
||||||
'conversion_bday_summary_template': str,
|
'conversion_bday_summary_template': check_template,
|
||||||
'conversion_bday_description_template': str,
|
'conversion_bday_description_template': check_template,
|
||||||
'conversion_bday_alarm_trigger_template': str,
|
'conversion_bday_alarm_trigger_template': check_template_alarm_trigger,
|
||||||
'conversion_bday_age_max': check_bday_max_age,
|
'conversion_bday_age_max': check_bday_max_age,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -872,12 +910,11 @@ class BaseSharing:
|
|||||||
if level1 in ACTIONS_WHITELIST:
|
if level1 in ACTIONS_WHITELIST:
|
||||||
for level2 in request_data['Actions'][level1]:
|
for level2 in request_data['Actions'][level1]:
|
||||||
if level2 in ACTIONS_WHITELIST[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]):
|
if callable(ACTIONS_WHITELIST[level1][level2]):
|
||||||
try:
|
try:
|
||||||
value = ACTIONS_WHITELIST[level1][level2](request_data['Actions'][level1][level2])
|
value = ACTIONS_WHITELIST[level1][level2](request_data['Actions'][level1][level2])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
hint = "'" + level1 + "': {'" + level2 + "'} is out-of-range"
|
hint = "'" + level1 + "': {'" + level2 + "'} is not supported"
|
||||||
valid = False
|
valid = False
|
||||||
break
|
break
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user