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", 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 }