189 lines
6.6 KiB
Go
189 lines
6.6 KiB
Go
package extra
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"fmt"
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
|
|
"nxcaldav/internal/backend"
|
|
"regexp"
|
|
"time"
|
|
)
|
|
|
|
type responseWriter struct {
|
|
http.ResponseWriter
|
|
buffer *bytes.Buffer
|
|
status int
|
|
}
|
|
|
|
func (rw *responseWriter) Write(b []byte) (int, error) {
|
|
return rw.buffer.Write(b)
|
|
}
|
|
|
|
func (rw *responseWriter) WriteHeader(status int) {
|
|
rw.status = status
|
|
}
|
|
|
|
func AddColorToCalendarPropfind(r *http.Request, ctx context.Context, handler http.Handler, w http.ResponseWriter, be *backend.DBBackend) {
|
|
reqBody, _ := io.ReadAll(r.Body)
|
|
r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
|
|
|
|
buf := &bytes.Buffer{}
|
|
rw := &responseWriter{w, buf, http.StatusOK}
|
|
handler.ServeHTTP(rw, r.WithContext(ctx))
|
|
|
|
body := buf.Bytes()
|
|
|
|
// 1. Add namespaces to the root multistatus tag
|
|
reMultistatus := regexp.MustCompile(`(<[a-zA-Z0-9]*:?multistatus)`)
|
|
body = reMultistatus.ReplaceAll(body, []byte(`$1 xmlns:ICAL="http://apple.com/ns/ical/" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/"`))
|
|
|
|
// 2. Response processing
|
|
reResponse := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?response.*?>.*?</[a-zA-Z0-9]*:?response>`)
|
|
reHref := regexp.MustCompile(`<[a-zA-Z0-9]*:?href.*?>(.*?)</[a-zA-Z0-9]*:?href>`)
|
|
rePropstat := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?propstat.*?>.*?</[a-zA-Z0-9]*:?propstat>`)
|
|
reStatusOk := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?status.*?>HTTP/1.1 200 OK</[a-zA-Z0-9]*:?status>`)
|
|
reProp := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?prop.*?>.*?</[a-zA-Z0-9]*:?prop>`)
|
|
rePropClose := regexp.MustCompile(`</[a-zA-Z0-9]*:?prop>`)
|
|
|
|
body = reResponse.ReplaceAllFunc(body, func(resp []byte) []byte {
|
|
hrefMatch := reHref.FindSubmatch(resp)
|
|
if len(hrefMatch) < 2 {
|
|
return resp
|
|
}
|
|
href := string(hrefMatch[1])
|
|
color := be.GetColor(r.Context(), href)
|
|
if color == "" {
|
|
return resp
|
|
}
|
|
|
|
// 1. Strip any existing conflicting tags that might be in 404 blocks
|
|
reStrip := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?(calendar-color|getctag|calendar-order).*?/>|<[a-zA-Z0-9]*:?(calendar-color|getctag|calendar-order).*?>.*?</[a-zA-Z0-9]*:?(calendar-color|getctag|calendar-order)>`)
|
|
resp = reStrip.ReplaceAll(resp, []byte(""))
|
|
|
|
fullColor := strings.ToLower(color)
|
|
if len(fullColor) == 7 && strings.HasPrefix(fullColor, "#") {
|
|
fullColor += "ff"
|
|
}
|
|
// Prepare the properties to inject
|
|
props := fmt.Sprintf("<ICAL:calendar-color>%s</ICAL:calendar-color>", fullColor)
|
|
props += fmt.Sprintf("<C:calendar-color>%s</C:calendar-color>", fullColor)
|
|
props += "<ICAL:calendar-order>0</ICAL:calendar-order>"
|
|
props += fmt.Sprintf("<CS:getctag>\"%d\"</CS:getctag>", time.Now().Unix())
|
|
|
|
// 2. Try to inject into an existing 200 OK propstat
|
|
has200 := false
|
|
resp = rePropstat.ReplaceAllFunc(resp, func(ps []byte) []byte {
|
|
if reStatusOk.Match(ps) {
|
|
has200 = true
|
|
return reProp.ReplaceAllFunc(ps, func(prop []byte) []byte {
|
|
return rePropClose.ReplaceAllFunc(prop, func(closeTag []byte) []byte {
|
|
return append([]byte(props), closeTag...)
|
|
})
|
|
})
|
|
}
|
|
return ps
|
|
})
|
|
|
|
// 3. If no 200 OK propstat was found, create one!
|
|
if !has200 {
|
|
newPropstat := fmt.Sprintf("<propstat xmlns=\"DAV:\"><prop>%s</prop><status>HTTP/1.1 200 OK</status></propstat>", props)
|
|
reResponseClose := regexp.MustCompile(`</[a-zA-Z0-9]*:?response>`)
|
|
resp = reResponseClose.ReplaceAllFunc(resp, func(closeTag []byte) []byte {
|
|
return append([]byte(newPropstat), closeTag...)
|
|
})
|
|
}
|
|
|
|
return resp
|
|
})
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
|
w.WriteHeader(rw.status)
|
|
w.Write(body)
|
|
}
|
|
|
|
func HandleDiscoveryPropfind(r *http.Request, ctx context.Context, handler http.Handler, w http.ResponseWriter, be *backend.DBBackend) {
|
|
reqBody, _ := io.ReadAll(r.Body)
|
|
r.Body = io.NopCloser(bytes.NewBuffer(reqBody))
|
|
|
|
buf := &bytes.Buffer{}
|
|
rw := &responseWriter{w, buf, http.StatusOK}
|
|
handler.ServeHTTP(rw, r.WithContext(ctx))
|
|
|
|
body := buf.Bytes()
|
|
|
|
// 1. Unconditionally add namespaces to root tag
|
|
reMultistatus := regexp.MustCompile(`(<[a-zA-Z0-9]*:?multistatus)`)
|
|
body = reMultistatus.ReplaceAll(body, []byte(`$1 xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:card="urn:ietf:params:xml:ns:carddav"`))
|
|
|
|
// 2. Response processing
|
|
reResponse := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?response.*?>.*?</[a-zA-Z0-9]*:?response>`)
|
|
reHref := regexp.MustCompile(`<[a-zA-Z0-9]*:?href.*?>(.*?)</[a-zA-Z0-9]*:?href>`)
|
|
rePropstat := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?propstat.*?>.*?</[a-zA-Z0-9]*:?propstat>`)
|
|
reStatusOk := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?status.*?>HTTP/1.1 200 OK</[a-zA-Z0-9]*:?status>`)
|
|
reProp := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?prop.*?>.*?</[a-zA-Z0-9]*:?prop>`)
|
|
rePropClose := regexp.MustCompile(`</[a-zA-Z0-9]*:?prop>`)
|
|
|
|
body = reResponse.ReplaceAllFunc(body, func(resp []byte) []byte {
|
|
hrefMatch := reHref.FindSubmatch(resp)
|
|
if len(hrefMatch) < 2 {
|
|
return resp
|
|
}
|
|
|
|
calHome, _ := be.CalendarHomeSetPath(ctx)
|
|
cardHome, _ := be.AddressBookHomeSetPath(ctx)
|
|
|
|
// Strip these tags ONLY from non-200 propstats
|
|
resp = rePropstat.ReplaceAllFunc(resp, func(ps []byte) []byte {
|
|
if !reStatusOk.Match(ps) {
|
|
reTags := regexp.MustCompile(`(?s)<[a-zA-Z0-9:]*(calendar-home-set|addressbook-home-set).*?/>|<[a-zA-Z0-9:]*(calendar-home-set|addressbook-home-set).*?>.*?</[a-zA-Z0-9:]*(calendar-home-set|addressbook-home-set)>`)
|
|
return reTags.ReplaceAll(ps, []byte(""))
|
|
}
|
|
return ps
|
|
})
|
|
|
|
props := ""
|
|
if calHome != "" && !strings.Contains(string(resp), "calendar-home-set") {
|
|
props += fmt.Sprintf("<C:calendar-home-set><href xmlns=\"DAV:\">%s</href></C:calendar-home-set>", calHome)
|
|
}
|
|
if cardHome != "" && !strings.Contains(string(resp), "addressbook-home-set") {
|
|
props += fmt.Sprintf("<card:addressbook-home-set><href xmlns=\"DAV:\">%s</href></card:addressbook-home-set>", cardHome)
|
|
}
|
|
|
|
if props == "" {
|
|
return resp
|
|
}
|
|
|
|
has200 := false
|
|
resp = rePropstat.ReplaceAllFunc(resp, func(ps []byte) []byte {
|
|
if reStatusOk.Match(ps) {
|
|
has200 = true
|
|
return reProp.ReplaceAllFunc(ps, func(prop []byte) []byte {
|
|
return rePropClose.ReplaceAllFunc(prop, func(closeTag []byte) []byte {
|
|
return append([]byte(props), closeTag...)
|
|
})
|
|
})
|
|
}
|
|
return ps
|
|
})
|
|
|
|
if !has200 {
|
|
newPropstat := fmt.Sprintf("<propstat xmlns=\"DAV:\"><prop>%s</prop><status>HTTP/1.1 200 OK</status></propstat>", props)
|
|
reResponseClose := regexp.MustCompile(`</[a-zA-Z0-9]*:?response>`)
|
|
resp = reResponseClose.ReplaceAllFunc(resp, func(closeTag []byte) []byte {
|
|
return append([]byte(newPropstat), closeTag...)
|
|
})
|
|
}
|
|
|
|
return resp
|
|
})
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
|
w.WriteHeader(rw.status)
|
|
w.Write(body)
|
|
}
|