first-commit
This commit is contained in:
398
services/wiki/wiki.go
Normal file
398
services/wiki/wiki.go
Normal file
@@ -0,0 +1,398 @@
|
||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package wiki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
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/globallock"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const DefaultRemote = "origin"
|
||||
|
||||
func getWikiWorkingLockKey(repoID int64) string {
|
||||
return fmt.Sprintf("wiki_working_%d", repoID)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil {
|
||||
return fmt.Errorf("InitRepository: %w", err)
|
||||
} else if err = gitrepo.CreateDelegateHooks(ctx, repo.WikiStorageRepo()); err != nil {
|
||||
return fmt.Errorf("createDelegateHooks: %w", err)
|
||||
} else if _, _, err = git.NewCommand("symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix+repo.DefaultWikiBranch).RunStdString(ctx, &git.RunOpts{Dir: repo.WikiPath()}); err != nil {
|
||||
return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
|
||||
unescaped := string(wikiPath) + ".md"
|
||||
gitPath := WebPathToGitPath(wikiPath)
|
||||
|
||||
// Look for both files
|
||||
filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Not a valid object name") {
|
||||
return false, gitPath, nil // branch doesn't exist
|
||||
}
|
||||
log.Error("Wiki LsTree failed, err: %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
|
||||
}
|
||||
releaser, err := globallock.Lock(ctx, getWikiWorkingLockKey(repo.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer releaser()
|
||||
|
||||
if err = InitWiki(ctx, repo); err != nil {
|
||||
return fmt.Errorf("InitWiki: %w", err)
|
||||
}
|
||||
|
||||
hasDefaultBranch := gitrepo.IsBranchExist(ctx, repo.WikiStorageRepo(), repo.DefaultWikiBranch)
|
||||
|
||||
basePath, cleanup, err := repo_module.CreateTemporaryPath("update-wiki")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
cloneOpts := git.CloneRepoOptions{
|
||||
Bare: true,
|
||||
Shared: true,
|
||||
}
|
||||
|
||||
if hasDefaultBranch {
|
||||
cloneOpts.Branch = repo.DefaultWikiBranch
|
||||
}
|
||||
|
||||
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 hasDefaultBranch {
|
||||
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.DefaultWikiBranch, 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.DefaultWikiBranch, 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.Key = signingKey
|
||||
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||
committer = signer
|
||||
}
|
||||
} else {
|
||||
commitTreeOpts.NoGPGSign = true
|
||||
}
|
||||
if hasDefaultBranch {
|
||||
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.DefaultWikiBranch),
|
||||
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
|
||||
}
|
||||
|
||||
releaser, err := globallock.Lock(ctx, getWikiWorkingLockKey(repo.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer releaser()
|
||||
|
||||
if err = InitWiki(ctx, repo); err != nil {
|
||||
return fmt.Errorf("InitWiki: %w", err)
|
||||
}
|
||||
|
||||
basePath, cleanup, err := repo_module.CreateTemporaryPath("update-wiki")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{
|
||||
Bare: true,
|
||||
Shared: true,
|
||||
Branch: repo.DefaultWikiBranch,
|
||||
}); 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.DefaultWikiBranch, 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.Key = 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.DefaultWikiBranch),
|
||||
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
|
||||
}
|
||||
|
||||
func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, newBranch string) error {
|
||||
if !git.IsValidRefPattern(newBranch) {
|
||||
return fmt.Errorf("invalid branch name: %s", newBranch)
|
||||
}
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
repo.DefaultWikiBranch = newBranch
|
||||
if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "default_wiki_branch"); err != nil {
|
||||
return fmt.Errorf("unable to update database: %w", err)
|
||||
}
|
||||
|
||||
if !repo.HasWiki() {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldDefBranch, err := gitrepo.GetDefaultBranch(ctx, repo.WikiStorageRepo())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get default branch: %w", err)
|
||||
}
|
||||
if oldDefBranch == newBranch {
|
||||
return nil
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return nil // no git repo on storage, no need to do anything else
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("unable to open repository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
err = gitRepo.RenameBranch(oldDefBranch, newBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to rename default branch: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
172
services/wiki/wiki_path.go
Normal file
172
services/wiki/wiki_path.go
Normal file
@@ -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.PathParam, 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),
|
||||
}
|
||||
}
|
328
services/wiki/wiki_test.go
Normal file
328
services/wiki/wiki_test.go
Normal file
@@ -0,0 +1,328 @@
|
||||
// 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.Equal(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.Equal(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.Equal(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)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, test.Expected, name)
|
||||
}
|
||||
for _, badFilename := range []string{
|
||||
"nofileextension",
|
||||
"wrongfileextension.txt",
|
||||
} {
|
||||
_, err := GitPathToWebPath(badFilename)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, repo_model.IsErrWikiInvalidFileName(err))
|
||||
}
|
||||
_, err := GitPathToWebPath("badescaping%%.md")
|
||||
assert.Error(t, err)
|
||||
assert.False(t, repo_model.IsErrWikiInvalidFileName(err))
|
||||
}
|
||||
|
||||
func TestUserWebGitPathConsistency(t *testing.T) {
|
||||
maxLen := 20
|
||||
b := make([]byte, maxLen)
|
||||
for range 1000 {
|
||||
l := rand.Intn(maxLen)
|
||||
for j := range l {
|
||||
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.Equal(t, userTitle, userTitle1, "UserTitle for userTitle: %q", userTitle)
|
||||
assert.Equal(t, webPath, webPath1, "WebPath for userTitle: %q", userTitle)
|
||||
assert.Equal(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})
|
||||
assert.NoError(t, InitWiki(git.DefaultContext, repo1))
|
||||
|
||||
// repo2 does not already have a wiki
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
assert.NoError(t, InitWiki(git.DefaultContext, repo2))
|
||||
assert.True(t, repo2.HasWiki())
|
||||
}
|
||||
|
||||
func TestRepository_AddWikiPage(t *testing.T) {
|
||||
assert.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)
|
||||
assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg))
|
||||
// Now need to show that the page has been added:
|
||||
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
|
||||
require.NoError(t, err)
|
||||
|
||||
defer gitRepo.Close()
|
||||
masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
|
||||
assert.NoError(t, err)
|
||||
gitPath := WebPathToGitPath(webPath)
|
||||
entry, err := masterTree.GetTreeEntryByPath(gitPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(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)
|
||||
assert.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)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, repo_model.IsErrWikiReservedName(err))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_EditWikiPage(t *testing.T) {
|
||||
assert.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)
|
||||
assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", webPath, newWikiContent, commitMsg))
|
||||
|
||||
// Now need to show that the page has been added:
|
||||
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
|
||||
assert.NoError(t, err)
|
||||
masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
|
||||
assert.NoError(t, err)
|
||||
gitPath := WebPathToGitPath(webPath)
|
||||
entry, err := masterTree.GetTreeEntryByPath(gitPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName)
|
||||
|
||||
if newWikiName != "Home" {
|
||||
_, err := masterTree.GetTreeEntryByPath("Home.md")
|
||||
assert.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})
|
||||
assert.NoError(t, DeleteWikiPage(git.DefaultContext, doer, repo, "Home"))
|
||||
|
||||
// Now need to show that the page has been added:
|
||||
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
|
||||
require.NoError(t, err)
|
||||
|
||||
defer gitRepo.Close()
|
||||
masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
|
||||
assert.NoError(t, err)
|
||||
gitPath := WebPathToGitPath("Home")
|
||||
_, err = masterTree.GetTreeEntryByPath(gitPath)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPrepareWikiFileName(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
|
||||
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, repo.DefaultWikiBranch, webPath)
|
||||
if (err != nil) != tt.wantErr {
|
||||
assert.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.Equal(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())
|
||||
assert.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)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(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.Empty(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"))
|
||||
}
|
Reference in New Issue
Block a user