mirror of
https://github.com/1f349/go-webdav.git
synced 2024-12-22 08:14:15 +00:00
13fa812f94
This is not yet complete (see TODOs in code), but basic filtering of a list of CaledarObjects works. Includes test data from the RFC, which allows to use the RFCs examples as test cases.
199 lines
4.5 KiB
Go
199 lines
4.5 KiB
Go
package caldav
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/emersion/go-ical"
|
|
)
|
|
|
|
// Filter returns the filtered list of calendar objects matching the provided query.
|
|
// A nil query will return the full list of calendar objects.
|
|
func Filter(query *CalendarQuery, cos []CalendarObject) ([]CalendarObject, error) {
|
|
if query == nil {
|
|
// FIXME: should we always return a copy of the provided slice?
|
|
return cos, nil
|
|
}
|
|
|
|
var out []CalendarObject
|
|
for _, co := range cos {
|
|
ok, err := Match(query.CompFilter, &co)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// TODO properties are not currently filtered even if requested
|
|
out = append(out, co)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Match reports whether the provided CalendarObject matches the query.
|
|
func Match(query CompFilter, co *CalendarObject) (matched bool, err error) {
|
|
if co.Data == nil || co.Data.Component == nil {
|
|
panic("request to process empty calendar object")
|
|
}
|
|
return match(query, co.Data.Component)
|
|
}
|
|
|
|
func match(filter CompFilter, comp *ical.Component) (bool, error) {
|
|
if comp.Name != filter.Name {
|
|
return filter.IsNotDefined, nil
|
|
}
|
|
|
|
var zeroDate time.Time
|
|
if filter.Start != zeroDate {
|
|
match, err := matchCompTimeRange(filter.Start, filter.End, comp)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
for _, compFilter := range filter.Comps {
|
|
match, err := matchCompFilter(compFilter, comp)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
for _, propFilter := range filter.Props {
|
|
match, err := matchPropFilter(propFilter, comp)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func matchCompFilter(filter CompFilter, comp *ical.Component) (bool, error) {
|
|
var matches []*ical.Component
|
|
|
|
for _, child := range comp.Children {
|
|
match, err := match(filter, child)
|
|
if err != nil {
|
|
return false, err
|
|
} else if match {
|
|
matches = append(matches, child)
|
|
}
|
|
}
|
|
if len(matches) == 0 {
|
|
return filter.IsNotDefined, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func matchPropFilter(filter PropFilter, comp *ical.Component) (bool, error) {
|
|
// TODO: this only matches first field, there can be multiple
|
|
field := comp.Props.Get(filter.Name)
|
|
if field == nil {
|
|
return filter.IsNotDefined, nil
|
|
}
|
|
|
|
for _, paramFilter := range filter.ParamFilter {
|
|
if !matchParamFilter(paramFilter, field) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
var zeroDate time.Time
|
|
if filter.Start != zeroDate {
|
|
match, err := matchPropTimeRange(filter.Start, filter.End, field)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
} else if filter.TextMatch != nil {
|
|
if !matchTextMatch(*filter.TextMatch, field.Value) {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
// empty prop-filter, property exists
|
|
return true, nil
|
|
}
|
|
|
|
func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error) {
|
|
// See https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
|
|
|
|
// TODO handle "infinity" values in query
|
|
// TODO handle recurring events
|
|
|
|
if comp.Name != ical.CompEvent {
|
|
return false, nil
|
|
}
|
|
event := ical.Event{comp}
|
|
|
|
eventStart, err := event.DateTimeStart(start.Location())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
eventEnd, err := event.DateTimeEnd(end.Location())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Event starts in time range
|
|
if eventStart.After(start) && eventStart.Before(end) {
|
|
return true, nil
|
|
}
|
|
// Event ends in time range
|
|
if eventEnd.After(start) && eventEnd.Before(end) {
|
|
return true, nil
|
|
}
|
|
// Event covers entire time range plus some
|
|
if eventStart.Before(start) && eventEnd.After(end) {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func matchPropTimeRange(start, end time.Time, field *ical.Prop) (bool, error) {
|
|
// See https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
|
|
|
|
// TODO handle "infinity" values in query
|
|
|
|
ptime, err := field.DateTime(start.Location())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if ptime.After(start) && ptime.Before(end) {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func matchParamFilter(filter ParamFilter, field *ical.Prop) bool {
|
|
// TODO there can be multiple values
|
|
value := field.Params.Get(filter.Name)
|
|
if value == "" {
|
|
return filter.IsNotDefined
|
|
} else if filter.IsNotDefined {
|
|
return false
|
|
}
|
|
if filter.TextMatch != nil {
|
|
return matchTextMatch(*filter.TextMatch, value)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func matchTextMatch(txt TextMatch, value string) bool {
|
|
// TODO: handle text-match collation attribute
|
|
match := strings.Contains(value, txt.Text)
|
|
if txt.NegateCondition {
|
|
match = !match
|
|
}
|
|
return match
|
|
}
|