diff --git a/caldav/server.go b/caldav/server.go
index 7ddfffcb..fdc93a40 100644
--- a/caldav/server.go
+++ b/caldav/server.go
@@ -228,6 +228,7 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *cal
Prefix: strings.TrimSuffix(h.Prefix, "/"),
}
propfind := internal.PropFind{
+ XMLName: query.XMLName,
Prop: query.Prop,
AllProp: query.AllProp,
PropName: query.PropName,
@@ -272,6 +273,7 @@ func (h *Handler) handleMultiget(ctx context.Context, w http.ResponseWriter, mul
Prefix: strings.TrimSuffix(h.Prefix, "/"),
}
propfind := internal.PropFind{
+ XMLName: multiget.XMLName,
Prop: multiget.Prop,
AllProp: multiget.AllProp,
PropName: multiget.PropName,
@@ -465,11 +467,14 @@ func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind)
}
props := map[xml.Name]internal.PropFindFunc{
- internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
- Href: internal.Href{Path: principalPath},
- }),
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
}
+ if propfind.AllProp == nil {
+ props[internal.CurrentUserPrincipalName] = internal.PropFindValue(&internal.CurrentUserPrincipal{
+ Href: internal.Href{Path: principalPath},
+ })
+ }
+
return internal.NewPropFindResponse(principalPath, propfind, props)
}
@@ -478,28 +483,28 @@ func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.
if err != nil {
return nil, err
}
- homeSetPath, err := b.Backend.CalendarHomeSetPath(ctx)
- if err != nil {
- return nil, err
- }
props := map[xml.Name]internal.PropFindFunc{
- internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
- Href: internal.Href{Path: principalPath},
- }),
- calendarHomeSetName: internal.PropFindValue(&calendarHomeSet{
- Href: internal.Href{Path: homeSetPath},
- }),
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
}
+
+ if propfind.AllProp == nil {
+ props[internal.CurrentUserPrincipalName] = internal.PropFindValue(&internal.CurrentUserPrincipal{
+ Href: internal.Href{Path: principalPath},
+ })
+ props[calendarHomeSetName] = func(*internal.RawXMLValue) (interface{}, error) {
+ homeSetPath, err := b.Backend.CalendarHomeSetPath(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &calendarHomeSet{Href: internal.Href{Path: homeSetPath}}, nil
+ }
+ }
+
return internal.NewPropFindResponse(principalPath, propfind, props)
}
func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
- principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
- if err != nil {
- return nil, err
- }
homeSetPath, err := b.Backend.CalendarHomeSetPath(ctx)
if err != nil {
return nil, err
@@ -507,33 +512,40 @@ func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFi
// TODO anything else to return here?
props := map[xml.Name]internal.PropFindFunc{
- internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
- Href: internal.Href{Path: principalPath},
- }),
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
}
+
+ if propfind.AllProp == nil {
+ props[internal.CurrentUserPrincipalName] = func(*internal.RawXMLValue) (interface{}, error) {
+ path, err := b.Backend.CurrentUserPrincipal(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
+ }
+ }
+
return internal.NewPropFindResponse(homeSetPath, propfind, props)
}
func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropFind, cal *Calendar) (*internal.Response, error) {
props := map[xml.Name]internal.PropFindFunc{
- internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
+ internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, calendarName)),
+ }
+ if propfind.AllProp == nil {
+ props[internal.CurrentUserPrincipalName] = func(*internal.RawXMLValue) (interface{}, error) {
path, err := b.Backend.CurrentUserPrincipal(ctx)
if err != nil {
return nil, err
}
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
- },
- internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, calendarName)),
- calendarDescriptionName: internal.PropFindValue(&calendarDescription{
- Description: cal.Description,
- }),
- supportedCalendarDataName: internal.PropFindValue(&supportedCalendarData{
+ }
+ props[supportedCalendarDataName] = internal.PropFindValue(&supportedCalendarData{
Types: []calendarDataType{
{ContentType: ical.MIMEType, Version: "2.0"},
},
- }),
- supportedCalendarComponentSetName: func(*internal.RawXMLValue) (interface{}, error) {
+ })
+ props[supportedCalendarComponentSetName] = func(*internal.RawXMLValue) (interface{}, error) {
components := []comp{}
if cal.SupportedComponentSet != nil {
for _, name := range cal.SupportedComponentSet {
@@ -545,7 +557,17 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF
return &supportedCalendarComponentSet{
Comp: components,
}, nil
- },
+ }
+ if cal.Description != "" {
+ props[calendarDescriptionName] = internal.PropFindValue(&calendarDescription{
+ Description: cal.Description,
+ })
+ }
+ if cal.MaxResourceSize > 0 {
+ props[maxResourceSizeName] = internal.PropFindValue(&maxResourceSize{
+ Size: cal.MaxResourceSize,
+ })
+ }
}
if cal.Name != "" {
@@ -553,16 +575,6 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF
Name: cal.Name,
})
}
- if cal.Description != "" {
- props[calendarDescriptionName] = internal.PropFindValue(&calendarDescription{
- Description: cal.Description,
- })
- }
- if cal.MaxResourceSize > 0 {
- props[maxResourceSizeName] = internal.PropFindValue(&maxResourceSize{
- Size: cal.MaxResourceSize,
- })
- }
// TODO: CALDAV:calendar-timezone, CALDAV:supported-calendar-component-set, CALDAV:min-date-time, CALDAV:max-date-time, CALDAV:max-instances, CALDAV:max-attendees-per-instance
@@ -595,25 +607,30 @@ func (b *backend) propFindAllCalendars(ctx context.Context, propfind *internal.P
func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal.PropFind, co *CalendarObject) (*internal.Response, error) {
props := map[xml.Name]internal.PropFindFunc{
- internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
+ internal.GetContentTypeName: internal.PropFindValue(&internal.GetContentType{
+ Type: ical.MIMEType,
+ }),
+ }
+
+ if propfind.AllProp == nil {
+ props[internal.CurrentUserPrincipalName] = func(*internal.RawXMLValue) (interface{}, error) {
path, err := b.Backend.CurrentUserPrincipal(ctx)
if err != nil {
return nil, err
}
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
- },
- internal.GetContentTypeName: internal.PropFindValue(&internal.GetContentType{
- Type: ical.MIMEType,
- }),
- // TODO: calendar-data can only be used in REPORT requests
- calendarDataName: func(*internal.RawXMLValue) (interface{}, error) {
+ }
+ }
+
+ if n := propfind.XMLName; n == calendarQueryName || n == calendarMultigetName {
+ props[calendarDataName] = func(*internal.RawXMLValue) (interface{}, error) {
var buf bytes.Buffer
if err := ical.NewEncoder(&buf).Encode(co.Data); err != nil {
return nil, err
}
return &calendarDataResp{Data: buf.Bytes()}, nil
- },
+ }
}
if co.ContentLength > 0 {
@@ -621,6 +638,7 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal
Length: co.ContentLength,
})
}
+
if !co.ModTime.IsZero() {
props[internal.GetLastModifiedName] = internal.PropFindValue(&internal.GetLastModified{
LastModified: internal.Time(co.ModTime),
diff --git a/caldav/server_test.go b/caldav/server_test.go
index 594b87b6..4476e382 100644
--- a/caldav/server_test.go
+++ b/caldav/server_test.go
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
- "io/ioutil"
"net/http/httptest"
"strings"
"testing"
@@ -38,7 +37,7 @@ func TestPropFindSupportedCalendarComponent(t *testing.T) {
res := w.Result()
defer res.Body.Close()
- data, err := ioutil.ReadAll(res.Body)
+ data, err := io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
@@ -73,7 +72,7 @@ func TestPropFindRoot(t *testing.T) {
res := w.Result()
defer res.Body.Close()
- data, err := ioutil.ReadAll(res.Body)
+ data, err := io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
@@ -128,7 +127,7 @@ func TestMultiCalendarBackend(t *testing.T) {
res := w.Result()
defer res.Body.Close()
- data, err := ioutil.ReadAll(res.Body)
+ data, err := io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
@@ -147,7 +146,7 @@ func TestMultiCalendarBackend(t *testing.T) {
res = w.Result()
defer res.Body.Close()
- data, err = ioutil.ReadAll(res.Body)
+ data, err = io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
@@ -167,7 +166,7 @@ func TestMultiCalendarBackend(t *testing.T) {
res = w.Result()
defer res.Body.Close()
- data, err = ioutil.ReadAll(res.Body)
+ data, err = io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
@@ -177,6 +176,311 @@ func TestMultiCalendarBackend(t *testing.T) {
}
}
+var propFindAllProp = `
+
+
+
+
+`
+
+var reportTest1 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+var propFindTest1 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+var calendarTestData1 = `
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
+DTSTAMP:20060206T001220Z
+DTSTART;TZID=US/Eastern:20060104T100000
+DURATION:PT1H
+LAST-MODIFIED:20060206T001330Z
+ORGANIZER:mailto:cyrus@example.com
+SEQUENCE:1
+STATUS:TENTATIVE
+SUMMARY:Event #3
+UID:DC6C50A017428C5216A2F1CD@example.com
+X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
+END:VEVENT
+END:VCALENDAR
+`
+
+func TestPropFindAllPropAndQuery(t *testing.T) {
+ calendar := Calendar{
+ Description: "This is a description which SHOULD NOT be returned in allprop",
+ Path: "/user/calendars/default/",
+ SupportedComponentSet: []string{"VEVENT", "VTODO"},
+ }
+ cal, err := ical.NewDecoder(strings.NewReader(calendarTestData1)).Decode()
+ if err != nil {
+ t.Fatal(err)
+ }
+ object := CalendarObject{
+ Path: "/user/calendars/default/DC6C50A017428C5216A2F1CD.ics",
+ Data: cal,
+ ETag: "191382932849",
+ }
+ handler := Handler{Backend: testBackend{
+ calendars: []Calendar{calendar},
+ objectMap: map[string][]CalendarObject{
+ calendar.Path: []CalendarObject{object},
+ },
+ }}
+
+ req := httptest.NewRequest("PROPFIND", "/user/calendars/default/", strings.NewReader(propFindAllProp))
+ req.Header.Set("Content-Type", "application/xml")
+ req.Header.Set("Depth", "0")
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+
+ res := w.Result()
+ defer res.Body.Close()
+ data, err := io.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp := string(data)
+ if !strings.Contains(resp, "") {
+ t.Fatalf("want resourcetype prop in allprop")
+ } else if !strings.Contains(resp, "") {
+ t.Fatalf("want collection resourcetype")
+ } else if !strings.Contains(resp, "") {
+ t.Fatalf("expect calendar resourcetype")
+ } else if strings.Contains(resp, "") {
+ t.Fatalf("do not want calendar-description in allprop")
+ } else if strings.Contains(resp, "DC6C50A017428C5216A2F1CD.ics") {
+ t.Fatalf("do not want children if Depth: 0")
+ }
+
+ req = httptest.NewRequest("PROPFIND", "/user/calendars/default/", strings.NewReader(propFindAllProp))
+ req.Header.Set("Content-Type", "application/xml")
+ req.Header.Set("Depth", "1")
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+
+ res = w.Result()
+ defer res.Body.Close()
+ data, err = io.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp = string(data)
+ if !strings.Contains(resp, fmt.Sprintf("%s", object.Path)) {
+ t.Fatalf("want child href in allprop")
+ } else if !strings.Contains(resp, object.ETag) {
+ t.Fatalf("want child ETag in allprop")
+ } else if !strings.Contains(resp, "text/calendar") {
+ t.Fatalf("want child getcontenttype in allprop")
+ } else if strings.Contains(resp, "") {
+ t.Fatalf("do not want calendar-data in allprop")
+ }
+
+ req = httptest.NewRequest("REPORT", "/user/calendars/default/", strings.NewReader(reportTest1))
+ req.Header.Set("Content-Type", "application/xml")
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+
+ res = w.Result()
+ defer res.Body.Close()
+ data, err = io.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp = string(data)
+ if !strings.Contains(resp, fmt.Sprintf("%s", object.Path)) {
+ t.Fatalf("want child href in REPORT")
+ } else if !strings.Contains(resp, object.ETag) {
+ t.Fatalf("want child ETag in REPORT")
+ } else if !strings.Contains(resp, "text/calendar") {
+ t.Fatalf("want child getcontenttype in REPORT")
+ } else if !strings.Contains(resp, "") {
+ t.Fatalf("do want calendar-data in REPORT")
+ } else if !strings.Contains(resp, "UID:DC6C50A017428C5216A2F1CD@example.com") {
+ t.Fatalf("calendar-data improperly returned")
+ }
+
+ req = httptest.NewRequest("PROPFIND", object.Path, strings.NewReader(propFindTest1))
+ req.Header.Set("Content-Type", "application/xml")
+ w = httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+
+ res = w.Result()
+ defer res.Body.Close()
+ data, err = io.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp = string(data)
+ if strings.Contains(resp, "UID:DC6C50A017428C5216A2F1CD@example.com") {
+ t.Fatalf("do not want calendar data in PROPFIND")
+ } else if !strings.Contains(resp, object.ETag) {
+ t.Fatalf("want child ETag in PROPFIND")
+ } else if !strings.Contains(resp, "text/calendar") {
+ t.Fatalf("want child getcontenttype in PROPFIND")
+ }
+}
+
+var calendarTestData2 = `
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTAMP:20060206T001102Z
+DTSTART;TZID=US/Eastern:20060102T100000
+DURATION:PT1H
+SUMMARY:Event #1
+Description:Go Steelers!
+UID:74855313FA803DA593CD579A@example.com
+END:VEVENT
+END:VCALENDAR
+`
+
+var multigetTest1 = `
+
+
+
+
+
+
+ /user/calendars/default/74855313FA803DA593CD579A.ics
+ /user/calendars/default/DC6C50A017428C5216A2F1CD.ics
+
+`
+
+func TestFindMultiget(t *testing.T) {
+ calendar := Calendar{
+ Description: "This is a description which SHOULD NOT be returned in allprop",
+ Path: "/user/calendars/default/",
+ SupportedComponentSet: []string{"VEVENT"},
+ }
+ cal, err := ical.NewDecoder(strings.NewReader(calendarTestData1)).Decode()
+ if err != nil {
+ t.Fatal(err)
+ }
+ object1 := CalendarObject{
+ Path: "/user/calendars/default/DC6C50A017428C5216A2F1CD.ics",
+ Data: cal,
+ ETag: "191382932849",
+ }
+ cal, err = ical.NewDecoder(strings.NewReader(calendarTestData2)).Decode()
+ if err != nil {
+ t.Fatal(err)
+ }
+ object2 := CalendarObject{
+ Path: "/user/calendars/default/74855313FA803DA593CD579A.ics",
+ Data: cal,
+ ETag: "191382932850",
+ }
+ handler := Handler{Backend: testBackend{
+ calendars: []Calendar{calendar},
+ objectMap: map[string][]CalendarObject{
+ calendar.Path: []CalendarObject{object1, object2},
+ },
+ }}
+
+ req := httptest.NewRequest("REPORT", "/user/calendars/default/", strings.NewReader(multigetTest1))
+ req.Header.Set("Content-Type", "application/xml")
+ req.Header.Set("Depth", "1")
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+
+ res := w.Result()
+ defer res.Body.Close()
+ data, err := io.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp := string(data)
+ if !strings.Contains(resp, "UID:DC6C50A017428C5216A2F1CD@example.com") {
+ t.Fatalf("want object1 in multiget report")
+ } else if !strings.Contains(resp, "UID:74855313FA803DA593CD579A@example.com") {
+ t.Fatalf("want object2 in multiget report")
+ } else if !strings.Contains(resp, object1.ETag) {
+ t.Fatalf("want object1 ETag in multiget report")
+ } else if !strings.Contains(resp, object2.ETag) {
+ t.Fatalf("want object2 ETag in multiget report")
+ }
+}
+
type testBackend struct {
calendars []Calendar
objectMap map[string][]CalendarObject
@@ -231,5 +535,9 @@ func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *
}
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) {
- return nil, nil
+ if cos, err := t.ListCalendarObjects(ctx, path, nil); err != nil {
+ return nil, err
+ } else {
+ return Filter(query, cos)
+ }
}