Merge remote-tracking branch 'upstream/main' into limit-repo-size

This commit is contained in:
DmitryFrolovTri
2023-05-03 14:40:58 +00:00
477 changed files with 7190 additions and 5990 deletions
+3 -1
View File
@@ -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:
+182
View File
@@ -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
}
}
}
+1 -1
View File
@@ -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,
+9 -19
View File
@@ -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
+5 -4
View File
@@ -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),
}
}
+4 -7
View File
@@ -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 {
+3 -3
View File
@@ -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)
}
+1 -2
View File
@@ -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 {
+3 -3
View File
@@ -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)
+28 -39
View File
@@ -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)
}
})
}
+3 -3
View File
@@ -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)
}
+15 -4
View File
@@ -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
View File
@@ -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
+153
View File
@@ -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
View File
@@ -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)
}