同步包路径与路由,去掉 SPA 构建步骤,对齐 Gitea 式 LICENSE,并更新规格/规则与占位 SSR。 Co-authored-by: Cursor <cursoragent@cursor.com>
220 lines
5.3 KiB
Go
220 lines
5.3 KiB
Go
package services
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
const (
|
||
SettingPermalinkEnabled = "permalink_enabled"
|
||
SettingPermalinkExt = "permalink_ext"
|
||
DefaultPermalinkExt = "html"
|
||
)
|
||
|
||
var (
|
||
permalinkExtRe = regexp.MustCompile(`(?i)^[a-z0-9]{1,16}$`)
|
||
slugPermalinkRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$|^[a-z0-9]$`)
|
||
// /post/123 或 /post/123.html
|
||
postPermalinkRe = regexp.MustCompile(`^/post/(\d+)(?:\.([A-Za-z0-9]{1,16}))?/?$`)
|
||
userPermalinkRe = regexp.MustCompile(`^/user/(\d+)(?:\.([A-Za-z0-9]{1,16}))?/?$`)
|
||
boardPermalinkRe = regexp.MustCompile(`^/board/(\d+)(?:\.([A-Za-z0-9]{1,16}))?/?$`)
|
||
pagePermalinkRe = regexp.MustCompile(`^/page/([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])(?:\.([A-Za-z0-9]{1,16}))?/?$`)
|
||
)
|
||
|
||
// PermalinkConfig 伪静态(固定链接)配置
|
||
type PermalinkConfig struct {
|
||
Enabled bool `json:"permalink_enabled"`
|
||
Ext string `json:"permalink_ext"` // 不含点,如 html / htm
|
||
}
|
||
|
||
// NormalizePermalinkExt 规范化后缀:去点、小写、仅字母数字
|
||
func NormalizePermalinkExt(raw string) (string, bool) {
|
||
ext := strings.TrimSpace(raw)
|
||
ext = strings.TrimPrefix(ext, ".")
|
||
ext = strings.ToLower(ext)
|
||
if ext == "" {
|
||
ext = DefaultPermalinkExt
|
||
}
|
||
if !permalinkExtRe.MatchString(ext) {
|
||
return "", false
|
||
}
|
||
return ext, true
|
||
}
|
||
|
||
// Permalink 读取伪静态配置
|
||
func (s *ForumSettingsService) Permalink() PermalinkConfig {
|
||
ext, ok := NormalizePermalinkExt(s.getString(SettingPermalinkExt, DefaultPermalinkExt))
|
||
if !ok {
|
||
ext = DefaultPermalinkExt
|
||
}
|
||
return PermalinkConfig{
|
||
Enabled: s.getString(SettingPermalinkEnabled, "0") == "1",
|
||
Ext: ext,
|
||
}
|
||
}
|
||
|
||
// Suffix 返回带点后缀(未启用时为空)
|
||
func (p PermalinkConfig) Suffix() string {
|
||
if !p.Enabled {
|
||
return ""
|
||
}
|
||
ext, ok := NormalizePermalinkExt(p.Ext)
|
||
if !ok {
|
||
ext = DefaultPermalinkExt
|
||
}
|
||
return "." + ext
|
||
}
|
||
|
||
// PostPath 帖子规范路径
|
||
func (p PermalinkConfig) PostPath(id uint) string {
|
||
return fmt.Sprintf("/post/%d%s", id, p.Suffix())
|
||
}
|
||
|
||
// UserPath 用户规范路径
|
||
func (p PermalinkConfig) UserPath(id uint) string {
|
||
return fmt.Sprintf("/user/%d%s", id, p.Suffix())
|
||
}
|
||
|
||
// BoardPath 板块规范路径
|
||
func (p PermalinkConfig) BoardPath(id uint) string {
|
||
return fmt.Sprintf("/board/%d%s", id, p.Suffix())
|
||
}
|
||
|
||
// PagePath 自定义单页规范路径
|
||
func (p PermalinkConfig) PagePath(slug string) string {
|
||
slug = strings.TrimSpace(strings.ToLower(slug))
|
||
if slug == "" {
|
||
return "/"
|
||
}
|
||
return fmt.Sprintf("/page/%s%s", slug, p.Suffix())
|
||
}
|
||
|
||
// NormalizePageSlug 校验单页 slug
|
||
func NormalizePageSlug(raw string) (string, bool) {
|
||
slug := strings.TrimSpace(strings.ToLower(raw))
|
||
if slug == "" || len(slug) > 64 {
|
||
return "", false
|
||
}
|
||
if !slugPermalinkRe.MatchString(slug) {
|
||
return "", false
|
||
}
|
||
return slug, true
|
||
}
|
||
|
||
// PermalinkMatch 路径解析结果
|
||
type PermalinkMatch struct {
|
||
ID uint
|
||
Ext string // 请求里的后缀(无点);无后缀为空
|
||
Canonical string // 当前配置下的规范路径
|
||
OK bool
|
||
}
|
||
|
||
// MatchPostPath 解析帖子公开路径(不含 /edit)
|
||
func (p PermalinkConfig) MatchPostPath(path string) PermalinkMatch {
|
||
m := postPermalinkRe.FindStringSubmatch(path)
|
||
if len(m) < 2 {
|
||
return PermalinkMatch{}
|
||
}
|
||
id64, err := strconv.ParseUint(m[1], 10, 64)
|
||
if err != nil || id64 == 0 {
|
||
return PermalinkMatch{}
|
||
}
|
||
ext := ""
|
||
if len(m) > 2 {
|
||
ext = strings.ToLower(m[2])
|
||
}
|
||
id := uint(id64)
|
||
return PermalinkMatch{
|
||
ID: id,
|
||
Ext: ext,
|
||
Canonical: p.PostPath(id),
|
||
OK: true,
|
||
}
|
||
}
|
||
|
||
// MatchBoardPath 解析板块公开路径
|
||
func (p PermalinkConfig) MatchBoardPath(path string) PermalinkMatch {
|
||
m := boardPermalinkRe.FindStringSubmatch(path)
|
||
if len(m) < 2 {
|
||
return PermalinkMatch{}
|
||
}
|
||
id64, err := strconv.ParseUint(m[1], 10, 64)
|
||
if err != nil || id64 == 0 {
|
||
return PermalinkMatch{}
|
||
}
|
||
ext := ""
|
||
if len(m) > 2 {
|
||
ext = strings.ToLower(m[2])
|
||
}
|
||
id := uint(id64)
|
||
return PermalinkMatch{
|
||
ID: id,
|
||
Ext: ext,
|
||
Canonical: p.BoardPath(id),
|
||
OK: true,
|
||
}
|
||
}
|
||
|
||
// PagePermalinkMatch slug 型路径解析结果
|
||
type PagePermalinkMatch struct {
|
||
Slug string
|
||
Ext string
|
||
Canonical string
|
||
OK bool
|
||
}
|
||
|
||
// MatchPagePath 解析自定义单页路径
|
||
func (p PermalinkConfig) MatchPagePath(path string) PagePermalinkMatch {
|
||
m := pagePermalinkRe.FindStringSubmatch(path)
|
||
if len(m) < 2 {
|
||
return PagePermalinkMatch{}
|
||
}
|
||
slug := strings.ToLower(m[1])
|
||
ext := ""
|
||
if len(m) > 2 {
|
||
ext = strings.ToLower(m[2])
|
||
}
|
||
return PagePermalinkMatch{
|
||
Slug: slug,
|
||
Ext: ext,
|
||
Canonical: p.PagePath(slug),
|
||
OK: true,
|
||
}
|
||
}
|
||
|
||
// MatchUserPath 解析用户公开路径
|
||
func (p PermalinkConfig) MatchUserPath(path string) PermalinkMatch {
|
||
m := userPermalinkRe.FindStringSubmatch(path)
|
||
if len(m) < 2 {
|
||
return PermalinkMatch{}
|
||
}
|
||
id64, err := strconv.ParseUint(m[1], 10, 64)
|
||
if err != nil || id64 == 0 {
|
||
return PermalinkMatch{}
|
||
}
|
||
ext := ""
|
||
if len(m) > 2 {
|
||
ext = strings.ToLower(m[2])
|
||
}
|
||
id := uint(id64)
|
||
return PermalinkMatch{
|
||
ID: id,
|
||
Ext: ext,
|
||
Canonical: p.UserPath(id),
|
||
OK: true,
|
||
}
|
||
}
|
||
|
||
// NeedsCanonicalRedirect 当前请求路径是否应 301 到规范 URL
|
||
func (m PermalinkMatch) NeedsCanonicalRedirect(requestPath string) bool {
|
||
if !m.OK {
|
||
return false
|
||
}
|
||
// 去掉末尾 / 再比
|
||
req := strings.TrimSuffix(requestPath, "/")
|
||
can := strings.TrimSuffix(m.Canonical, "/")
|
||
return req != can
|
||
}
|