diff --git a/caldav/server.go b/caldav/server.go index 3624cec..1e50827 100644 --- a/caldav/server.go +++ b/caldav/server.go @@ -7,7 +7,9 @@ import ( "fmt" "mime" "net/http" + "path" "strconv" + "strings" "time" "github.com/emersion/go-ical" @@ -44,6 +46,7 @@ type Backend interface { // server. type Handler struct { Backend Backend + Prefix string } // ServeHTTP implements http.Handler. @@ -69,7 +72,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case "REPORT": err = h.handleReport(w, r) default: - b := backend{h.Backend} + b := backend{ + Backend: h.Backend, + Prefix: strings.TrimSuffix(h.Prefix, "/"), + } hh := internal.Handler{&b} hh.ServeHTTP(w, r) } @@ -215,7 +221,10 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *cal var resps []internal.Response for _, co := range cos { - b := backend{h.Backend} + b := backend{ + Backend: h.Backend, + Prefix: strings.TrimSuffix(h.Prefix, "/"), + } propfind := internal.PropFind{ Prop: query.Prop, AllProp: query.AllProp, @@ -256,7 +265,10 @@ func (h *Handler) handleMultiget(ctx context.Context, w http.ResponseWriter, mul continue } - b := backend{h.Backend} + b := backend{ + Backend: h.Backend, + Prefix: strings.TrimSuffix(h.Prefix, "/"), + } propfind := internal.PropFind{ Prop: multiget.Prop, AllProp: multiget.AllProp, @@ -275,22 +287,35 @@ func (h *Handler) handleMultiget(ctx context.Context, w http.ResponseWriter, mul type backend struct { Backend Backend + Prefix string +} + +type resourceType int + +const ( + resourceTypeRoot resourceType = iota + resourceTypeUserPrincipal + resourceTypeCalendarHomeSet + resourceTypeCalendar + resourceTypeCalendarObject +) + +func (b *backend) resourceTypeAtPath(reqPath string) resourceType { + p := path.Clean(reqPath) + p = strings.TrimPrefix(p, b.Prefix) + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + if p == "/" { + return resourceTypeRoot + } + return resourceType(len(strings.Split(p, "/")) - 1) } func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) { caps = []string{"calendar-access"} - homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context()) - if err != nil { - return nil, nil, err - } - - principalPath, err := b.Backend.CurrentUserPrincipal(r.Context()) - if err != nil { - return nil, nil, err - } - - if r.URL.Path == "/" || r.URL.Path == principalPath || r.URL.Path == homeSetPath { + if b.resourceTypeAtPath(r.URL.Path) != resourceTypeCalendarObject { return caps, []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}, nil } @@ -340,58 +365,85 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error { } func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) { - homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context()) - if err != nil { - return nil, err - } - principalPath, err := b.Backend.CurrentUserPrincipal(r.Context()) - if err != nil { - return nil, err - } + resType := b.resourceTypeAtPath(r.URL.Path) var dataReq CalendarCompRequest - var resps []internal.Response - if r.URL.Path == principalPath { - resp, err := b.propFindUserPrincipal(r.Context(), propfind, homeSetPath) + switch resType { + case resourceTypeUserPrincipal: + principalPath, err := b.Backend.CurrentUserPrincipal(r.Context()) if err != nil { return nil, err } - resps = append(resps, *resp) - } else if r.URL.Path == homeSetPath { - cal, err := b.Backend.Calendar(r.Context()) - if err != nil { - return nil, err - } - - resp, err := b.propFindCalendar(r.Context(), propfind, cal) - if err != nil { - return nil, err - } - resps = append(resps, *resp) - - if depth != internal.DepthZero { - cos, err := b.Backend.ListCalendarObjects(r.Context(), &dataReq) + if r.URL.Path == principalPath { + resp, err := b.propFindUserPrincipal(r.Context(), propfind) if err != nil { return nil, err } - - for _, co := range cos { - resp, err := b.propFindCalendarObject(r.Context(), propfind, &co) + resps = append(resps, *resp) + if depth != internal.DepthZero { + resp, err := b.propFindHomeSet(r.Context(), propfind) if err != nil { return nil, err } resps = append(resps, *resp) + if depth == internal.DepthInfinity { + resps_, err := b.propFindAllCalendars(r.Context(), propfind, true) + if err != nil { + return nil, err + } + resps = append(resps, resps_...) + } } } - } else { - co, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq) + case resourceTypeCalendarHomeSet: + homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context()) + if err != nil { + return nil, err + } + if r.URL.Path == homeSetPath { + resp, err := b.propFindHomeSet(r.Context(), propfind) + if err != nil { + return nil, err + } + resps = append(resps, *resp) + if depth != internal.DepthZero { + recurse := depth == internal.DepthInfinity + resps_, err := b.propFindAllCalendars(r.Context(), propfind, recurse) + if err != nil { + return nil, err + } + resps = append(resps, resps_...) + } + } + case resourceTypeCalendar: + // TODO for multiple calendars, look through all of them + ab, err := b.Backend.Calendar(r.Context()) + if err != nil { + return nil, err + } + if r.URL.Path == ab.Path { + resp, err := b.propFindCalendar(r.Context(), propfind, ab) + if err != nil { + return nil, err + } + resps = append(resps, *resp) + if depth != internal.DepthZero { + resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab) + if err != nil { + return nil, err + } + resps = append(resps, resps_...) + } + } + case resourceTypeCalendarObject: + ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq) if err != nil { return nil, err } - resp, err := b.propFindCalendarObject(r.Context(), propfind, co) + resp, err := b.propFindCalendarObject(r.Context(), propfind, ao) if err != nil { return nil, err } @@ -401,11 +453,15 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i return internal.NewMultiStatus(resps...), nil } -func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.PropFind, homeSetPath string) (*internal.Response, error) { +func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) { principalPath, err := b.Backend.CurrentUserPrincipal(ctx) if err != nil { return nil, err } + homeSetPath, err := b.Backend.CalendarHomeSetPath(ctx) + if err != nil { + return nil, err + } props := map[xml.Name]internal.PropFindFunc{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) { @@ -414,10 +470,35 @@ func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal. calendarHomeSetName: func(*internal.RawXMLValue) (interface{}, error) { return &calendarHomeSet{Href: internal.Href{Path: homeSetPath}}, nil }, + internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) { + return internal.NewResourceType(internal.CollectionName), nil + }, } return internal.NewPropFindResponse(principalPath, propfind, props) } +func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) { + principalPath, err := b.Backend.CurrentUserPrincipal(ctx) + if err != nil { + return nil, err + } + homeSetPath, err := b.Backend.CalendarHomeSetPath(ctx) + if err != nil { + return nil, err + } + + // TODO anything else to return here? + props := map[xml.Name]internal.PropFindFunc{ + internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) { + return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil + }, + internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) { + return internal.NewResourceType(internal.CollectionName), nil + }, + } + return internal.NewPropFindResponse(homeSetPath, propfind, props) +} + func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropFind, cal *Calendar) (*internal.Response, error) { props := map[xml.Name]internal.PropFindFunc{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) { @@ -469,6 +550,32 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF return internal.NewPropFindResponse(cal.Path, propfind, props) } +func (b *backend) propFindAllCalendars(ctx context.Context, propfind *internal.PropFind, recurse bool) ([]internal.Response, error) { + // TODO iterate over all calendars once having multiple is supported + ab, err := b.Backend.Calendar(ctx) + if err != nil { + return nil, err + } + abs := []*Calendar{ab} + + var resps []internal.Response + for _, ab := range abs { + resp, err := b.propFindCalendar(ctx, propfind, ab) + if err != nil { + return nil, err + } + resps = append(resps, *resp) + if recurse { + resps_, err := b.propFindAllCalendarObjects(ctx, propfind, ab) + if err != nil { + return nil, err + } + resps = append(resps, resps_...) + } + } + return resps, nil +} + func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal.PropFind, co *CalendarObject) (*internal.Response, error) { props := map[xml.Name]internal.PropFindFunc{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) { @@ -481,7 +588,7 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) { return &internal.GetContentType{Type: ical.MIMEType}, nil }, - // TODO: address-data can only be used in REPORT requests + // TODO: calendar-data can only be used in REPORT requests calendarDataName: func(*internal.RawXMLValue) (interface{}, error) { var buf bytes.Buffer if err := ical.NewEncoder(&buf).Encode(co.Data); err != nil { @@ -512,6 +619,24 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal return internal.NewPropFindResponse(co.Path, propfind, props) } +func (b *backend) propFindAllCalendarObjects(ctx context.Context, propfind *internal.PropFind, cal *Calendar) ([]internal.Response, error) { + var dataReq CalendarCompRequest + aos, err := b.Backend.ListCalendarObjects(ctx, &dataReq) + if err != nil { + return nil, err + } + + var resps []internal.Response + for _, ao := range aos { + resp, err := b.propFindCalendarObject(ctx, propfind, &ao) + if err != nil { + return nil, err + } + resps = append(resps, *resp) + } + return resps, nil +} + func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) { panic("TODO") }