Files
RenewCMS/internal/domain/site/sitePublicURLPattern.go
T

118 lines
2.8 KiB
Go

package site
import (
"net/url"
"strings"
"unicode/utf8"
)
const (
MaxPublicURLPatternRunes = 512
LangPlaceholder = "{lang}"
SlugPlaceholder = "{slug}"
placeholderFiller = "x"
allowedURLSymbols = "-._~:/?#[]@!$&'()*+,;=%"
)
func NormalizePublicURLPattern(pattern *string) (*string, error) {
if pattern == nil {
return nil, nil
}
trimmed := strings.TrimSpace(*pattern)
if trimmed == "" {
return nil, nil
}
if utf8.RuneCountInString(trimmed) > MaxPublicURLPatternRunes {
return nil, ErrInvalidPublicURLPattern
}
if err := checkPublicURLScheme(trimmed); err != nil {
return nil, err
}
if err := checkPlaceholders(trimmed); err != nil {
return nil, err
}
filled := fillPlaceholders(trimmed)
if err := checkEncoding(filled); err != nil {
return nil, err
}
if !strings.Contains(trimmed, SlugPlaceholder) {
return nil, ErrPublicURLMissingSlug
}
if err := checkPublicURLHost(filled); err != nil {
return nil, err
}
return &trimmed, nil
}
func checkPublicURLScheme(pattern string) error {
lowered := strings.ToLower(pattern)
if !strings.HasPrefix(lowered, "https://") && !strings.HasPrefix(lowered, "http://") {
return ErrPublicURLScheme
}
return nil
}
func checkPlaceholders(pattern string) error {
remaining := pattern
for {
open := strings.IndexAny(remaining, "{}")
if open < 0 {
return nil
}
if remaining[open] == '}' {
return ErrPublicURLPlaceholder
}
rest := remaining[open:]
switch {
case strings.HasPrefix(rest, LangPlaceholder):
remaining = rest[len(LangPlaceholder):]
case strings.HasPrefix(rest, SlugPlaceholder):
remaining = rest[len(SlugPlaceholder):]
default:
return ErrPublicURLPlaceholder
}
}
}
func fillPlaceholders(pattern string) string {
return strings.NewReplacer(LangPlaceholder, placeholderFiller, SlugPlaceholder, placeholderFiller).Replace(pattern)
}
func checkEncoding(filled string) error {
for index := 0; index < len(filled); index++ {
character := filled[index]
if character == '%' {
if index+2 >= len(filled) || !isHex(filled[index+1]) || !isHex(filled[index+2]) {
return ErrPublicURLNotEncoded
}
index += 2
continue
}
if !isAllowedURLByte(character) {
return ErrPublicURLNotEncoded
}
}
return nil
}
func isHex(character byte) bool {
return character >= '0' && character <= '9' || character >= 'a' && character <= 'f' || character >= 'A' && character <= 'F'
}
func isAllowedURLByte(character byte) bool {
if character >= '0' && character <= '9' || character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' {
return true
}
return strings.IndexByte(allowedURLSymbols, character) >= 0
}
func checkPublicURLHost(filled string) error {
parsed, err := url.Parse(filled)
if err != nil || parsed.Hostname() == "" {
return ErrPublicURLHost
}
return nil
}