feat: site languages, setup and site URL settings
This commit is contained in:
1 parent
0806733d5c
commit
d9158025d4
106 files changed
+10184
-56
No files matched your search
@@ -20,6 +20,13 @@ var (
|
||||
ErrInvalidReassign = errors.New("invalid reassign destination")
|
||||
|
||||
ErrInvalidTitleTemplate = errors.New("the title template must contain %title%")
|
||||
|
||||
ErrInvalidPublicURLPattern = errors.New("invalid public url pattern")
|
||||
ErrPublicURLScheme = errors.New("the public url pattern must start with https:// or http://")
|
||||
ErrPublicURLHost = errors.New("the public url pattern must have a host")
|
||||
ErrPublicURLPlaceholder = errors.New("the public url pattern has an unknown placeholder")
|
||||
ErrPublicURLNotEncoded = errors.New("the public url pattern has characters that need encoding")
|
||||
ErrPublicURLMissingSlug = errors.New("the public url pattern must contain {slug}")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -60,6 +67,7 @@ type Site struct {
|
||||
SiteName string `json:"site_name"`
|
||||
TitleTemplate string `json:"title_template"`
|
||||
DefaultDescription string `json:"default_description"`
|
||||
PublicURLPattern *string `json:"public_url_pattern"`
|
||||
}
|
||||
|
||||
type Nullable[T any] struct {
|
||||
@@ -78,6 +86,7 @@ type Patch struct {
|
||||
SiteName *string
|
||||
TitleTemplate *string
|
||||
DefaultDescription *string
|
||||
PublicURLPattern Nullable[string]
|
||||
}
|
||||
|
||||
func IsValidKey(key string) bool {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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
|
||||
}
|
||||
@@ -20,6 +20,8 @@ type Repository interface {
|
||||
Create(site Site) (Site, error)
|
||||
Update(site Site) (Site, error)
|
||||
SetDefault(id uint32) (Site, error)
|
||||
LockSite(id uint32) error
|
||||
TryLockSites(ids []uint32) error
|
||||
DeleteMovingArticles(id uint32, reassignTo *uint32) error
|
||||
ClearImage(imageID uint32) ([]uint32, error)
|
||||
AssignImage(siteIDs []uint32, imageID uint32) error
|
||||
|
||||
Reference in new issue
Block a user