1190 lines
36 KiB
Go
1190 lines
36 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
|
||
"git.iioio.com/freefire/jiang13-forum/model"
|
||
)
|
||
|
||
// 论坛设置键名
|
||
const (
|
||
SettingPostEditWindowHours = "post_edit_window_hours"
|
||
SettingCommentEditWindowHours = "comment_edit_window_hours"
|
||
|
||
SettingRateLimitPost = "rate_limit_post"
|
||
SettingRateLimitComment = "rate_limit_comment"
|
||
SettingRateLimitRegister = "rate_limit_register"
|
||
SettingRateLimitLogin = "rate_limit_login"
|
||
SettingRateLimitWindow = "rate_limit_window_sec"
|
||
|
||
SettingPostTitleMax = "post_title_max"
|
||
SettingPostTagsMax = "post_tags_max"
|
||
SettingPostContentMax = "post_content_max"
|
||
|
||
SettingCommentMax = "comment_max"
|
||
|
||
SettingSearchKeywordMin = "search_keyword_min"
|
||
SettingSearchKeywordMax = "search_keyword_max"
|
||
|
||
SettingPageSizeDefault = "page_size_default"
|
||
|
||
SettingPasswordMinLen = "password_min_len"
|
||
SettingAvatarMaxMB = "avatar_max_mb"
|
||
SettingSignatureMax = "signature_max"
|
||
|
||
SettingOpenPostsInNewTab = "open_posts_in_new_tab"
|
||
SettingOpenContentLinksInNewTab = "open_content_links_in_new_tab"
|
||
|
||
// 伪静态键名见 permalink.go:SettingPermalinkEnabled / SettingPermalinkExt
|
||
|
||
SettingSMTPEnabled = "smtp_enabled"
|
||
SettingSMTPHost = "smtp_host"
|
||
SettingSMTPPort = "smtp_port"
|
||
SettingSMTPUsername = "smtp_username"
|
||
SettingSMTPPassword = "smtp_password"
|
||
SettingSMTPFrom = "smtp_from"
|
||
SettingSMTPFromName = "smtp_from_name"
|
||
SettingSMTPEncryption = "smtp_encryption"
|
||
|
||
SettingOIDCEnabled = "oidc_enabled"
|
||
SettingOIDCRootURL = "oidc_root_url"
|
||
SettingOIDCGroupClaim = "oidc_group_claim"
|
||
SettingOIDCAdminGroup = "oidc_admin_group"
|
||
SettingOIDCUserGroup = "oidc_user_group"
|
||
|
||
SettingGiteaSyncEnabled = "gitea_sync_enabled"
|
||
SettingGiteaBaseURL = "gitea_base_url"
|
||
SettingGiteaToken = "gitea_token"
|
||
SettingGiteaSyncIntervalMin = "gitea_sync_interval_min"
|
||
|
||
SettingStorageType = "storage_type"
|
||
SettingStorageEndpoint = "storage_endpoint"
|
||
SettingStorageRegion = "storage_region"
|
||
SettingStorageBucket = "storage_bucket"
|
||
SettingStorageAccessKey = "storage_access_key"
|
||
SettingStorageSecretKey = "storage_secret_key"
|
||
SettingStoragePublicBaseURL = "storage_public_base_url"
|
||
SettingStoragePrefix = "storage_prefix"
|
||
SettingStorageForcePathStyle = "storage_force_path_style"
|
||
SettingStorageImageDelivery = "storage_image_delivery"
|
||
|
||
SettingSiteName = "site_name"
|
||
SettingSiteSlogan = "site_slogan"
|
||
SettingSiteDescription = "site_description"
|
||
SettingSiteKeywords = "site_keywords"
|
||
SettingSiteLogoMark = "site_logo_mark"
|
||
SettingSiteLogo = "site_logo"
|
||
SettingSiteFavicon = "site_favicon"
|
||
SettingSiteOGImage = "site_og_image"
|
||
SettingSiteICPBeian = "site_icp_beian"
|
||
SettingSiteICPBeianURL = "site_icp_beian_url"
|
||
SettingSiteFriendLinks = "site_friend_links"
|
||
|
||
// pageSizeAPIMax 单次列表请求条数硬上限(防客户端传超大 size),非后台可配项
|
||
pageSizeAPIMax = 100
|
||
)
|
||
|
||
// ForumLimits 论坛可配置限制(API 传输结构)
|
||
type ForumLimits struct {
|
||
PostEditWindowHours int `json:"post_edit_window_hours"`
|
||
CommentEditWindowHours int `json:"comment_edit_window_hours"`
|
||
|
||
RateLimitPost int `json:"rate_limit_post"`
|
||
RateLimitComment int `json:"rate_limit_comment"`
|
||
RateLimitRegister int `json:"rate_limit_register"`
|
||
RateLimitLogin int `json:"rate_limit_login"`
|
||
RateLimitWindowSec int `json:"rate_limit_window_sec"`
|
||
|
||
PostTitleMax int `json:"post_title_max"`
|
||
PostTagsMax int `json:"post_tags_max"`
|
||
PostContentMax int `json:"post_content_max"`
|
||
|
||
CommentMax int `json:"comment_max"`
|
||
|
||
SearchKeywordMin int `json:"search_keyword_min"`
|
||
SearchKeywordMax int `json:"search_keyword_max"`
|
||
|
||
PageSizeDefault int `json:"page_size_default"`
|
||
|
||
PasswordMinLen int `json:"password_min_len"`
|
||
AvatarMaxMB int `json:"avatar_max_mb"`
|
||
SignatureMax int `json:"signature_max"`
|
||
|
||
OpenPostsInNewTab bool `json:"open_posts_in_new_tab"`
|
||
OpenContentLinksInNewTab bool `json:"open_content_links_in_new_tab"`
|
||
|
||
PermalinkEnabled bool `json:"permalink_enabled"`
|
||
PermalinkExt string `json:"permalink_ext"`
|
||
}
|
||
|
||
// ForumLimitsPublic 前台可见的限制(不含限流等内部配置)
|
||
type ForumLimitsPublic struct {
|
||
PostTitleMax int `json:"post_title_max"`
|
||
PostTagsMax int `json:"post_tags_max"`
|
||
PostContentMax int `json:"post_content_max"`
|
||
CommentMax int `json:"comment_max"`
|
||
SearchKeywordMin int `json:"search_keyword_min"`
|
||
SearchKeywordMax int `json:"search_keyword_max"`
|
||
PageSizeDefault int `json:"page_size_default"`
|
||
PasswordMinLen int `json:"password_min_len"`
|
||
AvatarMaxMB int `json:"avatar_max_mb"`
|
||
SignatureMax int `json:"signature_max"`
|
||
|
||
CommentEditWindowHours int `json:"comment_edit_window_hours"`
|
||
|
||
OpenPostsInNewTab bool `json:"open_posts_in_new_tab"`
|
||
OpenContentLinksInNewTab bool `json:"open_content_links_in_new_tab"`
|
||
|
||
PermalinkEnabled bool `json:"permalink_enabled"`
|
||
PermalinkExt string `json:"permalink_ext"`
|
||
}
|
||
|
||
type settingDef struct {
|
||
key string
|
||
defaultVal string
|
||
min int
|
||
max int // 0 表示不限制上限
|
||
}
|
||
|
||
var forumSettingDefs = []settingDef{
|
||
{SettingPostEditWindowHours, "24", 0, 0},
|
||
{SettingCommentEditWindowHours, "24", 0, 0},
|
||
|
||
{SettingRateLimitPost, "10", 1, 1000},
|
||
{SettingRateLimitComment, "10", 1, 1000},
|
||
{SettingRateLimitRegister, "10", 1, 1000},
|
||
{SettingRateLimitLogin, "10", 1, 1000},
|
||
{SettingRateLimitWindow, "60", 10, 3600},
|
||
|
||
{SettingPostTitleMax, "128", 1, 512},
|
||
{SettingPostTagsMax, "256", 0, 512},
|
||
{SettingPostContentMax, "50000", 0, 0},
|
||
|
||
{SettingCommentMax, "5000", 1, 50000},
|
||
|
||
{SettingSearchKeywordMin, "1", 0, 100},
|
||
{SettingSearchKeywordMax, "50", 1, 200},
|
||
|
||
{SettingPageSizeDefault, "30", 1, pageSizeAPIMax},
|
||
|
||
{SettingPasswordMinLen, "6", 4, 128},
|
||
{SettingAvatarMaxMB, "2", 1, 20},
|
||
{SettingSignatureMax, "200", 0, 512},
|
||
|
||
{SettingOpenPostsInNewTab, "1", 0, 1},
|
||
{SettingOpenContentLinksInNewTab, "1", 0, 1},
|
||
}
|
||
|
||
var mailSettingDefaults = map[string]string{
|
||
SettingSMTPEnabled: "0",
|
||
SettingSMTPHost: "",
|
||
SettingSMTPPort: "465",
|
||
SettingSMTPUsername: "",
|
||
SettingSMTPPassword: "",
|
||
SettingSMTPFrom: "",
|
||
SettingSMTPFromName: "姜十三论坛",
|
||
SettingSMTPEncryption: "ssl",
|
||
}
|
||
|
||
var oidcSettingDefaults = map[string]string{
|
||
SettingOIDCEnabled: "0",
|
||
SettingOIDCRootURL: "",
|
||
SettingOIDCGroupClaim: "groups",
|
||
SettingOIDCAdminGroup: "gitea-admin",
|
||
SettingOIDCUserGroup: "gitea-users",
|
||
}
|
||
|
||
var giteaSettingDefaults = map[string]string{
|
||
SettingGiteaSyncEnabled: "0",
|
||
SettingGiteaBaseURL: "",
|
||
SettingGiteaToken: "",
|
||
SettingGiteaSyncIntervalMin: "60",
|
||
}
|
||
|
||
var storageSettingDefaults = map[string]string{
|
||
SettingStorageType: "local",
|
||
SettingStorageEndpoint: "",
|
||
SettingStorageRegion: "us-east-1",
|
||
SettingStorageBucket: "",
|
||
SettingStorageAccessKey: "",
|
||
SettingStorageSecretKey: "",
|
||
SettingStoragePublicBaseURL: "",
|
||
SettingStoragePrefix: "",
|
||
SettingStorageForcePathStyle: "1",
|
||
SettingStorageImageDelivery: ImageDeliveryWebP,
|
||
}
|
||
|
||
var siteBrandingDefaults = map[string]string{
|
||
SettingSiteName: "姜十三论坛",
|
||
SettingSiteSlogan: "拾三一隅,自在交流",
|
||
SettingSiteDescription: "",
|
||
SettingSiteKeywords: "",
|
||
SettingSiteLogoMark: "姜",
|
||
SettingSiteLogo: "",
|
||
SettingSiteFavicon: "",
|
||
SettingSiteOGImage: "",
|
||
SettingSiteICPBeian: "",
|
||
SettingSiteICPBeianURL: "https://beian.miit.gov.cn/",
|
||
SettingSiteFriendLinks: "[]",
|
||
}
|
||
|
||
const (
|
||
maxFriendLinks = 20
|
||
maxFriendLinkName = 32
|
||
maxFriendLinkURL = 512
|
||
maxICPBeianLen = 64
|
||
maxICPBeianURLLen = 512
|
||
maxSiteDescriptionLen = 500
|
||
maxSiteKeywordsLen = 200
|
||
maxSiteKeywordItems = 20
|
||
defaultICPBeianURL = "https://beian.miit.gov.cn/"
|
||
)
|
||
|
||
// FriendLink 页脚友情链接
|
||
type FriendLink struct {
|
||
Name string `json:"name"`
|
||
URL string `json:"url"`
|
||
}
|
||
|
||
// SiteBranding 站点品牌配置(名称、Logo、Favicon、页脚等)
|
||
type SiteBranding struct {
|
||
Name string `json:"name"`
|
||
Slogan string `json:"slogan"`
|
||
Description string `json:"description"` // 站点简介(SEO / 首页可见)
|
||
Keywords string `json:"keywords"` // SEO keywords,逗号分隔
|
||
LogoMark string `json:"logo_mark"`
|
||
Logo string `json:"logo"`
|
||
Favicon string `json:"favicon"`
|
||
OGImage string `json:"og_image"` // 默认社交分享图(Open Graph)
|
||
ICPBeian string `json:"icp_beian"`
|
||
ICPBeianURL string `json:"icp_beian_url"`
|
||
FriendLinks []FriendLink `json:"friend_links"`
|
||
}
|
||
|
||
// DocumentTitle 浏览器标签标题:站点名 - 副标题(标语)
|
||
func (b SiteBranding) DocumentTitle() string {
|
||
name := strings.TrimSpace(b.Name)
|
||
subtitle := strings.TrimSpace(b.Slogan)
|
||
if subtitle != "" {
|
||
return name + " - " + subtitle
|
||
}
|
||
return name
|
||
}
|
||
|
||
// MetaDescription 用于 meta description:优先简介,其次标语
|
||
func (b SiteBranding) MetaDescription() string {
|
||
if d := strings.TrimSpace(b.Description); d != "" {
|
||
return d
|
||
}
|
||
return strings.TrimSpace(b.Slogan)
|
||
}
|
||
|
||
// MetaKeywords 用于 meta keywords
|
||
func (b SiteBranding) MetaKeywords() string {
|
||
return strings.TrimSpace(b.Keywords)
|
||
}
|
||
|
||
// DefaultShareImage 默认社交预览图:专用 OG 图 → Logo → Favicon
|
||
func (b SiteBranding) DefaultShareImage() string {
|
||
for _, u := range []string{b.OGImage, b.Logo, b.Favicon} {
|
||
if s := strings.TrimSpace(u); s != "" {
|
||
return s
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// GiteaSyncConfig Gitea 仓库同步配置
|
||
type GiteaSyncConfig struct {
|
||
Enabled bool `json:"enabled"`
|
||
BaseURL string `json:"base_url"`
|
||
Token string `json:"token,omitempty"` // 更新时传入;回显时为空
|
||
HasToken bool `json:"has_token"`
|
||
SyncIntervalMin int `json:"sync_interval_min"`
|
||
Ready bool `json:"ready"`
|
||
RepoCount int64 `json:"repo_count"`
|
||
}
|
||
|
||
// OIDCConfig OIDC Provider 全局配置(应用凭证见 oauth_clients)
|
||
type OIDCConfig struct {
|
||
Enabled bool `json:"enabled"`
|
||
RootURL string `json:"root_url"`
|
||
Ready bool `json:"ready"`
|
||
DiscoveryURL string `json:"discovery_url,omitempty"`
|
||
AuthorizeURL string `json:"authorize_url,omitempty"`
|
||
LogoutURL string `json:"logout_url,omitempty"`
|
||
GroupClaim string `json:"group_claim"`
|
||
AdminGroup string `json:"admin_group"`
|
||
UserGroup string `json:"user_group"`
|
||
ClientCount int64 `json:"client_count"`
|
||
}
|
||
|
||
// ForumSettingsService 论坛全局设置
|
||
type ForumSettingsService struct {
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
func NewForumSettingsService() *ForumSettingsService {
|
||
s := &ForumSettingsService{}
|
||
s.ensureDefaults()
|
||
return s
|
||
}
|
||
|
||
func (s *ForumSettingsService) ensureDefaults() {
|
||
for _, def := range forumSettingDefs {
|
||
var count int64
|
||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", def.key).Count(&count)
|
||
if count == 0 {
|
||
model.DB.Create(&model.ForumSetting{Key: def.key, Value: def.defaultVal})
|
||
}
|
||
}
|
||
for key, val := range mailSettingDefaults {
|
||
var count int64
|
||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||
if count == 0 {
|
||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||
}
|
||
}
|
||
for key, val := range oidcSettingDefaults {
|
||
var count int64
|
||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||
if count == 0 {
|
||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||
}
|
||
}
|
||
for key, val := range giteaSettingDefaults {
|
||
var count int64
|
||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||
if count == 0 {
|
||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||
}
|
||
}
|
||
for key, val := range storageSettingDefaults {
|
||
var count int64
|
||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||
if count == 0 {
|
||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||
}
|
||
}
|
||
for key, val := range siteBrandingDefaults {
|
||
var count int64
|
||
model.DB.Model(&model.ForumSetting{}).Where("`key` = ?", key).Count(&count)
|
||
if count == 0 {
|
||
model.DB.Create(&model.ForumSetting{Key: key, Value: val})
|
||
}
|
||
}
|
||
}
|
||
|
||
func (s *ForumSettingsService) getString(key, fallback string) string {
|
||
var setting model.ForumSetting
|
||
if err := model.DB.First(&setting, "`key` = ?", key).Error; err != nil {
|
||
return fallback
|
||
}
|
||
return setting.Value
|
||
}
|
||
|
||
func (s *ForumSettingsService) setString(key, value string) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
return model.DB.Save(&model.ForumSetting{Key: key, Value: value}).Error
|
||
}
|
||
|
||
func (s *ForumSettingsService) getInt(key string, fallback int) int {
|
||
var setting model.ForumSetting
|
||
if err := model.DB.First(&setting, "`key` = ?", key).Error; err != nil {
|
||
return fallback
|
||
}
|
||
v, err := strconv.Atoi(setting.Value)
|
||
if err != nil {
|
||
return fallback
|
||
}
|
||
return v
|
||
}
|
||
|
||
func (s *ForumSettingsService) setInt(key string, value int) error {
|
||
for _, def := range forumSettingDefs {
|
||
if def.key != key {
|
||
continue
|
||
}
|
||
if value < def.min {
|
||
return ErrInvalidSetting
|
||
}
|
||
if def.max > 0 && value > def.max {
|
||
return ErrInvalidSetting
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
return model.DB.Save(&model.ForumSetting{Key: key, Value: strconv.Itoa(value)}).Error
|
||
}
|
||
return ErrInvalidSetting
|
||
}
|
||
|
||
func (s *ForumSettingsService) Limits() ForumLimits {
|
||
permalink := s.Permalink()
|
||
return ForumLimits{
|
||
PostEditWindowHours: s.PostEditWindowHours(),
|
||
CommentEditWindowHours: s.CommentEditWindowHours(),
|
||
|
||
RateLimitPost: s.RateLimitFor("post"),
|
||
RateLimitComment: s.RateLimitFor("comment"),
|
||
RateLimitRegister: s.RateLimitFor("register"),
|
||
RateLimitLogin: s.RateLimitFor("login"),
|
||
RateLimitWindowSec: s.RateLimitWindowSec(),
|
||
|
||
PostTitleMax: s.PostTitleMax(),
|
||
PostTagsMax: s.PostTagsMax(),
|
||
PostContentMax: s.PostContentMax(),
|
||
|
||
CommentMax: s.CommentMax(),
|
||
|
||
SearchKeywordMin: s.SearchKeywordMin(),
|
||
SearchKeywordMax: s.SearchKeywordMax(),
|
||
|
||
PageSizeDefault: s.PageSizeDefault(),
|
||
|
||
PasswordMinLen: s.PasswordMinLen(),
|
||
AvatarMaxMB: s.AvatarMaxMB(),
|
||
SignatureMax: s.SignatureMax(),
|
||
|
||
OpenPostsInNewTab: s.OpenPostsInNewTab(),
|
||
OpenContentLinksInNewTab: s.OpenContentLinksInNewTab(),
|
||
|
||
PermalinkEnabled: permalink.Enabled,
|
||
PermalinkExt: permalink.Ext,
|
||
}
|
||
}
|
||
|
||
func (s *ForumSettingsService) PublicLimits() ForumLimitsPublic {
|
||
limits := s.Limits()
|
||
return ForumLimitsPublic{
|
||
PostTitleMax: limits.PostTitleMax,
|
||
PostTagsMax: limits.PostTagsMax,
|
||
PostContentMax: limits.PostContentMax,
|
||
CommentMax: limits.CommentMax,
|
||
SearchKeywordMin: limits.SearchKeywordMin,
|
||
SearchKeywordMax: limits.SearchKeywordMax,
|
||
PageSizeDefault: limits.PageSizeDefault,
|
||
PasswordMinLen: limits.PasswordMinLen,
|
||
AvatarMaxMB: limits.AvatarMaxMB,
|
||
SignatureMax: limits.SignatureMax,
|
||
|
||
CommentEditWindowHours: limits.CommentEditWindowHours,
|
||
|
||
OpenPostsInNewTab: limits.OpenPostsInNewTab,
|
||
OpenContentLinksInNewTab: limits.OpenContentLinksInNewTab,
|
||
|
||
PermalinkEnabled: limits.PermalinkEnabled,
|
||
PermalinkExt: limits.PermalinkExt,
|
||
}
|
||
}
|
||
|
||
func (s *ForumSettingsService) UpdateLimits(in ForumLimits) error {
|
||
updates := map[string]int{
|
||
SettingPostEditWindowHours: in.PostEditWindowHours,
|
||
SettingCommentEditWindowHours: in.CommentEditWindowHours,
|
||
SettingRateLimitPost: in.RateLimitPost,
|
||
SettingRateLimitComment: in.RateLimitComment,
|
||
SettingRateLimitRegister: in.RateLimitRegister,
|
||
SettingRateLimitLogin: in.RateLimitLogin,
|
||
SettingRateLimitWindow: in.RateLimitWindowSec,
|
||
SettingPostTitleMax: in.PostTitleMax,
|
||
SettingPostTagsMax: in.PostTagsMax,
|
||
SettingPostContentMax: in.PostContentMax,
|
||
SettingCommentMax: in.CommentMax,
|
||
SettingSearchKeywordMin: in.SearchKeywordMin,
|
||
SettingSearchKeywordMax: in.SearchKeywordMax,
|
||
SettingPageSizeDefault: in.PageSizeDefault,
|
||
SettingPasswordMinLen: in.PasswordMinLen,
|
||
SettingAvatarMaxMB: in.AvatarMaxMB,
|
||
SettingSignatureMax: in.SignatureMax,
|
||
}
|
||
if in.SearchKeywordMax > 0 && in.SearchKeywordMin > in.SearchKeywordMax {
|
||
return ErrInvalidSetting
|
||
}
|
||
for key, val := range updates {
|
||
if err := s.setInt(key, val); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
boolUpdates := map[string]bool{
|
||
SettingOpenPostsInNewTab: in.OpenPostsInNewTab,
|
||
SettingOpenContentLinksInNewTab: in.OpenContentLinksInNewTab,
|
||
SettingPermalinkEnabled: in.PermalinkEnabled,
|
||
}
|
||
for key, on := range boolUpdates {
|
||
v := "0"
|
||
if on {
|
||
v = "1"
|
||
}
|
||
if err := s.setString(key, v); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
ext, ok := NormalizePermalinkExt(in.PermalinkExt)
|
||
if !ok {
|
||
return ErrInvalidSetting
|
||
}
|
||
if err := s.setString(SettingPermalinkExt, ext); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *ForumSettingsService) PostEditWindowHours() int {
|
||
return s.getInt(SettingPostEditWindowHours, 24)
|
||
}
|
||
|
||
func (s *ForumSettingsService) CommentEditWindowHours() int {
|
||
return s.getInt(SettingCommentEditWindowHours, 24)
|
||
}
|
||
|
||
func (s *ForumSettingsService) RateLimitFor(action string) int {
|
||
switch action {
|
||
case "post":
|
||
return s.getInt(SettingRateLimitPost, 10)
|
||
case "comment":
|
||
return s.getInt(SettingRateLimitComment, 10)
|
||
case "register":
|
||
return s.getInt(SettingRateLimitRegister, 10)
|
||
case "login", "admin_login":
|
||
return s.getInt(SettingRateLimitLogin, 10)
|
||
default:
|
||
return 10
|
||
}
|
||
}
|
||
|
||
func (s *ForumSettingsService) RateLimitWindowSec() int {
|
||
return s.getInt(SettingRateLimitWindow, 60)
|
||
}
|
||
|
||
func (s *ForumSettingsService) PostTitleMax() int { return s.getInt(SettingPostTitleMax, 128) }
|
||
func (s *ForumSettingsService) PostTagsMax() int { return s.getInt(SettingPostTagsMax, 256) }
|
||
func (s *ForumSettingsService) PostContentMax() int { return s.getInt(SettingPostContentMax, 50000) }
|
||
func (s *ForumSettingsService) CommentMax() int { return s.getInt(SettingCommentMax, 5000) }
|
||
|
||
func (s *ForumSettingsService) SearchKeywordMin() int { return s.getInt(SettingSearchKeywordMin, 1) }
|
||
func (s *ForumSettingsService) SearchKeywordMax() int { return s.getInt(SettingSearchKeywordMax, 50) }
|
||
|
||
func (s *ForumSettingsService) PageSizeDefault() int { return s.getInt(SettingPageSizeDefault, 30) }
|
||
|
||
func (s *ForumSettingsService) PasswordMinLen() int { return s.getInt(SettingPasswordMinLen, 6) }
|
||
func (s *ForumSettingsService) AvatarMaxMB() int { return s.getInt(SettingAvatarMaxMB, 2) }
|
||
func (s *ForumSettingsService) SignatureMax() int { return s.getInt(SettingSignatureMax, 200) }
|
||
|
||
func (s *ForumSettingsService) OpenPostsInNewTab() bool {
|
||
return s.getString(SettingOpenPostsInNewTab, "1") == "1"
|
||
}
|
||
|
||
func (s *ForumSettingsService) OpenContentLinksInNewTab() bool {
|
||
return s.getString(SettingOpenContentLinksInNewTab, "1") == "1"
|
||
}
|
||
|
||
// MailConfig 读取 SMTP 配置(密码不回显明文)
|
||
func (s *ForumSettingsService) MailConfig() MailConfig {
|
||
port, _ := strconv.Atoi(s.getString(SettingSMTPPort, "465"))
|
||
if port <= 0 {
|
||
port = 465
|
||
}
|
||
password := s.getString(SettingSMTPPassword, "")
|
||
return MailConfig{
|
||
Enabled: s.getString(SettingSMTPEnabled, "0") == "1",
|
||
Host: s.getString(SettingSMTPHost, ""),
|
||
Port: port,
|
||
Username: s.getString(SettingSMTPUsername, ""),
|
||
Password: password,
|
||
From: s.getString(SettingSMTPFrom, ""),
|
||
FromName: s.getString(SettingSMTPFromName, "姜十三论坛"),
|
||
Encryption: normalizeEncryption(s.getString(SettingSMTPEncryption, "ssl")),
|
||
HasPassword: password != "",
|
||
}
|
||
}
|
||
|
||
// MailConfigPublic 管理端回显(不含密码明文)
|
||
func (s *ForumSettingsService) MailConfigPublic() MailConfig {
|
||
cfg := s.MailConfig()
|
||
cfg.Password = ""
|
||
return cfg
|
||
}
|
||
|
||
// MailReady 邮件服务是否可用于发信
|
||
func (s *ForumSettingsService) MailReady() bool {
|
||
cfg := s.MailConfig()
|
||
return cfg.Enabled &&
|
||
strings.TrimSpace(cfg.Host) != "" &&
|
||
cfg.Port > 0 &&
|
||
strings.TrimSpace(cfg.From) != "" &&
|
||
strings.TrimSpace(cfg.Username) != "" &&
|
||
cfg.HasPassword
|
||
}
|
||
|
||
// UpdateMailConfig 更新 SMTP 配置;密码为空表示保持原值
|
||
func (s *ForumSettingsService) UpdateMailConfig(in MailConfig) error {
|
||
enc := normalizeEncryption(in.Encryption)
|
||
if enc != "none" && enc != "starttls" && enc != "ssl" {
|
||
return ErrInvalidSetting
|
||
}
|
||
port := in.Port
|
||
if port <= 0 {
|
||
port = 465
|
||
}
|
||
enabled := "0"
|
||
if in.Enabled {
|
||
enabled = "1"
|
||
}
|
||
updates := map[string]string{
|
||
SettingSMTPEnabled: enabled,
|
||
SettingSMTPHost: strings.TrimSpace(in.Host),
|
||
SettingSMTPPort: strconv.Itoa(port),
|
||
SettingSMTPUsername: strings.TrimSpace(in.Username),
|
||
SettingSMTPFrom: strings.TrimSpace(in.From),
|
||
SettingSMTPFromName: strings.TrimSpace(in.FromName),
|
||
SettingSMTPEncryption: enc,
|
||
}
|
||
if strings.TrimSpace(in.Password) != "" {
|
||
updates[SettingSMTPPassword] = in.Password
|
||
}
|
||
for key, val := range updates {
|
||
if err := s.setString(key, val); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// OIDCConfig 读取 OIDC 全局配置
|
||
func (s *ForumSettingsService) OIDCConfig() OIDCConfig {
|
||
root := normalizeRootURL(s.getString(SettingOIDCRootURL, ""))
|
||
clientCount := CountEnabledOAuthClients()
|
||
cfg := OIDCConfig{
|
||
Enabled: s.getString(SettingOIDCEnabled, "0") == "1",
|
||
RootURL: root,
|
||
GroupClaim: strings.TrimSpace(s.getString(SettingOIDCGroupClaim, "groups")),
|
||
AdminGroup: strings.TrimSpace(s.getString(SettingOIDCAdminGroup, "gitea-admin")),
|
||
UserGroup: strings.TrimSpace(s.getString(SettingOIDCUserGroup, "gitea-users")),
|
||
ClientCount: clientCount,
|
||
}
|
||
if cfg.GroupClaim == "" {
|
||
cfg.GroupClaim = "groups"
|
||
}
|
||
cfg.Ready = cfg.Enabled && cfg.RootURL != "" && clientCount > 0
|
||
if root != "" {
|
||
cfg.DiscoveryURL = root + "/.well-known/openid-configuration"
|
||
cfg.AuthorizeURL = root + "/oauth/authorize"
|
||
cfg.LogoutURL = root + "/oauth/logout"
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
// OIDCConfigPublic 管理端回显
|
||
func (s *ForumSettingsService) OIDCConfigPublic() OIDCConfig {
|
||
return s.OIDCConfig()
|
||
}
|
||
|
||
// UpdateOIDCConfig 更新 OIDC 全局配置(不含 OAuth 应用凭证)
|
||
func (s *ForumSettingsService) UpdateOIDCConfig(in OIDCConfig) error {
|
||
root := normalizeRootURL(in.RootURL)
|
||
if root != "" && !strings.HasPrefix(root, "http://") && !strings.HasPrefix(root, "https://") {
|
||
return ErrInvalidSetting
|
||
}
|
||
enabled := "0"
|
||
if in.Enabled {
|
||
enabled = "1"
|
||
}
|
||
groupClaim := strings.TrimSpace(in.GroupClaim)
|
||
if groupClaim == "" {
|
||
groupClaim = "groups"
|
||
}
|
||
adminGroup := strings.TrimSpace(in.AdminGroup)
|
||
userGroup := strings.TrimSpace(in.UserGroup)
|
||
updates := map[string]string{
|
||
SettingOIDCEnabled: enabled,
|
||
SettingOIDCRootURL: root,
|
||
SettingOIDCGroupClaim: groupClaim,
|
||
SettingOIDCAdminGroup: adminGroup,
|
||
SettingOIDCUserGroup: userGroup,
|
||
}
|
||
for key, val := range updates {
|
||
if err := s.setString(key, val); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GiteaSyncConfig 读取 Gitea 同步配置(含 Token 明文,供服务内部使用)
|
||
func (s *ForumSettingsService) GiteaSyncConfig() GiteaSyncConfig {
|
||
interval, _ := strconv.Atoi(s.getString(SettingGiteaSyncIntervalMin, "60"))
|
||
if interval < 5 {
|
||
interval = 5
|
||
}
|
||
if interval > 24*60 {
|
||
interval = 24 * 60
|
||
}
|
||
token := s.getString(SettingGiteaToken, "")
|
||
base := normalizeRootURL(s.getString(SettingGiteaBaseURL, ""))
|
||
cfg := GiteaSyncConfig{
|
||
Enabled: s.getString(SettingGiteaSyncEnabled, "0") == "1",
|
||
BaseURL: base,
|
||
Token: token,
|
||
HasToken: token != "",
|
||
SyncIntervalMin: interval,
|
||
}
|
||
cfg.Ready = cfg.Enabled && base != "" && cfg.HasToken
|
||
var n int64
|
||
model.DB.Model(&model.GiteaRepo{}).Where("private = ?", false).Count(&n)
|
||
cfg.RepoCount = n
|
||
return cfg
|
||
}
|
||
|
||
// GiteaSyncConfigPublic 管理端回显(不含 Token 明文)
|
||
func (s *ForumSettingsService) GiteaSyncConfigPublic() GiteaSyncConfig {
|
||
cfg := s.GiteaSyncConfig()
|
||
cfg.Token = ""
|
||
return cfg
|
||
}
|
||
|
||
// UpdateGiteaSyncConfig 更新同步配置;Token 为空表示保持原值
|
||
func (s *ForumSettingsService) UpdateGiteaSyncConfig(in GiteaSyncConfig) error {
|
||
base := normalizeRootURL(in.BaseURL)
|
||
if base != "" && !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
|
||
return ErrInvalidSetting
|
||
}
|
||
interval := in.SyncIntervalMin
|
||
if interval <= 0 {
|
||
interval = 60
|
||
}
|
||
if interval < 5 {
|
||
interval = 5
|
||
}
|
||
if interval > 24*60 {
|
||
interval = 24 * 60
|
||
}
|
||
enabled := "0"
|
||
if in.Enabled {
|
||
enabled = "1"
|
||
}
|
||
updates := map[string]string{
|
||
SettingGiteaSyncEnabled: enabled,
|
||
SettingGiteaBaseURL: base,
|
||
SettingGiteaSyncIntervalMin: strconv.Itoa(interval),
|
||
}
|
||
if strings.TrimSpace(in.Token) != "" {
|
||
updates[SettingGiteaToken] = strings.TrimSpace(in.Token)
|
||
}
|
||
for key, val := range updates {
|
||
if err := s.setString(key, val); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// StorageConfig 读取上传存储配置(含密钥明文,供内部使用)
|
||
func (s *ForumSettingsService) StorageConfig() StorageConfig {
|
||
secret := s.getString(SettingStorageSecretKey, "")
|
||
region := strings.TrimSpace(s.getString(SettingStorageRegion, "us-east-1"))
|
||
if region == "" {
|
||
region = "us-east-1"
|
||
}
|
||
cfg := StorageConfig{
|
||
Type: normalizeStorageType(s.getString(SettingStorageType, "local")),
|
||
Endpoint: strings.TrimSpace(s.getString(SettingStorageEndpoint, "")),
|
||
Region: region,
|
||
Bucket: strings.TrimSpace(s.getString(SettingStorageBucket, "")),
|
||
AccessKey: strings.TrimSpace(s.getString(SettingStorageAccessKey, "")),
|
||
SecretKey: secret,
|
||
PublicBaseURL: normalizeRootURL(s.getString(SettingStoragePublicBaseURL, "")),
|
||
Prefix: normalizeObjectPrefix(s.getString(SettingStoragePrefix, "")),
|
||
ForcePathStyle: s.getString(SettingStorageForcePathStyle, "1") == "1",
|
||
HasSecretKey: secret != "",
|
||
ImageDelivery: normalizeImageDelivery(s.getString(SettingStorageImageDelivery, ImageDeliveryWebP)),
|
||
}
|
||
cfg.Ready = cfg.Type == "local" || (cfg.Endpoint != "" && cfg.Bucket != "" &&
|
||
cfg.AccessKey != "" && cfg.HasSecretKey && cfg.PublicBaseURL != "")
|
||
return cfg
|
||
}
|
||
|
||
// ImageDelivery 图片展示方案:webp | original
|
||
func (s *ForumSettingsService) ImageDelivery() string {
|
||
return normalizeImageDelivery(s.getString(SettingStorageImageDelivery, ImageDeliveryWebP))
|
||
}
|
||
|
||
// StorageConfigPublic 管理端回显(不含 Secret Key 明文)
|
||
func (s *ForumSettingsService) StorageConfigPublic() StorageConfig {
|
||
cfg := s.StorageConfig()
|
||
cfg.SecretKey = ""
|
||
return cfg
|
||
}
|
||
|
||
// UpdateStorageConfig 更新存储配置;Secret Key 为空表示保持原值
|
||
func (s *ForumSettingsService) UpdateStorageConfig(in StorageConfig) error {
|
||
typ := normalizeStorageType(in.Type)
|
||
if typ != "local" && typ != "s3" {
|
||
return ErrInvalidSetting
|
||
}
|
||
endpoint := strings.TrimSpace(in.Endpoint)
|
||
region := strings.TrimSpace(in.Region)
|
||
if region == "" {
|
||
region = "us-east-1"
|
||
}
|
||
bucket := strings.TrimSpace(in.Bucket)
|
||
accessKey := strings.TrimSpace(in.AccessKey)
|
||
publicBase := normalizeRootURL(in.PublicBaseURL)
|
||
prefix := normalizeObjectPrefix(in.Prefix)
|
||
forcePath := "0"
|
||
if in.ForcePathStyle {
|
||
forcePath = "1"
|
||
}
|
||
|
||
if typ == "s3" {
|
||
if endpoint == "" || bucket == "" || publicBase == "" {
|
||
return errors.New("启用 S3 时须填写 Endpoint、Bucket 与公开访问地址")
|
||
}
|
||
if _, _, err := parseS3Endpoint(endpoint); err != nil {
|
||
return err
|
||
}
|
||
if !strings.HasPrefix(publicBase, "http://") && !strings.HasPrefix(publicBase, "https://") {
|
||
return errors.New("公开访问地址须以 http:// 或 https:// 开头")
|
||
}
|
||
existingSecret := s.getString(SettingStorageSecretKey, "")
|
||
secret := strings.TrimSpace(in.SecretKey)
|
||
if accessKey == "" {
|
||
accessKey = strings.TrimSpace(s.getString(SettingStorageAccessKey, ""))
|
||
}
|
||
if secret == "" {
|
||
secret = existingSecret
|
||
}
|
||
if accessKey == "" || secret == "" {
|
||
return errors.New("启用 S3 时须填写 Access Key 与 Secret Key")
|
||
}
|
||
}
|
||
|
||
delivery := normalizeImageDelivery(in.ImageDelivery)
|
||
updates := map[string]string{
|
||
SettingStorageType: typ,
|
||
SettingStorageEndpoint: endpoint,
|
||
SettingStorageRegion: region,
|
||
SettingStorageBucket: bucket,
|
||
SettingStorageAccessKey: accessKey,
|
||
SettingStoragePublicBaseURL: publicBase,
|
||
SettingStoragePrefix: prefix,
|
||
SettingStorageForcePathStyle: forcePath,
|
||
SettingStorageImageDelivery: delivery,
|
||
}
|
||
if strings.TrimSpace(in.SecretKey) != "" {
|
||
updates[SettingStorageSecretKey] = strings.TrimSpace(in.SecretKey)
|
||
}
|
||
for key, val := range updates {
|
||
if err := s.setString(key, val); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SiteBranding 读取站点品牌配置
|
||
func (s *ForumSettingsService) SiteBranding() SiteBranding {
|
||
name := strings.TrimSpace(s.getString(SettingSiteName, siteBrandingDefaults[SettingSiteName]))
|
||
if name == "" {
|
||
name = siteBrandingDefaults[SettingSiteName]
|
||
}
|
||
mark := strings.TrimSpace(s.getString(SettingSiteLogoMark, siteBrandingDefaults[SettingSiteLogoMark]))
|
||
if mark == "" {
|
||
mark = siteBrandingDefaults[SettingSiteLogoMark]
|
||
}
|
||
// 字标取首个字符(支持中文)
|
||
runes := []rune(mark)
|
||
if len(runes) > 1 {
|
||
mark = string(runes[0])
|
||
}
|
||
links := parseFriendLinksJSON(s.getString(SettingSiteFriendLinks, "[]"))
|
||
return SiteBranding{
|
||
Name: name,
|
||
Slogan: strings.TrimSpace(s.getString(SettingSiteSlogan, siteBrandingDefaults[SettingSiteSlogan])),
|
||
Description: strings.TrimSpace(s.getString(SettingSiteDescription, "")),
|
||
Keywords: strings.TrimSpace(s.getString(SettingSiteKeywords, "")),
|
||
LogoMark: mark,
|
||
Logo: strings.TrimSpace(s.getString(SettingSiteLogo, "")),
|
||
Favicon: strings.TrimSpace(s.getString(SettingSiteFavicon, "")),
|
||
OGImage: strings.TrimSpace(s.getString(SettingSiteOGImage, "")),
|
||
ICPBeian: strings.TrimSpace(s.getString(SettingSiteICPBeian, "")),
|
||
ICPBeianURL: strings.TrimSpace(s.getString(SettingSiteICPBeianURL, defaultICPBeianURL)),
|
||
FriendLinks: links,
|
||
}
|
||
}
|
||
|
||
// UpdateSiteBranding 更新品牌文案与页脚信息;Logo/Favicon URL 由上传接口单独写入
|
||
func (s *ForumSettingsService) UpdateSiteBranding(in SiteBranding) error {
|
||
name := strings.TrimSpace(in.Name)
|
||
if name == "" {
|
||
return ErrInvalidSetting
|
||
}
|
||
if len([]rune(name)) > 64 {
|
||
return ErrInvalidSetting
|
||
}
|
||
mark := strings.TrimSpace(in.LogoMark)
|
||
if mark == "" {
|
||
mark = siteBrandingDefaults[SettingSiteLogoMark]
|
||
}
|
||
runes := []rune(mark)
|
||
if len(runes) > 1 {
|
||
mark = string(runes[0])
|
||
}
|
||
slogan := strings.TrimSpace(in.Slogan)
|
||
if len([]rune(slogan)) > 200 {
|
||
return ErrInvalidSetting
|
||
}
|
||
description := strings.TrimSpace(in.Description)
|
||
if len([]rune(description)) > maxSiteDescriptionLen {
|
||
return ErrInvalidSetting
|
||
}
|
||
keywords, err := normalizeSiteKeywords(in.Keywords)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
icp := strings.TrimSpace(in.ICPBeian)
|
||
if len([]rune(icp)) > maxICPBeianLen {
|
||
return ErrInvalidSetting
|
||
}
|
||
icpURL, err := normalizeOptionalHTTPURL(in.ICPBeianURL, defaultICPBeianURL, maxICPBeianURLLen)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
links, err := normalizeFriendLinks(in.FriendLinks)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
linksJSON, err := json.Marshal(links)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
updates := map[string]string{
|
||
SettingSiteName: name,
|
||
SettingSiteSlogan: slogan,
|
||
SettingSiteDescription: description,
|
||
SettingSiteKeywords: keywords,
|
||
SettingSiteLogoMark: mark,
|
||
SettingSiteICPBeian: icp,
|
||
SettingSiteICPBeianURL: icpURL,
|
||
SettingSiteFriendLinks: string(linksJSON),
|
||
}
|
||
for key, val := range updates {
|
||
if err := s.setString(key, val); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SetSiteLogo 写入 Logo URL(空串表示清除)
|
||
func (s *ForumSettingsService) SetSiteLogo(url string) error {
|
||
return s.setString(SettingSiteLogo, strings.TrimSpace(url))
|
||
}
|
||
|
||
// SetSiteFavicon 写入 Favicon URL(空串表示清除)
|
||
func (s *ForumSettingsService) SetSiteFavicon(url string) error {
|
||
return s.setString(SettingSiteFavicon, strings.TrimSpace(url))
|
||
}
|
||
|
||
// SetSiteOGImage 写入默认社交分享图 URL(空串表示清除)
|
||
func (s *ForumSettingsService) SetSiteOGImage(url string) error {
|
||
return s.setString(SettingSiteOGImage, strings.TrimSpace(url))
|
||
}
|
||
|
||
// normalizeSiteKeywords 统一中英文分隔符,去重并限制数量/长度
|
||
func normalizeSiteKeywords(raw string) (string, error) {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return "", nil
|
||
}
|
||
if len([]rune(raw)) > maxSiteKeywordsLen {
|
||
return "", ErrInvalidSetting
|
||
}
|
||
raw = strings.NewReplacer(",", ",", "、", ",", ";", ",", ";", ",").Replace(raw)
|
||
parts := strings.Split(raw, ",")
|
||
out := make([]string, 0, len(parts))
|
||
seen := make(map[string]struct{}, len(parts))
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[p]; ok {
|
||
continue
|
||
}
|
||
seen[p] = struct{}{}
|
||
out = append(out, p)
|
||
}
|
||
if len(out) > maxSiteKeywordItems {
|
||
return "", ErrInvalidSetting
|
||
}
|
||
return strings.Join(out, ","), nil
|
||
}
|
||
|
||
// JoinSEOKeywords 合并页面级与站点级关键词(逗号分隔)
|
||
func JoinSEOKeywords(parts ...string) string {
|
||
out := make([]string, 0, len(parts))
|
||
seen := make(map[string]struct{}, len(parts))
|
||
for _, part := range parts {
|
||
part = strings.TrimSpace(part)
|
||
if part == "" {
|
||
continue
|
||
}
|
||
for _, p := range strings.Split(strings.NewReplacer(",", ",", "、", ",").Replace(part), ",") {
|
||
p = strings.TrimSpace(p)
|
||
if p == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[p]; ok {
|
||
continue
|
||
}
|
||
seen[p] = struct{}{}
|
||
out = append(out, p)
|
||
}
|
||
}
|
||
return strings.Join(out, ",")
|
||
}
|
||
|
||
// normalizeOptionalHTTPURL 空值回落到 defaultURL;非空须为 http(s)
|
||
func normalizeOptionalHTTPURL(raw, defaultURL string, maxLen int) (string, error) {
|
||
href := strings.TrimSpace(raw)
|
||
if href == "" {
|
||
return defaultURL, nil
|
||
}
|
||
if len(href) > maxLen {
|
||
return "", ErrInvalidSetting
|
||
}
|
||
u, err := url.Parse(href)
|
||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||
return "", ErrInvalidSetting
|
||
}
|
||
scheme := strings.ToLower(u.Scheme)
|
||
if scheme != "http" && scheme != "https" {
|
||
return "", ErrInvalidSetting
|
||
}
|
||
return href, nil
|
||
}
|
||
|
||
func parseFriendLinksJSON(raw string) []FriendLink {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return []FriendLink{}
|
||
}
|
||
var links []FriendLink
|
||
if err := json.Unmarshal([]byte(raw), &links); err != nil {
|
||
return []FriendLink{}
|
||
}
|
||
out, err := normalizeFriendLinks(links)
|
||
if err != nil {
|
||
return []FriendLink{}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func normalizeFriendLinks(in []FriendLink) ([]FriendLink, error) {
|
||
if len(in) > maxFriendLinks {
|
||
return nil, ErrInvalidSetting
|
||
}
|
||
out := make([]FriendLink, 0, len(in))
|
||
for _, item := range in {
|
||
name := strings.TrimSpace(item.Name)
|
||
href := strings.TrimSpace(item.URL)
|
||
if name == "" && href == "" {
|
||
continue
|
||
}
|
||
if name == "" || href == "" {
|
||
return nil, ErrInvalidSetting
|
||
}
|
||
if len([]rune(name)) > maxFriendLinkName || len(href) > maxFriendLinkURL {
|
||
return nil, ErrInvalidSetting
|
||
}
|
||
u, err := url.Parse(href)
|
||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||
return nil, ErrInvalidSetting
|
||
}
|
||
scheme := strings.ToLower(u.Scheme)
|
||
if scheme != "http" && scheme != "https" {
|
||
return nil, ErrInvalidSetting
|
||
}
|
||
out = append(out, FriendLink{Name: name, URL: href})
|
||
}
|
||
if out == nil {
|
||
out = []FriendLink{}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func normalizeRootURL(raw string) string {
|
||
return strings.TrimRight(strings.TrimSpace(raw), "/")
|
||
}
|
||
|
||
func normalizeRedirectURIs(raw string) string {
|
||
parts := splitRedirectURIs(raw)
|
||
return strings.Join(parts, ",")
|
||
}
|
||
|
||
func splitRedirectURIs(raw string) []string {
|
||
raw = strings.ReplaceAll(raw, "\r\n", "\n")
|
||
raw = strings.ReplaceAll(raw, "\n", ",")
|
||
parts := strings.Split(raw, ",")
|
||
out := make([]string, 0, len(parts))
|
||
seen := map[string]struct{}{}
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[p]; ok {
|
||
continue
|
||
}
|
||
seen[p] = struct{}{}
|
||
out = append(out, p)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// NormalizeSearchKeyword 校验并规范化搜索关键词,空字符串表示无搜索
|
||
func (s *ForumSettingsService) NormalizeSearchKeyword(keyword string) (string, error) {
|
||
kw := trimRunes(keyword)
|
||
if kw == "" {
|
||
return "", nil
|
||
}
|
||
minLen := s.SearchKeywordMin()
|
||
maxLen := s.SearchKeywordMax()
|
||
runeLen := runeLen(kw)
|
||
if minLen > 0 && runeLen < minLen {
|
||
return "", ErrSearchKeywordTooShort
|
||
}
|
||
if maxLen > 0 && runeLen > maxLen {
|
||
return "", ErrSearchKeywordTooLong
|
||
}
|
||
return kw, nil
|
||
}
|
||
|
||
// NormalizePageSize 规范化分页大小
|
||
func (s *ForumSettingsService) NormalizePageSize(size int) int {
|
||
if size < 1 {
|
||
return s.PageSizeDefault()
|
||
}
|
||
if size > pageSizeAPIMax {
|
||
return pageSizeAPIMax
|
||
}
|
||
return size
|
||
}
|
||
|
||
// ValidateTextLength 校验文本长度,max=0 表示不限
|
||
func (s *ForumSettingsService) ValidateTextLength(text string, max int, tooLongErr error) error {
|
||
if max <= 0 {
|
||
return nil
|
||
}
|
||
if runeLen(text) > max {
|
||
return tooLongErr
|
||
}
|
||
return nil
|
||
}
|