Skip to content

Commit ef208fe

Browse files
committed
handle mkcalendar
1 parent 3cc7466 commit ef208fe

File tree

3 files changed

+179
-15
lines changed

3 files changed

+179
-15
lines changed

caldav/elements.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,11 @@ type mkcolReq struct {
235235
DisplayName string `xml:"set>prop>displayname"`
236236
// TODO this could theoretically contain all addressbook properties?
237237
}
238+
239+
type mkcalendarReq struct {
240+
XMLName xml.Name `xml:"urn:ietf:params:xml:ns:caldav mkcalendar"`
241+
SupportedCalendarComponentSet supportedCalendarComponentSet `xml:"set>prop>supported-calendar-component-set"`
242+
DisplayName string `xml:"set>prop>displayname"`
243+
CalendarDescription string `xml:"set>prop>calendar-description"`
244+
// TODO this could also contain max-resource-size, calendar-timezone, calendar-color, etc...
245+
}

caldav/server.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7373
switch r.Method {
7474
case "REPORT":
7575
err = h.handleReport(w, r)
76+
case "MKCALENDAR":
77+
err = h.handleMkCalendar(w, r)
7678
default:
7779
b := backend{
7880
Backend: h.Backend,
@@ -87,6 +89,51 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8789
}
8890
}
8991

92+
func (h *Handler) handleMkCalendar(w http.ResponseWriter, r *http.Request) error {
93+
if (&backend{}).resourceTypeAtPath(r.URL.Path) != resourceTypeCalendar {
94+
return internal.HTTPErrorf(http.StatusForbidden, "caldav: calendar creation not allowed at given location")
95+
}
96+
97+
cal := Calendar{
98+
Path: r.URL.Path,
99+
}
100+
101+
if !internal.IsRequestBodyEmpty(r) {
102+
var m mkcalendarReq
103+
if err := internal.DecodeXMLRequest(r, &m); err != nil {
104+
return internal.HTTPErrorf(http.StatusBadRequest, "caldav: error parsing mkcalendar request: %s", err.Error())
105+
}
106+
107+
cal.Name = m.DisplayName
108+
cal.Description = m.CalendarDescription
109+
110+
if d := len(m.SupportedCalendarComponentSet.Comp); d != 0 {
111+
cal.SupportedComponentSet = make([]string, len(m.SupportedCalendarComponentSet.Comp))
112+
for k, v := range m.SupportedCalendarComponentSet.Comp {
113+
cal.SupportedComponentSet[k] = v.Name
114+
}
115+
}
116+
117+
// TODO other props submitted by iOS: calendar-timezone, calendar-color, calendar-free-busy-set, calendar-order
118+
// TODO other props which should be handled: max-resource-size
119+
}
120+
121+
if e := h.Backend.CreateCalendar(r.Context(), &cal); e != nil {
122+
return internal.HTTPErrorf(http.StatusInternalServerError, "caldav: error parsing mkcalendar request: %s", e.Error())
123+
} else {
124+
w.Header().Add("Location", cal.Path)
125+
w.Header().Add("Content-Length", "0")
126+
w.WriteHeader(http.StatusCreated)
127+
//
128+
// If a response body for a successful request is included, it MUST
129+
// be a CALDAV:mkcalendar-response XML element.
130+
// <!ELEMENT mkcalendar-response ANY>
131+
//
132+
return nil
133+
}
134+
135+
}
136+
90137
func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) error {
91138
var report reportReq
92139
if err := internal.DecodeXMLRequest(r, &report); err != nil {
@@ -720,11 +767,11 @@ func (b *backend) Mkcol(r *http.Request) error {
720767
if !internal.IsRequestBodyEmpty(r) {
721768
var m mkcolReq
722769
if err := internal.DecodeXMLRequest(r, &m); err != nil {
723-
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: error parsing mkcol request: %s", err.Error())
770+
return internal.HTTPErrorf(http.StatusBadRequest, "caldav: error parsing mkcol request: %s", err.Error())
724771
}
725772

726773
if !m.ResourceType.Is(internal.CollectionName) || !m.ResourceType.Is(calendarName) {
727-
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: unexpected resource type")
774+
return internal.HTTPErrorf(http.StatusBadRequest, "caldav: unexpected resource type")
728775
}
729776
cal.Name = m.DisplayName
730777
// TODO ...

caldav/server_test.go

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"io/ioutil"
7+
"net/http"
88
"net/http/httptest"
99
"strings"
1010
"testing"
@@ -33,12 +33,12 @@ func TestPropFindSupportedCalendarComponent(t *testing.T) {
3333
req.Body = io.NopCloser(strings.NewReader(propFindSupportedCalendarComponentRequest))
3434
req.Header.Set("Content-Type", "application/xml")
3535
w := httptest.NewRecorder()
36-
handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}}
36+
handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}}
3737
handler.ServeHTTP(w, req)
3838

3939
res := w.Result()
4040
defer res.Body.Close()
41-
data, err := ioutil.ReadAll(res.Body)
41+
data, err := io.ReadAll(res.Body)
4242
if err != nil {
4343
t.Error(err)
4444
}
@@ -68,12 +68,12 @@ func TestPropFindRoot(t *testing.T) {
6868
req.Header.Set("Content-Type", "application/xml")
6969
w := httptest.NewRecorder()
7070
calendar := &Calendar{}
71-
handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}}
71+
handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}}
7272
handler.ServeHTTP(w, req)
7373

7474
res := w.Result()
7575
defer res.Body.Close()
76-
data, err := ioutil.ReadAll(res.Body)
76+
data, err := io.ReadAll(res.Body)
7777
if err != nil {
7878
t.Error(err)
7979
}
@@ -83,6 +83,108 @@ func TestPropFindRoot(t *testing.T) {
8383
}
8484
}
8585

86+
const TestMkCalendarReq = `
87+
<?xml version="1.0" encoding="UTF-8"?>
88+
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
89+
<A:set xmlns:A="DAV:">
90+
<A:prop>
91+
<B:calendar-timezone>BEGIN:VCALENDAR&#13;
92+
VERSION:2.0&#13;
93+
PRODID:-//Apple Inc.//iPhone OS 18.1.1//EN&#13;
94+
CALSCALE:GREGORIAN&#13;
95+
BEGIN:VTIMEZONE&#13;
96+
TZID:Europe/Paris&#13;
97+
BEGIN:DAYLIGHT&#13;
98+
TZOFFSETFROM:+0100&#13;
99+
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU&#13;
100+
DTSTART:19810329T020000&#13;
101+
TZNAME:UTC+2&#13;
102+
TZOFFSETTO:+0200&#13;
103+
END:DAYLIGHT&#13;
104+
BEGIN:STANDARD&#13;
105+
TZOFFSETFROM:+0200&#13;
106+
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU&#13;
107+
DTSTART:19961027T030000&#13;
108+
TZNAME:UTC+1&#13;
109+
TZOFFSETTO:+0100&#13;
110+
END:STANDARD&#13;
111+
END:VTIMEZONE&#13;
112+
END:VCALENDAR&#13;
113+
</B:calendar-timezone>
114+
<D:calendar-order xmlns:D="http://apple.com/ns/ical/">2</D:calendar-order>
115+
<B:supported-calendar-component-set>
116+
<B:comp name="VEVENT"/>
117+
</B:supported-calendar-component-set>
118+
<D:calendar-color xmlns:D="http://apple.com/ns/ical/" symbolic-color="red">#FF2968</D:calendar-color>
119+
<A:displayname>test calendar</A:displayname>
120+
<B:calendar-free-busy-set>
121+
<NO/>
122+
</B:calendar-free-busy-set>
123+
</A:prop>
124+
</A:set>
125+
</B:mkcalendar>
126+
`
127+
128+
const propFindTest2 = `
129+
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
130+
<d:prop>
131+
<d:resourcetype/>
132+
<c:supported-calendar-component-set/>
133+
<d:displayname/>
134+
<c:max-resource-size/>
135+
<c:calendar-description/>
136+
</d:prop>
137+
</d:propfind>
138+
`
139+
140+
func TestMkCalendar(t *testing.T) {
141+
handler := Handler{Backend: &testBackend{
142+
calendars: []Calendar{},
143+
objectMap: map[string][]CalendarObject{},
144+
}}
145+
146+
req := httptest.NewRequest("MKCALENDAR", "/user/calendars/default/", strings.NewReader(TestMkCalendarReq))
147+
req.Header.Set("Content-Type", "application/xml")
148+
w := httptest.NewRecorder()
149+
handler.ServeHTTP(w, req)
150+
151+
res := w.Result()
152+
if e := res.Body.Close(); e != nil {
153+
t.Fatal(e)
154+
} else if loc := res.Header.Get("Location"); loc != "/user/calendars/default/" {
155+
t.Fatalf("unexpected location: %s", loc)
156+
} else if sc := res.StatusCode; sc != http.StatusCreated {
157+
t.Fatalf("unexpected status code: %d", sc)
158+
}
159+
160+
req = httptest.NewRequest("PROPFIND", "/user/calendars/default/", strings.NewReader(propFindTest2))
161+
req.Header.Set("Content-Type", "application/xml")
162+
req.Header.Set("Depth", "0")
163+
w = httptest.NewRecorder()
164+
handler.ServeHTTP(w, req)
165+
166+
res = w.Result()
167+
defer res.Body.Close()
168+
data, err := io.ReadAll(res.Body)
169+
if err != nil {
170+
t.Fatal(err)
171+
}
172+
resp := string(data)
173+
if !strings.Contains(resp, fmt.Sprintf("<href>%s</href>", "/user/calendars/default/")) {
174+
t.Fatalf("want calendar href in response")
175+
} else if !strings.Contains(resp, "<resourcetype xmlns=\"DAV:\">") {
176+
t.Fatalf("want resource type in response")
177+
} else if !strings.Contains(resp, "<collection xmlns=\"DAV:\"></collection>") {
178+
t.Fatalf("want collection resource type in response")
179+
} else if !strings.Contains(resp, "<calendar xmlns=\"urn:ietf:params:xml:ns:caldav\"></calendar>") {
180+
t.Fatalf("want calendar resource type in response")
181+
} else if !strings.Contains(resp, "<displayname xmlns=\"DAV:\">test calendar</displayname>") {
182+
t.Fatalf("want display name in response")
183+
} else if !strings.Contains(resp, "<supported-calendar-component-set xmlns=\"urn:ietf:params:xml:ns:caldav\"><comp xmlns=\"urn:ietf:params:xml:ns:caldav\" name=\"VEVENT\"></comp></supported-calendar-component-set>") {
184+
t.Fatalf("want supported-calendar-component-set in response")
185+
}
186+
}
187+
86188
var reportCalendarData = `
87189
<?xml version="1.0" encoding="UTF-8"?>
88190
<B:calendar-multiget xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
@@ -118,7 +220,7 @@ func TestMultiCalendarBackend(t *testing.T) {
118220
req := httptest.NewRequest("PROPFIND", "/user/calendars/", strings.NewReader(propFindUserPrincipal))
119221
req.Header.Set("Content-Type", "application/xml")
120222
w := httptest.NewRecorder()
121-
handler := Handler{Backend: testBackend{
223+
handler := Handler{Backend: &testBackend{
122224
calendars: calendars,
123225
objectMap: map[string][]CalendarObject{
124226
calendarB.Path: []CalendarObject{object},
@@ -128,7 +230,7 @@ func TestMultiCalendarBackend(t *testing.T) {
128230

129231
res := w.Result()
130232
defer res.Body.Close()
131-
data, err := ioutil.ReadAll(res.Body)
233+
data, err := io.ReadAll(res.Body)
132234
if err != nil {
133235
t.Error(err)
134236
}
@@ -147,7 +249,7 @@ func TestMultiCalendarBackend(t *testing.T) {
147249

148250
res = w.Result()
149251
defer res.Body.Close()
150-
data, err = ioutil.ReadAll(res.Body)
252+
data, err = io.ReadAll(res.Body)
151253
if err != nil {
152254
t.Error(err)
153255
}
@@ -167,7 +269,7 @@ func TestMultiCalendarBackend(t *testing.T) {
167269

168270
res = w.Result()
169271
defer res.Body.Close()
170-
data, err = ioutil.ReadAll(res.Body)
272+
data, err = io.ReadAll(res.Body)
171273
if err != nil {
172274
t.Error(err)
173275
}
@@ -182,8 +284,15 @@ type testBackend struct {
182284
objectMap map[string][]CalendarObject
183285
}
184286

185-
func (t testBackend) CreateCalendar(ctx context.Context, calendar *Calendar) error {
186-
return nil
287+
func (t *testBackend) CreateCalendar(ctx context.Context, calendar *Calendar) error {
288+
if v, e := t.CalendarHomeSetPath(ctx); e != nil {
289+
return e
290+
} else if !strings.HasPrefix(calendar.Path, v) || len(calendar.Path) == len(v) {
291+
return fmt.Errorf("cannot create calendar at location %s", calendar.Path)
292+
} else {
293+
t.calendars = append(t.calendars, *calendar)
294+
return nil
295+
}
187296
}
188297

189298
func (t testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) {
@@ -207,7 +316,7 @@ func (t testBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
207316
return "/user/", nil
208317
}
209318

210-
func (t testBackend) DeleteCalendarObject(ctx context.Context, path string) error {
319+
func (t *testBackend) DeleteCalendarObject(ctx context.Context, path string) error {
211320
return nil
212321
}
213322

@@ -222,7 +331,7 @@ func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *Ca
222331
return nil, fmt.Errorf("Couldn't find calendar object at: %s", path)
223332
}
224333

225-
func (t testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) {
334+
func (t *testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) {
226335
return nil, nil
227336
}
228337

0 commit comments

Comments
 (0)