1aecd6f2e1
Lists, campaigns, and subscribers tables now support server-side sorting from the UI. This significantly changes the internal queries from prepared to string interpolated to support dynamic sort params.
145 lines
3.2 KiB
Go
145 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
var (
|
|
|
|
// This replaces all special characters
|
|
tagRegexp = regexp.MustCompile(`[^a-z0-9\-\s]`)
|
|
tagRegexpSpaces = regexp.MustCompile(`[\s]+`)
|
|
)
|
|
|
|
// validateMIME is a helper function to validate uploaded file's MIME type
|
|
// against the slice of MIME types is given.
|
|
func validateMIME(typ string, mimes []string) (ok bool) {
|
|
if len(mimes) > 0 {
|
|
var (
|
|
ok = false
|
|
)
|
|
for _, m := range mimes {
|
|
if typ == m {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// generateFileName appends the incoming file's name with a small random hash.
|
|
func generateFileName(fName string) string {
|
|
name := strings.TrimSpace(fName)
|
|
if name == "" {
|
|
name, _ = generateRandomString(10)
|
|
}
|
|
return name
|
|
}
|
|
|
|
// Given an error, pqErrMsg will try to return pq error details
|
|
// if it's a pq error.
|
|
func pqErrMsg(err error) string {
|
|
if err, ok := err.(*pq.Error); ok {
|
|
if err.Detail != "" {
|
|
return fmt.Sprintf("%s. %s", err, err.Detail)
|
|
}
|
|
}
|
|
return err.Error()
|
|
}
|
|
|
|
// normalizeTags takes a list of string tags and normalizes them by
|
|
// lowercasing and removing all special characters except for dashes.
|
|
func normalizeTags(tags []string) []string {
|
|
var (
|
|
out []string
|
|
space = []byte(" ")
|
|
dash = []byte("-")
|
|
)
|
|
|
|
for _, t := range tags {
|
|
rep := bytes.TrimSpace(tagRegexp.ReplaceAll(bytes.ToLower([]byte(t)), space))
|
|
rep = tagRegexpSpaces.ReplaceAll(rep, dash)
|
|
|
|
if len(rep) > 0 {
|
|
out = append(out, string(rep))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// makeMsgTpl takes a page title, heading, and message and returns
|
|
// a msgTpl that can be rendered as a HTML view. This is used for
|
|
// rendering arbitrary HTML views with error and success messages.
|
|
func makeMsgTpl(pageTitle, heading, msg string) msgTpl {
|
|
if heading == "" {
|
|
heading = pageTitle
|
|
}
|
|
err := msgTpl{}
|
|
err.Title = pageTitle
|
|
err.MessageTitle = heading
|
|
err.Message = msg
|
|
return err
|
|
}
|
|
|
|
// parseStringIDs takes a slice of numeric string IDs and
|
|
// parses each number into an int64 and returns a slice of the
|
|
// resultant values.
|
|
func parseStringIDs(s []string) ([]int64, error) {
|
|
vals := make([]int64, 0, len(s))
|
|
for _, v := range s {
|
|
i, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if i < 1 {
|
|
return nil, fmt.Errorf("%d is not a valid ID", i)
|
|
}
|
|
|
|
vals = append(vals, i)
|
|
}
|
|
|
|
return vals, nil
|
|
}
|
|
|
|
// generateRandomString generates a cryptographically random, alphanumeric string of length n.
|
|
func generateRandomString(n int) (string, error) {
|
|
const dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
var bytes = make([]byte, n)
|
|
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
for k, v := range bytes {
|
|
bytes[k] = dictionary[v%byte(len(dictionary))]
|
|
}
|
|
|
|
return string(bytes), nil
|
|
}
|
|
|
|
// strHasLen checks if the given string has a length within min-max.
|
|
func strHasLen(str string, min, max int) bool {
|
|
return len(str) >= min && len(str) <= max
|
|
}
|
|
|
|
// strSliceContains checks if a string is present in the string slice.
|
|
func strSliceContains(str string, sl []string) bool {
|
|
for _, s := range sl {
|
|
if s == str {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|