diff options
author | qwerty287 <80460567+qwerty287@users.noreply.github.com> | 2021-10-25 05:43:40 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-25 11:43:40 +0800 |
commit | 3676fafdacaef7ef0a6b72be99a342f91c8d5e5d (patch) | |
tree | fcf5a5155bdb1d9b1329e977ea3094fbca7577f2 /routers/api/v1 | |
parent | 843bc9deeb67cba2ac71165fcd0bdcce835cfab5 (diff) | |
download | gitea-3676fafdacaef7ef0a6b72be99a342f91c8d5e5d.tar.gz gitea-3676fafdacaef7ef0a6b72be99a342f91c8d5e5d.zip |
Add API to get/edit wiki (#17278)
* Add API to get/edit wiki
* Add swagger docs, various improvements
* fmt
* Fix lint and rm comment
* Add page parameter
* Add pagination to pages
* Add tests
* fmt
* Update func names
* Update error handling
* Update type name
* Fix lint
* Don't delete Home
* Update func name
* Update routers/api/v1/repo/wiki.go
Co-authored-by: delvh <dev.lh@web.de>
* Remove unnecessary check
* Fix lint
* Use English strings
* Update integrations/api_wiki_test.go
Co-authored-by: delvh <dev.lh@web.de>
* Update func and test names
* Remove unsed check and avoid duplicated error reports
* Improve error handling
* Return after error
* Document 404 error
* Update swagger
* Fix lint
* Apply suggestions from code review
Co-authored-by: delvh <dev.lh@web.de>
* Document file encoding
* fmt
* Apply suggestions
* Use convert
* Fix integration test
* simplify permissions
* unify duplicate key Title/Name
* improve types & return UTC timestamps
* improve types pt.2
- add WikiPageMetaData.LastCommit
- add WikiPageMetaData.HTMLURL
- replace WikiPageMetaData.Updated with .LastCommit.Committer.Created
also delete convert.ToWikiPage(), as it received too many arguments and
only had one callsite anyway. sorry for bad advice earlier 🙃
* WikiPage.Content is base64 encoded
* simplify error handling in wikiContentsByName()
* update swagger
* fix & DRY findWikiRepoCommit() error handling
ListWikiPages() previously wrote error twice when repo wiki didn't exist
* rename Content -> ContentBase64
* Fix test
* Fix tests
* Update var name
* suburl -> sub_url
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Norwin <git@nroo.de>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'routers/api/v1')
-rw-r--r-- | routers/api/v1/api.go | 16 | ||||
-rw-r--r-- | routers/api/v1/repo/wiki.go | 514 | ||||
-rw-r--r-- | routers/api/v1/swagger/options.go | 3 | ||||
-rw-r--r-- | routers/api/v1/swagger/repo.go | 21 |
4 files changed, 554 insertions, 0 deletions
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index d915b76f78..70d7cb40f8 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -521,6 +521,13 @@ func mustEnableIssuesOrPulls(ctx *context.APIContext) { } } +func mustEnableWiki(ctx *context.APIContext) { + if !(ctx.Repo.CanRead(models.UnitTypeWiki)) { + ctx.NotFound() + return + } +} + func mustNotBeArchived(ctx *context.APIContext) { if ctx.Repo.Repository.IsArchived { ctx.NotFound() @@ -791,6 +798,15 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route { m.Combo("").Get(repo.ListTrackedTimesByRepository) m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser) }, mustEnableIssues, reqToken()) + m.Group("/wiki", func() { + m.Combo("/page/{pageName}"). + Get(repo.GetWikiPage). + Patch(mustNotBeArchived, reqRepoWriter(models.UnitTypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage). + Delete(mustNotBeArchived, reqRepoWriter(models.UnitTypeWiki), repo.DeleteWikiPage) + m.Get("/revisions/{pageName}", repo.ListPageRevisions) + m.Post("/new", mustNotBeArchived, reqRepoWriter(models.UnitTypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage) + m.Get("/pages", repo.ListWikiPages) + }, mustEnableWiki) m.Group("/issues", func() { m.Combo("").Get(repo.ListIssues). Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue) diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go new file mode 100644 index 0000000000..f8969067be --- /dev/null +++ b/routers/api/v1/repo/wiki.go @@ -0,0 +1,514 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web" + wiki_service "code.gitea.io/gitea/services/wiki" +) + +// NewWikiPage response for wiki create request +func NewWikiPage(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/wiki/new repository repoCreateWikiPage + // --- + // summary: Create a wiki page + // consumes: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateWikiPageOptions" + // responses: + // "201": + // "$ref": "#/responses/WikiPage" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + + form := web.GetForm(ctx).(*api.CreateWikiPageOptions) + + if util.IsEmptyString(form.Title) { + ctx.Error(http.StatusBadRequest, "emptyTitle", nil) + return + } + + wikiName := wiki_service.NormalizeWikiName(form.Title) + + if len(form.Message) == 0 { + form.Message = fmt.Sprintf("Add '%s'", form.Title) + } + + content, err := base64.StdEncoding.DecodeString(form.ContentBase64) + if err != nil { + ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err) + return + } + form.ContentBase64 = string(content) + + if err := wiki_service.AddWikiPage(ctx.User, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil { + if models.IsErrWikiReservedName(err) { + ctx.Error(http.StatusBadRequest, "IsErrWikiReservedName", err) + } else if models.IsErrWikiAlreadyExist(err) { + ctx.Error(http.StatusBadRequest, "IsErrWikiAlreadyExists", err) + } else { + ctx.Error(http.StatusInternalServerError, "AddWikiPage", err) + } + return + } + + wikiPage := getWikiPage(ctx, wikiName) + + if !ctx.Written() { + ctx.JSON(http.StatusCreated, wikiPage) + } +} + +// EditWikiPage response for wiki modify request +func EditWikiPage(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo}/wiki/page/{pageName} repository repoEditWikiPage + // --- + // summary: Edit a wiki page + // consumes: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: pageName + // in: path + // description: name of the page + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/CreateWikiPageOptions" + // responses: + // "200": + // "$ref": "#/responses/WikiPage" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + + form := web.GetForm(ctx).(*api.CreateWikiPageOptions) + + oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + newWikiName := wiki_service.NormalizeWikiName(form.Title) + + if len(newWikiName) == 0 { + newWikiName = oldWikiName + } + + if len(form.Message) == 0 { + form.Message = fmt.Sprintf("Update '%s'", newWikiName) + } + + content, err := base64.StdEncoding.DecodeString(form.ContentBase64) + if err != nil { + ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err) + return + } + form.ContentBase64 = string(content) + + if err := wiki_service.EditWikiPage(ctx.User, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil { + ctx.Error(http.StatusInternalServerError, "EditWikiPage", err) + return + } + + wikiPage := getWikiPage(ctx, newWikiName) + + if !ctx.Written() { + ctx.JSON(http.StatusOK, wikiPage) + } +} + +func getWikiPage(ctx *context.APIContext, title string) *api.WikiPage { + title = wiki_service.NormalizeWikiName(title) + + wikiRepo, commit := findWikiRepoCommit(ctx) + if wikiRepo != nil { + defer wikiRepo.Close() + } + if ctx.Written() { + return nil + } + + //lookup filename in wiki - get filecontent, real filename + content, pageFilename := wikiContentsByName(ctx, commit, title, false) + if ctx.Written() { + return nil + } + + sidebarContent, _ := wikiContentsByName(ctx, commit, "_Sidebar", true) + if ctx.Written() { + return nil + } + + footerContent, _ := wikiContentsByName(ctx, commit, "_Footer", true) + if ctx.Written() { + return nil + } + + // get commit count - wiki revisions + commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + + // Get last change information. + lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetCommitByPath", err) + return nil + } + + return &api.WikiPage{ + WikiPageMetaData: convert.ToWikiPageMetaData(title, lastCommit, ctx.Repo.Repository), + ContentBase64: content, + CommitCount: commitsCount, + Sidebar: sidebarContent, + Footer: footerContent, + } +} + +// DeleteWikiPage delete wiki page +func DeleteWikiPage(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/wiki/page/{pageName} repository repoDeleteWikiPage + // --- + // summary: Delete a wiki page + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: pageName + // in: path + // description: name of the page + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + wikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + + if err := wiki_service.DeleteWikiPage(ctx.User, ctx.Repo.Repository, wikiName); err != nil { + if err.Error() == "file does not exist" { + ctx.NotFound(err) + return + } + ctx.Error(http.StatusInternalServerError, "DeleteWikiPage", err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// ListWikiPages get wiki pages list +func ListWikiPages(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/wiki/pages repository repoGetWikiPages + // --- + // summary: Get all wiki pages + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WikiPageList" + // "404": + // "$ref": "#/responses/notFound" + + wikiRepo, commit := findWikiRepoCommit(ctx) + if wikiRepo != nil { + defer wikiRepo.Close() + } + if ctx.Written() { + return + } + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + limit := ctx.FormInt("limit") + if limit <= 1 { + limit = setting.API.DefaultPagingNum + } + + skip := (page - 1) * limit + max := page * limit + + entries, err := commit.ListEntries() + if err != nil { + ctx.ServerError("ListEntries", err) + return + } + pages := make([]*api.WikiPageMetaData, 0, len(entries)) + for i, entry := range entries { + if i < skip || i >= max || !entry.IsRegular() { + continue + } + c, err := wikiRepo.GetCommitByPath(entry.Name()) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetCommit", err) + return + } + wikiName, err := wiki_service.FilenameToName(entry.Name()) + if err != nil { + if models.IsErrWikiInvalidFileName(err) { + continue + } + ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err) + return + } + pages = append(pages, convert.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository)) + } + + ctx.JSON(http.StatusOK, pages) +} + +// GetWikiPage get single wiki page +func GetWikiPage(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/wiki/page/{pageName} repository repoGetWikiPage + // --- + // summary: Get a wiki page + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: pageName + // in: path + // description: name of the page + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WikiPage" + // "404": + // "$ref": "#/responses/notFound" + + // get requested pagename + pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + + wikiPage := getWikiPage(ctx, pageName) + if !ctx.Written() { + ctx.JSON(http.StatusOK, wikiPage) + } +} + +// ListPageRevisions renders file revision list of wiki page +func ListPageRevisions(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/wiki/revisions/{pageName} repository repoGetWikiPageRevisions + // --- + // summary: Get revisions of a wiki page + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: pageName + // in: path + // description: name of the page + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WikiCommitList" + // "404": + // "$ref": "#/responses/notFound" + + wikiRepo, commit := findWikiRepoCommit(ctx) + if wikiRepo != nil { + defer wikiRepo.Close() + } + if ctx.Written() { + return + } + + // get requested pagename + pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + if len(pageName) == 0 { + pageName = "Home" + } + + //lookup filename in wiki - get filecontent, gitTree entry , real filename + _, pageFilename := wikiContentsByName(ctx, commit, pageName, false) + if ctx.Written() { + return + } + + // get commit count - wiki revisions + commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) + + page := ctx.FormInt("page") + if page <= 1 { + page = 1 + } + + // get Commit Count + commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page) + if err != nil { + ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRangeNoFollow", err) + return + } + + ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount)) +} + +// findEntryForFile finds the tree entry for a target filepath. +func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) { + entry, err := commit.GetTreeEntryByPath(target) + if err != nil { + return nil, err + } + if entry != nil { + return entry, nil + } + + // Then the unescaped, shortest alternative + var unescapedTarget string + if unescapedTarget, err = url.QueryUnescape(target); err != nil { + return nil, err + } + return commit.GetTreeEntryByPath(unescapedTarget) +} + +// findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error. +// The caller is responsible for closing the returned repo again +func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { + wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath()) + if err != nil { + + if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { + ctx.NotFound(err) + } else { + ctx.Error(http.StatusInternalServerError, "OpenRepository", err) + } + return nil, nil + } + + commit, err := wikiRepo.GetBranchCommit("master") + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(err) + } else { + ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err) + } + return wikiRepo, nil + } + return wikiRepo, commit +} + +// wikiContentsByEntry returns the contents of the wiki page referenced by the +// given tree entry, encoded with base64. Writes to ctx if an error occurs. +func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { + blob := entry.Blob() + if blob.Size() > setting.API.DefaultMaxBlobSize { + return "" + } + content, err := blob.GetBlobContentBase64() + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetBlobContentBase64", err) + return "" + } + return content +} + +// 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) + + if err != nil { + if git.IsErrNotExist(err) { + if !isSidebarOrFooter { + ctx.NotFound() + } + } else { + ctx.ServerError("findEntryForFile", err) + } + return "", "" + } + return wikiContentsByEntry(ctx, entry), pageFilename +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 3f0c6e2d52..2bd43c6180 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -169,4 +169,7 @@ type swaggerParameterBodies struct { // in:body UserSettingsOptions api.UserSettingsOptions + + // in:body + CreateWikiPageOptions api.CreateWikiPageOptions } diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index ed5fe5169e..40aeca677d 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -323,3 +323,24 @@ type swaggerCombinedStatus struct { // in: body Body api.CombinedStatus `json:"body"` } + +// WikiPageList +// swagger:response WikiPageList +type swaggerWikiPageList struct { + // in:body + Body []api.WikiPageMetaData `json:"body"` +} + +// WikiPage +// swagger:response WikiPage +type swaggerWikiPage struct { + // in:body + Body api.WikiPage `json:"body"` +} + +// WikiCommitList +// swagger:response WikiCommitList +type swaggerWikiCommitList struct { + // in:body + Body api.WikiCommitList `json:"body"` +} |