Better caldav support (#73)
This commit is contained in:
36
vendor/github.com/samedi/caldav-go/handlers/builder.go
generated
vendored
Normal file
36
vendor/github.com/samedi/caldav-go/handlers/builder.go
generated
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HandlerInterface represents a CalDAV request handler. It has only one function `Handle`,
|
||||
// which is used to handle the CalDAV request and returns the response.
|
||||
type HandlerInterface interface {
|
||||
Handle() *Response
|
||||
}
|
||||
|
||||
// NewHandler returns a new CalDAV request handler object based on the provided request.
|
||||
// With the returned request handler, you can call `Handle()` to handle the request.
|
||||
func NewHandler(request *http.Request) HandlerInterface {
|
||||
response := NewResponse()
|
||||
|
||||
switch request.Method {
|
||||
case "GET":
|
||||
return getHandler{request, response, false}
|
||||
case "HEAD":
|
||||
return getHandler{request, response, true}
|
||||
case "PUT":
|
||||
return putHandler{request, response}
|
||||
case "DELETE":
|
||||
return deleteHandler{request, response}
|
||||
case "PROPFIND":
|
||||
return propfindHandler{request, response}
|
||||
case "OPTIONS":
|
||||
return optionsHandler{response}
|
||||
case "REPORT":
|
||||
return reportHandler{request, response}
|
||||
default:
|
||||
return notImplementedHandler{response}
|
||||
}
|
||||
}
|
40
vendor/github.com/samedi/caldav-go/handlers/delete.go
generated
vendored
Normal file
40
vendor/github.com/samedi/caldav-go/handlers/delete.go
generated
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type deleteHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (dh deleteHandler) Handle() *Response {
|
||||
precond := requestPreconditions{dh.request}
|
||||
|
||||
// get the event from the storage
|
||||
resource, _, err := global.Storage.GetShallowResource(dh.request.URL.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
|
||||
// TODO: Handle delete on collections
|
||||
if resource.IsCollection() {
|
||||
return dh.response.Set(http.StatusMethodNotAllowed, "")
|
||||
}
|
||||
|
||||
// check ETag pre-condition
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if !precond.IfMatch(resourceEtag) {
|
||||
return dh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
// delete event after pre-condition passed
|
||||
err = global.Storage.DeleteResource(resource.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
|
||||
return dh.response.Set(http.StatusNoContent, "")
|
||||
}
|
37
vendor/github.com/samedi/caldav-go/handlers/get.go
generated
vendored
Normal file
37
vendor/github.com/samedi/caldav-go/handlers/get.go
generated
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type getHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
onlyHeaders bool
|
||||
}
|
||||
|
||||
func (gh getHandler) Handle() *Response {
|
||||
resource, _, err := global.Storage.GetResource(gh.request.URL.Path)
|
||||
if err != nil {
|
||||
return gh.response.SetError(err)
|
||||
}
|
||||
|
||||
var response string
|
||||
if gh.onlyHeaders {
|
||||
response = ""
|
||||
} else {
|
||||
response, _ = resource.GetContentData()
|
||||
}
|
||||
|
||||
etag, _ := resource.GetEtag()
|
||||
lastm, _ := resource.GetLastModified(http.TimeFormat)
|
||||
ctype, _ := resource.GetContentType()
|
||||
|
||||
gh.response.SetHeader("ETag", etag).
|
||||
SetHeader("Last-Modified", lastm).
|
||||
SetHeader("Content-Type", ctype).
|
||||
Set(http.StatusOK, response)
|
||||
|
||||
return gh.response
|
||||
}
|
27
vendor/github.com/samedi/caldav-go/handlers/headers.go
generated
vendored
Normal file
27
vendor/github.com/samedi/caldav-go/handlers/headers.go
generated
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
HD_DEPTH = "Depth"
|
||||
HD_DEPTH_DEEP = "1"
|
||||
HD_PREFER = "Prefer"
|
||||
HD_PREFER_MINIMAL = "return=minimal"
|
||||
HD_PREFERENCE_APPLIED = "Preference-Applied"
|
||||
)
|
||||
|
||||
type headers struct {
|
||||
http.Header
|
||||
}
|
||||
|
||||
func (h headers) IsDeep() bool {
|
||||
depth := h.Get(HD_DEPTH)
|
||||
return (depth == HD_DEPTH_DEEP)
|
||||
}
|
||||
|
||||
func (h headers) IsMinimal() bool {
|
||||
prefer := h.Get(HD_PREFER)
|
||||
return (prefer == HD_PREFER_MINIMAL)
|
||||
}
|
207
vendor/github.com/samedi/caldav-go/handlers/multistatus.go
generated
vendored
Normal file
207
vendor/github.com/samedi/caldav-go/handlers/multistatus.go
generated
vendored
Normal file
@ -0,0 +1,207 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Wraps a multistatus response. It contains the set of `Responses`
|
||||
// that will serve to build the final XML. Multistatus responses are
|
||||
// used by the REPORT and PROPFIND methods.
|
||||
type multistatusResp struct {
|
||||
// The set of multistatus responses used to build each of the <DAV:response> nodes.
|
||||
Responses []msResponse
|
||||
// Flag that XML should be minimal or not
|
||||
// [defined in the draft https://tools.ietf.org/html/draft-murchison-webdav-prefer-05]
|
||||
Minimal bool
|
||||
}
|
||||
|
||||
type msResponse struct {
|
||||
Href string
|
||||
Found bool
|
||||
Propstats msPropstats
|
||||
}
|
||||
|
||||
type msPropstats map[int]msProps
|
||||
|
||||
// Adds a msProp to the map with the key being the prop status.
|
||||
func (stats msPropstats) Add(prop msProp) {
|
||||
stats[prop.Status] = append(stats[prop.Status], prop)
|
||||
}
|
||||
|
||||
func (stats msPropstats) Clone() msPropstats {
|
||||
clone := make(msPropstats)
|
||||
|
||||
for k, v := range stats {
|
||||
clone[k] = v
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
type msProps []msProp
|
||||
|
||||
type msProp struct {
|
||||
Tag xml.Name
|
||||
Content string
|
||||
Contents []string
|
||||
Status int
|
||||
}
|
||||
|
||||
// Function that processes all the required props for a given resource.
|
||||
// ## Params
|
||||
// resource: the target calendar resource.
|
||||
// reqprops: set of required props that must be processed for the resource.
|
||||
// ## Returns
|
||||
// The set of props (msProp) processed. Each prop is mapped to a HTTP status code.
|
||||
// So if a prop is found and processed ok, it'll be mapped to 200. If it's not found,
|
||||
// it'll be mapped to 404, and so on.
|
||||
func (ms *multistatusResp) Propstats(resource *data.Resource, reqprops []xml.Name) msPropstats {
|
||||
if resource == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(msPropstats)
|
||||
|
||||
for _, ptag := range reqprops {
|
||||
pvalue := msProp{
|
||||
Tag: ptag,
|
||||
Status: http.StatusOK,
|
||||
}
|
||||
|
||||
pfound := false
|
||||
switch ptag {
|
||||
case ixml.CALENDAR_DATA_TG:
|
||||
pvalue.Content, pfound = resource.GetContentData()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_ETAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.GET_CONTENT_TYPE_TG:
|
||||
pvalue.Content, pfound = resource.GetContentType()
|
||||
case ixml.GET_CONTENT_LENGTH_TG:
|
||||
pvalue.Content, pfound = resource.GetContentLength()
|
||||
case ixml.DISPLAY_NAME_TG:
|
||||
pvalue.Content, pfound = resource.GetDisplayName()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_LAST_MODIFIED_TG:
|
||||
pvalue.Content, pfound = resource.GetLastModified(http.TimeFormat)
|
||||
case ixml.OWNER_TG:
|
||||
pvalue.Content, pfound = resource.GetOwnerPath()
|
||||
case ixml.GET_CTAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.PRINCIPAL_URL_TG,
|
||||
ixml.PRINCIPAL_COLLECTION_SET_TG,
|
||||
ixml.CALENDAR_USER_ADDRESS_SET_TG,
|
||||
ixml.CALENDAR_HOME_SET_TG:
|
||||
pvalue.Content, pfound = ixml.HrefTag(resource.Path), true
|
||||
case ixml.RESOURCE_TYPE_TG:
|
||||
if resource.IsCollection() {
|
||||
pvalue.Content, pfound = ixml.Tag(ixml.COLLECTION_TG, "")+ixml.Tag(ixml.CALENDAR_TG, ""), true
|
||||
|
||||
if resource.IsPrincipal() {
|
||||
pvalue.Content += ixml.Tag(ixml.PRINCIPAL_TG, "")
|
||||
}
|
||||
} else {
|
||||
// resourcetype must be returned empty for non-collection elements
|
||||
pvalue.Content, pfound = "", true
|
||||
}
|
||||
case ixml.CURRENT_USER_PRINCIPAL_TG:
|
||||
if global.User != nil {
|
||||
path := fmt.Sprintf("/%s/", global.User.Name)
|
||||
pvalue.Content, pfound = ixml.HrefTag(path), true
|
||||
}
|
||||
case ixml.SUPPORTED_CALENDAR_COMPONENT_SET_TG:
|
||||
if resource.IsCollection() {
|
||||
for _, component := range global.SupportedComponents {
|
||||
// TODO: use ixml somehow to build the below tag
|
||||
compTag := fmt.Sprintf(`<C:comp name="%s"/>`, component)
|
||||
pvalue.Contents = append(pvalue.Contents, compTag)
|
||||
}
|
||||
pfound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !pfound {
|
||||
pvalue.Status = http.StatusNotFound
|
||||
}
|
||||
|
||||
result.Add(pvalue)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Adds a new `msResponse` to the `Responses` array.
|
||||
func (ms *multistatusResp) AddResponse(href string, found bool, propstats msPropstats) {
|
||||
ms.Responses = append(ms.Responses, msResponse{
|
||||
Href: href,
|
||||
Found: found,
|
||||
Propstats: propstats,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *multistatusResp) ToXML() string {
|
||||
// init multistatus
|
||||
var bf lib.StringBuffer
|
||||
bf.Write(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
bf.Write(`<D:multistatus %s>`, ixml.Namespaces())
|
||||
|
||||
// iterate over event hrefs and build multistatus XML on the fly
|
||||
for _, response := range ms.Responses {
|
||||
bf.Write("<D:response>")
|
||||
bf.Write(ixml.HrefTag(response.Href))
|
||||
|
||||
if response.Found {
|
||||
propstats := response.Propstats.Clone()
|
||||
|
||||
if ms.Minimal {
|
||||
delete(propstats, http.StatusNotFound)
|
||||
|
||||
if len(propstats) == 0 {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop/>")
|
||||
bf.Write(ixml.StatusTag(http.StatusOK))
|
||||
bf.Write("</D:propstat>")
|
||||
bf.Write("</D:response>")
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for status, props := range propstats {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop>")
|
||||
for _, prop := range props {
|
||||
bf.Write(ms.propToXML(prop))
|
||||
}
|
||||
bf.Write("</D:prop>")
|
||||
bf.Write(ixml.StatusTag(status))
|
||||
bf.Write("</D:propstat>")
|
||||
}
|
||||
} else {
|
||||
// if does not find the resource set 404
|
||||
bf.Write(ixml.StatusTag(http.StatusNotFound))
|
||||
}
|
||||
bf.Write("</D:response>")
|
||||
}
|
||||
bf.Write("</D:multistatus>")
|
||||
|
||||
return bf.String()
|
||||
}
|
||||
|
||||
func (ms *multistatusResp) propToXML(prop msProp) string {
|
||||
for _, content := range prop.Contents {
|
||||
prop.Content += content
|
||||
}
|
||||
xmlString := ixml.Tag(prop.Tag, prop.Content)
|
||||
return xmlString
|
||||
}
|
13
vendor/github.com/samedi/caldav-go/handlers/not_implemented.go
generated
vendored
Normal file
13
vendor/github.com/samedi/caldav-go/handlers/not_implemented.go
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type notImplementedHandler struct {
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (h notImplementedHandler) Handle() *Response {
|
||||
return h.response.Set(http.StatusNotImplemented, "")
|
||||
}
|
23
vendor/github.com/samedi/caldav-go/handlers/options.go
generated
vendored
Normal file
23
vendor/github.com/samedi/caldav-go/handlers/options.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type optionsHandler struct {
|
||||
response *Response
|
||||
}
|
||||
|
||||
// Returns the allowed methods and the DAV features implemented by the current server.
|
||||
// For more information about the values and format read RFC4918 Sections 10.1 and 18.
|
||||
func (oh optionsHandler) Handle() *Response {
|
||||
// Set the DAV compliance header:
|
||||
// 1: Server supports all the requirements specified in RFC2518
|
||||
// 3: Server supports all the revisions specified in RFC4918
|
||||
// calendar-access: Server supports all the extensions specified in RFC4791
|
||||
oh.response.SetHeader("DAV", "1, 3, calendar-access").
|
||||
SetHeader("Allow", "GET, HEAD, PUT, DELETE, OPTIONS, PROPFIND, REPORT").
|
||||
Set(http.StatusOK, "")
|
||||
|
||||
return oh.response
|
||||
}
|
23
vendor/github.com/samedi/caldav-go/handlers/preconditions.go
generated
vendored
Normal file
23
vendor/github.com/samedi/caldav-go/handlers/preconditions.go
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type requestPreconditions struct {
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfMatch(etag string) bool {
|
||||
etagMatch := p.request.Header["If-Match"]
|
||||
return len(etagMatch) == 0 || etagMatch[0] == "*" || etagMatch[0] == etag
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfMatchPresent() bool {
|
||||
return len(p.request.Header["If-Match"]) != 0
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfNoneMatch(value string) bool {
|
||||
valueMatch := p.request.Header["If-None-Match"]
|
||||
return len(valueMatch) == 1 && valueMatch[0] == value
|
||||
}
|
49
vendor/github.com/samedi/caldav-go/handlers/propfind.go
generated
vendored
Normal file
49
vendor/github.com/samedi/caldav-go/handlers/propfind.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type propfindHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (ph propfindHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(ph.request)
|
||||
header := headers{ph.request.Header}
|
||||
|
||||
// get the target resources based on the request URL
|
||||
resources, err := global.Storage.GetResources(ph.request.URL.Path, header.IsDeep())
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
// read body string to xml struct
|
||||
type XMLProp2 struct {
|
||||
Tags []xml.Name `xml:",any"`
|
||||
}
|
||||
type XMLRoot2 struct {
|
||||
XMLName xml.Name
|
||||
Prop XMLProp2 `xml:"DAV: prop"`
|
||||
}
|
||||
var requestXML XMLRoot2
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, resource := range resources {
|
||||
propstats := multistatus.Propstats(&resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(resource.Path, true, propstats)
|
||||
}
|
||||
|
||||
if multistatus.Minimal {
|
||||
ph.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
|
||||
return ph.response.Set(207, multistatus.ToXML())
|
||||
}
|
65
vendor/github.com/samedi/caldav-go/handlers/put.go
generated
vendored
Normal file
65
vendor/github.com/samedi/caldav-go/handlers/put.go
generated
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type putHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (ph putHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(ph.request)
|
||||
precond := requestPreconditions{ph.request}
|
||||
success := false
|
||||
|
||||
// check if resource exists
|
||||
resourcePath := ph.request.URL.Path
|
||||
resource, found, err := global.Storage.GetShallowResource(resourcePath)
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
// PUT is allowed in 2 cases:
|
||||
//
|
||||
// 1. Item NOT FOUND and there is NO ETAG match header: CREATE a new item
|
||||
if !found && !precond.IfMatchPresent() {
|
||||
// create new event resource
|
||||
resource, err = global.Storage.CreateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
success = true
|
||||
}
|
||||
|
||||
if found {
|
||||
// TODO: Handle PUT on collections
|
||||
if resource.IsCollection() {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
// 2. Item exists, the resource etag is verified and there's no IF-NONE-MATCH=* header: UPDATE the item
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if found && precond.IfMatch(resourceEtag) && !precond.IfNoneMatch("*") {
|
||||
// update resource
|
||||
resource, err = global.Storage.UpdateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
return ph.response.SetHeader("ETag", resourceEtag).
|
||||
Set(http.StatusCreated, "")
|
||||
}
|
168
vendor/github.com/samedi/caldav-go/handlers/report.go
generated
vendored
Normal file
168
vendor/github.com/samedi/caldav-go/handlers/report.go
generated
vendored
Normal file
@ -0,0 +1,168 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
)
|
||||
|
||||
type reportHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
// See more at RFC4791#section-7.1
|
||||
func (rh reportHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(rh.request)
|
||||
header := headers{rh.request.Header}
|
||||
|
||||
urlResource, found, err := global.Storage.GetShallowResource(rh.request.URL.Path)
|
||||
if !found {
|
||||
return rh.response.Set(http.StatusNotFound, "")
|
||||
} else if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
|
||||
// read body string to xml struct
|
||||
var requestXML reportRootXML
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
|
||||
// The resources to be reported are fetched by the type of the request. If it is
|
||||
// a `calendar-multiget`, the resources come based on a set of `hrefs` in the request body.
|
||||
// If it is a `calendar-query`, the resources are calculated based on set of filters in the request.
|
||||
var resourcesToReport []reportRes
|
||||
switch requestXML.XMLName {
|
||||
case ixml.CALENDAR_MULTIGET_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByList(urlResource, requestXML.Hrefs)
|
||||
case ixml.CALENDAR_QUERY_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByFilters(urlResource, requestXML.Filters)
|
||||
default:
|
||||
return rh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, r := range resourcesToReport {
|
||||
propstats := multistatus.Propstats(r.resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(r.href, r.found, propstats)
|
||||
}
|
||||
|
||||
if multistatus.Minimal {
|
||||
rh.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
|
||||
return rh.response.Set(207, multistatus.ToXML())
|
||||
}
|
||||
|
||||
type reportPropXML struct {
|
||||
Tags []xml.Name `xml:",any"`
|
||||
}
|
||||
|
||||
type reportRootXML struct {
|
||||
XMLName xml.Name
|
||||
Prop reportPropXML `xml:"DAV: prop"`
|
||||
Hrefs []string `xml:"DAV: href"`
|
||||
Filters reportFilterXML `xml:"urn:ietf:params:xml:ns:caldav filter"`
|
||||
}
|
||||
|
||||
type reportFilterXML struct {
|
||||
XMLName xml.Name
|
||||
InnerContent string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (rfXml reportFilterXML) toString() string {
|
||||
return fmt.Sprintf("<%s>%s</%s>", rfXml.XMLName.Local, rfXml.InnerContent, rfXml.XMLName.Local)
|
||||
}
|
||||
|
||||
// Wraps a resource that has to be reported, either fetched by filters or by a list.
|
||||
// Basically it contains the original requested `href`, the actual `resource` (can be nil)
|
||||
// and if the `resource` was `found` or not
|
||||
type reportRes struct {
|
||||
href string
|
||||
resource *data.Resource
|
||||
found bool
|
||||
}
|
||||
|
||||
// The resources are fetched based on the origin resource and a set of filters.
|
||||
// If the origin resource is a collection, the filters are checked against each of the collection's resources
|
||||
// to see if they match. The collection's resources that match the filters are returned. The ones that will be returned
|
||||
// are the resources that were not found (does not exist) and the ones that matched the filters. The ones that did not
|
||||
// match the filter will not appear in the response result.
|
||||
// If the origin resource is not a collection, the function just returns it and ignore any filter processing.
|
||||
// [See RFC4791#section-7.8]
|
||||
func (rh reportHandler) fetchResourcesByFilters(origin *data.Resource, filtersXML reportFilterXML) ([]reportRes, error) {
|
||||
// The list of resources that has to be reported back in the response.
|
||||
reps := []reportRes{}
|
||||
|
||||
if origin.IsCollection() {
|
||||
filters, _ := data.ParseResourceFilters(filtersXML.toString())
|
||||
resources, err := global.Storage.GetResourcesByFilters(origin.Path, filters)
|
||||
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
|
||||
for in, resource := range resources {
|
||||
reps = append(reps, reportRes{resource.Path, &resources[in], true})
|
||||
}
|
||||
} else {
|
||||
// the origin resource is not a collection, so returns just that as the result
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
|
||||
return reps, nil
|
||||
}
|
||||
|
||||
// The hrefs can come from (1) the request URL or (2) from the request body itself.
|
||||
// If the origin resource from the URL points to a collection (2), we will check the request body
|
||||
// to get the requested `hrefs` (resource paths). Each requested href has to be related to the collection.
|
||||
// The ones that are not, we simply ignore them.
|
||||
// If the resource from the URL is NOT a collection (1) we process the the report only for this resource
|
||||
// and ignore any othre requested hrefs that might be present in the request body.
|
||||
// [See RFC4791#section-7.9]
|
||||
func (rh reportHandler) fetchResourcesByList(origin *data.Resource, requestedPaths []string) ([]reportRes, error) {
|
||||
reps := []reportRes{}
|
||||
|
||||
if origin.IsCollection() {
|
||||
resources, err := global.Storage.GetResourcesByList(requestedPaths)
|
||||
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
|
||||
// we put all the resources found in a map path -> resource.
|
||||
// this will be used later to query which requested resource was found
|
||||
// or not and mount the response
|
||||
resourcesMap := make(map[string]*data.Resource)
|
||||
for _, resource := range resources {
|
||||
r := resource
|
||||
resourcesMap[resource.Path] = &r
|
||||
}
|
||||
|
||||
for _, requestedPath := range requestedPaths {
|
||||
// if the requested path does not belong to the origin collection, skip
|
||||
// ('belonging' means that the path's prefix is the same as the collection path)
|
||||
if !strings.HasPrefix(requestedPath, origin.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
resource, found := resourcesMap[requestedPath]
|
||||
reps = append(reps, reportRes{requestedPath, resource, found})
|
||||
}
|
||||
} else {
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
|
||||
return reps, nil
|
||||
}
|
72
vendor/github.com/samedi/caldav-go/handlers/response.go
generated
vendored
Normal file
72
vendor/github.com/samedi/caldav-go/handlers/response.go
generated
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Response represents the handled CalDAV response. Used this when one needs to proxy the generated
|
||||
// response before being sent back to the client.
|
||||
type Response struct {
|
||||
Status int
|
||||
Header http.Header
|
||||
Body string
|
||||
Error error
|
||||
}
|
||||
|
||||
// NewResponse initializes a new response object.
|
||||
func NewResponse() *Response {
|
||||
return &Response{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the the status and body of the response.
|
||||
func (r *Response) Set(status int, body string) *Response {
|
||||
r.Status = status
|
||||
r.Body = body
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// SetHeader adds a header to the response.
|
||||
func (r *Response) SetHeader(key, value string) *Response {
|
||||
r.Header.Set(key, value)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// SetError sets the response as an error. It inflects the response status based on the provided error.
|
||||
func (r *Response) SetError(err error) *Response {
|
||||
r.Error = err
|
||||
|
||||
switch err {
|
||||
case errs.ResourceNotFoundError:
|
||||
r.Status = http.StatusNotFound
|
||||
case errs.UnauthorizedError:
|
||||
r.Status = http.StatusUnauthorized
|
||||
case errs.ForbiddenError:
|
||||
r.Status = http.StatusForbidden
|
||||
default:
|
||||
r.Status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Write writes the response back to the client using the provided `ResponseWriter`.
|
||||
func (r *Response) Write(writer http.ResponseWriter) {
|
||||
if r.Error == errs.UnauthorizedError {
|
||||
r.SetHeader("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
}
|
||||
|
||||
for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
writer.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteHeader(r.Status)
|
||||
io.WriteString(writer, r.Body)
|
||||
}
|
18
vendor/github.com/samedi/caldav-go/handlers/shared.go
generated
vendored
Normal file
18
vendor/github.com/samedi/caldav-go/handlers/shared.go
generated
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// This function reads the request body and restore its content, so that
|
||||
// the request body can be read a second time.
|
||||
func readRequestBody(request *http.Request) string {
|
||||
// Read the content
|
||||
body, _ := ioutil.ReadAll(request.Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
// Use the content
|
||||
return string(body)
|
||||
}
|
Reference in New Issue
Block a user