Files
sm-seo-plugin/classes/field_types.go

140 lines
4.5 KiB
Go

package classes
import (
"context"
"fmt"
"net/url"
"strings"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
)
// The elements of the SEO field types (assets/src/elements).
const (
ElementText = "golem15-seo-text"
ElementRobots = "golem15-seo-robots"
ElementSERP = "golem15-seo-serp"
ElementImage = "golem15-seo-image"
)
// The element bundle every SEO field type loads.
var (
elementScripts = []string{"assets/dist/seo.js"}
elementStyles = []string{"assets/dist/seo.css"}
)
// errRobotsInvalid is the refusal of a malformed robots value.
const errRobotsInvalid = "golem15.seo::lang.robots.invalid"
// FieldTypes returns the SEO admin field types:
// - seotext and mlseotext (E1): text with a live length counter; option
// measure: title (input) or description (textarea);
// - seorobots and mlseorobots (E2): the robots directives editor; option
// scope: record, rule or type; Decode refuses a malformed value;
// - seoimage (E7): an uploaded social image stored in the media bucket,
// its URL as the value (D-100); uploads need permission.
//
// app is asked for the booted application at request time.
func FieldTypes(app func() *backpack.App, permission string) []pact.FormFieldType {
text := func(name string, translatable bool) pact.FormFieldType {
return pact.FormFieldType{
Name: name, Element: ElementText, Translatable: translatable,
Scripts: elementScripts, Styles: elementStyles,
Options: []string{"measure"}, CheckOptions: checkMeasure, Decode: decodeText,
}
}
robots := func(name string, translatable bool) pact.FormFieldType {
return pact.FormFieldType{
Name: name, Element: ElementRobots, Translatable: translatable,
Scripts: elementScripts, Styles: elementStyles,
Options: []string{"scope"}, CheckOptions: checkScope, Decode: decodeRobots,
}
}
return []pact.FormFieldType{
text("seotext", false),
text("mlseotext", true),
robots("seorobots", false),
robots("mlseorobots", true),
{
Name: "seoimage", Element: ElementImage,
Scripts: elementScripts, Styles: elementStyles,
Decode: decodeImage, Upload: UploadImage(app, permission),
},
}
}
func checkMeasure(options map[string]any) error {
if raw, ok := options["measure"]; ok {
if s, _ := raw.(string); s != "title" && s != "description" {
return fmt.Errorf("measure must be title or description, got %v", raw)
}
}
return nil
}
func checkScope(options map[string]any) error {
if raw, ok := options["scope"]; ok {
if s, _ := raw.(string); s != "record" && s != "rule" && s != "type" {
return fmt.Errorf("scope must be record, rule or type, got %v", raw)
}
}
return nil
}
// stringValue reads a submitted value as text; nil is "".
func stringValue(v any) (string, bool) {
switch s := v.(type) {
case nil:
return "", true
case string:
return s, true
}
return "", false
}
func decodeText(_ context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) {
s, ok := stringValue(in.Value)
if !ok {
return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{"The " + in.Field + " field must be text."}}
}
return pact.FormFieldDecoded{Value: strings.TrimSpace(s)}, nil
}
// decodeRobots stores a robots value in the fixed order; a malformed value
// (reachable only through MCP or an import) is refused.
func decodeRobots(_ context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) {
s, ok := stringValue(in.Value)
if !ok {
return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{errRobotsInvalid}}
}
normal, err := NormalizeRobots(s)
if err != nil {
return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{errRobotsInvalid}}
}
return pact.FormFieldDecoded{Value: normal}, nil
}
// decodeImage accepts an empty value, a path or an http(s) URL.
func decodeImage(_ context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) {
s, ok := stringValue(in.Value)
s = strings.TrimSpace(s)
if !ok || !ValidImageURL(s) {
return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{errImageURL}}
}
return pact.FormFieldDecoded{Value: s}, nil
}
// ValidURLRef reports whether v is a path ("/x", not "//x") or an absolute
// http(s) URL with a host, without spaces or control characters.
func ValidURLRef(v string) bool {
if v == "" || strings.ContainsFunc(v, func(r rune) bool { return r <= ' ' || r == 0x7f }) {
return false
}
if strings.HasPrefix(v, "/") {
return !strings.HasPrefix(v, "//")
}
u, err := url.Parse(v)
return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
}