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.
This commit is contained in:
Conrad Hoffmann 2022-03-31 22:15:04 +02:00 committed by Simon Ser
parent 7dafedd290
commit 6401d9ed45
3 changed files with 95 additions and 18 deletions

View File

@ -27,19 +27,30 @@ type CalendarCompRequest struct {
} }
type CompFilter struct { type CompFilter struct {
Name string Name string
Start, End time.Time IsNotDefined bool
Props []PropFilter Start, End time.Time
Comps []CompFilter Props []PropFilter
Comps []CompFilter
}
type ParamFilter struct {
Name string
IsNotDefined bool
TextMatch *TextMatch
} }
type PropFilter struct { type PropFilter struct {
Name string Name string
TextMatch *TextMatch IsNotDefined bool
Start, End time.Time
TextMatch *TextMatch
ParamFilter []ParamFilter
} }
type TextMatch struct { type TextMatch struct {
Text string Text string
NegateCondition bool
} }
type CalendarQuery struct { type CalendarQuery struct {

View File

@ -21,7 +21,8 @@ var (
calendarQueryName = xml.Name{namespace, "calendar-query"} calendarQueryName = xml.Name{namespace, "calendar-query"}
calendarMultigetName = xml.Name{namespace, "calendar-multiget"} 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 // 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 // https://tools.ietf.org/html/rfc4791#section-9.7.2
type propFilter struct { 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"` Name string `xml:"name,attr"`
IsNotDefined *struct{} `xml:"is-not-defined,omitempty"` IsNotDefined *struct{} `xml:"is-not-defined,omitempty"`
TimeRange *timeRange `xml:"time-range,omitempty"`
TextMatch *textMatch `xml:"text-match,omitempty"` TextMatch *textMatch `xml:"text-match,omitempty"`
// TODO: param-filter
} }
// https://tools.ietf.org/html/rfc4791#section-9.7.5 // https://tools.ietf.org/html/rfc4791#section-9.7.5
type textMatch struct { type textMatch struct {
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav text-match"` XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav text-match"`
Text string `xml:",chardata"` Text string `xml:",chardata"`
// TODO: collation, negate-condition 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 // https://tools.ietf.org/html/rfc4791#section-9.9

View File

@ -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") return internal.HTTPErrorf(http.StatusBadRequest, "caldav: expected calendar-query or calendar-multiget element in REPORT request")
} }
func decodePropFilter(el *propFilter) (*PropFilter, error) { func decodeParamFilter(el *paramFilter) (*ParamFilter, error) {
pf := &PropFilter{Name: el.Name} 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 { if el.TextMatch != nil {
pf.TextMatch = &TextMatch{Text: el.TextMatch.Text} 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(&paramEl)
if err != nil {
return nil, err
}
pf.ParamFilter = append(pf.ParamFilter, *paramFi)
}
return pf, nil return pf, nil
} }
func decodeCompFilter(el *compFilter) (*CompFilter, error) { func decodeCompFilter(el *compFilter) (*CompFilter, error) {
cf := &CompFilter{Name: el.Name} 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 { if el.TimeRange != nil {
cf.Start = time.Time(el.TimeRange.Start) cf.Start = time.Time(el.TimeRange.Start)
cf.End = time.Time(el.TimeRange.End) cf.End = time.Time(el.TimeRange.End)
@ -98,7 +134,6 @@ func decodeCompFilter(el *compFilter) (*CompFilter, error) {
} }
cf.Comps = append(cf.Comps, *child) cf.Comps = append(cf.Comps, *child)
} }
// TODO: IsNotDefined
return cf, nil return cf, nil
} }