forms

package
v0.8.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 24, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package forms captures HTML form posts for static sites: parsing, spam classification, storage, notification and export.

Index

Constants

View Source
const (
	EnvMaxBodyBytes       = "FORMS_MAX_BODY_BYTES"
	EnvMaxFields          = "FORMS_MAX_FIELDS"
	EnvMaxFieldBytes      = "FORMS_MAX_FIELD_BYTES"
	EnvRatePerIPPerMinute = "FORMS_RATE_PER_IP_PER_MINUTE"
	EnvRatePerFormPerMin  = "FORMS_RATE_PER_FORM_PER_MINUTE"
	EnvRetentionDays      = "FORMS_RETENTION_DAYS"
	EnvMaxStoredPerForm   = "FORMS_MAX_STORED_PER_FORM"
	EnvMaxFormsPerApp     = "FORMS_MAX_PER_APP"
	EnvSpamMaxLinks       = "FORMS_SPAM_MAX_LINKS"
)

Environment variables that bound the public endpoint.

View Source
const (
	TypeText     = "text"
	TypeEmail    = "email"
	TypeTextarea = "textarea"
	TypeSelect   = "select"
	TypeCheckbox = "checkbox"
	TypeNumber   = "number"
	TypeURL      = "url"
	TypeHidden   = "hidden"
)

Field input types a form can declare.

View Source
const (
	SpamHoneypot = "honeypot"
	SpamLinks    = "too_many_links"
	SpamEmpty    = "empty"
	SpamManual   = "manual"
)

Spam reasons stored with a flagged submission.

View Source
const DefaultHoneypot = "bot-field"

DefaultHoneypot is the hidden field name used when a form sets none.

Variables

View Source
var (
	ErrTooLarge        = errors.New("forms: body too large")
	ErrUnsupportedType = errors.New("forms: unsupported content type")
	ErrTooManyFields   = errors.New("forms: too many fields")
	ErrFieldTooLong    = errors.New("forms: field too long")
	ErrEmpty           = errors.New("forms: no fields")
	ErrMalformed       = errors.New("forms: malformed body")
)

Parse failures; the API maps each to a distinct status.

Functions

func Classify

func Classify(fields map[string]string, honeypot string, maxLinks int) (clean map[string]string, spam bool, reason string)

Classify reports whether fields look like spam and returns them without the honeypot field. maxLinks <= 0 disables the link heuristic.

func HashIP

func HashIP(salt, ip string) string

HashIP returns a short salted digest of ip; the raw address is never stored.

func NewID

func NewID() (string, error)

NewID returns a random form id.

func NewSalt

func NewSalt() (string, error)

NewSalt returns the random per-form salt used to hash submitter addresses.

func NoticeText

func NoticeText(f store.Form, s store.FormSubmission, dashboardURL string) string

NoticeText is the plain-text body sent to channels. Values are flattened and truncated so a submission cannot inject markup-length payloads or mentions into a chat channel.

func ParseRequest

func ParseRequest(r *http.Request, cfg Config) (map[string]string, error)

ParseRequest reads urlencoded, multipart or JSON bodies into string fields. Multipart files are never stored; only the file name is kept as the value.

func SampleValues

func SampleValues(s Schema) map[string]string

SampleValues builds a plausible submission for the test button.

func Snippet

func Snippet(endpoint string, f store.Form) string

Snippet returns a copy-paste HTML form generated from the form's schema.

func SnippetFetch

func SnippetFetch(endpoint string, f store.Form) string

SnippetFetch returns an HTML form plus a script that posts it as JSON without leaving the page.

func SnippetMinimal

func SnippetMinimal(endpoint string, f store.Form) string

SnippetMinimal returns bare markup: inputs with placeholders, no labels or styling hooks.

func SortedKeys

func SortedKeys(fields map[string]string) []string

SortedKeys returns the field names in a stable order for display and CSV columns.

func ValidateHoneypot

func ValidateHoneypot(field string) error

ValidateHoneypot accepts an empty value (the default) or a plain field name.

func ValidateName

func ValidateName(name string) error

ValidateName accepts lowercase letters, digits, dash and underscore, up to 63 characters.

func ValidateRedirect

func ValidateRedirect(raw string) error

ValidateRedirect accepts an empty value or an absolute http(s) URL without credentials.

func ValidateSchema

func ValidateSchema(s Schema, honeypot string) error

ValidateSchema checks the design itself, not a submission.

func ValidateSubmission

func ValidateSubmission(s Schema, honeypot string, in map[string]string) (map[string]string, map[string]string)

ValidateSubmission checks fields against the schema. It returns the cleaned map (declared names plus the honeypot key, unknown names dropped) and a message per invalid field. A schema with no fields accepts everything unchanged.

func WriteCSV

func WriteCSV(w io.Writer, subs []store.FormSubmission) error

WriteCSV writes submissions with one column per field name seen across them. Cells a spreadsheet would run as a formula are prefixed with a quote.

Types

type Config

type Config struct {
	MaxBodyBytes       int64
	MaxFields          int
	MaxFieldBytes      int
	RatePerIPPerMinute int
	RatePerFormPerMin  int
	Retention          time.Duration
	MaxStoredPerForm   int
	MaxFormsPerApp     int
	SpamMaxLinks       int
}

Config holds the limits read from the environment; zero disables a limit.

func LoadConfig

func LoadConfig(lookup func(string) (string, bool)) Config

LoadConfig reads the limits through lookup (os.LookupEnv in production).

type Field

type Field struct {
	Name        string   `json:"name"`
	Type        string   `json:"type"`
	Label       string   `json:"label,omitempty"`
	Required    bool     `json:"required,omitempty"`
	Placeholder string   `json:"placeholder,omitempty"`
	MinLength   int      `json:"min_length,omitempty"`
	MaxLength   int      `json:"max_length,omitempty"`
	Options     []string `json:"options,omitempty"`
	// Default is the value a hidden field carries in the snippet.
	Default string `json:"default,omitempty"`
}

Field is one input of a form.

func DefaultFields

func DefaultFields() []Field

DefaultFields is the layout used by forms that never set a schema.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, f store.Form, s store.FormSubmission) error
}

Notifier tells the form's channels about a new, non-spam submission.

type Schema

type Schema struct {
	Fields         []Field  `json:"fields"`
	SuccessMessage string   `json:"success_message,omitempty"`
	AllowedOrigins []string `json:"allowed_origins,omitempty"`
}

Schema is a form's editable design. No fields means the default three (DefaultFields). With fields set, submitted names that are not declared are dropped, not stored.

func ParseSchema

func ParseSchema(raw string) (Schema, error)

ParseSchema decodes the stored JSON; empty or "{}" yields the zero Schema.

func (Schema) EffectiveFields

func (s Schema) EffectiveFields() []Field

EffectiveFields returns the declared fields, or the default three when none are set.

type Service

type Service struct {

	// Dispatch runs notification work; tests replace it to run inline.
	Dispatch func(func())
	Now      func() time.Time
	// contains filtered or unexported fields
}

Service stores submissions and fires notifications.

func NewService

func NewService(st Store, n Notifier, cfg Config, logger *slog.Logger) *Service

NewService builds a Service; notifier may be nil to disable notifications.

func (*Service) Config

func (s *Service) Config() Config

Config returns the limits the service was built with.

func (*Service) Submit

func (s *Service) Submit(ctx context.Context, f store.Form, fields map[string]string, clientIP string) (store.FormSubmission, error)

Submit classifies and stores one post. A failed notification never fails the submission: the row is already saved, so it is logged and the visitor still gets success.

func (*Service) SubmitTest

func (s *Service) SubmitTest(ctx context.Context, f store.Form, fields map[string]string) (store.FormSubmission, error)

SubmitTest stores a dashboard test post, flagged as a test and never notified.

type Store

type Store interface {
	AddFormSubmission(ctx context.Context, s store.FormSubmission, maxStored int) (store.FormSubmission, error)
	PruneFormSubmissions(ctx context.Context, cutoff time.Time) (int64, error)
}

Store is the persistence surface Submit needs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL