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 }