mirror of
https://github.com/1f349/go-webdav.git
synced 2024-12-23 00:34:23 +00:00
5b5b542f2f
Matches on open time ranges (i.e. no end date) were not properly handled, as `end` is simply the zero time, which confuses the `.Before()` and `.After()` logic employed here. This commit fixes that by adding the appropriate `.IsZero()` checks and also adds a test case. The current behavior unfortunately broke compatibility with DAVx5, which by default queries only events less than 90 days ago (by using an open time range).
206 lines
4.8 KiB
Go
206 lines
4.8 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
|
|
|
|
// evaluate recurring components
|
|
rset, err := comp.RecurrenceSet(start.Location())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if rset != nil {
|
|
// TODO we can only set inclusive to true or false, but really the
|
|
// start time is inclusive while the end time is not :/
|
|
return len(rset.Between(start, end, true)) > 0, nil
|
|
}
|
|
|
|
// TODO handle more than just 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) && (end.IsZero() || eventStart.Before(end)) {
|
|
return true, nil
|
|
}
|
|
// Event ends in time range
|
|
if eventEnd.After(start) && (end.IsZero() || eventEnd.Before(end)) {
|
|
return true, nil
|
|
}
|
|
// Event covers entire time range plus some
|
|
if eventStart.Before(start) && (!end.IsZero() && 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
|
|
|
|
ptime, err := field.DateTime(start.Location())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if ptime.After(start) && (end.IsZero() || 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
|
|
}
|