From 99438eb854393799bd369de90b263cb9327992aa Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Wed, 15 Jan 2025 22:43:07 +0100 Subject: [PATCH] handle mkcalendar --- caldav/elements.go | 8 +++ caldav/server.go | 51 +++++++++++++++++- caldav/server_test.go | 121 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 173 insertions(+), 7 deletions(-) diff --git a/caldav/elements.go b/caldav/elements.go index 5759f31..6d6598e 100644 --- a/caldav/elements.go +++ b/caldav/elements.go @@ -235,3 +235,11 @@ type mkcolReq struct { DisplayName string `xml:"set>prop>displayname"` // TODO this could theoretically contain all addressbook properties? } + +type mkcalendarReq struct { + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav mkcalendar"` + SupportedCalendarComponentSet supportedCalendarComponentSet `xml:"set>prop>supported-calendar-component-set"` + DisplayName string `xml:"set>prop>displayname"` + CalendarDescription string `xml:"set>prop>calendar-description"` + // TODO this could also contain max-resource-size, calendar-timezone, calendar-color, etc... +} diff --git a/caldav/server.go b/caldav/server.go index 7ddfffc..1103098 100644 --- a/caldav/server.go +++ b/caldav/server.go @@ -73,6 +73,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case "REPORT": err = h.handleReport(w, r) + case "MKCALENDAR": + err = h.handleMkCalendar(w, r) default: b := backend{ Backend: h.Backend, @@ -87,6 +89,51 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (h *Handler) handleMkCalendar(w http.ResponseWriter, r *http.Request) error { + if (&backend{}).resourceTypeAtPath(r.URL.Path) != resourceTypeCalendar { + return internal.HTTPErrorf(http.StatusForbidden, "caldav: calendar creation not allowed at given location") + } + + cal := Calendar{ + Path: r.URL.Path, + } + + if !internal.IsRequestBodyEmpty(r) { + var m mkcalendarReq + if err := internal.DecodeXMLRequest(r, &m); err != nil { + return internal.HTTPErrorf(http.StatusBadRequest, "caldav: error parsing mkcalendar request: %s", err.Error()) + } + + cal.Name = m.DisplayName + cal.Description = m.CalendarDescription + + if d := len(m.SupportedCalendarComponentSet.Comp); d != 0 { + cal.SupportedComponentSet = make([]string, len(m.SupportedCalendarComponentSet.Comp)) + for k, v := range m.SupportedCalendarComponentSet.Comp { + cal.SupportedComponentSet[k] = v.Name + } + } + + // TODO other props submitted by iOS: calendar-timezone, calendar-color, calendar-free-busy-set, calendar-order + // TODO other props which should be handled: max-resource-size + } + + if e := h.Backend.CreateCalendar(r.Context(), &cal); e != nil { + return internal.HTTPErrorf(http.StatusInternalServerError, "caldav: error parsing mkcalendar request: %s", e.Error()) + } else { + w.Header().Add("Location", cal.Path) + w.Header().Add("Content-Length", "0") + w.WriteHeader(http.StatusCreated) + // + // If a response body for a successful request is included, it MUST + // be a CALDAV:mkcalendar-response XML element. + // + // + return nil + } + +} + func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) error { var report reportReq if err := internal.DecodeXMLRequest(r, &report); err != nil { @@ -720,11 +767,11 @@ func (b *backend) Mkcol(r *http.Request) error { if !internal.IsRequestBodyEmpty(r) { var m mkcolReq if err := internal.DecodeXMLRequest(r, &m); err != nil { - return internal.HTTPErrorf(http.StatusBadRequest, "carddav: error parsing mkcol request: %s", err.Error()) + return internal.HTTPErrorf(http.StatusBadRequest, "caldav: error parsing mkcol request: %s", err.Error()) } if !m.ResourceType.Is(internal.CollectionName) || !m.ResourceType.Is(calendarName) { - return internal.HTTPErrorf(http.StatusBadRequest, "carddav: unexpected resource type") + return internal.HTTPErrorf(http.StatusBadRequest, "caldav: unexpected resource type") } cal.Name = m.DisplayName // TODO ... diff --git a/caldav/server_test.go b/caldav/server_test.go index 594b87b..3fc7d04 100644 --- a/caldav/server_test.go +++ b/caldav/server_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "net/http/httptest" "strings" "testing" @@ -33,7 +34,7 @@ func TestPropFindSupportedCalendarComponent(t *testing.T) { req.Body = io.NopCloser(strings.NewReader(propFindSupportedCalendarComponentRequest)) req.Header.Set("Content-Type", "application/xml") w := httptest.NewRecorder() - handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}} + handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}} handler.ServeHTTP(w, req) res := w.Result() @@ -68,7 +69,7 @@ func TestPropFindRoot(t *testing.T) { req.Header.Set("Content-Type", "application/xml") w := httptest.NewRecorder() calendar := &Calendar{} - handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}} + handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}} handler.ServeHTTP(w, req) res := w.Result() @@ -118,7 +119,7 @@ func TestMultiCalendarBackend(t *testing.T) { req := httptest.NewRequest("PROPFIND", "/user/calendars/", strings.NewReader(propFindUserPrincipal)) req.Header.Set("Content-Type", "application/xml") w := httptest.NewRecorder() - handler := Handler{Backend: testBackend{ + handler := Handler{Backend: &testBackend{ calendars: calendars, objectMap: map[string][]CalendarObject{ calendarB.Path: []CalendarObject{object}, @@ -177,13 +178,123 @@ func TestMultiCalendarBackend(t *testing.T) { } } +const TestMkCalendarReq = ` + + + + + BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iPhone OS 18.1.1//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:UTC+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:UTC+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR + + 2 + + + + #FF2968 + test calendar + + + + + + +` + +const propFindTest2 = ` + + + + + + + + + +` + +func TestMkCalendar(t *testing.T) { + handler := Handler{Backend: &testBackend{ + calendars: []Calendar{}, + objectMap: map[string][]CalendarObject{}, + }} + + req := httptest.NewRequest("MKCALENDAR", "/user/calendars/default/", strings.NewReader(TestMkCalendarReq)) + req.Header.Set("Content-Type", "application/xml") + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + res := w.Result() + if e := res.Body.Close(); e != nil { + t.Fatal(e) + } else if loc := res.Header.Get("Location"); loc != "/user/calendars/default/" { + t.Fatalf("unexpected location: %s", loc) + } else if sc := res.StatusCode; sc != http.StatusCreated { + t.Fatalf("unexpected status code: %d", sc) + } + + req = httptest.NewRequest("PROPFIND", "/user/calendars/default/", strings.NewReader(propFindTest2)) + 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, fmt.Sprintf("%s", "/user/calendars/default/")) { + t.Fatalf("want calendar href in response") + } else if !strings.Contains(resp, "") { + t.Fatalf("want resource type in response") + } else if !strings.Contains(resp, "") { + t.Fatalf("want collection resource type in response") + } else if !strings.Contains(resp, "") { + t.Fatalf("want calendar resource type in response") + } else if !strings.Contains(resp, "test calendar") { + t.Fatalf("want display name in response") + } else if !strings.Contains(resp, "") { + t.Fatalf("want supported-calendar-component-set in response") + } +} + + type testBackend struct { calendars []Calendar objectMap map[string][]CalendarObject } -func (t testBackend) CreateCalendar(ctx context.Context, calendar *Calendar) error { - return nil +func (t *testBackend) CreateCalendar(ctx context.Context, calendar *Calendar) error { + if v, e := t.CalendarHomeSetPath(ctx); e != nil { + return e + } else if !strings.HasPrefix(calendar.Path, v) || len(calendar.Path) == len(v) { + return fmt.Errorf("cannot create calendar at location %s", calendar.Path) + } else { + t.calendars = append(t.calendars, *calendar) + return nil + } } func (t testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) {