Fix calendar-color not applied by DAVx5 (duplicate propstat)

DAVx5 explicitly requests the Apple calendar-color property by name.
go-webdav's stock property map doesn't know it, so it always emitted a
404 Not Found propstat for it. Our injection was adding a *second*,
200 OK propstat with the color into the same <response>, producing a
response with two propstats for the same property name — invalid
multistatus that real clients (dav4jvm/DAVx5) resolved by preferring the
404, so the color was silently ignored.

Now the bogus 404-only propstat for calendar-color is stripped before
injecting the 200 OK one, leaving a single, valid propstat per response.
Verified against Nextcloud's documented behavior (single propstat with
the color) and against a live PROPFIND matching DAVx5's actual request
shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-20 07:04:49 +02:00
co-authored by Copilot
parent ebbc7a2a2b
commit 5f72e58ad3
2 changed files with 89 additions and 6 deletions
+31 -6
View File
@@ -457,10 +457,24 @@ func (h *colorInjectingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
// (non-greedy) inside a multistatus document, capturing its href.
var responseBlockRe = regexp.MustCompile(`(?s)<response[^>]*>.*?<href[^>]*>([^<]*)</href>.*?</response>`)
// propstatBlockRe matches a single <propstat>...</propstat> element
// within a response, non-greedily.
var propstatBlockRe = regexp.MustCompile(`(?s)<propstat[^>]*>.*?</propstat>`)
// okPropRe matches the first <prop xmlns="DAV:"> opening tag inside a
// 200 OK propstat block, used to find where to insert new property XML.
var okPropRe = regexp.MustCompile(`<prop xmlns="DAV:">`)
// injectCalendarColors scans a multistatus PROPFIND response body and, for
// each <response> whose href is a calendar collection with a color set,
// inserts a <apple:calendar-color xmlns:apple="http://apple.com/ns/ical/">
// element into its first 200 OK <prop>.
// inserts a <calendar-color xmlns="http://apple.com/ns/ical/"> element
// into its first 200 OK <prop>. Since go-webdav's stock property map
// doesn't know this property, a client that explicitly asks for it (as
// DAVx5 does) gets back a 404 propstat for it — that bogus 404 entry is
// stripped first, since leaving both a 404 and our injected 200 for the
// same property name in one <response> is invalid multistatus and
// confuses clients (dav4jvm/DAVx5 was observed to keep showing no color
// when both were present).
func (h *colorInjectingHandler) injectCalendarColors(ctx context.Context, body []byte) []byte {
p := auth.FromContext(ctx)
if p == nil {
@@ -485,12 +499,23 @@ func (h *colorInjectingHandler) injectCalendarColors(ctx context.Context, body [
if err != nil || color == "" {
return block
}
propEl := []byte(`<prop xmlns="DAV:">`)
idx := bytes.Index(block, propEl)
if idx < 0 {
// Drop any propstat block that only complains calendar-color is
// unknown (a 404/not-found propstat containing a bare, empty
// calendar-color element), from either namespace clients might
// have queried it in.
block = propstatBlockRe.ReplaceAllFunc(block, func(ps []byte) []byte {
if bytes.Contains(ps, []byte("calendar-color")) && !bytes.Contains(ps, []byte("200 OK")) {
return nil
}
return ps
})
idx := okPropRe.FindIndex(block)
if idx == nil {
return block
}
insertAt := idx + len(propEl)
insertAt := idx[1]
colorEl := []byte(fmt.Sprintf(`<calendar-color xmlns="http://apple.com/ns/ical/">%s</calendar-color>`, xmlEscapeColor(color)))
out := make([]byte, 0, len(block)+len(colorEl))
out = append(out, block[:insertAt]...)