diff options
author | Daniel Baumann <daniel@debian.org> | 2024-10-18 20:33:49 +0200 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-12-12 23:57:56 +0100 |
commit | e68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch) | |
tree | 97775d6c13b0f416af55314eb6a89ef792474615 /services/wiki | |
parent | Initial commit. (diff) | |
download | forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip |
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r-- | services/wiki/wiki.go | 449 | ||||
-rw-r--r-- | services/wiki/wiki_path.go | 172 | ||||
-rw-r--r-- | services/wiki/wiki_test.go | 327 |
3 files changed, 948 insertions, 0 deletions
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go new file mode 100644 index 0000000..aba1115 --- /dev/null +++ b/services/wiki/wiki.go @@ -0,0 +1,449 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. +// SPDX-License-Identifier: MIT + +package wiki + +import ( + "context" + "fmt" + "os" + "strings" + + repo_model "code.gitea.io/gitea/models/repo" + system_model "code.gitea.io/gitea/models/system" + "code.gitea.io/gitea/models/unit" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/sync" + asymkey_service "code.gitea.io/gitea/services/asymkey" + repo_service "code.gitea.io/gitea/services/repository" +) + +// TODO: use clustered lock (unique queue? or *abuse* cache) +var wikiWorkingPool = sync.NewExclusivePool() + +const ( + DefaultRemote = "origin" +) + +// InitWiki initializes a wiki for repository, +// it does nothing when repository already has wiki. +func InitWiki(ctx context.Context, repo *repo_model.Repository) error { + if repo.HasWiki() { + return nil + } + + branch := repo.GetWikiBranchName() + + if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil { + return fmt.Errorf("InitRepository: %w", err) + } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil { + return fmt.Errorf("createDelegateHooks: %w", err) + } else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + branch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil { + return fmt.Errorf("unable to set default wiki branch to %s: %w", branch, err) + } + return nil +} + +// NormalizeWikiBranch renames a repository wiki's branch to `setting.Repository.DefaultBranch` +func NormalizeWikiBranch(ctx context.Context, repo *repo_model.Repository, to string) error { + from := repo.GetWikiBranchName() + + if err := repo.MustNotBeArchived(); err != nil { + return err + } + + updateDB := func() error { + repo.WikiBranch = to + return repo_model.UpdateRepositoryCols(ctx, repo, "wiki_branch") + } + + if !repo.HasWiki() { + return updateDB() + } + + if from == to { + return nil + } + + gitRepo, err := git.OpenRepository(ctx, repo.WikiPath()) + if err != nil { + return err + } + defer gitRepo.Close() + + if gitRepo.IsBranchExist(to) { + return nil + } + + if !gitRepo.IsBranchExist(from) { + return nil + } + + if err := gitRepo.RenameBranch(from, to); err != nil { + return err + } + + if err := gitrepo.SetDefaultBranch(ctx, repo, to); err != nil { + return err + } + + return updateDB() +} + +// 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 prepareGitPath(gitRepo *git.Repository, branch string, wikiPath WebPath) (bool, string, error) { + unescaped := string(wikiPath) + ".md" + gitPath := WebPathToGitPath(wikiPath) + + // Look for both files + filesInIndex, err := gitRepo.LsTree(branch, unescaped, gitPath) + if err != nil { + if strings.Contains(err.Error(), "Not a valid object name "+branch) { + return false, gitPath, nil + } + log.Error("%v", err) + return false, gitPath, err + } + + foundEscaped := false + for _, filename := range filesInIndex { + switch filename { + case unescaped: + // if we find the unescaped file return it + return true, unescaped, nil + case gitPath: + foundEscaped = true + } + } + + // If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility. + 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 WebPath, content, message string, isNew bool) (err error) { + err = repo.MustNotBeArchived() + if err != nil { + return err + } + + if err = validateWebPath(newWikiName); err != nil { + return err + } + wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID)) + defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID)) + + if err = InitWiki(ctx, repo); err != nil { + return fmt.Errorf("InitWiki: %w", err) + } + + hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), repo.GetWikiBranchName()) + + basePath, err := repo_module.CreateTemporaryPath("update-wiki") + if err != nil { + return err + } + defer func() { + if err := repo_module.RemoveTemporaryPath(basePath); err != nil { + log.Error("Merge: RemoveTemporaryPath: %s", err) + } + }() + + cloneOpts := git.CloneRepoOptions{ + Bare: true, + Shared: true, + } + + if hasMasterBranch { + cloneOpts.Branch = repo.GetWikiBranchName() + } + + 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) + } + + 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) + } + 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("fnable to read HEAD tree to index in: %s %w", basePath, err) + } + } + + isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.GetWikiBranchName(), newWikiName) + if err != nil { + return err + } + + if isNew { + if isWikiExist { + return repo_model.ErrWikiAlreadyExist{ + Title: newWikiPath, + } + } + } else { + // avoid check existence again if wiki name is not changed since gitRepo.LsFiles(...) is not free. + isOldWikiExist := true + oldWikiPath := newWikiPath + if oldWikiName != newWikiName { + isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.GetWikiBranchName(), oldWikiName) + if err != nil { + return err + } + } + + if isOldWikiExist { + err := gitRepo.RemoveFilesFromIndex(oldWikiPath) + if err != nil { + log.Error("RemoveFilesFromIndex failed: %v", err) + return err + } + } + } + + // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here + + objectHash, err := gitRepo.HashObject(strings.NewReader(content)) + if err != nil { + log.Error("HashObject failed: %v", err) + return err + } + + if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil { + log.Error("AddObjectToIndex failed: %v", err) + return err + } + + tree, err := gitRepo.WriteTree() + if err != nil { + log.Error("WriteTree failed: %v", err) + return err + } + + commitTreeOpts := git.CommitTreeOpts{ + Message: message, + } + + committer := doer.NewGitSig() + + sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) + if sign { + commitTreeOpts.KeyID = signingKey + if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { + committer = signer + } + } else { + commitTreeOpts.NoGPGSign = true + } + if hasMasterBranch { + commitTreeOpts.Parents = []string{"HEAD"} + } + + commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) + if err != nil { + log.Error("CommitTree failed: %v", err) + return err + } + + if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ + Remote: DefaultRemote, + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.GetWikiBranchName()), + Env: repo_module.FullPushingEnvironment( + doer, + doer, + repo, + repo.Name+".wiki", + 0, + ), + }); err != nil { + log.Error("Push failed: %v", err) + if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { + return err + } + return fmt.Errorf("failed to push: %w", err) + } + + return nil +} + +// AddWikiPage adds a new wiki page with a given wikiPath. +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 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 WebPath) (err error) { + err = repo.MustNotBeArchived() + if err != nil { + return err + } + + wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID)) + defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID)) + + if err = InitWiki(ctx, repo); err != nil { + return fmt.Errorf("InitWiki: %w", err) + } + + basePath, err := repo_module.CreateTemporaryPath("update-wiki") + if err != nil { + return err + } + defer func() { + if err := repo_module.RemoveTemporaryPath(basePath); err != nil { + log.Error("Merge: RemoveTemporaryPath: %s", err) + } + }() + + if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{ + Bare: true, + Shared: true, + Branch: repo.GetWikiBranchName(), + }); 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) + } + + 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) + } + 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) + } + + found, wikiPath, err := prepareGitPath(gitRepo, repo.GetWikiBranchName(), wikiName) + if err != nil { + return err + } + if found { + err := gitRepo.RemoveFilesFromIndex(wikiPath) + if err != nil { + return err + } + } else { + return os.ErrNotExist + } + + // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here + + tree, err := gitRepo.WriteTree() + if err != nil { + return err + } + message := fmt.Sprintf("Delete page %q", wikiName) + commitTreeOpts := git.CommitTreeOpts{ + Message: message, + Parents: []string{"HEAD"}, + } + + committer := doer.NewGitSig() + + sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) + if sign { + commitTreeOpts.KeyID = signingKey + if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { + committer = signer + } + } else { + commitTreeOpts.NoGPGSign = true + } + + commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) + if err != nil { + return err + } + + if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{ + Remote: DefaultRemote, + Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.GetWikiBranchName()), + Env: repo_module.FullPushingEnvironment( + doer, + doer, + repo, + repo.Name+".wiki", + 0, + ), + }); err != nil { + if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { + return err + } + return fmt.Errorf("Push: %w", err) + } + + return nil +} + +// DeleteWiki removes the actual and local copy of repository wiki. +func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error { + if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil { + return err + } + + system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath()) + return nil +} + +type SearchContentsResult struct { + *git.GrepResult + Title string +} + +func SearchWikiContents(ctx context.Context, repo *repo_model.Repository, keyword string) ([]SearchContentsResult, error) { + gitRepo, err := git.OpenRepository(ctx, repo.WikiPath()) + if err != nil { + return nil, err + } + defer gitRepo.Close() + + grepRes, err := git.GrepSearch(ctx, gitRepo, keyword, git.GrepOptions{ + ContextLineNumber: 0, + IsFuzzy: true, + RefName: repo.GetWikiBranchName(), + MaxResultLimit: 10, + MatchesPerFile: 3, + }) + if err != nil { + return nil, err + } + + res := make([]SearchContentsResult, 0, len(grepRes)) + for _, entry := range grepRes { + wp, err := GitPathToWebPath(entry.Filename) + if err != nil { + return nil, err + } + _, title := WebPathToUserTitle(wp) + + res = append(res, SearchContentsResult{ + GrepResult: entry, + Title: title, + }) + } + + return res, nil +} diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go new file mode 100644 index 0000000..74c7064 --- /dev/null +++ b/services/wiki/wiki_path.go @@ -0,0 +1,172 @@ +// 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/git" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/convert" +) + +// 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 the unescaped value 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 ability 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 +// * This problem should have been 99% fixed, but it needs more tests. +// * 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") { + ret, _ := url.PathUnescape(string(s)) + return util.PathJoinRelX(ret) + } + + 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)) + if strings.HasSuffix(display, ".md") { + display = strings.TrimSuffix(display, ".md") + display, _ = url.PathUnescape(display) + } + 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: no support for subdirectory, because 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) +} + +// ToWikiPageMetaData converts meta information to a WikiPageMetaData +func ToWikiPageMetaData(wikiName WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData { + subURL := string(wikiName) + _, title := WebPathToUserTitle(wikiName) + return &api.WikiPageMetaData{ + Title: title, + HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL), + SubURL: subURL, + LastCommit: convert.ToWikiCommit(lastCommit), + } +} diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go new file mode 100644 index 0000000..efcc13d --- /dev/null +++ b/services/wiki/wiki_test.go @@ -0,0 +1,327 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package wiki + +import ( + "math/rand" + "strings" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" + + _ "code.gitea.io/gitea/models/actions" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} + +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 + UserTitle string + } + for _, test := range []test{ + {"unnamed", ""}, + {"unnamed", "."}, + {"unnamed", ".."}, + {"wiki-name", "wiki name"}, + {"title.md.-", "title.md"}, + {"wiki-name.-", "wiki-name"}, + {"the+wiki-name.-", "the wiki-name"}, + {"a%2Fb", "a/b"}, + {"a%25b", "a%b"}, + } { + assert.EqualValues(t, test.Expected, UserTitleToWebPath("", test.UserTitle)) + } +} + +func TestWebPathToDisplayName(t *testing.T) { + type test struct { + Expected string + WebPath WebPath + } + for _, test := range []test{ + {"wiki name", "wiki-name"}, + {"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"}, + {"a b", "a%20b.md"}, + } { + _, displayName := WebPathToUserTitle(test.WebPath) + assert.EqualValues(t, test.Expected, displayName) + } +} + +func TestWebPathToGitPath(t *testing.T) { + type test struct { + Expected string + WikiName WebPath + } + for _, test := range []test{ + {"wiki-name.md", "wiki%20name"}, + {"wiki-name.md", "wiki+name"}, + {"wiki name.md", "wiki%20name.md"}, + {"wiki%20name.md", "wiki%2520name.md"}, + {"2000-01-02-meeting.md", "2000-01-02+meeting"}, + {"2000-01-02 meeting.-.md", "2000-01-02%20meeting.-"}, + } { + assert.EqualValues(t, test.Expected, WebPathToGitPath(test.WikiName)) + } +} + +func TestGitPathToWebPath(t *testing.T) { + type test struct { + Expected string + Filename string + } + for _, test := range []test{ + {"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 := GitPathToWebPath(test.Filename) + require.NoError(t, err) + assert.EqualValues(t, test.Expected, name) + } + for _, badFilename := range []string{ + "nofileextension", + "wrongfileextension.txt", + } { + _, err := GitPathToWebPath(badFilename) + require.Error(t, err) + assert.True(t, repo_model.IsErrWikiInvalidFileName(err)) + } + _, err := GitPathToWebPath("badescaping%%.md") + require.Error(t, err) + assert.False(t, repo_model.IsErrWikiInvalidFileName(err)) +} + +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 == "." || 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) + } +} + +func TestRepository_InitWiki(t *testing.T) { + unittest.PrepareTestEnv(t) + // repo1 already has a wiki + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + require.NoError(t, InitWiki(git.DefaultContext, repo1)) + + // repo2 does not already have a wiki + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) + require.NoError(t, InitWiki(git.DefaultContext, repo2)) + assert.True(t, repo2.HasWiki()) +} + +func TestRepository_AddWikiPage(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + const wikiContent = "This is the wiki content" + const commitMsg = "Commit message" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + for _, userTitle := range []string{ + "Another page", + "Here's a <tag> and a/slash", + } { + t.Run("test wiki exist: "+userTitle, func(t *testing.T) { + webPath := UserTitleToWebPath("", userTitle) + require.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg)) + // Now need to show that the page has been added: + gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + require.NoError(t, err) + + defer gitRepo.Close() + masterTree, err := gitRepo.GetTree("master") + require.NoError(t, err) + gitPath := WebPathToGitPath(webPath) + entry, err := masterTree.GetTreeEntryByPath(gitPath) + require.NoError(t, err) + assert.EqualValues(t, gitPath, entry.Name(), "%s not added correctly", userTitle) + }) + } + + t.Run("check wiki already exist", func(t *testing.T) { + t.Parallel() + // test for already-existing wiki name + err := AddWikiPage(git.DefaultContext, doer, repo, "Home", wikiContent, commitMsg) + require.Error(t, err) + assert.True(t, repo_model.IsErrWikiAlreadyExist(err)) + }) + + t.Run("check wiki reserved name", func(t *testing.T) { + t.Parallel() + // test for reserved wiki name + err := AddWikiPage(git.DefaultContext, doer, repo, "_edit", wikiContent, commitMsg) + require.Error(t, err) + assert.True(t, repo_model.IsErrWikiReservedName(err)) + }) +} + +func TestRepository_EditWikiPage(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + const newWikiContent = "This is the new content" + const commitMsg = "Commit message" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + for _, newWikiName := range []string{ + "Home", // same name as before + "New home", + "New/name/with/slashes", + } { + webPath := UserTitleToWebPath("", newWikiName) + unittest.PrepareTestEnv(t) + require.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", webPath, newWikiContent, commitMsg)) + + // Now need to show that the page has been added: + gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + require.NoError(t, err) + masterTree, err := gitRepo.GetTree("master") + require.NoError(t, err) + gitPath := WebPathToGitPath(webPath) + entry, err := masterTree.GetTreeEntryByPath(gitPath) + require.NoError(t, err) + assert.EqualValues(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName) + + if newWikiName != "Home" { + _, err := masterTree.GetTreeEntryByPath("Home.md") + require.Error(t, err) + } + gitRepo.Close() + } +} + +func TestRepository_DeleteWikiPage(t *testing.T) { + unittest.PrepareTestEnv(t) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + require.NoError(t, DeleteWikiPage(git.DefaultContext, doer, repo, "Home")) + + // Now need to show that the page has been added: + gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + require.NoError(t, err) + defer gitRepo.Close() + masterTree, err := gitRepo.GetTree("master") + require.NoError(t, err) + gitPath := WebPathToGitPath("Home") + _, err = masterTree.GetTreeEntryByPath(gitPath) + require.Error(t, err) +} + +func TestPrepareWikiFileName(t *testing.T) { + unittest.PrepareTestEnv(t) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) + require.NoError(t, err) + + defer gitRepo.Close() + + tests := []struct { + name string + arg string + existence bool + wikiPath string + wantErr bool + }{{ + name: "add suffix", + arg: "Home", + existence: true, + wikiPath: "Home.md", + wantErr: false, + }, { + name: "test special chars", + arg: "home of and & or wiki page!", + existence: false, + wikiPath: "home-of-and-%26-or-wiki-page%21.md", + wantErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + webPath := UserTitleToWebPath("", tt.arg) + existence, newWikiPath, err := prepareGitPath(gitRepo, "master", webPath) + if (err != nil) != tt.wantErr { + require.NoError(t, err) + return + } + if existence != tt.existence { + if existence { + t.Errorf("expect to find no escaped file but we detect one") + } else { + t.Errorf("expect to find an escaped file but we could not detect one") + } + } + assert.EqualValues(t, tt.wikiPath, newWikiPath) + }) + } +} + +func TestPrepareWikiFileName_FirstPage(t *testing.T) { + unittest.PrepareTestEnv(t) + + // Now create a temporaryDirectory + tmpDir := t.TempDir() + + err := git.InitRepository(git.DefaultContext, tmpDir, true, git.Sha1ObjectFormat.Name()) + require.NoError(t, err) + + gitRepo, err := git.OpenRepository(git.DefaultContext, tmpDir) + require.NoError(t, err) + + defer gitRepo.Close() + + existence, newWikiPath, err := prepareGitPath(gitRepo, "master", "Home") + assert.False(t, existence) + require.NoError(t, err) + assert.EqualValues(t, "Home.md", newWikiPath) +} + +func TestWebPathConversion(t *testing.T) { + assert.Equal(t, "path/wiki", WebPathToURLPath(WebPath("path/wiki"))) + assert.Equal(t, "wiki", WebPathToURLPath(WebPath("wiki"))) + assert.Equal(t, "", WebPathToURLPath(WebPath(""))) +} + +func TestWebPathFromRequest(t *testing.T) { + assert.Equal(t, WebPath("a%2Fb"), WebPathFromRequest("a/b")) + assert.Equal(t, WebPath("a"), WebPathFromRequest("a")) + assert.Equal(t, WebPath("b"), WebPathFromRequest("a/../b")) +} |