diff --git a/radicale/app/put.py b/radicale/app/put.py index ac07bf4..ec49587 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -141,7 +141,7 @@ class ApplicationPartPut(ApplicationBase): content_type = environ.get("CONTENT_TYPE", "").split(";", maxsplit=1)[0] try: - vobject_items = list(vobject.readComponents(content or "")) + vobject_items = radicale_item.read_components(content or "") except Exception as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) diff --git a/radicale/item/__init__.py b/radicale/item/__init__.py index fd293dd..3bce12a 100644 --- a/radicale/item/__init__.py +++ b/radicale/item/__init__.py @@ -27,6 +27,7 @@ import binascii import contextlib import math import os +import re import sys from datetime import datetime, timedelta from hashlib import sha256 @@ -42,6 +43,16 @@ from radicale.item import filter as radicale_filter from radicale.log import logger +def read_components(s: str) -> List[vobject.base.Component]: + """Wrapper for vobject.readComponents""" + # Workaround for bug in InfCloud + # PHOTO is a data URI + s = re.sub(r"^(PHOTO(?:;[^:\r\n]*)?;ENCODING=b(?:;[^:\r\n]*)?:)" + r"data:[^;,\r\n]*;base64,", r"\1", s, + flags=re.MULTILINE | re.IGNORECASE) + return list(vobject.readComponents(s)) + + def predict_tag_of_parent_collection( vobject_items: Sequence[vobject.base.Component]) -> Optional[str]: """Returns the predicted tag or `None`""" diff --git a/radicale/storage/multifilesystem/get.py b/radicale/storage/multifilesystem/get.py index e4f184e..0a1fd73 100644 --- a/radicale/storage/multifilesystem/get.py +++ b/radicale/storage/multifilesystem/get.py @@ -21,8 +21,6 @@ import sys import time from typing import Iterable, Iterator, Optional, Tuple -import vobject - import radicale.item as radicale_item from radicale import pathutils from radicale.log import logger @@ -93,8 +91,8 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock, cache_content = self._load_item_cache(href, cache_hash) if cache_content is None: try: - vobject_items = list(vobject.readComponents( - raw_text.decode(self._encoding))) + vobject_items = radicale_item.read_components( + raw_text.decode(self._encoding)) radicale_item.check_and_sanitize_items( vobject_items, tag=self.tag) vobject_item, = vobject_items diff --git a/radicale/tests/static/contact_photo_with_data_uri.vcf b/radicale/tests/static/contact_photo_with_data_uri.vcf new file mode 100644 index 0000000..b443546 --- /dev/null +++ b/radicale/tests/static/contact_photo_with_data_uri.vcf @@ -0,0 +1,8 @@ +BEGIN:VCARD +VERSION:3.0 +UID:contact +N:Contact;;;; +FN:Contact +NICKNAME:test +PHOTO;ENCODING=b;TYPE=png:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAD0lEQVQIHQEEAPv/AP///wX+Av4DfRnGAAAAAElFTkSuQmCC +END:VCARD diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 52c482e..2a8c47f 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -139,6 +139,12 @@ permissions: RrWw""") _, answer = self.get(path) assert "UID:contact1" in answer + def test_add_contact_photo_with_data_uri(self) -> None: + """Test workaround for broken PHOTO data from InfCloud""" + self.create_addressbook("/contacts.vcf/") + contact = get_file_content("contact_photo_with_data_uri.vcf") + self.put("/contacts.vcf/contact.vcf", contact) + def test_add_contact_without_uid(self) -> None: """Add a contact without UID.""" self.create_addressbook("/contacts.vcf/")