From 6401d9ed459f21f30be7743c6e35dce880ed8cdb Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann Date: Thu, 31 Mar 2022 22:15:04 +0200 Subject: [PATCH] caldav: extend query filter types The basic types related to queries and filtering are missing some features specified in the RFC (as also noted in the TODO comments). This adds several of the missing elements, working towards being able to handle all RFC-compliant queries. The work is not fully done, e.g. the collation for text-match is still not handled, but it's getting pretty close. --- caldav/caldav.go | 25 ++++++++++++++++++------- caldav/elements.go | 45 ++++++++++++++++++++++++++++++++++++++------- caldav/server.go | 43 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/caldav/caldav.go b/caldav/caldav.go index 3840c7d..62cc94d 100644 --- a/caldav/caldav.go +++ b/caldav/caldav.go @@ -27,19 +27,30 @@ type CalendarCompRequest struct { } type CompFilter struct { - Name string - Start, End time.Time - Props []PropFilter - Comps []CompFilter + Name string + IsNotDefined bool + Start, End time.Time + Props []PropFilter + Comps []CompFilter +} + +type ParamFilter struct { + Name string + IsNotDefined bool + TextMatch *TextMatch } type PropFilter struct { - Name string - TextMatch *TextMatch + Name string + IsNotDefined bool + Start, End time.Time + TextMatch *TextMatch + ParamFilter []ParamFilter } type TextMatch struct { - Text string + Text string + NegateCondition bool } type CalendarQuery struct { diff --git a/caldav/elements.go b/caldav/elements.go index e1f8c32..7b00fd5 100644 --- a/caldav/elements.go +++ b/caldav/elements.go @@ -21,7 +21,8 @@ var ( calendarQueryName = xml.Name{namespace, "calendar-query"} calendarMultigetName = xml.Name{namespace, "calendar-multiget"} - calendarName = xml.Name{namespace, "calendar"} + calendarName = xml.Name{namespace, "calendar"} + calendarDataName = xml.Name{namespace, "calendar-data"} ) // https://tools.ietf.org/html/rfc4791#section-6.2.1 @@ -98,19 +99,49 @@ type compFilter struct { // https://tools.ietf.org/html/rfc4791#section-9.7.2 type propFilter struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop-filter"` + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav prop-filter"` + Name string `xml:"name,attr"` + IsNotDefined *struct{} `xml:"is-not-defined,omitempty"` + TimeRange *timeRange `xml:"time-range,omitempty"` + TextMatch *textMatch `xml:"text-match,omitempty"` + ParamFilter []paramFilter `xml:"param-filter,omitempty"` +} + +// https://tools.ietf.org/html/rfc4791#section-9.7.3 +type paramFilter struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav param-filter"` Name string `xml:"name,attr"` IsNotDefined *struct{} `xml:"is-not-defined,omitempty"` - TimeRange *timeRange `xml:"time-range,omitempty"` TextMatch *textMatch `xml:"text-match,omitempty"` - // TODO: param-filter } // https://tools.ietf.org/html/rfc4791#section-9.7.5 type textMatch struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav text-match"` - Text string `xml:",chardata"` - // TODO: collation, negate-condition + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav text-match"` + Text string `xml:",chardata"` + Collation string `xml:"collation,attr,omitempty"` + NegateCondition negateCondition `xml:"negate-condition,attr,omitempty"` +} + +type negateCondition bool + +func (nc *negateCondition) UnmarshalText(b []byte) error { + switch s := string(b); s { + case "yes": + *nc = true + case "no": + *nc = false + default: + return fmt.Errorf("caldav: invalid negate-condition value: %q", s) + } + return nil +} + +func (nc negateCondition) MarshalText() ([]byte, error) { + if nc { + return []byte("yes"), nil + } + return nil, nil } // https://tools.ietf.org/html/rfc4791#section-9.9 diff --git a/caldav/server.go b/caldav/server.go index b0ea301..c72acde 100644 --- a/caldav/server.go +++ b/caldav/server.go @@ -69,17 +69,53 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) error { return internal.HTTPErrorf(http.StatusBadRequest, "caldav: expected calendar-query or calendar-multiget element in REPORT request") } -func decodePropFilter(el *propFilter) (*PropFilter, error) { - pf := &PropFilter{Name: el.Name} +func decodeParamFilter(el *paramFilter) (*ParamFilter, error) { + pf := &ParamFilter{Name: el.Name} + if el.IsNotDefined != nil { + if el.TextMatch != nil { + return nil, fmt.Errorf("caldav: failed to parse param-filter: if is-not-defined is provided, text-match can't be provided") + } + pf.IsNotDefined = true + } if el.TextMatch != nil { pf.TextMatch = &TextMatch{Text: el.TextMatch.Text} } - // TODO: IsNotDefined, TimeRange + return pf, nil +} + +func decodePropFilter(el *propFilter) (*PropFilter, error) { + pf := &PropFilter{Name: el.Name} + if el.IsNotDefined != nil { + if el.TextMatch != nil || el.TimeRange != nil || len(el.ParamFilter) > 0 { + return nil, fmt.Errorf("caldav: failed to parse prop-filter: if is-not-defined is provided, text-match, time-range, or param-filter can't be provided") + } + pf.IsNotDefined = true + } + if el.TextMatch != nil { + pf.TextMatch = &TextMatch{Text: el.TextMatch.Text} + } + if el.TimeRange != nil { + pf.Start = time.Time(el.TimeRange.Start) + pf.End = time.Time(el.TimeRange.End) + } + for _, paramEl := range el.ParamFilter { + paramFi, err := decodeParamFilter(¶mEl) + if err != nil { + return nil, err + } + pf.ParamFilter = append(pf.ParamFilter, *paramFi) + } return pf, nil } func decodeCompFilter(el *compFilter) (*CompFilter, error) { cf := &CompFilter{Name: el.Name} + if el.IsNotDefined != nil { + if el.TimeRange != nil || len(el.PropFilters) > 0 || len(el.CompFilters) > 0 { + return nil, fmt.Errorf("caldav: failed to parse comp-filter: if is-not-defined is provided, time-range, prop-filter, or comp-filter can't be provided") + } + cf.IsNotDefined = true + } if el.TimeRange != nil { cf.Start = time.Time(el.TimeRange.Start) cf.End = time.Time(el.TimeRange.End) @@ -98,7 +134,6 @@ func decodeCompFilter(el *compFilter) (*CompFilter, error) { } cf.Comps = append(cf.Comps, *child) } - // TODO: IsNotDefined return cf, nil }