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..e31f678 100644
--- a/caldav/server_test.go
+++ b/caldav/server_test.go
@@ -4,7 +4,7 @@ import (
"context"
"fmt"
"io"
- "io/ioutil"
+ "net/http"
"net/http/httptest"
"strings"
"testing"
@@ -33,12 +33,12 @@ 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()
defer res.Body.Close()
- data, err := ioutil.ReadAll(res.Body)
+ data, err := io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
@@ -68,12 +68,12 @@ 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()
defer res.Body.Close()
- data, err := ioutil.ReadAll(res.Body)
+ data, err := io.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
@@ -83,6 +83,108 @@ func TestPropFindRoot(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")
+ }
+}
+
var reportCalendarData = `
@@ -118,7 +220,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},
@@ -128,7 +230,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 +249,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 +269,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)
}
@@ -182,8 +284,15 @@ type testBackend struct {
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) {
@@ -207,7 +316,7 @@ func (t testBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
return "/user/", nil
}
-func (t testBackend) DeleteCalendarObject(ctx context.Context, path string) error {
+func (t *testBackend) DeleteCalendarObject(ctx context.Context, path string) error {
return nil
}
@@ -222,7 +331,7 @@ func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *Ca
return nil, fmt.Errorf("Couldn't find calendar object at: %s", path)
}
-func (t testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) {
+func (t *testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) {
return nil, nil
}