Skip to content

Commit

Permalink
Support addressbook-query. Fixes #143
Browse files Browse the repository at this point in the history
  • Loading branch information
jelmer committed Jan 17, 2022
1 parent c72ec7a commit 54a3842
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 20 deletions.
32 changes: 17 additions & 15 deletions xandikos/carddav.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,18 +225,14 @@ def addressbook_from_resource(resource):
return None
except KeyError:
return None
return resource.file.addressbook
return resource.file.addressbook.contents


def apply_text_match(el, value):
collation = el.get("collation", "i;ascii-casemap")
negate_condition = el.get("negate-condition", "no")
# TODO(jelmer): Handle match-type: 'contains', 'equals', 'starts-with',
# 'ends-with'
match_type = el.get("match-type", "contains")
if match_type != "contains":
raise NotImplementedError("match_type != contains: %r" % match_type)
matches = _mod_collation.collations[collation](el.text, value)
matches = _mod_collation.collations[collation](value, el.text, match_type)

if negate_condition == "yes":
return not matches
Expand Down Expand Up @@ -264,7 +260,7 @@ def apply_param_filter(el, prop):


def apply_prop_filter(el, ab):
name = el.get("name")
name = el.get("name").lower()
# From https://tools.ietf.org/html/rfc6352
# A CARDDAV:prop-filter is said to match if:

Expand All @@ -279,14 +275,20 @@ def apply_prop_filter(el, ab):
except KeyError:
return False

for subel in el:
if subel.tag == "{urn:ietf:params:xml:ns:carddav}text-match":
if not apply_text_match(subel, prop):
return False
elif subel.tag == "{urn:ietf:params:xml:ns:carddav}param-filter":
if not apply_param_filter(subel, prop):
return False
return True
for prop_el in prop:
matched = True
for subel in el:
if subel.tag == "{urn:ietf:params:xml:ns:carddav}text-match":
if not apply_text_match(subel, str(prop_el)):
matched = False
break
elif subel.tag == "{urn:ietf:params:xml:ns:carddav}param-filter":
if not apply_param_filter(subel, prop_el):
matched = False
break
if matched:
return True
return False


def apply_filter(el, resource):
Expand Down
24 changes: 21 additions & 3 deletions xandikos/collation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,29 @@ def __init__(self, collation: str):
self.collation = collation


def _match(a, b, k):
if k == "equals":
return a == b
elif k == "contains":
return b in a
elif k == "starts-with":
return a.startswith(b)
elif k == "ends-with":
return b.endswith(b)
else:
raise NotImplementedError


collations = {
"i;ascii-casemap": lambda a, b: (
a.decode("ascii").upper() == b.decode("ascii").upper()
"i;ascii-casemap": lambda a, b, k: _match(
a.decode("ascii").upper(), b.decode("ascii").upper(), k
),
"i;octet": lambda a, b: a == b,
"i;octet": lambda a, b, k: _match(a, b, k),
# TODO(jelmer): Follow all rules as specified in https://datatracker.ietf.org/doc/html/rfc5051
"i;unicode-casemap": lambda a, b, k: _match(
a.encode('utf-8', 'surrogateescape').upper(),
b.encode('utf-8', 'surrogateescape').upper(),
k),
}


Expand Down
2 changes: 1 addition & 1 deletion xandikos/icalendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ def match_indexes(self, indexes):
def match(self, prop):
if isinstance(prop, vText):
prop = prop.encode()
matches = self.collation(self.text, prop)
matches = self.collation(self.text, prop, 'equals')
if self.negate_condition:
return not matches
else:
Expand Down
1 change: 1 addition & 0 deletions xandikos/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_suite():
names = [
"api",
"caldav",
"carddav",
"config",
"icalendar",
"store",
Expand Down
45 changes: 45 additions & 0 deletions xandikos/tests/test_carddav.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Xandikos
# Copyright (C) 2022 Jelmer Vernooij <[email protected]>, et al.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 3
# of the License or (at your option) any later version of
# the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.

import unittest

from ..carddav import apply_filter, NAMESPACE
from ..vcard import VCardFile
from ..webdav import ET
from .test_vcard import EXAMPLE_VCARD1


class TestApplyFilter(unittest.TestCase):

def setUp(self):
self.file = VCardFile([EXAMPLE_VCARD1], "text/vcard")

def get_content_type(self):
return "text/vcard"

def test_apply_filter(self):
el = ET.Element("{%s}filter" % NAMESPACE)
el.set("test", "anyof")
pf = ET.SubElement(el, "{%s}prop-filter" % NAMESPACE)
pf.set("name", "FN")
tm = ET.SubElement(pf, "{%s}text-match" % NAMESPACE)
tm.set("collation", "i;unicode-casemap")
tm.set("match-type", "contains")
tm.text = "Jeffrey"
self.assertTrue(apply_filter(el, self))
2 changes: 1 addition & 1 deletion xandikos/tests/test_vcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@
class ParseVcardTests(unittest.TestCase):

def test_validate(self):
fi = VCardFile([EXAMPLE_VCARD1], "text/calendar")
fi = VCardFile([EXAMPLE_VCARD1], "text/vcard")
fi.validate()

0 comments on commit 54a3842

Please sign in to comment.