- seaglass (new module): bluemonday v1.0.27 policy ported from Journal without the Blog Editor rules, byte-stable token comparison, SanitizeReport, Sanitize, Check, Removals - cabana: type richeditor (common keys plus size); every create and update value sanitized before Fill, non-string values are a 422, removals become meta.notices; settings and relation forms refuse the type - backend lang richeditor.removed_* and markup_changed (en, pl plurals) - TestSeaglassSmoke, TestCompanionsRichEditorSmoke, README and docs
136 lines
4.0 KiB
Go
136 lines
4.0 KiB
Go
package cabana
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/seaglass"
|
|
"github.com/goccy/go-yaml/ast"
|
|
)
|
|
|
|
// richEditorType is the core HTML WYSIWYG field type (D-10).
|
|
const richEditorType = "richeditor"
|
|
|
|
// The phrase keys of the notices a richeditor save reports (backend::lang).
|
|
const (
|
|
msgRichRemovedElements = "backend::lang.richeditor.removed_elements"
|
|
msgRichRemovedAttributes = "backend::lang.richeditor.removed_attributes"
|
|
msgRichRemovedComments = "backend::lang.richeditor.removed_comments"
|
|
msgRichMarkupChanged = "backend::lang.richeditor.markup_changed"
|
|
)
|
|
|
|
// richEditorKeys are the fields.yaml keys a richeditor field accepts: the
|
|
// common field keys plus size.
|
|
var richEditorKeys = map[string]bool{
|
|
"label": true, "comment": true, "span": true, "type": true, "required": true,
|
|
"tab": true, "context": true, "size": true, "default": true,
|
|
}
|
|
|
|
// compileRichEditorKeys refuses every key a richeditor field does not take.
|
|
func compileRichEditorKeys(typ string, values map[string]ast.Node) error {
|
|
if typ != richEditorType {
|
|
return nil
|
|
}
|
|
for key := range values {
|
|
if !richEditorKeys[key] {
|
|
return fmt.Errorf("%s is not valid on type: richeditor", key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkRichEditorValues refuses, before the save transaction, a submitted
|
|
// richeditor value that is neither a string nor null: it is a 422 on the
|
|
// field, never silently dropped.
|
|
func checkRichEditorValues(cc *CompiledController, body map[string]any, op string) error {
|
|
if cc == nil || cc.Form == nil {
|
|
return nil
|
|
}
|
|
for _, field := range cc.Form.Fields {
|
|
if field.Type != richEditorType || !contextAllows(cc, field.Name, op) {
|
|
continue
|
|
}
|
|
value, present := body[field.Name]
|
|
if !present {
|
|
continue
|
|
}
|
|
switch value.(type) {
|
|
case nil, string:
|
|
default:
|
|
return &ValidationError{Details: fillTypeDetails(field.Name)}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sanitizeRichEditors runs every projected richeditor value through
|
|
// seaglass.SanitizeReport and writes the clean value back, so no richeditor
|
|
// value reaches Fill unsanitized. HTML the policy keeps whole is stored byte
|
|
// for byte; what it removed is returned as notices on the field.
|
|
func sanitizeRichEditors(cc *CompiledController, projected map[string]any) []pluginNotice {
|
|
if cc == nil || cc.Form == nil {
|
|
return nil
|
|
}
|
|
var notices []pluginNotice
|
|
for _, field := range cc.Form.Fields {
|
|
if field.Type != richEditorType {
|
|
continue
|
|
}
|
|
key := fillKeyOf(cc, field.Name)
|
|
value, present := projected[key]
|
|
if !present {
|
|
continue
|
|
}
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
clean, report := seaglass.SanitizeReport(text)
|
|
projected[key] = clean
|
|
for _, notice := range richEditorNotices(report) {
|
|
notices = append(notices, pluginNotice{field: field.Name, notice: notice})
|
|
}
|
|
}
|
|
return notices
|
|
}
|
|
|
|
// fillKeyOf is the fill key bound to a writable field name.
|
|
func fillKeyOf(cc *CompiledController, name string) string {
|
|
for _, field := range cc.Writable {
|
|
if field.Name == name {
|
|
return field.FillKey
|
|
}
|
|
}
|
|
return name
|
|
}
|
|
|
|
// richEditorNotices turns a removal report into admin notices: removed
|
|
// elements, removed attributes, removed comments, in that order and only when
|
|
// non-zero, and markup_changed when the policy changed the HTML in a way none
|
|
// of them describe.
|
|
func richEditorNotices(r seaglass.Removals) []pact.FormFieldNotice {
|
|
var out []pact.FormFieldNotice
|
|
if r.ElementCount > 0 {
|
|
out = append(out, pact.FormFieldNotice{
|
|
Message: msgRichRemovedElements,
|
|
Params: map[string]string{"names": strings.Join(r.Elements, ", ")},
|
|
Count: r.ElementCount,
|
|
})
|
|
}
|
|
if r.AttributeCount > 0 {
|
|
out = append(out, pact.FormFieldNotice{
|
|
Message: msgRichRemovedAttributes,
|
|
Params: map[string]string{"names": strings.Join(r.Attributes, ", ")},
|
|
Count: r.AttributeCount,
|
|
})
|
|
}
|
|
if r.Comments > 0 {
|
|
out = append(out, pact.FormFieldNotice{Message: msgRichRemovedComments, Count: r.Comments})
|
|
}
|
|
if r.Changed && len(out) == 0 {
|
|
out = append(out, pact.FormFieldNotice{Message: msgRichMarkupChanged})
|
|
}
|
|
return out
|
|
}
|