Skip to content
16 changes: 8 additions & 8 deletions internal/api/script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ type CreateRequest struct {
mux.Meta `path:"/scripts" method:"POST"`
Content string `form:"content" binding:"required,max=102400" label:"脚本详细描述"`
Code string `form:"code" binding:"required,max=10485760" label:"脚本代码"`
Name string `form:"name" binding:"max=128" label:"库的名字"`
Description string `form:"description" binding:"max=10240" label:"库的描述"`
Name string `form:"name" binding:"max=50" label:"库的名字"`
Description string `form:"description" binding:"max=200" label:"库的描述"`
Definition string `form:"definition" binding:"max=10240" label:"库的定义文件"`
Version string `form:"version" binding:"max=32" label:"库的版本"`
Tags []string `form:"tags" binding:"omitempty,max=64" label:"标签"` // 标签,只有脚本类型为库时才有意义
Tags []string `form:"tags" binding:"omitempty,max=5" label:"标签"` // 标签,只有脚本类型为库时才有意义
CategoryID int64 `form:"category" binding:"omitempty,numeric" label:"分类ID"` // 分类ID
Type script_entity.Type `form:"type" binding:"required,oneof=1 2 3" label:"脚本类型"` // 脚本类型:1 用户脚本 2 订阅脚本(不支持) 3 脚本引用库
Public script_entity.Public `form:"public" binding:"required,oneof=1 2 3" label:"公开类型"` // 公开类型:1 公开 2 半公开 3 私有
Expand Down Expand Up @@ -120,7 +120,7 @@ type UpdateCodeRequest struct {
//Name string `form:"name" binding:"max=128" label:"库的名字"`
//Description string `form:"description" binding:"max=102400" label:"库的描述"`
Version string `binding:"required,max=128" form:"version" label:"库的版本号"`
Tags []string `form:"tags" binding:"omitempty,max=64" label:"标签"` // 标签,只有脚本类型为库时才有意义
Tags []string `form:"tags" binding:"omitempty,max=5" label:"标签"` // 标签,只有脚本类型为库时才有意义
Content string `binding:"required,max=102400" form:"content" label:"脚本详细描述"`
Code string `binding:"required,max=10485760" form:"code" label:"脚本代码"`
Definition string `binding:"max=102400" form:"definition" label:"库的定义文件"`
Expand Down Expand Up @@ -260,8 +260,8 @@ type GetSettingResponse struct {
type UpdateSettingRequest struct {
mux.Meta `path:"/scripts/:id/setting" method:"PUT"`
ID int64 `uri:"id" binding:"required"`
Name string `json:"name" binding:"max=128" label:"库的名字"`
Description string `json:"description" binding:"max=102400" label:"库的描述"`
Name string `json:"name" binding:"max=50" label:"库的名字"`
Description string `json:"description" binding:"max=200" label:"库的描述"`
SyncUrl string `json:"sync_url" binding:"omitempty,url,max=1024" label:"代码同步url"`
ContentUrl string `json:"content_url" binding:"omitempty,url,max=1024" label:"详细描述同步url"`
DefinitionUrl string `json:"definition_url" binding:"omitempty,url,max=1024" label:"定义文件同步url"`
Expand All @@ -276,8 +276,8 @@ type UpdateSettingResponse struct {
// UpdateLibInfoRequest 更新库信息
type UpdateLibInfoRequest struct {
mux.Meta `path:"/scripts/:id/lib-info" method:"PUT"`
Name string `json:"name" binding:"max=128" label:"库的名字"`
Description string `json:"description" binding:"max=102400" label:"库的描述"`
Name string `json:"name" binding:"max=50" label:"库的名字"`
Description string `json:"description" binding:"max=200" label:"库的描述"`
}

type UpdateLibInfoResponse struct {
Expand Down
5 changes: 5 additions & 0 deletions internal/pkg/code/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ const (

ScriptDeleteReleaseNotLatest
ScriptCategoryNotFound
ScriptNameTooLong
ScriptDescTooLong
ScriptTagsTooMany
ScriptNameInvalid
ScriptDescInvalid
)

// issue
Expand Down
5 changes: 5 additions & 0 deletions internal/pkg/code/zh_cn.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ var zhCN = map[int]string{
WebhookRepositoryNotFound: "仓库不存在",
ScriptDeleteReleaseNotLatest: "删除发布版本失败,没有新的正式版本了",
ScriptCategoryNotFound: "脚本分类不存在",
ScriptNameTooLong: "脚本名称过长,最多50个字符",
ScriptDescTooLong: "脚本描述过长,最多200个字符",
ScriptTagsTooMany: "标签数量过多,最多5个",
ScriptNameInvalid: "脚本名称格式无效,名称应为简单名称,不能包含换行符",
ScriptDescInvalid: "脚本描述格式无效,描述应为一段话,不能包含换行符",

IssueLabelNotExist: "标签不存在",
IssueNotFound: "反馈不存在",
Expand Down
36 changes: 36 additions & 0 deletions internal/service/script_svc/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"

"github.com/scriptscat/scriptlist/internal/repository/issue_repo"
"github.com/scriptscat/scriptlist/internal/repository/report_repo"
Expand Down Expand Up @@ -302,6 +303,33 @@ func (s *scriptSvc) scriptCode(ctx context.Context, script *script_entity.Script
return ret
}

// validateScriptMeta validates the name, description, and tags extracted from script metadata.
// If nameUnchanged and descUnchanged are both true (i.e. neither field changed from the stored
// value), the length/format checks on name and description are skipped so existing scripts that
// pre-date these limits can still be updated without forcing the author to rename them.
func validateScriptMeta(ctx context.Context, name, description string, tags []string, nameUnchanged, descUnchanged bool) error {
if !nameUnchanged {
if strings.ContainsAny(name, "\r\n") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

只是判断换行符太简单了,而且本来也就不允许换行,有没有库可以判断?需要支持i18n,描述也要如此。或者名字禁止标点符号之类,描述判断是不是一句话

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in commit 5e02e37 with two compiled regex patterns (no extra dependencies needed, standard regexp package):

nameInvalidRe — rejects SEO keyword-list separators in names: commas (,/), pipes (|), semicolons (;/), and newlines. Pattern: [\r\n,,|;;]

multiSentenceRe — rejects descriptions with more than one sentence:

  • Newlines ([\r\n])
  • Chinese sentence-enders ([。!?]) followed by optional whitespace then any non-whitespace (covers both 第一句。第二句 and 第一句。 第二句)
  • English sentence-enders ([.!?]) followed by at least one space then a capital letter or CJK character (catches First. Second without false-positives on v1.0 or URLs)

Pattern: [\r\n]|[。!?]\s*\S|[.!?]\s+[A-Z\x{4e00}-\x{9fa5}]

26 unit test cases covering all paths, each asserting the specific *httputils.Error code.

return i18n.NewError(ctx, code.ScriptNameInvalid)
}
if utf8.RuneCountInString(name) > 50 {
return i18n.NewError(ctx, code.ScriptNameTooLong)
}
}
if !descUnchanged {
if strings.ContainsAny(description, "\r\n") {
return i18n.NewError(ctx, code.ScriptDescInvalid)
}
if utf8.RuneCountInString(description) > 200 {
return i18n.NewError(ctx, code.ScriptDescTooLong)
}
}
if len(tags) > 5 {
return i18n.NewError(ctx, code.ScriptTagsTooMany)
}
return nil
}

// Create 创建脚本
func (s *scriptSvc) Create(ctx context.Context, req *api.CreateRequest) (*api.CreateResponse, error) {
script := &script_entity.Script{
Expand Down Expand Up @@ -353,6 +381,9 @@ func (s *scriptSvc) Create(ctx context.Context, req *api.CreateRequest) (*api.Cr
script.Description = metaJson["description"][0]
// 处理tag关联
tags = metaJson["tags"]
if err := validateScriptMeta(ctx, script.Name, script.Description, tags, false, false); err != nil {
return err
}
if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 {
tags = append(tags, "后台脚本")
}
Expand Down Expand Up @@ -499,9 +530,14 @@ func (s *scriptSvc) UpdateCode(ctx context.Context, req *api.UpdateCodeRequest)
}
}
// 更新名字和描述
oldName, oldDescription := script.Name, script.Description
script.Name = metaJson["name"][0]
script.Description = metaJson["description"][0]
tags = req.Tags
if err := validateScriptMeta(ctx, script.Name, script.Description, tags,
script.Name == oldName, script.Description == oldDescription); err != nil {
return nil, err
}
if len(metaJson["background"]) > 0 || len(metaJson["crontab"]) > 0 {
tags = append(tags, "后台脚本")
}
Expand Down
150 changes: 150 additions & 0 deletions internal/service/script_svc/script_validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package script_svc

import (
"context"
"testing"

"github.com/cago-frame/cago/pkg/utils/httputils"
"github.com/scriptscat/scriptlist/internal/pkg/code"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// assertErrCode checks that err is a *httputils.Error with the expected code.
func assertErrCode(t *testing.T, err error, wantCode int) {
t.Helper()
require.Error(t, err)
var herr *httputils.Error
require.ErrorAs(t, err, &herr, "expected *httputils.Error")
assert.Equal(t, wantCode, herr.Code)
}

func TestValidateScriptMeta(t *testing.T) {
ctx := context.Background()

tests := []struct {
name string
scriptName string
description string
tags []string
nameUnchanged bool
descUnchanged bool
wantErrCode int
}{
{
name: "valid name and description",
scriptName: "My Script",
description: "A simple one-line description.",
tags: []string{"tag1", "tag2"},
wantErrCode: 0,
},
{
name: "name with newline",
scriptName: "My Script\nWith Newline",
description: "A simple description.",
tags: nil,
wantErrCode: code.ScriptNameInvalid,
},
{
name: "name with carriage return",
scriptName: "My Script\rWith CR",
description: "A simple description.",
tags: nil,
wantErrCode: code.ScriptNameInvalid,
},
{
name: "name exactly 51 runes",
scriptName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxy", // 51 chars
description: "A simple description.",
tags: nil,
wantErrCode: code.ScriptNameTooLong,
},
{
name: "name exactly 50 runes",
scriptName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx", // 50 chars
description: "A simple description.",
tags: nil,
wantErrCode: 0,
},
{
name: "description with newline",
scriptName: "My Script",
description: "First line.\nSecond line.",
tags: nil,
wantErrCode: code.ScriptDescInvalid,
},
{
name: "description with carriage return",
scriptName: "My Script",
description: "First line.\rSecond line.",
tags: nil,
wantErrCode: code.ScriptDescInvalid,
},
{
name: "description too long (>200 runes)",
scriptName: "My Script",
description: "这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述这是一段非常长的描述还要再多一点",
tags: nil,
wantErrCode: code.ScriptDescTooLong,
},
{
name: "too many tags",
scriptName: "My Script",
description: "A simple description.",
tags: []string{"t1", "t2", "t3", "t4", "t5", "t6"},
wantErrCode: code.ScriptTagsTooMany,
},
{
name: "exactly 5 tags",
scriptName: "My Script",
description: "A simple description.",
tags: []string{"t1", "t2", "t3", "t4", "t5"},
wantErrCode: 0,
},
{
name: "skip name validation when name unchanged",
scriptName: "Old Name\nWith Newline",
description: "A simple description.",
tags: nil,
nameUnchanged: true,
wantErrCode: 0,
},
{
name: "skip desc validation when desc unchanged",
scriptName: "My Script",
description: "Old description\nWith Newline",
tags: nil,
descUnchanged: true,
wantErrCode: 0,
},
{
name: "still validate tags when name and desc unchanged",
scriptName: "My Script",
description: "A simple description.",
tags: []string{"t1", "t2", "t3", "t4", "t5", "t6"},
nameUnchanged: true,
descUnchanged: true,
wantErrCode: code.ScriptTagsTooMany,
},
{
name: "do not skip desc when only name unchanged",
scriptName: "My Script",
description: "New description\nWith Newline",
tags: nil,
nameUnchanged: true,
descUnchanged: false,
wantErrCode: code.ScriptDescInvalid,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateScriptMeta(ctx, tt.scriptName, tt.description, tt.tags, tt.nameUnchanged, tt.descUnchanged)
if tt.wantErrCode == 0 {
assert.NoError(t, err)
} else {
assertErrCode(t, err, tt.wantErrCode)
}
})
}
}