Skip to content

Commit

Permalink
handle mkcalendar
Browse files Browse the repository at this point in the history
  • Loading branch information
barkyq committed Jan 15, 2025
1 parent 3cc7466 commit ef208fe
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 15 deletions.
8 changes: 8 additions & 0 deletions caldav/elements.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...
}
51 changes: 49 additions & 2 deletions caldav/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
// <!ELEMENT mkcalendar-response ANY>
//
return nil
}

}

func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) error {
var report reportReq
if err := internal.DecodeXMLRequest(r, &report); err != nil {
Expand Down Expand Up @@ -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 ...
Expand Down
135 changes: 122 additions & 13 deletions caldav/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -83,6 +83,108 @@ func TestPropFindRoot(t *testing.T) {
}
}

const TestMkCalendarReq = `
<?xml version="1.0" encoding="UTF-8"?>
<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
<A:set xmlns:A="DAV:">
<A:prop>
<B:calendar-timezone>BEGIN:VCALENDAR&#13;
VERSION:2.0&#13;
PRODID:-//Apple Inc.//iPhone OS 18.1.1//EN&#13;
CALSCALE:GREGORIAN&#13;
BEGIN:VTIMEZONE&#13;
TZID:Europe/Paris&#13;
BEGIN:DAYLIGHT&#13;
TZOFFSETFROM:+0100&#13;
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU&#13;
DTSTART:19810329T020000&#13;
TZNAME:UTC+2&#13;
TZOFFSETTO:+0200&#13;
END:DAYLIGHT&#13;
BEGIN:STANDARD&#13;
TZOFFSETFROM:+0200&#13;
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU&#13;
DTSTART:19961027T030000&#13;
TZNAME:UTC+1&#13;
TZOFFSETTO:+0100&#13;
END:STANDARD&#13;
END:VTIMEZONE&#13;
END:VCALENDAR&#13;
</B:calendar-timezone>
<D:calendar-order xmlns:D="http://apple.com/ns/ical/">2</D:calendar-order>
<B:supported-calendar-component-set>
<B:comp name="VEVENT"/>
</B:supported-calendar-component-set>
<D:calendar-color xmlns:D="http://apple.com/ns/ical/" symbolic-color="red">#FF2968</D:calendar-color>
<A:displayname>test calendar</A:displayname>
<B:calendar-free-busy-set>
<NO/>
</B:calendar-free-busy-set>
</A:prop>
</A:set>
</B:mkcalendar>
`

const propFindTest2 = `
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:resourcetype/>
<c:supported-calendar-component-set/>
<d:displayname/>
<c:max-resource-size/>
<c:calendar-description/>
</d:prop>
</d:propfind>
`

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("<href>%s</href>", "/user/calendars/default/")) {
t.Fatalf("want calendar href in response")
} else if !strings.Contains(resp, "<resourcetype xmlns=\"DAV:\">") {
t.Fatalf("want resource type in response")
} else if !strings.Contains(resp, "<collection xmlns=\"DAV:\"></collection>") {
t.Fatalf("want collection resource type in response")
} else if !strings.Contains(resp, "<calendar xmlns=\"urn:ietf:params:xml:ns:caldav\"></calendar>") {
t.Fatalf("want calendar resource type in response")
} else if !strings.Contains(resp, "<displayname xmlns=\"DAV:\">test calendar</displayname>") {
t.Fatalf("want display name in response")
} 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>") {
t.Fatalf("want supported-calendar-component-set in response")
}
}

var reportCalendarData = `
<?xml version="1.0" encoding="UTF-8"?>
<B:calendar-multiget xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
Expand Down Expand Up @@ -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},
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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) {
Expand All @@ -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
}

Expand All @@ -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
}

Expand Down

0 comments on commit ef208fe

Please sign in to comment.