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 } // Add Color To Calendar Propfind func InjectColor(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() // this models after the Radicale Response, largely AI code // 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.*?>.*?`) reHref := regexp.MustCompile(`<[a-zA-Z0-9]*:?href.*?>(.*?)`) rePropstat := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?propstat.*?>.*?`) reStatusOk := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?status.*?>HTTP/1.1 200 OK`) reProp := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?prop.*?>.*?`) rePropClose := regexp.MustCompile(``) 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).*?>.*?`) 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("%s", fullColor) props += fmt.Sprintf("%s", fullColor) props += "0" props += fmt.Sprintf("\"%d\"", 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("%sHTTP/1.1 200 OK", props) reResponseClose := regexp.MustCompile(``) 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) } // modify caledar probfind // // this again modeled after the Radicale response // Largly AI generated agian 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.*?>.*?`) reHref := regexp.MustCompile(`<[a-zA-Z0-9]*:?href.*?>(.*?)`) rePropstat := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?propstat.*?>.*?`) reStatusOk := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?status.*?>HTTP/1.1 200 OK`) reProp := regexp.MustCompile(`(?s)<[a-zA-Z0-9]*:?prop.*?>.*?`) rePropClose := regexp.MustCompile(``) 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).*?>.*?`) return reTags.ReplaceAll(ps, []byte("")) } return ps }) props := "" if calHome != "" && !strings.Contains(string(resp), "calendar-home-set") { props += fmt.Sprintf("%s", calHome) } if cardHome != "" && !strings.Contains(string(resp), "addressbook-home-set") { props += fmt.Sprintf("%s", cardHome) } if props == "" { return resp } // 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("%sHTTP/1.1 200 OK", props) reResponseClose := regexp.MustCompile(``) 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) }