diff --git a/CHANGELOG.md b/CHANGELOG.md index 036c34bc..f5c45514 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Fix: time-range filter treated a VEVENT with a whole-day DURATION (e.g. P1D, P2D) as zero-length (timedelta.seconds instead of total_seconds), so such events were missing from calendar-query REPORT results * Fix: calendar-data expand (REPORT) left recurrence properties (e.g. RDATE) on the expanded single-occurrence VEVENTs; a single try/except around the sequential delattr() calls stopped at the first absent property (e.g. missing EXDATE), so later ones were never removed * Fix: text-match filter on a structured property (e.g. vCard N or ADR) crashed with HTTP 500 (AttributeError: 'Name'/'Address' object has no attribute 'lower') because vobject parses these into non-string objects; their text representation is now used +* Fix: sharing bday-to-ICS conversion assigned the empty-FN fallback marker to the {nickname} placeholder instead of {fn}, so a VCARD with an empty FN got its NICKNAME overwritten with "!fn!" in the generated SUMMARY/DESCRIPTION and the {fn} fallback never resolved ## 3.7.6 * Extension: item verification on commandline diff --git a/radicale/item/__init__.py b/radicale/item/__init__.py index 7a33ce03..11076d10 100644 --- a/radicale/item/__init__.py +++ b/radicale/item/__init__.py @@ -717,7 +717,7 @@ class Item: if hasattr(self.vobject_item, "fn") and self.vobject_item.fn.value != "": placeholder_mapping['{fn}'] = self.vobject_item.fn.value else: - placeholder_mapping['{nickname}'] = '!fn!' + placeholder_mapping['{fn}'] = '!fn!' # rfc6350#6.2 FamilyName;GivenName;AdditionalNames;HonorificPrefixes;HonorificSuffixes if hasattr(self.vobject_item, "n") and self.vobject_item.n.value.family != "": diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 78601aa4..1c908430 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -28,11 +28,13 @@ import re import sys import tempfile import urllib -from typing import Dict, Sequence, Tuple, Union +from typing import Dict, Sequence, Tuple, Union, cast import pytest +import vobject -from radicale import pathutils, sharing, xmlutils +from radicale import pathutils, sharing, storage, xmlutils +from radicale.item import Item from radicale.tests import BaseTest from radicale.tests.helpers import get_file_content @@ -5328,6 +5330,39 @@ permissions: RrWw""") assert "Test-FN-C3 (1990/5)" in answer assert "Test-FN-C3 (1990/6)" in answer + def test_sharing_bday_conversion_empty_fn(self) -> None: + """BDAY-to-ICS conversion of a VCARD with an empty FN property. + + vCard mandates FN, but some clients emit it empty. The fallback for + an empty ``{fn}`` must be assigned to the ``{fn}`` placeholder, not to + ``{nickname}``: otherwise a present NICKNAME gets clobbered with the + ``!fn!`` marker and the ``{fn}`` fallback never triggers. + """ + vcard = vobject.readOne( + "BEGIN:VCARD\r\n" + "VERSION:3.0\r\n" + "UID:contact-empty-fn\r\n" + "N:FamilyTest;GivenTest;;;\r\n" + "FN:\r\n" + "NICKNAME:Test-NICKNAME\r\n" + "BDAY:1990-05-06\r\n" + "END:VCARD\r\n") + + class _StubCollection: + path = "test" + + item = Item(collection=cast(storage.BaseCollection, _StubCollection()), + vobject_item=vcard, href="contact-empty-fn.vcf") + converted = item.convert_vcf_to_ics(ShareActions={"config": { + "conversion_bday_summary_template": "{nickname} (BDAY)", + "conversion_bday_description_template": "[{fn}|no-fn]"}}) + assert converted is not None + serialized = converted.serialize() + # NICKNAME must survive, not be overwritten by the '!fn!' marker + assert "SUMMARY:Test-NICKNAME (BDAY)" in serialized + # empty {fn} must expose the '!fn!' marker so the fallback resolves + assert "DESCRIPTION:no-fn" in serialized + def test_sharing_api_map_vcf_bday_age_template(self) -> None: """share-by-map with conversion=bday template tests with age.""" self.configure({"auth": {"type": "htpasswd",