2022-02-16 10:52:55 +00:00
|
|
|
package caldav
|
|
|
|
|
|
|
|
import (
|
2022-05-12 13:37:22 +01:00
|
|
|
"bytes"
|
2022-03-11 15:50:40 +00:00
|
|
|
"context"
|
2022-02-16 10:52:55 +00:00
|
|
|
"encoding/xml"
|
2022-03-31 11:30:14 +01:00
|
|
|
"fmt"
|
2022-05-12 13:37:22 +01:00
|
|
|
"mime"
|
2022-02-16 10:52:55 +00:00
|
|
|
"net/http"
|
2022-11-15 20:24:46 +00:00
|
|
|
"path"
|
2022-05-23 20:34:02 +01:00
|
|
|
"strconv"
|
2022-11-15 20:24:46 +00:00
|
|
|
"strings"
|
2022-02-16 10:52:55 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/emersion/go-ical"
|
2022-03-21 08:16:50 +00:00
|
|
|
"github.com/emersion/go-webdav"
|
2022-02-16 10:52:55 +00:00
|
|
|
"github.com/emersion/go-webdav/internal"
|
|
|
|
)
|
|
|
|
|
2022-05-12 13:37:22 +01:00
|
|
|
// TODO if nothing more Caldav-specific needs to be added this should be merged with carddav.PutAddressObjectOptions
|
|
|
|
type PutCalendarObjectOptions struct {
|
|
|
|
// IfNoneMatch indicates that the client does not want to overwrite
|
|
|
|
// an existing resource.
|
2022-10-31 11:05:27 +00:00
|
|
|
IfNoneMatch webdav.ConditionalMatch
|
2022-05-12 13:37:22 +01:00
|
|
|
// IfMatch provides the ETag of the resource that the client intends
|
|
|
|
// to overwrite, can be ""
|
2022-10-31 11:05:27 +00:00
|
|
|
IfMatch webdav.ConditionalMatch
|
2022-05-12 13:37:22 +01:00
|
|
|
}
|
|
|
|
|
2022-02-16 10:52:55 +00:00
|
|
|
// Backend is a CalDAV server backend.
|
|
|
|
type Backend interface {
|
2022-03-21 08:16:50 +00:00
|
|
|
CalendarHomeSetPath(ctx context.Context) (string, error)
|
2023-08-21 12:06:59 +01:00
|
|
|
ListCalendars(ctx context.Context) ([]Calendar, error)
|
|
|
|
GetCalendar(ctx context.Context, path string) (*Calendar, error)
|
2022-03-11 15:50:40 +00:00
|
|
|
GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error)
|
2023-08-21 12:06:59 +01:00
|
|
|
ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error)
|
2024-02-02 13:09:22 +00:00
|
|
|
QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error)
|
2022-05-12 13:37:22 +01:00
|
|
|
PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (loc string, err error)
|
2022-10-28 10:19:34 +01:00
|
|
|
DeleteCalendarObject(ctx context.Context, path string) error
|
2022-03-21 08:16:50 +00:00
|
|
|
|
|
|
|
webdav.UserPrincipalBackend
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Handler handles CalDAV HTTP requests. It can be used to create a CalDAV
|
|
|
|
// server.
|
|
|
|
type Handler struct {
|
|
|
|
Backend Backend
|
2022-11-15 20:24:46 +00:00
|
|
|
Prefix string
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ServeHTTP implements http.Handler.
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if h.Backend == nil {
|
|
|
|
http.Error(w, "caldav: no backend available", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.URL.Path == "/.well-known/caldav" {
|
2022-05-12 17:53:47 +01:00
|
|
|
principalPath, err := h.Backend.CurrentUserPrincipal(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, "caldav: failed to determine current user principal", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-20 14:58:52 +01:00
|
|
|
http.Redirect(w, r, principalPath, http.StatusPermanentRedirect)
|
2022-02-16 10:52:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-12 17:53:47 +01:00
|
|
|
var err error
|
2022-02-16 10:52:55 +00:00
|
|
|
switch r.Method {
|
|
|
|
case "REPORT":
|
|
|
|
err = h.handleReport(w, r)
|
|
|
|
default:
|
2022-11-15 20:24:46 +00:00
|
|
|
b := backend{
|
|
|
|
Backend: h.Backend,
|
|
|
|
Prefix: strings.TrimSuffix(h.Prefix, "/"),
|
|
|
|
}
|
2022-02-16 10:52:55 +00:00
|
|
|
hh := internal.Handler{&b}
|
|
|
|
hh.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
internal.ServeError(w, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
var report reportReq
|
|
|
|
if err := internal.DecodeXMLRequest(r, &report); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if report.Query != nil {
|
2022-03-11 15:50:40 +00:00
|
|
|
return h.handleQuery(r, w, report.Query)
|
2022-02-16 10:52:55 +00:00
|
|
|
} else if report.Multiget != nil {
|
2022-03-31 10:28:26 +01:00
|
|
|
return h.handleMultiget(r.Context(), w, report.Multiget)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
return internal.HTTPErrorf(http.StatusBadRequest, "caldav: expected calendar-query or calendar-multiget element in REPORT request")
|
|
|
|
}
|
|
|
|
|
2022-03-31 21:15:04 +01:00
|
|
|
func decodeParamFilter(el *paramFilter) (*ParamFilter, error) {
|
|
|
|
pf := &ParamFilter{Name: el.Name}
|
|
|
|
if el.IsNotDefined != nil {
|
|
|
|
if el.TextMatch != nil {
|
|
|
|
return nil, fmt.Errorf("caldav: failed to parse param-filter: if is-not-defined is provided, text-match can't be provided")
|
|
|
|
}
|
|
|
|
pf.IsNotDefined = true
|
|
|
|
}
|
|
|
|
if el.TextMatch != nil {
|
|
|
|
pf.TextMatch = &TextMatch{Text: el.TextMatch.Text}
|
|
|
|
}
|
|
|
|
return pf, nil
|
|
|
|
}
|
|
|
|
|
2022-02-16 10:52:55 +00:00
|
|
|
func decodePropFilter(el *propFilter) (*PropFilter, error) {
|
|
|
|
pf := &PropFilter{Name: el.Name}
|
2022-03-31 21:15:04 +01:00
|
|
|
if el.IsNotDefined != nil {
|
|
|
|
if el.TextMatch != nil || el.TimeRange != nil || len(el.ParamFilter) > 0 {
|
|
|
|
return nil, fmt.Errorf("caldav: failed to parse prop-filter: if is-not-defined is provided, text-match, time-range, or param-filter can't be provided")
|
|
|
|
}
|
|
|
|
pf.IsNotDefined = true
|
|
|
|
}
|
2022-02-16 10:52:55 +00:00
|
|
|
if el.TextMatch != nil {
|
|
|
|
pf.TextMatch = &TextMatch{Text: el.TextMatch.Text}
|
|
|
|
}
|
2022-03-31 21:15:04 +01:00
|
|
|
if el.TimeRange != nil {
|
|
|
|
pf.Start = time.Time(el.TimeRange.Start)
|
|
|
|
pf.End = time.Time(el.TimeRange.End)
|
|
|
|
}
|
|
|
|
for _, paramEl := range el.ParamFilter {
|
|
|
|
paramFi, err := decodeParamFilter(¶mEl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pf.ParamFilter = append(pf.ParamFilter, *paramFi)
|
|
|
|
}
|
2022-02-16 10:52:55 +00:00
|
|
|
return pf, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeCompFilter(el *compFilter) (*CompFilter, error) {
|
|
|
|
cf := &CompFilter{Name: el.Name}
|
2022-03-31 21:15:04 +01:00
|
|
|
if el.IsNotDefined != nil {
|
|
|
|
if el.TimeRange != nil || len(el.PropFilters) > 0 || len(el.CompFilters) > 0 {
|
|
|
|
return nil, fmt.Errorf("caldav: failed to parse comp-filter: if is-not-defined is provided, time-range, prop-filter, or comp-filter can't be provided")
|
|
|
|
}
|
|
|
|
cf.IsNotDefined = true
|
|
|
|
}
|
2022-02-16 10:52:55 +00:00
|
|
|
if el.TimeRange != nil {
|
|
|
|
cf.Start = time.Time(el.TimeRange.Start)
|
|
|
|
cf.End = time.Time(el.TimeRange.End)
|
|
|
|
}
|
|
|
|
for _, pfEl := range el.PropFilters {
|
|
|
|
pf, err := decodePropFilter(&pfEl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cf.Props = append(cf.Props, *pf)
|
|
|
|
}
|
|
|
|
for _, childEl := range el.CompFilters {
|
|
|
|
child, err := decodeCompFilter(&childEl)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cf.Comps = append(cf.Comps, *child)
|
|
|
|
}
|
|
|
|
return cf, nil
|
|
|
|
}
|
|
|
|
|
2022-03-31 10:28:26 +01:00
|
|
|
func decodeComp(comp *comp) (*CalendarCompRequest, error) {
|
|
|
|
if comp == nil {
|
|
|
|
return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: unexpected empty calendar-data in request")
|
|
|
|
}
|
|
|
|
if comp.Allprop != nil && len(comp.Prop) > 0 {
|
|
|
|
return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: only one of allprop or prop can be specified in calendar-data")
|
|
|
|
}
|
|
|
|
if comp.Allcomp != nil && len(comp.Comp) > 0 {
|
|
|
|
return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: only one of allcomp or comp can be specified in calendar-data")
|
|
|
|
}
|
|
|
|
|
|
|
|
req := &CalendarCompRequest{
|
|
|
|
AllProps: comp.Allprop != nil,
|
|
|
|
AllComps: comp.Allcomp != nil,
|
|
|
|
}
|
|
|
|
for _, p := range comp.Prop {
|
|
|
|
req.Props = append(req.Props, p.Name)
|
|
|
|
}
|
|
|
|
for _, c := range comp.Comp {
|
|
|
|
comp, err := decodeComp(&c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
req.Comps = append(req.Comps, *comp)
|
|
|
|
}
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeCalendarDataReq(calendarData *calendarDataReq) (*CalendarCompRequest, error) {
|
|
|
|
if calendarData.Comp == nil {
|
|
|
|
return &CalendarCompRequest{
|
|
|
|
AllProps: true,
|
|
|
|
AllComps: true,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
return decodeComp(calendarData.Comp)
|
|
|
|
}
|
|
|
|
|
2022-03-11 15:50:40 +00:00
|
|
|
func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *calendarQuery) error {
|
2022-02-16 10:52:55 +00:00
|
|
|
var q CalendarQuery
|
|
|
|
// TODO: calendar-data in query.Prop
|
|
|
|
cf, err := decodeCompFilter(&query.Filter.CompFilter)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
q.CompFilter = *cf
|
|
|
|
|
2024-02-02 13:09:22 +00:00
|
|
|
cos, err := h.Backend.QueryCalendarObjects(r.Context(), r.URL.Path, &q)
|
2022-02-16 10:52:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resps []internal.Response
|
|
|
|
for _, co := range cos {
|
2022-11-15 20:24:46 +00:00
|
|
|
b := backend{
|
|
|
|
Backend: h.Backend,
|
|
|
|
Prefix: strings.TrimSuffix(h.Prefix, "/"),
|
|
|
|
}
|
2022-05-31 16:32:12 +01:00
|
|
|
propfind := internal.PropFind{
|
2022-02-16 10:52:55 +00:00
|
|
|
Prop: query.Prop,
|
|
|
|
AllProp: query.AllProp,
|
|
|
|
PropName: query.PropName,
|
|
|
|
}
|
2022-05-31 16:32:12 +01:00
|
|
|
resp, err := b.propFindCalendarObject(r.Context(), &propfind, &co)
|
2022-02-16 10:52:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resps = append(resps, *resp)
|
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
ms := internal.NewMultiStatus(resps...)
|
2022-02-16 10:52:55 +00:00
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
return internal.ServeMultiStatus(w, ms)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 10:28:26 +01:00
|
|
|
func (h *Handler) handleMultiget(ctx context.Context, w http.ResponseWriter, multiget *calendarMultiget) error {
|
|
|
|
var dataReq CalendarCompRequest
|
|
|
|
if multiget.Prop != nil {
|
|
|
|
var calendarData calendarDataReq
|
|
|
|
if err := multiget.Prop.Decode(&calendarData); err != nil && !internal.IsNotFound(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
decoded, err := decodeCalendarDataReq(&calendarData)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
dataReq = *decoded
|
|
|
|
}
|
|
|
|
|
|
|
|
var resps []internal.Response
|
|
|
|
for _, href := range multiget.Hrefs {
|
|
|
|
co, err := h.Backend.GetCalendarObject(ctx, href.Path, &dataReq)
|
|
|
|
if err != nil {
|
|
|
|
resp := internal.NewErrorResponse(href.Path, err)
|
|
|
|
resps = append(resps, *resp)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
b := backend{
|
|
|
|
Backend: h.Backend,
|
|
|
|
Prefix: strings.TrimSuffix(h.Prefix, "/"),
|
|
|
|
}
|
2022-05-31 16:32:12 +01:00
|
|
|
propfind := internal.PropFind{
|
2022-03-31 10:28:26 +01:00
|
|
|
Prop: multiget.Prop,
|
|
|
|
AllProp: multiget.AllProp,
|
|
|
|
PropName: multiget.PropName,
|
|
|
|
}
|
2022-05-31 16:32:12 +01:00
|
|
|
resp, err := b.propFindCalendarObject(ctx, &propfind, co)
|
2022-03-31 10:28:26 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
resps = append(resps, *resp)
|
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
ms := internal.NewMultiStatus(resps...)
|
|
|
|
return internal.ServeMultiStatus(w, ms)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type backend struct {
|
|
|
|
Backend Backend
|
2022-11-15 20:24:46 +00:00
|
|
|
Prefix string
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
type resourceType int
|
2022-02-16 10:52:55 +00:00
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
const (
|
|
|
|
resourceTypeRoot resourceType = iota
|
|
|
|
resourceTypeUserPrincipal
|
|
|
|
resourceTypeCalendarHomeSet
|
|
|
|
resourceTypeCalendar
|
|
|
|
resourceTypeCalendarObject
|
|
|
|
)
|
2022-03-21 08:16:50 +00:00
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
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
|
2022-03-21 08:16:50 +00:00
|
|
|
}
|
2022-11-15 20:24:46 +00:00
|
|
|
return resourceType(len(strings.Split(p, "/")) - 1)
|
|
|
|
}
|
2022-03-21 08:16:50 +00:00
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
|
|
|
|
caps = []string{"calendar-access"}
|
|
|
|
|
|
|
|
if b.resourceTypeAtPath(r.URL.Path) != resourceTypeCalendarObject {
|
2022-05-12 13:37:22 +01:00
|
|
|
return caps, []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}, nil
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var dataReq CalendarCompRequest
|
2022-03-11 15:50:40 +00:00
|
|
|
_, err = b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
2022-02-16 10:52:55 +00:00
|
|
|
if httpErr, ok := err.(*internal.HTTPError); ok && httpErr.Code == http.StatusNotFound {
|
|
|
|
return caps, []string{http.MethodOptions, http.MethodPut}, nil
|
|
|
|
} else if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return caps, []string{
|
|
|
|
http.MethodOptions,
|
|
|
|
http.MethodHead,
|
|
|
|
http.MethodGet,
|
|
|
|
http.MethodPut,
|
|
|
|
http.MethodDelete,
|
|
|
|
"PROPFIND",
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
2022-05-12 13:37:22 +01:00
|
|
|
var dataReq CalendarCompRequest
|
|
|
|
if r.Method != http.MethodHead {
|
|
|
|
dataReq.AllProps = true
|
|
|
|
}
|
|
|
|
co, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", ical.MIMEType)
|
2022-05-23 20:34:02 +01:00
|
|
|
if co.ContentLength > 0 {
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(co.ContentLength, 10))
|
|
|
|
}
|
2022-05-12 13:37:22 +01:00
|
|
|
if co.ETag != "" {
|
|
|
|
w.Header().Set("ETag", internal.ETag(co.ETag).String())
|
|
|
|
}
|
|
|
|
if !co.ModTime.IsZero() {
|
|
|
|
w.Header().Set("Last-Modified", co.ModTime.UTC().Format(http.TimeFormat))
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Method != http.MethodHead {
|
|
|
|
return ical.NewEncoder(w).Encode(co.Data)
|
|
|
|
}
|
|
|
|
return nil
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
2022-11-15 20:24:46 +00:00
|
|
|
resType := b.resourceTypeAtPath(r.URL.Path)
|
2022-03-21 08:16:50 +00:00
|
|
|
|
2022-10-18 14:45:35 +01:00
|
|
|
var dataReq CalendarCompRequest
|
2022-02-16 10:52:55 +00:00
|
|
|
var resps []internal.Response
|
2022-03-21 08:16:50 +00:00
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
switch resType {
|
2023-07-06 11:12:07 +01:00
|
|
|
case resourceTypeRoot:
|
|
|
|
resp, err := b.propFindRoot(r.Context(), propfind)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
resps = append(resps, *resp)
|
2022-11-15 20:24:46 +00:00
|
|
|
case resourceTypeUserPrincipal:
|
|
|
|
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
2022-03-21 08:16:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-11-15 20:24:46 +00:00
|
|
|
if r.URL.Path == principalPath {
|
|
|
|
resp, err := b.propFindUserPrincipal(r.Context(), propfind)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
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_...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case resourceTypeCalendarHomeSet:
|
|
|
|
homeSetPath, err := b.Backend.CalendarHomeSetPath(r.Context())
|
2022-02-16 10:52:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-11-15 20:24:46 +00:00
|
|
|
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:
|
2023-08-21 12:06:59 +01:00
|
|
|
ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
|
2022-02-16 10:52:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-21 12:06:59 +01:00
|
|
|
resps = append(resps, *resp)
|
|
|
|
if depth != internal.DepthZero {
|
|
|
|
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
|
2022-10-18 14:45:35 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-21 12:06:59 +01:00
|
|
|
resps = append(resps, resps_...)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
2022-11-15 20:24:46 +00:00
|
|
|
case resourceTypeCalendarObject:
|
|
|
|
ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
2022-10-18 14:45:35 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
resp, err := b.propFindCalendarObject(r.Context(), propfind, ao)
|
2022-10-18 14:45:35 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
resps = append(resps, *resp)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
return internal.NewMultiStatus(resps...), nil
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
2023-07-06 11:12:07 +01:00
|
|
|
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
|
|
|
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
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(principalPath, propfind, props)
|
|
|
|
}
|
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
2022-03-21 08:16:50 +00:00
|
|
|
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-11-15 20:24:46 +00:00
|
|
|
homeSetPath, err := b.Backend.CalendarHomeSetPath(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-05-12 17:53:47 +01:00
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
props := map[xml.Name]internal.PropFindFunc{
|
2022-03-21 08:16:50 +00:00
|
|
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
|
|
|
},
|
|
|
|
calendarHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &calendarHomeSet{Href: internal.Href{Path: homeSetPath}}, nil
|
|
|
|
},
|
2022-11-15 20:24:46 +00:00
|
|
|
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return internal.NewResourceType(internal.CollectionName), nil
|
|
|
|
},
|
2022-03-21 08:16:50 +00:00
|
|
|
}
|
2022-05-31 16:32:12 +01:00
|
|
|
return internal.NewPropFindResponse(principalPath, propfind, props)
|
2022-03-21 08:16:50 +00:00
|
|
|
}
|
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropFind, cal *Calendar) (*internal.Response, error) {
|
|
|
|
props := map[xml.Name]internal.PropFindFunc{
|
2022-03-21 08:16:50 +00:00
|
|
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
2022-05-12 17:53:47 +01:00
|
|
|
path, err := b.Backend.CurrentUserPrincipal(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
2022-03-21 08:16:50 +00:00
|
|
|
},
|
2022-02-16 10:52:55 +00:00
|
|
|
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return internal.NewResourceType(internal.CollectionName, calendarName), nil
|
|
|
|
},
|
|
|
|
internal.DisplayNameName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &internal.DisplayName{Name: cal.Name}, nil
|
|
|
|
},
|
2022-05-12 13:36:04 +01:00
|
|
|
calendarDescriptionName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &calendarDescription{Description: cal.Description}, nil
|
|
|
|
},
|
2022-02-16 10:52:55 +00:00
|
|
|
supportedCalendarDataName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &supportedCalendarData{
|
|
|
|
Types: []calendarDataType{
|
|
|
|
{ContentType: ical.MIMEType, Version: "2.0"},
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
},
|
|
|
|
supportedCalendarComponentSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
2023-07-03 09:47:34 +01:00
|
|
|
components := []comp{}
|
|
|
|
if cal.SupportedComponentSet != nil {
|
|
|
|
for _, name := range cal.SupportedComponentSet {
|
|
|
|
components = append(components, comp{Name: name})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
components = append(components, comp{Name: ical.CompEvent})
|
|
|
|
}
|
2022-02-16 10:52:55 +00:00
|
|
|
return &supportedCalendarComponentSet{
|
2023-07-03 09:47:34 +01:00
|
|
|
Comp: components,
|
2022-02-16 10:52:55 +00:00
|
|
|
}, nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if cal.Description != "" {
|
|
|
|
props[calendarDescriptionName] = func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &calendarDescription{Description: cal.Description}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if cal.MaxResourceSize > 0 {
|
|
|
|
props[maxResourceSizeName] = func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &maxResourceSize{Size: cal.MaxResourceSize}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: CALDAV:calendar-timezone, CALDAV:supported-calendar-component-set, CALDAV:min-date-time, CALDAV:max-date-time, CALDAV:max-instances, CALDAV:max-attendees-per-instance
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
return internal.NewPropFindResponse(cal.Path, propfind, props)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
func (b *backend) propFindAllCalendars(ctx context.Context, propfind *internal.PropFind, recurse bool) ([]internal.Response, error) {
|
2023-08-21 12:06:59 +01:00
|
|
|
abs, err := b.Backend.ListCalendars(ctx)
|
2022-11-15 20:24:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var resps []internal.Response
|
|
|
|
for _, ab := range abs {
|
2023-08-21 12:06:59 +01:00
|
|
|
resp, err := b.propFindCalendar(ctx, propfind, &ab)
|
2022-11-15 20:24:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
resps = append(resps, *resp)
|
|
|
|
if recurse {
|
2023-08-21 12:06:59 +01:00
|
|
|
resps_, err := b.propFindAllCalendarObjects(ctx, propfind, &ab)
|
2022-11-15 20:24:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
resps = append(resps, resps_...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return resps, nil
|
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal.PropFind, co *CalendarObject) (*internal.Response, error) {
|
|
|
|
props := map[xml.Name]internal.PropFindFunc{
|
2022-05-12 13:37:22 +01:00
|
|
|
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
2022-05-12 17:53:47 +01:00
|
|
|
path, err := b.Backend.CurrentUserPrincipal(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
2022-05-12 13:37:22 +01:00
|
|
|
},
|
|
|
|
internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &internal.GetContentType{Type: ical.MIMEType}, nil
|
|
|
|
},
|
2022-11-15 20:24:46 +00:00
|
|
|
// TODO: calendar-data can only be used in REPORT requests
|
2022-05-12 13:37:22 +01:00
|
|
|
calendarDataName: func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := ical.NewEncoder(&buf).Encode(co.Data); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &calendarDataResp{Data: buf.Bytes()}, nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2022-05-17 13:43:17 +01:00
|
|
|
if co.ContentLength > 0 {
|
|
|
|
props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &internal.GetContentLength{Length: co.ContentLength}, nil
|
|
|
|
}
|
|
|
|
}
|
2022-05-12 13:37:22 +01:00
|
|
|
if !co.ModTime.IsZero() {
|
|
|
|
props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &internal.GetLastModified{LastModified: internal.Time(co.ModTime)}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if co.ETag != "" {
|
|
|
|
props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
|
|
|
|
return &internal.GetETag{ETag: internal.ETag(co.ETag)}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
return internal.NewPropFindResponse(co.Path, propfind, props)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
2022-11-15 20:24:46 +00:00
|
|
|
func (b *backend) propFindAllCalendarObjects(ctx context.Context, propfind *internal.PropFind, cal *Calendar) ([]internal.Response, error) {
|
|
|
|
var dataReq CalendarCompRequest
|
2023-08-21 12:06:59 +01:00
|
|
|
aos, err := b.Backend.ListCalendarObjects(ctx, cal.Path, &dataReq)
|
2022-11-15 20:24:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-05-31 16:32:12 +01:00
|
|
|
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
|
2022-02-16 10:52:55 +00:00
|
|
|
panic("TODO")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) Put(r *http.Request) (*internal.Href, error) {
|
2022-10-31 11:05:27 +00:00
|
|
|
ifNoneMatch := webdav.ConditionalMatch(r.Header.Get("If-None-Match"))
|
|
|
|
ifMatch := webdav.ConditionalMatch(r.Header.Get("If-Match"))
|
2022-05-12 13:37:22 +01:00
|
|
|
|
|
|
|
opts := PutCalendarObjectOptions{
|
2022-10-31 11:05:27 +00:00
|
|
|
IfNoneMatch: ifNoneMatch,
|
|
|
|
IfMatch: ifMatch,
|
2022-05-12 13:37:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: malformed Content-Type: %v", err)
|
|
|
|
}
|
|
|
|
if t != ical.MIMEType {
|
|
|
|
// TODO: send CALDAV:supported-calendar-data error
|
|
|
|
return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: unsupported Content-Type %q", t)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: check CALDAV:max-resource-size precondition
|
|
|
|
cal, err := ical.NewDecoder(r.Body).Decode()
|
|
|
|
if err != nil {
|
|
|
|
// TODO: send CALDAV:valid-calendar-data error
|
|
|
|
return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: failed to parse iCalendar: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
loc, err := b.Backend.PutCalendarObject(r.Context(), r.URL.Path, cal, &opts)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &internal.Href{Path: loc}, nil
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) Delete(r *http.Request) error {
|
2022-10-28 10:19:34 +01:00
|
|
|
return b.Backend.DeleteCalendarObject(r.Context(), r.URL.Path)
|
2022-02-16 10:52:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) Mkcol(r *http.Request) error {
|
|
|
|
panic("TODO")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) {
|
|
|
|
panic("TODO")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) {
|
|
|
|
panic("TODO")
|
|
|
|
}
|
2022-03-31 11:30:14 +01:00
|
|
|
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.2.1
|
|
|
|
type PreconditionType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
PreconditionNoUIDConflict PreconditionType = "no-uid-conflict"
|
|
|
|
PreconditionSupportedCalendarData PreconditionType = "supported-calendar-data"
|
|
|
|
PreconditionSupportedCalendarComponent PreconditionType = "supported-calendar-component"
|
|
|
|
PreconditionValidCalendarData PreconditionType = "valid-calendar-data"
|
|
|
|
PreconditionValidCalendarObjectResource PreconditionType = "valid-calendar-object-resource"
|
|
|
|
PreconditionCalendarCollectionLocationOk PreconditionType = "calendar-collection-location-ok"
|
|
|
|
PreconditionMaxResourceSize PreconditionType = "max-resource-size"
|
|
|
|
PreconditionMinDateTime PreconditionType = "min-date-time"
|
|
|
|
PreconditionMaxDateTime PreconditionType = "max-date-time"
|
|
|
|
PreconditionMaxInstances PreconditionType = "max-instances"
|
|
|
|
PreconditionMaxAttendeesPerInstance PreconditionType = "max-attendees-per-instance"
|
|
|
|
)
|
|
|
|
|
|
|
|
func NewPreconditionError(err PreconditionType) error {
|
|
|
|
name := xml.Name{"urn:ietf:params:xml:ns:caldav", string(err)}
|
|
|
|
elem := internal.NewRawXMLElement(name, nil, nil)
|
2022-05-02 10:37:45 +01:00
|
|
|
return &internal.HTTPError{
|
2022-03-31 11:30:14 +01:00
|
|
|
Code: 409,
|
2022-05-02 10:37:45 +01:00
|
|
|
Err: &internal.Error{
|
|
|
|
Raw: []internal.RawXMLValue{*elem},
|
|
|
|
},
|
2022-03-31 11:30:14 +01:00
|
|
|
}
|
|
|
|
}
|