Fix: bday conversion clobbers NICKNAME when FN is empty
The share-by-map BDAY-to-ICS conversion builds a placeholder mapping for
the SUMMARY/DESCRIPTION/alarm templates. Each block sets the fallback
marker for its own placeholder (e.g. {nickname} -> '!nickname!', {n:f} ->
'!n:f!'), but the {fn} block wrote the empty-FN fallback to {nickname}
instead of {fn}.
As a result, for a VCARD whose FN is present but empty, a genuine
NICKNAME was overwritten with '!fn!' in the generated event, and the
{fn} placeholder never received its '!fn!' marker, so a '[{fn}|...]'
fallback in a template failed to resolve.
Assign the fallback to {fn}, matching the surrounding blocks.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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: 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: 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: 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
|
## 3.7.6
|
||||||
* Extension: item verification on commandline
|
* Extension: item verification on commandline
|
||||||
|
|||||||
@@ -717,7 +717,7 @@ class Item:
|
|||||||
if hasattr(self.vobject_item, "fn") and self.vobject_item.fn.value != "":
|
if hasattr(self.vobject_item, "fn") and self.vobject_item.fn.value != "":
|
||||||
placeholder_mapping['{fn}'] = self.vobject_item.fn.value
|
placeholder_mapping['{fn}'] = self.vobject_item.fn.value
|
||||||
else:
|
else:
|
||||||
placeholder_mapping['{nickname}'] = '!fn!'
|
placeholder_mapping['{fn}'] = '!fn!'
|
||||||
|
|
||||||
# rfc6350#6.2 FamilyName;GivenName;AdditionalNames;HonorificPrefixes;HonorificSuffixes
|
# rfc6350#6.2 FamilyName;GivenName;AdditionalNames;HonorificPrefixes;HonorificSuffixes
|
||||||
if hasattr(self.vobject_item, "n") and self.vobject_item.n.value.family != "":
|
if hasattr(self.vobject_item, "n") and self.vobject_item.n.value.family != "":
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import urllib
|
import urllib
|
||||||
from typing import Dict, Sequence, Tuple, Union
|
from typing import Dict, Sequence, Tuple, Union, cast
|
||||||
|
|
||||||
import pytest
|
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 import BaseTest
|
||||||
from radicale.tests.helpers import get_file_content
|
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/5)" in answer
|
||||||
assert "Test-FN-C3 (1990/6)" 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:
|
def test_sharing_api_map_vcf_bday_age_template(self) -> None:
|
||||||
"""share-by-map with conversion=bday template tests with age."""
|
"""share-by-map with conversion=bday template tests with age."""
|
||||||
self.configure({"auth": {"type": "htpasswd",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
|
|||||||
Reference in New Issue
Block a user