- plugin golem15.feedback (requires golem15.user), its config, lang (en, pl), models and migrations
- typed settings singleton golem15_feedback_settings with the WinterCMS defaults and a widget key generated on first creation
- GET /_feedback/api/v1/{key}/config behind the constant-time key check and the fail-closed Origin allow-list, with the feedback-config and feedback-submit buckets
100 lines
2.3 KiB
Go
100 lines
2.3 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/wire"
|
|
)
|
|
|
|
// writeJSON answers like Laravel's response()->json behind WinterCMS's
|
|
// json.response middleware: the JSON body with Cache-Control
|
|
// "no-cache, private". The plugin keeps its own writers because a plugin in
|
|
// its own repository cannot import an application's controllers.
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
w.Header().Set("Cache-Control", "no-cache, private")
|
|
wire.WriteJSON(w, status, v)
|
|
}
|
|
|
|
// writeError is the widget routes' {"error":true,"message":...} envelope.
|
|
func writeError(w http.ResponseWriter, status int, message string) {
|
|
writeJSON(w, status, errorBody{Error: true, Message: message})
|
|
}
|
|
|
|
type errorBody struct {
|
|
Error bool `json:"error"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// writeOpaque500 is the house opaque 500 for a failure that is not the
|
|
// client's (no database, a failed write).
|
|
func writeOpaque500(w http.ResponseWriter) {
|
|
wire.WriteOpaque500(w)
|
|
}
|
|
|
|
// orderedErrors marshals a Laravel message bag with its keys in rule order
|
|
// (lagoon.ErrorKeys), the remaining keys sorted after them.
|
|
type orderedErrors struct {
|
|
errs map[string][]string
|
|
order []string
|
|
}
|
|
|
|
func (o orderedErrors) MarshalJSON() ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
buf.WriteByte('{')
|
|
seen := map[string]bool{}
|
|
first := true
|
|
write := func(k string) error {
|
|
msgs, ok := o.errs[k]
|
|
if !ok || seen[k] {
|
|
return nil
|
|
}
|
|
seen[k] = true
|
|
if !first {
|
|
buf.WriteByte(',')
|
|
}
|
|
first = false
|
|
kb, err := marshalNoEscape(k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vb, err := marshalNoEscape(msgs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf.Write(kb)
|
|
buf.WriteByte(':')
|
|
buf.Write(vb)
|
|
return nil
|
|
}
|
|
for _, k := range o.order {
|
|
if err := write(k); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
rest := make([]string, 0, len(o.errs))
|
|
for k := range o.errs {
|
|
rest = append(rest, k)
|
|
}
|
|
sort.Strings(rest)
|
|
for _, k := range rest {
|
|
if err := write(k); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
buf.WriteByte('}')
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func marshalNoEscape(v any) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
enc := json.NewEncoder(&buf)
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(v); err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes.TrimRight(buf.Bytes(), "\n"), nil
|
|
}
|