Skip to content

Commit 99438eb

Browse files
committed
handle mkcalendar
1 parent 3cc7466 commit 99438eb

File tree

3 files changed

+173
-7
lines changed

3 files changed

+173
-7
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: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"io/ioutil"
8+
"net/http"
89
"net/http/httptest"
910
"strings"
1011
"testing"
@@ -33,7 +34,7 @@ func TestPropFindSupportedCalendarComponent(t *testing.T) {
3334
req.Body = io.NopCloser(strings.NewReader(propFindSupportedCalendarComponentRequest))
3435
req.Header.Set("Content-Type", "application/xml")
3536
w := httptest.NewRecorder()
36-
handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}}
37+
handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}}
3738
handler.ServeHTTP(w, req)
3839

3940
res := w.Result()
@@ -68,7 +69,7 @@ func TestPropFindRoot(t *testing.T) {
6869
req.Header.Set("Content-Type", "application/xml")
6970
w := httptest.NewRecorder()
7071
calendar := &Calendar{}
71-
handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}}
72+
handler := Handler{Backend: &testBackend{calendars: []Calendar{*calendar}}}
7273
handler.ServeHTTP(w, req)
7374

7475
res := w.Result()
@@ -118,7 +119,7 @@ func TestMultiCalendarBackend(t *testing.T) {
118119
req := httptest.NewRequest("PROPFIND", "/user/calendars/", strings.NewReader(propFindUserPrincipal))
119120
req.Header.Set("Content-Type", "application/xml")
120121
w := httptest.NewRecorder()
121-
handler := Handler{Backend: testBackend{
122+
handler := Handler{Backend: &testBackend{
122123
calendars: calendars,
123124
objectMap: map[string][]CalendarObject{
124125
calendarB.Path: []CalendarObject{object},
@@ -177,13 +178,123 @@ func TestMultiCalendarBackend(t *testing.T) {
177178
}
178179
}
179180

181+
const TestMkCalendarReq = `
182+
<?xml version="1.0" encoding="UTF-8"?>
183+
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
184+
<A:set xmlns:A="DAV:">
185+
<A:prop>
186+
<B:calendar-timezone>BEGIN:VCALENDAR&#13;
187+
VERSION:2.0&#13;
188+
PRODID:-//Apple Inc.//iPhone OS 18.1.1//EN&#13;
189+
CALSCALE:GREGORIAN&#13;
190+
BEGIN:VTIMEZONE&#13;
191+
TZID:Europe/Paris&#13;
192+
BEGIN:DAYLIGHT&#13;
193+
TZOFFSETFROM:+0100&#13;
194+
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU&#13;
195+
DTSTART:19810329T020000&#13;
196+
TZNAME:UTC+2&#13;
197+
TZOFFSETTO:+0200&#13;
198+
END:DAYLIGHT&#13;
199+
BEGIN:STANDARD&#13;
200+
TZOFFSETFROM:+0200&#13;
201+
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU&#13;
202+
DTSTART:19961027T030000&#13;
203+
TZNAME:UTC+1&#13;
204+
TZOFFSETTO:+0100&#13;
205+
END:STANDARD&#13;
206+
END:VTIMEZONE&#13;
207+
END:VCALENDAR&#13;
208+
</B:calendar-timezone>
209+
<D:calendar-order xmlns:D="http://apple.com/ns/ical/">2</D:calendar-order>
210+
<B:supported-calendar-component-set>
211+
<B:comp name="VEVENT"/>
212+
</B:supported-calendar-component-set>
213+
<D:calendar-color xmlns:D="http://apple.com/ns/ical/" symbolic-color="red">#FF2968</D:calendar-color>
214+
<A:displayname>test calendar</A:displayname>
215+
<B:calendar-free-busy-set>
216+
<NO/>
217+
</B:calendar-free-busy-set>
218+
</A:prop>
219+
</A:set>
220+
</B:mkcalendar>
221+
`
222+
223+
const propFindTest2 = `
224+
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
225+
<d:prop>
226+
<d:resourcetype/>
227+
<c:supported-calendar-component-set/>
228+
<d:displayname/>
229+
<c:max-resource-size/>
230+
<c:calendar-description/>
231+
</d:prop>
232+
</d:propfind>
233+
`
234+
235+
func TestMkCalendar(t *testing.T) {
236+
handler := Handler{Backend: &testBackend{
237+
calendars: []Calendar{},
238+
objectMap: map[string][]CalendarObject{},
239+
}}
240+
241+
req := httptest.NewRequest("MKCALENDAR", "/user/calendars/default/", strings.NewReader(TestMkCalendarReq))
242+
req.Header.Set("Content-Type", "application/xml")
243+
w := httptest.NewRecorder()
244+
handler.ServeHTTP(w, req)
245+
246+
res := w.Result()
247+
if e := res.Body.Close(); e != nil {
248+
t.Fatal(e)
249+
} else if loc := res.Header.Get("Location"); loc != "/user/calendars/default/" {
250+
t.Fatalf("unexpected location: %s", loc)
251+
} else if sc := res.StatusCode; sc != http.StatusCreated {
252+
t.Fatalf("unexpected status code: %d", sc)
253+
}
254+
255+
req = httptest.NewRequest("PROPFIND", "/user/calendars/default/", strings.NewReader(propFindTest2))
256+
req.Header.Set("Content-Type", "application/xml")
257+
req.Header.Set("Depth", "0")
258+
w = httptest.NewRecorder()
259+
handler.ServeHTTP(w, req)
260+
261+
res = w.Result()
262+
defer res.Body.Close()
263+
data, err := io.ReadAll(res.Body)
264+
if err != nil {
265+
t.Fatal(err)
266+
}
267+
resp := string(data)
268+
if !strings.Contains(resp, fmt.Sprintf("<href>%s</href>", "/user/calendars/default/")) {
269+
t.Fatalf("want calendar href in response")
270+
} else if !strings.Contains(resp, "<resourcetype xmlns=\"DAV:\">") {
271+
t.Fatalf("want resource type in response")
272+
} else if !strings.Contains(resp, "<collection xmlns=\"DAV:\"></collection>") {
273+
t.Fatalf("want collection resource type in response")
274+
} else if !strings.Contains(resp, "<calendar xmlns=\"urn:ietf:params:xml:ns:caldav\"></calendar>") {
275+
t.Fatalf("want calendar resource type in response")
276+
} else if !strings.Contains(resp, "<displayname xmlns=\"DAV:\">test calendar</displayname>") {
277+
t.Fatalf("want display name in response")
278+
} 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>") {
279+
t.Fatalf("want supported-calendar-component-set in response")
280+
}
281+
}
282+
283+
180284
type testBackend struct {
181285
calendars []Calendar
182286
objectMap map[string][]CalendarObject
183287
}
184288

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

189300
func (t testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) {

0 commit comments

Comments
 (0)