mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-21 07:35:39 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
@@ -137,8 +137,10 @@ func toCommitStatus(status actions_model.Status) api.CommitStatusState {
|
||||
switch status {
|
||||
case actions_model.StatusSuccess, actions_model.StatusSkipped:
|
||||
return api.CommitStatusSuccess
|
||||
case actions_model.StatusFailure, actions_model.StatusCancelled:
|
||||
case actions_model.StatusFailure:
|
||||
return api.CommitStatusFailure
|
||||
case actions_model.StatusCancelled:
|
||||
return api.CommitStatusWarning
|
||||
case actions_model.StatusWaiting, actions_model.StatusBlocked:
|
||||
return api.CommitStatusPending
|
||||
case actions_model.StatusRunning:
|
||||
|
||||
@@ -5,9 +5,12 @@ package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
@@ -58,3 +61,182 @@ func authShared(ctx *context.Context, authMethod Method) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyOptions contains required or check options
|
||||
type VerifyOptions struct {
|
||||
SignInRequired bool
|
||||
SignOutRequired bool
|
||||
AdminRequired bool
|
||||
DisableCSRF bool
|
||||
}
|
||||
|
||||
// Checks authentication according to options
|
||||
func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
// Check prohibit login users.
|
||||
if ctx.IsSigned {
|
||||
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||
ctx.HTML(http.StatusOK, "user/auth/activate")
|
||||
return
|
||||
}
|
||||
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
|
||||
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer.MustChangePassword {
|
||||
if ctx.Req.URL.Path != "/user/settings/change_password" {
|
||||
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
|
||||
ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password"))
|
||||
return
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
|
||||
ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
|
||||
if ctx.Req.URL.Path != "/user/events" {
|
||||
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
||||
}
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
|
||||
return
|
||||
}
|
||||
} else if ctx.Req.URL.Path == "/user/settings/change_password" {
|
||||
// make sure that the form cannot be accessed by users who don't need this
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to dashboard if user tries to visit any non-login page.
|
||||
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
return
|
||||
}
|
||||
|
||||
if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" {
|
||||
ctx.Csrf.Validate(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if options.SignInRequired {
|
||||
if !ctx.IsSigned {
|
||||
if ctx.Req.URL.Path != "/user/events" {
|
||||
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
||||
}
|
||||
ctx.Redirect(setting.AppSubURL + "/user/login")
|
||||
return
|
||||
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||
ctx.HTML(http.StatusOK, "user/auth/activate")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
||||
if !options.SignOutRequired && !ctx.IsSigned &&
|
||||
len(ctx.GetSiteCookie(setting.CookieUserName)) > 0 {
|
||||
if ctx.Req.URL.Path != "/user/events" {
|
||||
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
||||
}
|
||||
ctx.Redirect(setting.AppSubURL + "/user/login")
|
||||
return
|
||||
}
|
||||
|
||||
if options.AdminRequired {
|
||||
if !ctx.Doer.IsAdmin {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks authentication according to options
|
||||
func VerifyAuthWithOptionsAPI(options *VerifyOptions) func(ctx *context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
// Check prohibit login users.
|
||||
if ctx.IsSigned {
|
||||
if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "This account is not activated.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin {
|
||||
log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr())
|
||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "This account is prohibited from signing in, please contact your site administrator.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer.MustChangePassword {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "You must change your password. Change it at: " + setting.AppURL + "/user/change_password",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to dashboard if user tries to visit any non-login page.
|
||||
if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
return
|
||||
}
|
||||
|
||||
if options.SignInRequired {
|
||||
if !ctx.IsSigned {
|
||||
// Restrict API calls with error message.
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in user is allowed to call APIs.",
|
||||
})
|
||||
return
|
||||
} else if !ctx.Doer.IsActive && setting.Service.RegisterEmailConfirm {
|
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||
ctx.HTML(http.StatusOK, "user/auth/activate")
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned && ctx.IsBasicAuth {
|
||||
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
||||
return // Skip 2FA
|
||||
}
|
||||
twofa, err := auth.GetTwoFactorByUID(ctx.Doer.ID)
|
||||
if err != nil {
|
||||
if auth.IsErrTwoFactorNotEnrolled(err) {
|
||||
return // No 2FA enrollment for this user
|
||||
}
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
|
||||
ok, err := twofa.ValidateTOTP(otpHeader)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in user is allowed to call APIs.",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.AdminRequired {
|
||||
if !ctx.Doer.IsAdmin {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "You have no permission to request for this.",
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx.Data["PageIsAdmin"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func Authenticate(user *user_model.User, login, password string) (*user_model.Us
|
||||
}
|
||||
|
||||
// WARN: DON'T check user.IsActive, that will be checked on reqSign so that
|
||||
// user could be hint to resend confirm email.
|
||||
// user could be hinted to resend confirm email.
|
||||
if user.ProhibitLogin {
|
||||
return nil, user_model.ErrUserProhibitLogin{
|
||||
UID: user.ID,
|
||||
|
||||
@@ -18,14 +18,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/minio/sha256-simd"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// ErrInvalidAlgorithmType represents an invalid algorithm error.
|
||||
@@ -316,8 +314,7 @@ func InitSigningKey() error {
|
||||
case "HS384":
|
||||
fallthrough
|
||||
case "HS512":
|
||||
key, err = loadOrCreateSymmetricKey()
|
||||
|
||||
key, err = loadSymmetricKey()
|
||||
case "RS256":
|
||||
fallthrough
|
||||
case "RS384":
|
||||
@@ -332,7 +329,6 @@ func InitSigningKey() error {
|
||||
fallthrough
|
||||
case "EdDSA":
|
||||
key, err = loadOrCreateAsymmetricKey()
|
||||
|
||||
default:
|
||||
return ErrInvalidAlgorithmType{setting.OAuth2.JWTSigningAlgorithm}
|
||||
}
|
||||
@@ -351,22 +347,16 @@ func InitSigningKey() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadOrCreateSymmetricKey checks if the configured secret is valid.
|
||||
// If it is not valid a new secret is created and saved in the configuration file.
|
||||
func loadOrCreateSymmetricKey() (interface{}, error) {
|
||||
// loadSymmetricKey checks if the configured secret is valid.
|
||||
// If it is not valid, it will return an error.
|
||||
func loadSymmetricKey() (interface{}, error) {
|
||||
key := make([]byte, 32)
|
||||
n, err := base64.RawURLEncoding.Decode(key, []byte(setting.OAuth2.JWTSecretBase64))
|
||||
if err != nil || n != 32 {
|
||||
key, err = generate.NewJwtSecret()
|
||||
if err != nil {
|
||||
log.Fatal("error generating JWT secret: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setting.CreateOrAppendToCustomConf("oauth2.JWT_SECRET", func(cfg *ini.File) {
|
||||
secretBase64 := base64.RawURLEncoding.EncodeToString(key)
|
||||
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(secretBase64)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != 32 {
|
||||
return nil, fmt.Errorf("JWT secret must be 32 bytes long")
|
||||
}
|
||||
|
||||
return key, nil
|
||||
|
||||
@@ -48,12 +48,13 @@ func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList {
|
||||
}
|
||||
|
||||
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
|
||||
func ToWikiPageMetaData(title string, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
|
||||
suburl := wiki_service.NameToSubURL(title)
|
||||
func ToWikiPageMetaData(wikiName wiki_service.WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData {
|
||||
subURL := string(wikiName)
|
||||
_, title := wiki_service.WebPathToUserTitle(wikiName)
|
||||
return &api.WikiPageMetaData{
|
||||
Title: title,
|
||||
HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", suburl),
|
||||
SubURL: suburl,
|
||||
HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL),
|
||||
SubURL: subURL,
|
||||
LastCommit: ToWikiCommit(lastCommit),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ index 0000000..6bb8f39
|
||||
Docker Pulls
|
||||
+ cut off
|
||||
+ cut off`
|
||||
result, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff), "")
|
||||
_, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff), "")
|
||||
if err != nil {
|
||||
t.Errorf("ParsePatch failed: %s", err)
|
||||
}
|
||||
@@ -536,11 +536,10 @@ index 0000000..6bb8f39
|
||||
Docker Pulls
|
||||
+ cut off
|
||||
+ cut off`
|
||||
result, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff2), "")
|
||||
_, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff2), "")
|
||||
if err != nil {
|
||||
t.Errorf("ParsePatch failed: %s", err)
|
||||
}
|
||||
println(result)
|
||||
|
||||
diff2a := `diff --git "a/A \\ B" b/A/B
|
||||
--- "a/A \\ B"
|
||||
@@ -553,11 +552,10 @@ index 0000000..6bb8f39
|
||||
Docker Pulls
|
||||
+ cut off
|
||||
+ cut off`
|
||||
result, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff2a), "")
|
||||
_, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff2a), "")
|
||||
if err != nil {
|
||||
t.Errorf("ParsePatch failed: %s", err)
|
||||
}
|
||||
println(result)
|
||||
|
||||
diff3 := `diff --git a/README.md b/README.md
|
||||
--- a/README.md
|
||||
@@ -570,11 +568,10 @@ index 0000000..6bb8f39
|
||||
Docker Pulls
|
||||
+ cut off
|
||||
+ cut off`
|
||||
result, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff3), "")
|
||||
_, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(diff3), "")
|
||||
if err != nil {
|
||||
t.Errorf("ParsePatch failed: %s", err)
|
||||
}
|
||||
println(result)
|
||||
}
|
||||
|
||||
func setupDefaultDiff() *Diff {
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
||||
// Fake issue with assignees
|
||||
issue, err := issues_model.GetIssueWithAttrsByID(1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, len(issue.Assignees))
|
||||
assert.Len(t, issue.Assignees, 1)
|
||||
|
||||
user1, err := user_model.GetUserByID(db.DefaultContext, 1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him
|
||||
assert.NoError(t, err)
|
||||
@@ -33,10 +33,10 @@ func TestDeleteNotPassedAssignee(t *testing.T) {
|
||||
// Clean everyone
|
||||
err = DeleteNotPassedAssignee(db.DefaultContext, issue, user1, []*user_model.User{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, len(issue.Assignees))
|
||||
assert.Empty(t, issue.Assignees)
|
||||
|
||||
// Check they're gone
|
||||
assert.NoError(t, issue.LoadAssignees(db.DefaultContext))
|
||||
assert.EqualValues(t, 0, len(issue.Assignees))
|
||||
assert.Empty(t, issue.Assignees)
|
||||
assert.Empty(t, issue.Assignee)
|
||||
}
|
||||
|
||||
@@ -90,8 +90,7 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
|
||||
|
||||
// UpdateComment updates information of comment.
|
||||
func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_model.User, oldContent string) error {
|
||||
needsContentHistory := c.Content != oldContent &&
|
||||
(c.Type == issues_model.CommentTypeComment || c.Type == issues_model.CommentTypeReview || c.Type == issues_model.CommentTypeCode)
|
||||
needsContentHistory := c.Content != oldContent && c.Type.HasContentSupport()
|
||||
if needsContentHistory {
|
||||
hasContentHistory, err := issues_model.HasIssueContentHistory(ctx, c.IssueID, c.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
|
||||
|
||||
issueIDs, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 5, len(issueIDs))
|
||||
assert.Len(t, issueIDs, 5)
|
||||
|
||||
issue := &issues_model.Issue{
|
||||
RepoID: 1,
|
||||
@@ -48,7 +48,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, len(issueIDs))
|
||||
assert.Len(t, issueIDs, 4)
|
||||
|
||||
// check attachment removal
|
||||
attachments, err := repo_model.GetAttachmentsByIssueID(db.DefaultContext, 4)
|
||||
@@ -57,7 +57,7 @@ func TestIssue_DeleteIssue(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
err = deleteIssue(db.DefaultContext, issue)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, len(attachments))
|
||||
assert.Len(t, attachments, 2)
|
||||
for i := range attachments {
|
||||
attachment, err := repo_model.GetAttachmentByUUID(db.DefaultContext, attachments[i].UUID)
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
base "code.gitea.io/gitea/modules/migration"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -312,10 +312,11 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
}))
|
||||
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
head string
|
||||
assertContent func(t *testing.T, content string)
|
||||
pr base.PullRequest
|
||||
name string
|
||||
head string
|
||||
logFilter []string
|
||||
logFiltered []bool
|
||||
pr base.PullRequest
|
||||
}{
|
||||
{
|
||||
name: "fork, good Head.SHA",
|
||||
@@ -362,9 +363,8 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
OwnerName: forkRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
assertContent: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, "Fetch branch from")
|
||||
},
|
||||
logFilter: []string{"Fetch branch from"},
|
||||
logFiltered: []bool{true},
|
||||
},
|
||||
{
|
||||
name: "invalid fork CloneURL",
|
||||
@@ -388,9 +388,8 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
OwnerName: "WRONG",
|
||||
},
|
||||
},
|
||||
assertContent: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, "AddRemote")
|
||||
},
|
||||
logFilter: []string{"AddRemote"},
|
||||
logFiltered: []bool{true},
|
||||
},
|
||||
{
|
||||
name: "no fork, good Head.SHA",
|
||||
@@ -437,10 +436,8 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
assertContent: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, "Empty reference")
|
||||
assert.NotContains(t, content, "Cannot remove local head")
|
||||
},
|
||||
logFilter: []string{"Empty reference", "Cannot remove local head"},
|
||||
logFiltered: []bool{true, false},
|
||||
},
|
||||
{
|
||||
name: "no fork, invalid Head.SHA",
|
||||
@@ -464,9 +461,8 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
assertContent: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, "Deprecated local head")
|
||||
},
|
||||
logFilter: []string{"Deprecated local head"},
|
||||
logFiltered: []bool{true},
|
||||
},
|
||||
{
|
||||
name: "no fork, not found Head.SHA",
|
||||
@@ -490,36 +486,29 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
|
||||
OwnerName: fromRepo.OwnerName,
|
||||
},
|
||||
},
|
||||
assertContent: func(t *testing.T, content string) {
|
||||
assert.Contains(t, content, "Deprecated local head")
|
||||
assert.NotContains(t, content, "Cannot remove local head")
|
||||
},
|
||||
logFilter: []string{"Deprecated local head", "Cannot remove local head"},
|
||||
logFiltered: []bool{true, false},
|
||||
},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
logger, ok := log.NamedLoggers.Load(log.DEFAULT)
|
||||
assert.True(t, ok)
|
||||
logger.SetLogger("buffer", "buffer", "{}")
|
||||
defer logger.DelLogger("buffer")
|
||||
stopMark := fmt.Sprintf(">>>>>>>>>>>>>STOP: %s<<<<<<<<<<<<<<<", testCase.name)
|
||||
|
||||
logChecker, cleanup := test.NewLogChecker(log.DEFAULT)
|
||||
logChecker.Filter(testCase.logFilter...).StopMark(stopMark)
|
||||
defer cleanup()
|
||||
|
||||
testCase.pr.EnsuredSafe = true
|
||||
|
||||
head, err := uploader.updateGitForPullRequest(&testCase.pr)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, testCase.head, head)
|
||||
if testCase.assertContent != nil {
|
||||
fence := fmt.Sprintf(">>>>>>>>>>>>>FENCE %s<<<<<<<<<<<<<<<", testCase.name)
|
||||
log.Error(fence)
|
||||
var content string
|
||||
for i := 0; i < 5000; i++ {
|
||||
content, err = logger.GetLoggerProviderContent("buffer")
|
||||
assert.NoError(t, err)
|
||||
if strings.Contains(content, fence) {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
testCase.assertContent(t, content)
|
||||
|
||||
log.Info(stopMark)
|
||||
|
||||
logFiltered, logStopped := logChecker.Check(5 * time.Second)
|
||||
assert.True(t, logStopped)
|
||||
if len(testCase.logFilter) > 0 {
|
||||
assert.EqualValues(t, testCase.logFiltered, logFiltered, "for log message filters: %v", testCase.logFilter)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestCheckUnadoptedRepositories_Add(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, total, unadopted.index)
|
||||
assert.Equal(t, end-start, len(unadopted.repositories))
|
||||
assert.Len(t, unadopted.repositories, end-start)
|
||||
}
|
||||
|
||||
func TestCheckUnadoptedRepositories(t *testing.T) {
|
||||
@@ -41,7 +41,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
|
||||
unadopted := &unadoptedRepositories{start: 0, end: 100}
|
||||
err := checkUnadoptedRepositories(db.DefaultContext, "notauser", []string{"repo"}, unadopted)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(unadopted.repositories))
|
||||
assert.Empty(t, unadopted.repositories)
|
||||
//
|
||||
// Unadopted repository is returned
|
||||
// Existing (adopted) repository is not returned
|
||||
@@ -59,7 +59,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
|
||||
unadopted = &unadoptedRepositories{start: 0, end: 100}
|
||||
err = checkUnadoptedRepositories(db.DefaultContext, userName, []string{repoName}, unadopted)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(unadopted.repositories))
|
||||
assert.Empty(t, unadopted.repositories)
|
||||
assert.Equal(t, 0, unadopted.index)
|
||||
}
|
||||
|
||||
|
||||
@@ -86,11 +86,22 @@ func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use
|
||||
return err
|
||||
}
|
||||
defer t.Close()
|
||||
if err := t.Clone(opts.OldBranch); err != nil {
|
||||
return err
|
||||
|
||||
hasOldBranch := true
|
||||
if err = t.Clone(opts.OldBranch); err != nil {
|
||||
if !git.IsErrBranchNotExist(err) || !repo.IsEmpty {
|
||||
return err
|
||||
}
|
||||
if err = t.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
hasOldBranch = false
|
||||
opts.LastCommitID = ""
|
||||
}
|
||||
if err := t.SetDefaultIndex(); err != nil {
|
||||
return err
|
||||
if hasOldBranch {
|
||||
if err = t.SetDefaultIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var filename2attribute2info map[string]map[string]string
|
||||
|
||||
+33
-72
@@ -7,7 +7,6 @@ package wiki
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -19,61 +18,17 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/sync"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
)
|
||||
|
||||
var (
|
||||
reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
|
||||
// TODO: use clustered lock (unique queue? or *abuse* cache)
|
||||
wikiWorkingPool = sync.NewExclusivePool()
|
||||
)
|
||||
// TODO: use clustered lock (unique queue? or *abuse* cache)
|
||||
var wikiWorkingPool = sync.NewExclusivePool()
|
||||
|
||||
const (
|
||||
DefaultRemote = "origin"
|
||||
DefaultBranch = "master"
|
||||
)
|
||||
|
||||
func nameAllowed(name string) error {
|
||||
if util.SliceContainsString(reservedWikiNames, name) {
|
||||
return repo_model.ErrWikiReservedName{
|
||||
Title: name,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NameToSubURL converts a wiki name to its corresponding sub-URL.
|
||||
func NameToSubURL(name string) string {
|
||||
return url.PathEscape(strings.ReplaceAll(name, " ", "-"))
|
||||
}
|
||||
|
||||
// NormalizeWikiName normalizes a wiki name
|
||||
func NormalizeWikiName(name string) string {
|
||||
return strings.ReplaceAll(name, "-", " ")
|
||||
}
|
||||
|
||||
// NameToFilename converts a wiki name to its corresponding filename.
|
||||
func NameToFilename(name string) string {
|
||||
name = strings.ReplaceAll(name, " ", "-")
|
||||
return url.QueryEscape(name) + ".md"
|
||||
}
|
||||
|
||||
// FilenameToName converts a wiki filename to its corresponding page name.
|
||||
func FilenameToName(filename string) (string, error) {
|
||||
if !strings.HasSuffix(filename, ".md") {
|
||||
return "", repo_model.ErrWikiInvalidFileName{
|
||||
FileName: filename,
|
||||
}
|
||||
}
|
||||
basename := filename[:len(filename)-3]
|
||||
unescaped, err := url.QueryUnescape(basename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return NormalizeWikiName(unescaped), nil
|
||||
}
|
||||
|
||||
// InitWiki initializes a wiki for repository,
|
||||
// it does nothing when repository already has wiki.
|
||||
func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
|
||||
@@ -91,20 +46,20 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareWikiFileName try to find a suitable file path with file name by the given raw wiki name.
|
||||
// prepareGitPath try to find a suitable file path with file name by the given raw wiki name.
|
||||
// return: existence, prepared file path with name, error
|
||||
func prepareWikiFileName(gitRepo *git.Repository, wikiName string) (bool, string, error) {
|
||||
unescaped := wikiName + ".md"
|
||||
escaped := NameToFilename(wikiName)
|
||||
func prepareGitPath(gitRepo *git.Repository, wikiPath WebPath) (bool, string, error) {
|
||||
unescaped := string(wikiPath) + ".md"
|
||||
gitPath := WebPathToGitPath(wikiPath)
|
||||
|
||||
// Look for both files
|
||||
filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, escaped)
|
||||
filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, gitPath)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Not a valid object name master") {
|
||||
return false, escaped, nil
|
||||
return false, gitPath, nil
|
||||
}
|
||||
log.Error("%v", err)
|
||||
return false, escaped, err
|
||||
return false, gitPath, err
|
||||
}
|
||||
|
||||
foundEscaped := false
|
||||
@@ -113,18 +68,18 @@ func prepareWikiFileName(gitRepo *git.Repository, wikiName string) (bool, string
|
||||
case unescaped:
|
||||
// if we find the unescaped file return it
|
||||
return true, unescaped, nil
|
||||
case escaped:
|
||||
case gitPath:
|
||||
foundEscaped = true
|
||||
}
|
||||
}
|
||||
|
||||
// If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility.
|
||||
return foundEscaped, escaped, nil
|
||||
return foundEscaped, gitPath, nil
|
||||
}
|
||||
|
||||
// updateWikiPage adds a new page or edits an existing page in repository wiki.
|
||||
func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName, content, message string, isNew bool) (err error) {
|
||||
if err = nameAllowed(newWikiName); err != nil {
|
||||
func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string, isNew bool) (err error) {
|
||||
if err = validateWebPath(newWikiName); err != nil {
|
||||
return err
|
||||
}
|
||||
wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
|
||||
@@ -157,24 +112,24 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
|
||||
if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil {
|
||||
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
||||
return fmt.Errorf("Failed to clone repository: %s (%w)", repo.FullName(), err)
|
||||
return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, basePath)
|
||||
if err != nil {
|
||||
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
|
||||
return fmt.Errorf("Failed to open new temporary repository in: %s %w", basePath, err)
|
||||
return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if hasMasterBranch {
|
||||
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
|
||||
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
||||
return fmt.Errorf("Unable to read HEAD tree to index in: %s %w", basePath, err)
|
||||
return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
isWikiExist, newWikiPath, err := prepareWikiFileName(gitRepo, newWikiName)
|
||||
isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, newWikiName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -190,7 +145,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
isOldWikiExist := true
|
||||
oldWikiPath := newWikiPath
|
||||
if oldWikiName != newWikiName {
|
||||
isOldWikiExist, oldWikiPath, err = prepareWikiFileName(gitRepo, oldWikiName)
|
||||
isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, oldWikiName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -271,18 +226,18 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
}
|
||||
|
||||
// AddWikiPage adds a new wiki page with a given wikiPath.
|
||||
func AddWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName, content, message string) error {
|
||||
func AddWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath, content, message string) error {
|
||||
return updateWikiPage(ctx, doer, repo, "", wikiName, content, message, true)
|
||||
}
|
||||
|
||||
// EditWikiPage updates a wiki page identified by its wikiPath,
|
||||
// optionally also changing wikiPath.
|
||||
func EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName, content, message string) error {
|
||||
func EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string) error {
|
||||
return updateWikiPage(ctx, doer, repo, oldWikiName, newWikiName, content, message, false)
|
||||
}
|
||||
|
||||
// DeleteWikiPage deletes a wiki page identified by its path.
|
||||
func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName string) (err error) {
|
||||
func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath) (err error) {
|
||||
wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
|
||||
defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
|
||||
|
||||
@@ -306,22 +261,22 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
Branch: DefaultBranch,
|
||||
}); err != nil {
|
||||
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
||||
return fmt.Errorf("Failed to clone repository: %s (%w)", repo.FullName(), err)
|
||||
return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, basePath)
|
||||
if err != nil {
|
||||
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
|
||||
return fmt.Errorf("Failed to open new temporary repository in: %s %w", basePath, err)
|
||||
return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
|
||||
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
||||
return fmt.Errorf("Unable to read HEAD tree to index in: %s %w", basePath, err)
|
||||
return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err)
|
||||
}
|
||||
|
||||
found, wikiPath, err := prepareWikiFileName(gitRepo, wikiName)
|
||||
found, wikiPath, err := prepareGitPath(gitRepo, wikiName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -340,7 +295,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message := "Delete page '" + wikiName + "'"
|
||||
message := fmt.Sprintf("Delete page %q", wikiName)
|
||||
commitTreeOpts := git.CommitTreeOpts{
|
||||
Message: message,
|
||||
Parents: []string{"HEAD"},
|
||||
@@ -366,7 +321,13 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||
if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
|
||||
Remote: DefaultRemote,
|
||||
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
|
||||
Env: repo_module.PushingEnvironment(doer, repo),
|
||||
Env: repo_module.FullPushingEnvironment(
|
||||
doer,
|
||||
doer,
|
||||
repo,
|
||||
repo.Name+".wiki",
|
||||
0,
|
||||
),
|
||||
}); err != nil {
|
||||
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// To define the wiki related concepts:
|
||||
// * Display Segment: the text what user see for a wiki page (aka, the title):
|
||||
// - "Home Page"
|
||||
// - "100% Free"
|
||||
// - "2000-01-02 meeting"
|
||||
// * Web Path:
|
||||
// - "/wiki/Home-Page"
|
||||
// - "/wiki/100%25+Free"
|
||||
// - "/wiki/2000-01-02+meeting.-"
|
||||
// - If a segment has a suffix "DashMarker(.-)", it means that there is no dash-space conversion for this segment.
|
||||
// - If a WebPath is a "*.md" pattern, then use it directly as GitPath, to make users can access the raw file.
|
||||
// * Git Path (only space doesn't need to be escaped):
|
||||
// - "/.wiki.git/Home-Page.md"
|
||||
// - "/.wiki.git/100%25 Free.md"
|
||||
// - "/.wiki.git/2000-01-02 meeting.-.md"
|
||||
// TODO: support subdirectory in the future
|
||||
//
|
||||
// Although this package now has the ablity to support subdirectory, but the route package doesn't:
|
||||
// * Double-escaping problem: the URL "/wiki/abc%2Fdef" becomes "/wiki/abc/def" by ctx.Params, which is incorrect
|
||||
// * The old wiki code's behavior is always using %2F, instead of subdirectory, so there are a lot of legacy "%2F" files in user wikis.
|
||||
|
||||
type WebPath string
|
||||
|
||||
var reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
|
||||
|
||||
func validateWebPath(name WebPath) error {
|
||||
for _, s := range WebPathSegments(name) {
|
||||
if util.SliceContainsString(reservedWikiNames, s) {
|
||||
return repo_model.ErrWikiReservedName{Title: s}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasDashMarker(s string) bool {
|
||||
return strings.HasSuffix(s, ".-")
|
||||
}
|
||||
|
||||
func removeDashMarker(s string) string {
|
||||
return strings.TrimSuffix(s, ".-")
|
||||
}
|
||||
|
||||
func addDashMarker(s string) string {
|
||||
return s + ".-"
|
||||
}
|
||||
|
||||
func unescapeSegment(s string) (string, error) {
|
||||
if hasDashMarker(s) {
|
||||
s = removeDashMarker(s)
|
||||
} else {
|
||||
s = strings.ReplaceAll(s, "-", " ")
|
||||
}
|
||||
unescaped, err := url.QueryUnescape(s)
|
||||
if err != nil {
|
||||
return s, err // un-escaping failed, but it's still safe to return the original string, because it is only a title for end users
|
||||
}
|
||||
return unescaped, nil
|
||||
}
|
||||
|
||||
func escapeSegToWeb(s string, hadDashMarker bool) string {
|
||||
if hadDashMarker || strings.Contains(s, "-") || strings.HasSuffix(s, ".md") {
|
||||
s = addDashMarker(s)
|
||||
} else {
|
||||
s = strings.ReplaceAll(s, " ", "-")
|
||||
}
|
||||
s = url.QueryEscape(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func WebPathSegments(s WebPath) []string {
|
||||
a := strings.Split(string(s), "/")
|
||||
for i := range a {
|
||||
a[i], _ = unescapeSegment(a[i])
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func WebPathToGitPath(s WebPath) string {
|
||||
if strings.HasSuffix(string(s), ".md") {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
a := strings.Split(string(s), "/")
|
||||
for i := range a {
|
||||
shouldAddDashMarker := hasDashMarker(a[i])
|
||||
a[i], _ = unescapeSegment(a[i])
|
||||
a[i] = escapeSegToWeb(a[i], shouldAddDashMarker)
|
||||
a[i] = strings.ReplaceAll(a[i], "%20", " ") // space is safe to be kept in git path
|
||||
a[i] = strings.ReplaceAll(a[i], "+", " ")
|
||||
}
|
||||
return strings.Join(a, "/") + ".md"
|
||||
}
|
||||
|
||||
func GitPathToWebPath(s string) (wp WebPath, err error) {
|
||||
if !strings.HasSuffix(s, ".md") {
|
||||
return "", repo_model.ErrWikiInvalidFileName{FileName: s}
|
||||
}
|
||||
s = strings.TrimSuffix(s, ".md")
|
||||
a := strings.Split(s, "/")
|
||||
for i := range a {
|
||||
shouldAddDashMarker := hasDashMarker(a[i])
|
||||
if a[i], err = unescapeSegment(a[i]); err != nil {
|
||||
return "", err
|
||||
}
|
||||
a[i] = escapeSegToWeb(a[i], shouldAddDashMarker)
|
||||
}
|
||||
return WebPath(strings.Join(a, "/")), nil
|
||||
}
|
||||
|
||||
func WebPathToUserTitle(s WebPath) (dir, display string) {
|
||||
dir = path.Dir(string(s))
|
||||
display = path.Base(string(s))
|
||||
display = strings.TrimSuffix(display, ".md")
|
||||
display, _ = unescapeSegment(display)
|
||||
return dir, display
|
||||
}
|
||||
|
||||
func WebPathToURLPath(s WebPath) string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func WebPathFromRequest(s string) WebPath {
|
||||
s = util.PathJoinRelX(s)
|
||||
// The old wiki code's behavior is always using %2F, instead of subdirectory.
|
||||
s = strings.ReplaceAll(s, "/", "%2F")
|
||||
return WebPath(s)
|
||||
}
|
||||
|
||||
func UserTitleToWebPath(base, title string) WebPath {
|
||||
// TODO: ctx.Params does un-escaping, so the URL "/wiki/abc%2Fdef" becomes "wiki path = `abc/def`", which is incorrect.
|
||||
// And the old wiki code's behavior is always using %2F, instead of subdirectory.
|
||||
// So we do not add the support for writing slashes in title at the moment.
|
||||
title = strings.TrimSpace(title)
|
||||
title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
|
||||
if title == "" || title == "." {
|
||||
title = "unnamed"
|
||||
}
|
||||
return WebPath(title)
|
||||
}
|
||||
+85
-65
@@ -4,7 +4,9 @@
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -21,91 +23,114 @@ func TestMain(m *testing.M) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWikiNameToSubURL(t *testing.T) {
|
||||
func TestWebPathSegments(t *testing.T) {
|
||||
a := WebPathSegments("a%2Fa/b+c/d-e/f-g.-")
|
||||
assert.EqualValues(t, []string{"a/a", "b c", "d e", "f-g"}, a)
|
||||
}
|
||||
|
||||
func TestUserTitleToWebPath(t *testing.T) {
|
||||
type test struct {
|
||||
Expected string
|
||||
WikiName string
|
||||
Expected string
|
||||
UserTitle string
|
||||
}
|
||||
for _, test := range []test{
|
||||
{"wiki-name", "wiki name"},
|
||||
{"wiki-name", "wiki-name"},
|
||||
{"name-with%2Fslash", "name with/slash"},
|
||||
{"name-with%25percent", "name with%percent"},
|
||||
{"title.md.-", "title.md"},
|
||||
{"wiki-name.-", "wiki-name"},
|
||||
{"the+wiki-name.-", "the wiki-name"},
|
||||
{"a%2Fb", "a/b"},
|
||||
{"a%25b", "a%b"},
|
||||
} {
|
||||
assert.Equal(t, test.Expected, NameToSubURL(test.WikiName))
|
||||
assert.EqualValues(t, test.Expected, UserTitleToWebPath("", test.UserTitle))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeWikiName(t *testing.T) {
|
||||
func TestWebPathToDisplayName(t *testing.T) {
|
||||
type test struct {
|
||||
Expected string
|
||||
WikiName string
|
||||
WebPath WebPath
|
||||
}
|
||||
for _, test := range []test{
|
||||
{"wiki name", "wiki name"},
|
||||
{"wiki name", "wiki-name"},
|
||||
{"name with/slash", "name with/slash"},
|
||||
{"name with%percent", "name-with%percent"},
|
||||
{"%2F", "%2F"},
|
||||
{"wiki-name", "wiki-name.-"},
|
||||
{"name with / slash", "name-with %2F slash"},
|
||||
{"name with % percent", "name-with %25 percent"},
|
||||
{"2000-01-02 meeting", "2000-01-02+meeting.-.md"},
|
||||
} {
|
||||
assert.Equal(t, test.Expected, NormalizeWikiName(test.WikiName))
|
||||
_, displayName := WebPathToUserTitle(test.WebPath)
|
||||
assert.EqualValues(t, test.Expected, displayName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiNameToFilename(t *testing.T) {
|
||||
func TestWebPathToGitPath(t *testing.T) {
|
||||
type test struct {
|
||||
Expected string
|
||||
WikiName string
|
||||
WikiName WebPath
|
||||
}
|
||||
for _, test := range []test{
|
||||
{"wiki-name.md", "wiki name"},
|
||||
{"wiki-name.md", "wiki-name"},
|
||||
{"name-with%2Fslash.md", "name with/slash"},
|
||||
{"name-with%25percent.md", "name with%percent"},
|
||||
{"wiki-name.md", "wiki%20name"},
|
||||
{"wiki-name.md", "wiki+name"},
|
||||
{"wiki%20name.md", "wiki%20name.md"},
|
||||
{"2000-01-02-meeting.md", "2000-01-02+meeting"},
|
||||
{"2000-01-02 meeting.-.md", "2000-01-02%20meeting.-"},
|
||||
} {
|
||||
assert.Equal(t, test.Expected, NameToFilename(test.WikiName))
|
||||
assert.EqualValues(t, test.Expected, WebPathToGitPath(test.WikiName))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiFilenameToName(t *testing.T) {
|
||||
func TestGitPathToWebPath(t *testing.T) {
|
||||
type test struct {
|
||||
Expected string
|
||||
Filename string
|
||||
}
|
||||
for _, test := range []test{
|
||||
{"hello world", "hello-world.md"},
|
||||
{"symbols/?*", "symbols%2F%3F%2A.md"},
|
||||
{"hello-world", "hello-world.md"}, // this shouldn't happen, because it should always have a ".-" suffix
|
||||
{"hello-world", "hello world.md"},
|
||||
{"hello-world.-", "hello-world.-.md"},
|
||||
{"hello+world.-", "hello world.-.md"},
|
||||
{"symbols-%2F", "symbols %2F.md"},
|
||||
} {
|
||||
name, err := FilenameToName(test.Filename)
|
||||
name, err := GitPathToWebPath(test.Filename)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.Expected, name)
|
||||
assert.EqualValues(t, test.Expected, name)
|
||||
}
|
||||
for _, badFilename := range []string{
|
||||
"nofileextension",
|
||||
"wrongfileextension.txt",
|
||||
} {
|
||||
_, err := FilenameToName(badFilename)
|
||||
_, err := GitPathToWebPath(badFilename)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, repo_model.IsErrWikiInvalidFileName(err))
|
||||
}
|
||||
_, err := FilenameToName("badescaping%%.md")
|
||||
_, err := GitPathToWebPath("badescaping%%.md")
|
||||
assert.Error(t, err)
|
||||
assert.False(t, repo_model.IsErrWikiInvalidFileName(err))
|
||||
}
|
||||
|
||||
func TestWikiNameToFilenameToName(t *testing.T) {
|
||||
// converting from wiki name to filename, then back to wiki name should
|
||||
// return the original (normalized) name
|
||||
for _, name := range []string{
|
||||
"wiki-name",
|
||||
"wiki name",
|
||||
"wiki name with/slash",
|
||||
"$$$%%%^^&&!@#$(),.<>",
|
||||
} {
|
||||
filename := NameToFilename(name)
|
||||
resultName, err := FilenameToName(filename)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, NormalizeWikiName(name), resultName)
|
||||
func TestUserWebGitPathConsistency(t *testing.T) {
|
||||
maxLen := 20
|
||||
b := make([]byte, maxLen)
|
||||
for i := 0; i < 1000; i++ {
|
||||
l := rand.Intn(maxLen)
|
||||
for j := 0; j < l; j++ {
|
||||
r := rand.Intn(0x80-0x20) + 0x20
|
||||
b[j] = byte(r)
|
||||
}
|
||||
|
||||
userTitle := strings.TrimSpace(string(b[:l]))
|
||||
if userTitle == "" || userTitle == "." {
|
||||
continue
|
||||
}
|
||||
webPath := UserTitleToWebPath("", userTitle)
|
||||
gitPath := WebPathToGitPath(webPath)
|
||||
|
||||
webPath1, _ := GitPathToWebPath(gitPath)
|
||||
_, userTitle1 := WebPathToUserTitle(webPath1)
|
||||
gitPath1 := WebPathToGitPath(webPath1)
|
||||
|
||||
assert.EqualValues(t, userTitle, userTitle1, "UserTitle for userTitle: %q", userTitle)
|
||||
assert.EqualValues(t, webPath, webPath1, "WebPath for userTitle: %q", userTitle)
|
||||
assert.EqualValues(t, gitPath, gitPath1, "GitPath for userTitle: %q", userTitle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,24 +152,23 @@ func TestRepository_AddWikiPage(t *testing.T) {
|
||||
const commitMsg = "Commit message"
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
for _, wikiName := range []string{
|
||||
for _, userTitle := range []string{
|
||||
"Another page",
|
||||
"Here's a <tag> and a/slash",
|
||||
} {
|
||||
wikiName := wikiName
|
||||
t.Run("test wiki exist: "+wikiName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, wikiName, wikiContent, commitMsg))
|
||||
t.Run("test wiki exist: "+userTitle, func(t *testing.T) {
|
||||
webPath := UserTitleToWebPath("", userTitle)
|
||||
assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg))
|
||||
// Now need to show that the page has been added:
|
||||
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
masterTree, err := gitRepo.GetTree(DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
wikiPath := NameToFilename(wikiName)
|
||||
entry, err := masterTree.GetTreeEntryByPath(wikiPath)
|
||||
gitPath := WebPathToGitPath(webPath)
|
||||
entry, err := masterTree.GetTreeEntryByPath(gitPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, wikiPath, entry.Name(), "%s not added correctly", wikiName)
|
||||
assert.EqualValues(t, gitPath, entry.Name(), "%s not added correctly", userTitle)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,18 +201,19 @@ func TestRepository_EditWikiPage(t *testing.T) {
|
||||
"New home",
|
||||
"New/name/with/slashes",
|
||||
} {
|
||||
webPath := UserTitleToWebPath("", newWikiName)
|
||||
unittest.PrepareTestEnv(t)
|
||||
assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", newWikiName, newWikiContent, commitMsg))
|
||||
assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", webPath, newWikiContent, commitMsg))
|
||||
|
||||
// Now need to show that the page has been added:
|
||||
gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath())
|
||||
assert.NoError(t, err)
|
||||
masterTree, err := gitRepo.GetTree(DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
wikiPath := NameToFilename(newWikiName)
|
||||
entry, err := masterTree.GetTreeEntryByPath(wikiPath)
|
||||
gitPath := WebPathToGitPath(webPath)
|
||||
entry, err := masterTree.GetTreeEntryByPath(gitPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, wikiPath, entry.Name(), "%s not edited correctly", newWikiName)
|
||||
assert.EqualValues(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName)
|
||||
|
||||
if newWikiName != "Home" {
|
||||
_, err := masterTree.GetTreeEntryByPath("Home.md")
|
||||
@@ -210,8 +235,8 @@ func TestRepository_DeleteWikiPage(t *testing.T) {
|
||||
defer gitRepo.Close()
|
||||
masterTree, err := gitRepo.GetTree(DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
wikiPath := NameToFilename("Home")
|
||||
_, err = masterTree.GetTreeEntryByPath(wikiPath)
|
||||
gitPath := WebPathToGitPath("Home")
|
||||
_, err = masterTree.GetTreeEntryByPath(gitPath)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -240,16 +265,11 @@ func TestPrepareWikiFileName(t *testing.T) {
|
||||
existence: false,
|
||||
wikiPath: "home-of-and-%26-or-wiki-page%21.md",
|
||||
wantErr: false,
|
||||
}, {
|
||||
name: "found unescaped cases",
|
||||
arg: "Unescaped File",
|
||||
existence: true,
|
||||
wikiPath: "Unescaped File.md",
|
||||
wantErr: false,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
existence, newWikiPath, err := prepareWikiFileName(gitRepo, tt.arg)
|
||||
webPath := UserTitleToWebPath("", tt.arg)
|
||||
existence, newWikiPath, err := prepareGitPath(gitRepo, webPath)
|
||||
if (err != nil) != tt.wantErr {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
@@ -261,7 +281,7 @@ func TestPrepareWikiFileName(t *testing.T) {
|
||||
t.Errorf("expect to find an escaped file but we could not detect one")
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.wikiPath, newWikiPath)
|
||||
assert.EqualValues(t, tt.wikiPath, newWikiPath)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -279,8 +299,8 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) {
|
||||
defer gitRepo.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
existence, newWikiPath, err := prepareWikiFileName(gitRepo, "Home")
|
||||
existence, newWikiPath, err := prepareGitPath(gitRepo, "Home")
|
||||
assert.False(t, existence)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Home.md", newWikiPath)
|
||||
assert.EqualValues(t, "Home.md", newWikiPath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user