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) + } }