Files
gotenberg/pkg/modules/api/formdata.go
T
2025-04-17 11:15:52 +02:00

529 lines
14 KiB
Go

package api
import (
"fmt"
"math"
"net/http"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"go.uber.org/multierr"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
// FormData is a helper for validating and hydrating values from a
// "multipart/form-data" request.
//
// form := ctx.FormData()
type FormData struct {
values map[string][]string
files map[string]string
errors error
}
// Validate returns nil or an error related to the [FormData] values, with a
// [SentinelHttpError] (status code 400, errors' details as a message) wrapped
// inside.
//
// var foo string
//
// err := ctx.FormData().
// MandatoryString("foo", &foo, "bar").
// Validate()
func (form *FormData) Validate() error {
if form.errors == nil {
return nil
}
return WrapError(
form.errors,
NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("Invalid form data: %s", form.errors)),
)
}
// String binds a form field to a string variable.
//
// var foo string
//
// ctx.FormData().String("foo", &foo, "bar")
func (form *FormData) String(key string, target *string, defaultValue string) *FormData {
return form.mustValue(key, target, defaultValue)
}
// MandatoryString binds a form field to a string variable. It populates
// an error if the value is empty or the "key" does not exist.
//
// var foo string
//
// ctx.FormData().MandatoryString("foo", &foo)
func (form *FormData) MandatoryString(key string, target *string) *FormData {
return form.mustMandatoryField(key, target)
}
// Bool binds a form field to a bool variable. It populates an error if
// the value is not bool.
//
// var foo bool
//
// ctx.FormData().Bool("foo", &foo, true)
func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormData {
return form.mustValue(key, target, defaultValue)
}
// MandatoryBool binds a form field to a bool variable. It populates an
// error if the value is not bool, is empty, or the "key" does not exist.
//
// var foo bool
//
// ctx.FormData().MandatoryBool("foo", &foo)
func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
return form.mustMandatoryField(key, target)
}
// Int binds a form field to an int variable. It populates an error if the
// value is not int.
//
// var foo int
//
// ctx.FormData().Int("foo", &foo, 2)
func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
return form.mustValue(key, target, defaultValue)
}
// MandatoryInt binds a form field to an int variable. It populates an
// error if the value is not int, or is empty, or the "key" does not exist.
//
// var foo int
//
// ctx.FormData().MandatoryInt("foo", &foo)
func (form *FormData) MandatoryInt(key string, target *int) *FormData {
return form.mustMandatoryField(key, target)
}
// Float64 binds a form field to a float64 variable. It populates an error
// if the value is not float64.
//
// var foo float64
//
// ctx.FormData().Float64("foo", &foo, 2.0)
func (form *FormData) Float64(key string, target *float64, defaultValue float64) *FormData {
return form.mustValue(key, target, defaultValue)
}
// MandatoryFloat64 binds a form field to a float64 variable. It populates
// an error if the value is not float64, is empty, or the "key" does not exist.
//
// var foo float64
//
// ctx.FormData().MandatoryFloat64("foo", &foo)
func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData {
return form.mustMandatoryField(key, target)
}
// Duration binds a form field to a time.Duration variable. It populates
// an error if the form field is not time.Duration.
//
// var foo time.Duration
//
// ctx.FormData().Duration("foo", &foo, time.Duration(2) * time.Second)
func (form *FormData) Duration(key string, target *time.Duration, defaultValue time.Duration) *FormData {
return form.mustValue(key, target, defaultValue)
}
// MandatoryDuration binds a form field to a time.Duration variable. It
// populates an error if the value is not time.Duration, or is empty, or the
// "key" does not exist.
//
// var foo time.Duration
//
// ctx.FormData().MandatoryDuration("foo", &foo)
func (form *FormData) MandatoryDuration(key string, target *time.Duration) *FormData {
return form.mustMandatoryField(key, target)
}
// Inches bind a form field to a float64 variable. It populates an error
// if the value cannot be computed back to inches.
//
// var foo float64
//
// ctx.FormData().Inches("foo", &foo, 2.0)
func (form *FormData) Inches(key string, target *float64, defaultValue float64) *FormData {
form.inches(key, target)
if *target == -math.MaxFloat64 {
*target = defaultValue
}
return form
}
// MandatoryInches binds a form field to a float64 variable. It populates
// an error if the value cannot be computed back to inches, is empty, or the
// "key" does not exist.
//
// var foo float64
//
// ctx.FormData().MandatoryInches("foo", &foo)
func (form *FormData) MandatoryInches(key string, target *float64) *FormData {
val, ok := form.values[key]
if !ok || val[0] == "" {
form.append(
fmt.Errorf("form field '%s' is required", key),
)
return form
}
return form.inches(key, target)
}
// inches tries to compute a string value to inches.
func (form *FormData) inches(key string, target *float64) *FormData {
var value string
form.mustValue(key, &value, "")
if value == "" {
*target = -math.MaxFloat64
return form
}
for _, unit := range []string{"pt", "px", "in", "mm", "cm", "pc"} {
if !strings.HasSuffix(value, unit) {
continue
}
val, err := strconv.ParseFloat(strings.TrimSuffix(value, unit), 64)
if err != nil {
form.append(
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
return form
}
switch unit {
case "pt":
*target = val * (1.0 / 72.0)
case "px":
*target = val * (1.0 / 96.0)
case "in":
*target = val
case "mm":
*target = val * (1.0 / 25.4)
case "cm":
*target = val * (1.0 / 2.54)
case "pc":
*target = val * (1.0 / 6.0)
}
return form
}
val, err := strconv.ParseFloat(value, 64)
if err != nil {
form.append(
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
return form
}
*target = val
return form
}
// Custom helps to define a custom binding function for a form field.
//
// var foo map[string]string
//
// ctx.FormData().Custom("foo", func(value string) error {
// if value == "" {
// foo = "bar"
//
// return nil
// }
//
// err := json.Unmarshal([]byte(value), &foo)
// if err != nil {
// return fmt.Errorf("unmarshal foo: %w", err)
// }
//
// return nil
// })
func (form *FormData) Custom(key string, assign func(value string) error) *FormData {
var value string
form.mustValue(key, &value, "")
err := assign(value)
if err != nil {
form.append(
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
}
return form
}
// MandatoryCustom helps to define a custom binding function for a form field.
// It populates an error if the value is empty or the "key" does not exist.
//
// var foo map[string]string
//
// ctx.FormData().MandatoryCustom("foo", func(value string) error {
// err := json.Unmarshal([]byte(value), &foo)
// if err != nil {
// return fmt.Errorf("unmarshal foo: %w", err)
// }
//
// return nil
// })
func (form *FormData) MandatoryCustom(key string, assign func(value string) error) *FormData {
var value string
form.mustMandatoryField(key, &value)
if value == "" {
return form
}
err := assign(value)
if err != nil {
form.append(
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
}
return form
}
// Path binds the absolute path of a form data file to a string variable.
//
// var path string
//
// ctx.FormData().Path("foo.txt", &path)
func (form *FormData) Path(filename string, target *string) *FormData {
return form.path(filename, target)
}
// MandatoryPath binds the absolute path of a form data file to a string
// variable. It populates an error if the file does not exist.
//
// var path string
//
// ctx.FormData().MandatoryPath("foo.txt", &path)
func (form *FormData) MandatoryPath(filename string, target *string) *FormData {
return form.mandatoryPath(filename, target)
}
// Content binds the content of a form data file to a string variable.
//
// var content string
//
// ctx.FormData().Content("foo.txt", &content, "bar")
func (form *FormData) Content(filename string, target *string, defaultValue string) *FormData {
var path string
form.path(filename, &path)
if path == "" {
*target = defaultValue
return form
}
return form.readFile(path, filename, target)
}
// MandatoryContent binds the content of a form data file to a string variable.
// It populates an error if the file does not exist.
//
// var content string
//
// ctx.FormData().MandatoryContent("foo.txt", &content)
func (form *FormData) MandatoryContent(filename string, target *string) *FormData {
var path string
form.mandatoryPath(filename, &path)
if path == "" {
return form
}
return form.readFile(path, filename, target)
}
// Paths bind the absolute paths of form data files, according to a list of
// file extensions, to a string slice variable.
//
// var paths []string
//
// ctx.FormData().Paths([]string{".txt"}, &paths)
func (form *FormData) Paths(extensions []string, target *[]string) *FormData {
return form.paths(extensions, target)
}
// MandatoryPaths binds the absolute paths of form data files, according to a
// list of file extensions, to a string slice variable. It populates an error
// if there is no file for given file extensions.
//
// var paths []string
//
// ctx.FormData().MandatoryPaths([]string{".txt"}, &paths)
func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *FormData {
form.paths(extensions, target)
if len(*target) > 0 {
return form
}
form.append(
fmt.Errorf("no form file found for extensions: %v", extensions),
)
return form
}
// paths bind the absolute paths of form data files, according to a list of
// file extensions, to a string slice variable.
func (form *FormData) paths(extensions []string, target *[]string) *FormData {
for filename, path := range form.files {
for _, ext := range extensions {
// See https://github.com/gotenberg/gotenberg/issues/228.
if strings.ToLower(filepath.Ext(filename)) == ext {
*target = append(*target, path)
}
}
}
// See https://github.com/gotenberg/gotenberg/issues/139.
sort.Sort(gotenberg.AlphanumericSort(*target))
return form
}
// append adds an error to the list of errors.
func (form *FormData) append(err error) {
form.errors = multierr.Append(form.errors, err)
}
// mustValue binds the target interface with a form field. If the value is
// empty or the "key" does not exist, it binds the default value. Currently,
// only the string, bool, int, float64 and time.Duration types are bindable.
func (form *FormData) mustValue(key string, target interface{}, defaultValue interface{}) *FormData {
val, ok := form.values[key]
if !ok || val[0] == "" {
switch t := (target).(type) {
case *string:
*t = defaultValue.(string)
case *bool:
*t = defaultValue.(bool)
case *int:
*t = defaultValue.(int)
case *float64:
*t = defaultValue.(float64)
case *time.Duration:
*t = defaultValue.(time.Duration)
default:
panic("target type not supported")
}
return form
}
return form.mustAssign(key, val[0], target)
}
// mustMandatoryField binds the target interface with a form field. It
// populates an error if the value is empty or the "key" does not exist.
// Currently, only the string, bool, int, float64 and time.Duration types are
// bindable.
func (form *FormData) mustMandatoryField(key string, target interface{}) *FormData {
val, ok := form.values[key]
if !ok || val[0] == "" {
form.append(
fmt.Errorf("form field '%s' is required", key),
)
return form
}
form.mustAssign(key, val[0], target)
return form
}
// mustAssign parses the string value and tries to convert it to the target
// interface real type. Currently, only the string, bool, int, float64 and
// time.Duration types are bindable.
func (form *FormData) mustAssign(key, value string, target interface{}) *FormData {
var err error
switch t := (target).(type) {
case *string:
*t = value
case *bool:
*t, err = strconv.ParseBool(value)
case *int:
*t, err = strconv.Atoi(value)
case *float64:
*t, err = strconv.ParseFloat(value, 64)
case *time.Duration:
*t, err = time.ParseDuration(value)
default:
panic("target type not supported")
}
if err != nil {
form.append(
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
}
return form
}
// path binds the absolute path of a form data file to a string variable.
func (form *FormData) path(filename string, target *string) *FormData {
for name, path := range form.files {
// See https://github.com/gotenberg/gotenberg/issues/228.
nameLowerExt := strings.TrimSuffix(name, filepath.Ext(name)) + strings.ToLower(filepath.Ext(name))
if name == filename || nameLowerExt == filename {
*target = path
return form
}
}
return form
}
// mandatoryPath binds the absolute path of a form data file to a string
// variable. It populates an error if the file does not exist.
func (form *FormData) mandatoryPath(filename string, target *string) *FormData {
form.path(filename, target)
if *target != "" {
return form
}
form.append(
fmt.Errorf("form file '%s' is required", filename),
)
return form
}
// readFile binds the content of a file to a string variable. It populates an
// error if it fails to read the file content.
func (form *FormData) readFile(path, filename string, target *string) *FormData {
b, err := os.ReadFile(path)
if err != nil {
form.append(
fmt.Errorf("form file '%s' is invalid (%w)", filename, err),
)
return form
}
*target = string(b)
return form
}