// kafka — a privacy-respecting metasearch engine // Copyright (C) 2026-present metamorphosis-dev // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package search import ( "bytes" "encoding/csv" "encoding/json" "fmt" "net/http" "net/url" "encoding/xml" "strconv" "strings" ) func WriteSearchResponse(w http.ResponseWriter, format OutputFormat, resp SearchResponse) error { switch format { case FormatJSON: w.Header().Set("Content-Type", "application/json; charset=utf-8") return json.NewEncoder(w).Encode(resp) case FormatCSV: w.Header().Set("Content-Type", "text/csv; charset=utf-8") if err := writeCSV(w, resp); err != nil { return err } return nil case FormatRSS: w.Header().Set("Content-Type", "text/xml; charset=utf-8") if err := writeRSS(w, resp); err != nil { return err } return nil case FormatHTML: w.WriteHeader(http.StatusNotImplemented) _, _ = w.Write([]byte("format=html not implemented yet")) return nil default: return fmt.Errorf("unsupported format: %s", format) } } // csvRowHeader matches the CSV writer key order. var csvRowHeader = []string{"title", "url", "content", "host", "engine", "score", "type"} func writeCSV(w http.ResponseWriter, resp SearchResponse) error { cw := csv.NewWriter(w) defer cw.Flush() if err := cw.Write(csvRowHeader); err != nil { return err } for _, r := range resp.Results { urlStr := "" if r.URL != nil { urlStr = *r.URL } host := hostFromURL(urlStr) scoreStr := strconv.FormatFloat(r.Score, 'f', -1, 64) row := []string{ r.Title, urlStr, r.Content, host, r.Engine, scoreStr, "result", } if err := cw.Write(row); err != nil { return err } } for _, ans := range resp.Answers { title := asString(ans["title"]) urlStr := asString(ans["url"]) content := asString(ans["content"]) engine := asString(ans["engine"]) scoreStr := scoreString(ans["score"]) host := hostFromURL(urlStr) row := []string{ title, urlStr, content, host, engine, scoreStr, "answer", } if err := cw.Write(row); err != nil { return err } } for _, s := range resp.Suggestions { row := []string{s, "", "", "", "", "", "suggestion"} if err := cw.Write(row); err != nil { return err } } for _, c := range resp.Corrections { row := []string{c, "", "", "", "", "", "correction"} if err := cw.Write(row); err != nil { return err } } return nil } func writeRSS(w http.ResponseWriter, resp SearchResponse) error { q := resp.Query escapedTitle := xmlEscape("kafka search: " + q) escapedDesc := xmlEscape("Search results for \"" + q + "\" - kafka") escapedQueryTerms := xmlEscape(q) link := "/search?q=" + url.QueryEscape(q) opensearchQuery := fmt.Sprintf(``, escapedQueryTerms) // The template uses the number of results for both totalResults and itemsPerPage. nr := resp.NumberOfResults var items bytes.Buffer for _, r := range resp.Results { title := xmlEscape(r.Title) urlStr := "" if r.URL != nil { urlStr = *r.URL } linkEsc := xmlEscape(urlStr) desc := xmlEscape(r.Content) pub := "" if r.Pubdate != nil && strings.TrimSpace(*r.Pubdate) != "" { pub = "" + xmlEscape(*r.Pubdate) + "" } items.WriteString( fmt.Sprintf( `%sresult%s%s%s`, title, linkEsc, desc, pub, ), ) } xml := fmt.Sprintf( ` %s %s %s %d 1 %d `, escapedTitle, xmlEscape(link), escapedDesc, nr, nr, opensearchQuery, items.String(), ) _, err := w.Write([]byte(xml)) return err } func xmlEscape(s string) string { var b bytes.Buffer _ = xml.EscapeText(&b, []byte(s)) return b.String() } func hostFromURL(urlStr string) string { if strings.TrimSpace(urlStr) == "" { return "" } u, err := url.Parse(urlStr) if err != nil { return "" } return u.Host } func asString(v any) string { s, _ := v.(string) return s } func scoreString(v any) string { switch t := v.(type) { case float64: return strconv.FormatFloat(t, 'f', -1, 64) case float32: return strconv.FormatFloat(float64(t), 'f', -1, 64) case int: return strconv.Itoa(t) case int64: return strconv.FormatInt(t, 10) case json.Number: if f, err := t.Float64(); err == nil { return strconv.FormatFloat(f, 'f', -1, 64) } return "" default: return "" } }