168 lines
4.5 KiB
Go
168 lines
4.5 KiB
Go
package site
|
|
|
|
import (
|
|
"errors"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var (
|
|
ErrSiteNotFound = errors.New("site not found")
|
|
ErrSiteKeyTaken = errors.New("a site with that key already exists")
|
|
ErrSiteHasArticles = errors.New("the site still has articles")
|
|
ErrDefaultSite = errors.New("the default site cannot be deleted")
|
|
ErrKeyImmutable = errors.New("the key of a site cannot be changed")
|
|
ErrInvalidSiteURL = errors.New("invalid site url")
|
|
ErrInvalidLocale = errors.New("invalid locale")
|
|
ErrUseSetDefault = errors.New("the default flag is changed through the default route")
|
|
ErrInvalidReassign = errors.New("invalid reassign destination")
|
|
|
|
ErrInvalidTitleTemplate = errors.New("the title template must contain %title%")
|
|
)
|
|
|
|
const (
|
|
RobotsIndex = "index"
|
|
RobotsNoIndex = "noindex"
|
|
|
|
DefaultName = "Default"
|
|
DefaultKey = "default"
|
|
|
|
MinNameRunes = 1
|
|
MaxNameRunes = 100
|
|
|
|
MaxLocaleRunes = 35
|
|
MaxBaseURLRunes = 255
|
|
|
|
MinKeyRunes = 2
|
|
MaxKeyRunes = 32
|
|
|
|
TitlePlaceholder = "%title%"
|
|
DefaultTitleTemplate = "%title% | %site%"
|
|
)
|
|
|
|
var keyPattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
|
|
var localePattern = regexp.MustCompile(`^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$`)
|
|
|
|
var loopbackHosts = []string{"localhost", "127.0.0.1", "::1"}
|
|
|
|
type Site struct {
|
|
ID uint32 `json:"id"`
|
|
Name string `json:"name"`
|
|
Key string `json:"key"`
|
|
BaseURL string `json:"base_url"`
|
|
DefaultSocialImageID *uint32 `json:"default_social_image_id"`
|
|
DefaultRobots string `json:"default_robots"`
|
|
Locale *string `json:"locale"`
|
|
IsDefault bool `json:"is_default"`
|
|
ArticleCount int `json:"article_count"`
|
|
SiteName string `json:"site_name"`
|
|
TitleTemplate string `json:"title_template"`
|
|
DefaultDescription string `json:"default_description"`
|
|
}
|
|
|
|
type Nullable[T any] struct {
|
|
Set bool
|
|
Value *T
|
|
}
|
|
|
|
type Patch struct {
|
|
Name *string
|
|
Key *string
|
|
BaseURL *string
|
|
DefaultRobots *string
|
|
Locale Nullable[string]
|
|
DefaultSocialImageID Nullable[uint32]
|
|
IsDefault *bool
|
|
SiteName *string
|
|
TitleTemplate *string
|
|
DefaultDescription *string
|
|
}
|
|
|
|
func IsValidKey(key string) bool {
|
|
length := utf8.RuneCountInString(key)
|
|
return length >= MinKeyRunes && length <= MaxKeyRunes && keyPattern.MatchString(key)
|
|
}
|
|
|
|
func IsValidName(name string) bool {
|
|
length := utf8.RuneCountInString(name)
|
|
return length >= MinNameRunes && length <= MaxNameRunes
|
|
}
|
|
|
|
func ValidateTitleTemplate(template string) error {
|
|
if !strings.Contains(template, TitlePlaceholder) {
|
|
return ErrInvalidTitleTemplate
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NormalizeLocale(locale *string) (*string, error) {
|
|
if locale == nil || *locale == "" {
|
|
return nil, nil
|
|
}
|
|
if utf8.RuneCountInString(*locale) > MaxLocaleRunes || !localePattern.MatchString(*locale) {
|
|
return nil, ErrInvalidLocale
|
|
}
|
|
return locale, nil
|
|
}
|
|
|
|
func isLoopback(hostname string) bool {
|
|
for _, loopback := range loopbackHosts {
|
|
if hostname == loopback {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type origin struct {
|
|
scheme string
|
|
host string
|
|
hostname string
|
|
parsed *url.URL
|
|
}
|
|
|
|
func parseOrigin(raw string) (origin, error) {
|
|
parsed, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil || parsed.Host == "" || parsed.Hostname() == "" {
|
|
return origin{}, ErrInvalidSiteURL
|
|
}
|
|
scheme := strings.ToLower(parsed.Scheme)
|
|
if scheme != "http" && scheme != "https" {
|
|
return origin{}, ErrInvalidSiteURL
|
|
}
|
|
return origin{scheme: scheme, host: strings.ToLower(parsed.Host), hostname: strings.ToLower(parsed.Hostname()), parsed: parsed}, nil
|
|
}
|
|
|
|
func NormalizeBaseURL(raw string) (string, error) {
|
|
found, err := parseOrigin(raw)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parsed := found.parsed
|
|
if parsed.User != nil || parsed.RawQuery != "" || parsed.ForceQuery || parsed.Fragment != "" || (parsed.Path != "" && parsed.Path != "/") {
|
|
return "", ErrInvalidSiteURL
|
|
}
|
|
if found.scheme == "http" && !isLoopback(found.hostname) {
|
|
return "", ErrInvalidSiteURL
|
|
}
|
|
normalized := found.scheme + "://" + found.host
|
|
if utf8.RuneCountInString(normalized) > MaxBaseURLRunes {
|
|
return "", ErrInvalidSiteURL
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func SeedBaseURL(publicURL string) (string, error) {
|
|
found, err := parseOrigin(publicURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
scheme := found.scheme
|
|
if scheme == "http" && !isLoopback(found.hostname) {
|
|
scheme = "https"
|
|
}
|
|
return scheme + "://" + found.host, nil
|
|
}
|