169 lines
5.6 KiB
Go
169 lines
5.6 KiB
Go
|
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
|
||
|
}
|