From b39a5bbbd610ba30651218658caaec1c86d6bca1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Thu, 20 Apr 2023 01:50:10 +0800 Subject: [PATCH] Make wiki title supports dashes and improve wiki name related features (#24143) Close #7570 1. Clearly define the wiki path behaviors, see `services/wiki/wiki_path.go` and tests 2. Keep compatibility with old contents 3. Allow to use dashes in titles, eg: "2000-01-02 Meeting record" 4. Add a "Pages" link in the dropdown, otherwise users can't go to the Pages page easily. 5. Add a "View original git file" link in the Pages list, even if some file names are broken, users still have a chance to edit or remove it, without cloning the wiki repo to local. 6. Fix 500 error when the name contains prefix spaces. This PR also introduces the ability to support sub-directories, but it can't be done at the moment due to there are a lot of legacy wiki data, which use "%2F" in file names. ![image](https://user-images.githubusercontent.com/2114189/232239004-3359d7b9-7bf3-4ff3-8446-bfb0e79645dd.png) ![image](https://user-images.githubusercontent.com/2114189/232239020-74b92c72-bf73-4377-a319-1c85609f82b1.png) Co-authored-by: Giteabot --- modules/git/repo_commit.go | 3 + options/locale/locale_en-US.ini | 1 + routers/api/v1/repo/wiki.go | 40 ++++---- routers/web/repo/wiki.go | 104 +++++++++++--------- routers/web/repo/wiki_test.go | 31 +++--- services/convert/wiki.go | 9 +- services/wiki/wiki.go | 97 +++++------------- services/wiki/wiki_path.go | 153 +++++++++++++++++++++++++++++ services/wiki/wiki_test.go | 149 ++++++++++++++++------------ templates/repo/wiki/pages.tmpl | 11 +-- templates/repo/wiki/view.tmpl | 44 ++++----- web_src/css/repository.css | 8 ++ web_src/js/features/repo-legacy.js | 6 -- 13 files changed, 400 insertions(+), 256 deletions(-) create mode 100644 services/wiki/wiki_path.go diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 2b780feb5c..153a116b06 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -84,6 +84,9 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { if err != nil { return nil, err } + if len(commits) == 0 { + return nil, ErrNotExist{ID: relpath} + } return commits[0], nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f168334d9d..d1ea8f8ed6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1791,6 +1791,7 @@ wiki.reserved_page = The wiki page name "%s" is reserved. wiki.pages = Pages wiki.last_updated = Last updated %s wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'. +wiki.original_git_entry_tooltip = View original Git file instead of using friendly link. activity = Activity activity.period.filter_label = Period: diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 764530a671..0b9a36ec47 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -58,10 +58,10 @@ func NewWikiPage(ctx *context.APIContext) { return } - wikiName := wiki_service.NormalizeWikiName(form.Title) + wikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { - form.Message = fmt.Sprintf("Add '%s'", form.Title) + form.Message = fmt.Sprintf("Add %q", form.Title) } content, err := base64.StdEncoding.DecodeString(form.ContentBase64) @@ -85,7 +85,7 @@ func NewWikiPage(ctx *context.APIContext) { wikiPage := getWikiPage(ctx, wikiName) if !ctx.Written() { - notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Message) + notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName), form.Message) ctx.JSON(http.StatusCreated, wikiPage) } } @@ -127,15 +127,15 @@ func EditWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) - oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) - newWikiName := wiki_service.NormalizeWikiName(form.Title) + oldWikiName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) + newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(newWikiName) == 0 { newWikiName = oldWikiName } if len(form.Message) == 0 { - form.Message = fmt.Sprintf("Update '%s'", newWikiName) + form.Message = fmt.Sprintf("Update %q", newWikiName) } content, err := base64.StdEncoding.DecodeString(form.ContentBase64) @@ -153,14 +153,12 @@ func EditWikiPage(ctx *context.APIContext) { wikiPage := getWikiPage(ctx, newWikiName) if !ctx.Written() { - notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, newWikiName, form.Message) + notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message) ctx.JSON(http.StatusOK, wikiPage) } } -func getWikiPage(ctx *context.APIContext, title string) *api.WikiPage { - title = wiki_service.NormalizeWikiName(title) - +func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.WikiPage { wikiRepo, commit := findWikiRepoCommit(ctx) if wikiRepo != nil { defer wikiRepo.Close() @@ -170,7 +168,7 @@ func getWikiPage(ctx *context.APIContext, title string) *api.WikiPage { } // lookup filename in wiki - get filecontent, real filename - content, pageFilename := wikiContentsByName(ctx, commit, title, false) + content, pageFilename := wikiContentsByName(ctx, commit, wikiName, false) if ctx.Written() { return nil } @@ -196,7 +194,7 @@ func getWikiPage(ctx *context.APIContext, title string) *api.WikiPage { } return &api.WikiPage{ - WikiPageMetaData: convert.ToWikiPageMetaData(title, lastCommit, ctx.Repo.Repository), + WikiPageMetaData: convert.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository), ContentBase64: content, CommitCount: commitsCount, Sidebar: sidebarContent, @@ -233,7 +231,7 @@ func DeleteWikiPage(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - wikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + wikiName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { if err.Error() == "file does not exist" { @@ -244,7 +242,7 @@ func DeleteWikiPage(ctx *context.APIContext) { return } - notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName) + notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName)) ctx.Status(http.StatusNoContent) } @@ -316,7 +314,7 @@ func ListWikiPages(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "GetCommit", err) return } - wikiName, err := wiki_service.FilenameToName(entry.Name()) + wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) if err != nil { if repo_model.IsErrWikiInvalidFileName(err) { continue @@ -361,7 +359,7 @@ func GetWikiPage(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) wikiPage := getWikiPage(ctx, pageName) if !ctx.Written() { @@ -411,7 +409,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) if len(pageName) == 0 { pageName = "Home" } @@ -502,9 +500,9 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { // wikiContentsByName returns the contents of a wiki page, along with a boolean // indicating whether the page exists. Writes to ctx if an error occurs. -func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName string, isSidebarOrFooter bool) (string, string) { - pageFilename := wiki_service.NameToFilename(wikiName) - entry, err := findEntryForFile(commit, pageFilename) +func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) { + gitFilename := wiki_service.WebPathToGitPath(wikiName) + entry, err := findEntryForFile(commit, gitFilename) if err != nil { if git.IsErrNotExist(err) { if !isSidebarOrFooter { @@ -515,5 +513,5 @@ func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName st } return "", "" } - return wikiContentsByEntry(ctx, entry), pageFilename + return wikiContentsByEntry(ctx, entry), gitFilename } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 0c5c5eed7d..374d1bf2e0 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -68,9 +68,10 @@ func MustEnableWiki(ctx *context.Context) { // PageMeta wiki page meta information type PageMeta struct { - Name string - SubURL string - UpdatedUnix timeutil.TimeStamp + Name string + SubURL string + GitEntryName string + UpdatedUnix timeutil.TimeStamp } // findEntryForFile finds the tree entry for a target filepath. @@ -83,7 +84,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) return entry, nil } - // Then the unescaped, shortest alternative + // Then the unescaped, the shortest alternative var unescapedTarget string if unescapedTarget, err = url.QueryUnescape(target); err != nil { return nil, err @@ -124,16 +125,16 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte { // wikiContentsByName returns the contents of a wiki page, along with a boolean // indicating whether the page exists. Writes to ctx if an error occurs. -func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) { - pageFilename := wiki_service.NameToFilename(wikiName) - entry, err := findEntryForFile(commit, pageFilename) +func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_service.WebPath) ([]byte, *git.TreeEntry, string, bool) { + gitFilename := wiki_service.WebPathToGitPath(wikiName) + entry, err := findEntryForFile(commit, gitFilename) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findEntryForFile", err) return nil, nil, "", false } else if entry == nil { return nil, nil, "", true } - return wikiContentsByEntry(ctx, entry), entry, pageFilename, false + return wikiContentsByEntry(ctx, entry), entry, gitFilename, false } func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { @@ -162,7 +163,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { if !entry.IsRegular() { continue } - wikiName, err := wiki_service.FilenameToName(entry.Name()) + wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) if err != nil { if repo_model.IsErrWikiInvalidFileName(err) { continue @@ -175,22 +176,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } else if wikiName == "_Sidebar" || wikiName == "_Footer" { continue } + _, displayName := wiki_service.WebPathToUserTitle(wikiName) pages = append(pages, PageMeta{ - Name: wikiName, - SubURL: wiki_service.NameToSubURL(wikiName), + Name: displayName, + SubURL: wiki_service.WebPathToURLPath(wikiName), + GitEntryName: entry.Name(), }) } ctx.Data["Pages"] = pages - // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) + // get requested page name + pageName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } - ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName) - ctx.Data["old_title"] = pageName - ctx.Data["Title"] = pageName - ctx.Data["title"] = pageName + + _, displayName := wiki_service.WebPathToUserTitle(pageName) + ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName) + ctx.Data["old_title"] = displayName + ctx.Data["Title"] = displayName + ctx.Data["title"] = displayName isSideBar := pageName == "_Sidebar" isFooter := pageName == "_Footer" @@ -328,14 +333,17 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) } // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) + pageName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } - ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName) - ctx.Data["old_title"] = pageName - ctx.Data["Title"] = pageName - ctx.Data["title"] = pageName + + _, displayName := wiki_service.WebPathToUserTitle(pageName) + ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName) + ctx.Data["old_title"] = displayName + ctx.Data["Title"] = displayName + ctx.Data["title"] = displayName + ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name @@ -403,14 +411,16 @@ func renderEditPage(ctx *context.Context) { }() // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) + pageName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } - ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName) - ctx.Data["old_title"] = pageName - ctx.Data["Title"] = pageName - ctx.Data["title"] = pageName + + _, displayName := wiki_service.WebPathToUserTitle(pageName) + ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName) + ctx.Data["old_title"] = displayName + ctx.Data["Title"] = displayName + ctx.Data["title"] = displayName // lookup filename in wiki - get filecontent, gitTree entry , real filename data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName) @@ -594,7 +604,7 @@ func WikiPages(ctx *context.Context) { ctx.ServerError("GetCommit", err) return } - wikiName, err := wiki_service.FilenameToName(entry.Name()) + wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) if err != nil { if repo_model.IsErrWikiInvalidFileName(err) { continue @@ -602,10 +612,12 @@ func WikiPages(ctx *context.Context) { ctx.ServerError("WikiFilenameToName", err) return } + _, displayName := wiki_service.WebPathToUserTitle(wikiName) pages = append(pages, PageMeta{ - Name: wikiName, - SubURL: wiki_service.NameToSubURL(wikiName), - UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()), + Name: displayName, + SubURL: wiki_service.WebPathToURLPath(wikiName), + GitEntryName: entry.Name(), + UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()), }) } ctx.Data["Pages"] = pages @@ -631,12 +643,12 @@ func WikiRaw(ctx *context.Context) { return } - providedPath := ctx.Params("*") - + providedWebPath := wiki_service.WebPathFromRequest(ctx.Params("*")) + providedGitPath := wiki_service.WebPathToGitPath(providedWebPath) var entry *git.TreeEntry if commit != nil { // Try to find a file with that name - entry, err = findEntryForFile(commit, providedPath) + entry, err = findEntryForFile(commit, providedGitPath) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findFile", err) return @@ -644,10 +656,8 @@ func WikiRaw(ctx *context.Context) { if entry == nil { // Try to find a wiki page with that name - providedPath = strings.TrimSuffix(providedPath, ".md") - - wikiPath := wiki_service.NameToFilename(providedPath) - entry, err = findEntryForFile(commit, wikiPath) + providedGitPath = strings.TrimSuffix(providedGitPath, ".md") + entry, err = findEntryForFile(commit, providedGitPath) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findFile", err) return @@ -694,7 +704,7 @@ func NewWikiPost(ctx *context.Context) { return } - wikiName := wiki_service.NormalizeWikiName(form.Title) + wikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { form.Message = ctx.Tr("repo.editor.add", form.Title) @@ -713,9 +723,9 @@ func NewWikiPost(ctx *context.Context) { return } - notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Message) + notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName), form.Message) - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(wikiName)) + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.WebPathToURLPath(wikiName)) } // EditWiki render wiki modify page @@ -745,8 +755,8 @@ func EditWikiPost(ctx *context.Context) { return } - oldWikiName := wiki_service.NormalizeWikiName(ctx.Params("*")) - newWikiName := wiki_service.NormalizeWikiName(form.Title) + oldWikiName := wiki_service.WebPathFromRequest(ctx.Params("*")) + newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { form.Message = ctx.Tr("repo.editor.update", form.Title) @@ -757,14 +767,14 @@ func EditWikiPost(ctx *context.Context) { return } - notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, newWikiName, form.Message) + notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message) - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(newWikiName)) + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.WebPathToURLPath(newWikiName)) } // DeleteWikiPagePost delete wiki page func DeleteWikiPagePost(ctx *context.Context) { - wikiName := wiki_service.NormalizeWikiName(ctx.Params("*")) + wikiName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(wikiName) == 0 { wikiName = "Home" } @@ -774,7 +784,7 @@ func DeleteWikiPagePost(ctx *context.Context) { return } - notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName) + notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName)) ctx.JSON(http.StatusOK, map[string]interface{}{ "redirect": ctx.Repo.RepoLink + "/wiki/", diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 4699f5379a..e51820a520 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -6,6 +6,7 @@ package repo import ( "io" "net/http" + "net/url" "testing" repo_model "code.gitea.io/gitea/models/repo" @@ -24,7 +25,7 @@ const ( message = "Wiki commit message for unit tests" ) -func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName string) *git.TreeEntry { +func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry { wikiRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) assert.NoError(t, err) defer wikiRepo.Close() @@ -33,14 +34,14 @@ func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName string) *git. entries, err := commit.ListEntries() assert.NoError(t, err) for _, entry := range entries { - if entry.Name() == wiki_service.NameToFilename(wikiName) { + if entry.Name() == wiki_service.WebPathToGitPath(wikiName) { return entry } } return nil } -func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName string) string { +func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) string { entry := wikiEntry(t, repo, wikiName) if !assert.NotNil(t, entry) { return "" @@ -53,11 +54,11 @@ func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName string) str return string(bytes) } -func assertWikiExists(t *testing.T, repo *repo_model.Repository, wikiName string) { +func assertWikiExists(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) { assert.NotNil(t, wikiEntry(t, repo, wikiName)) } -func assertWikiNotExists(t *testing.T, repo *repo_model.Repository, wikiName string) { +func assertWikiNotExists(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) { assert.Nil(t, wikiEntry(t, repo, wikiName)) } @@ -124,8 +125,8 @@ func TestNewWikiPost(t *testing.T) { }) NewWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) - assertWikiExists(t, ctx.Repo.Repository, title) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content) + assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) } } @@ -176,8 +177,8 @@ func TestEditWikiPost(t *testing.T) { }) EditWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) - assertWikiExists(t, ctx.Repo.Repository, title) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content) + assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) if title != "Home" { assertWikiNotExists(t, ctx.Repo.Repository, "Home") } @@ -201,17 +202,21 @@ func TestWikiRaw(t *testing.T) { "images/jpeg.jpg": "image/jpeg", "Page With Spaced Name": "text/plain; charset=utf-8", "Page-With-Spaced-Name": "text/plain; charset=utf-8", - "Page With Spaced Name.md": "text/plain; charset=utf-8", + "Page With Spaced Name.md": "", // there is no "Page With Spaced Name.md" in repo "Page-With-Spaced-Name.md": "text/plain; charset=utf-8", } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+filepath) + ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+url.PathEscape(filepath)) ctx.SetParams("*", filepath) test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) WikiRaw(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) - assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type")) + if filetype == "" { + assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath) + } else { + assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath) + assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath) + } } } diff --git a/services/convert/wiki.go b/services/convert/wiki.go index 20d76162c7..1f04843483 100644 --- a/services/convert/wiki.go +++ b/services/convert/wiki.go @@ -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), } } diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index e5cb2db02b..9ceb8e5817 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -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"}, diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go new file mode 100644 index 0000000000..45c6a5a84e --- /dev/null +++ b/services/wiki/wiki_path.go @@ -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, "-") { + 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) +} diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index 268d8848c5..716ea6104a 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -4,7 +4,9 @@ package wiki import ( + "math/rand" "path/filepath" + "strings" "testing" repo_model "code.gitea.io/gitea/models/repo" @@ -21,91 +23,113 @@ 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"}, + {"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 +151,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 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 +200,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 +234,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 +264,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 +280,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 +298,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) } diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl index 743574d0a5..97abf9b543 100644 --- a/templates/repo/wiki/pages.tmpl +++ b/templates/repo/wiki/pages.tmpl @@ -3,22 +3,21 @@ {{template "repo/header" .}}

-
- {{.locale.Tr "repo.wiki.pages"}} -
-
+ {{.locale.Tr "repo.wiki.pages"}} + {{if and .CanWriteWiki (not .IsRepositoryMirror)}} {{.locale.Tr "repo.wiki.new_page_button"}} {{end}} -
+

- +
{{range .Pages}} {{$timeSince := TimeSinceUnix .UpdatedUnix $.locale}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 318006d96e..123511d011 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -5,34 +5,32 @@
-
-
{{svg "octicon-file"}} {{.Name}} + {{svg "octicon-chevron-right"}} {{$.locale.Tr "repo.wiki.last_updated" $timeSince | Safe}}