From 3676fafdacaef7ef0a6b72be99a342f91c8d5e5d Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Mon, 25 Oct 2021 05:43:40 +0200 Subject: [PATCH] Add API to get/edit wiki (#17278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Remove unnecessary check * Fix lint * Use English strings * Update integrations/api_wiki_test.go Co-authored-by: delvh * 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 * 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 Co-authored-by: Norwin Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: zeripath Co-authored-by: wxiaoguang --- integrations/api_wiki_test.go | 251 +++++++++++++++ modules/convert/wiki.go | 60 ++++ modules/structs/repo_wiki.go | 47 +++ routers/api/v1/api.go | 16 + routers/api/v1/repo/wiki.go | 514 ++++++++++++++++++++++++++++++ routers/api/v1/swagger/options.go | 3 + routers/api/v1/swagger/repo.go | 21 ++ templates/swagger/v1_json.tmpl | 425 +++++++++++++++++++++++- 8 files changed, 1336 insertions(+), 1 deletion(-) create mode 100644 integrations/api_wiki_test.go create mode 100644 modules/convert/wiki.go create mode 100644 modules/structs/repo_wiki.go create mode 100644 routers/api/v1/repo/wiki.go diff --git a/integrations/api_wiki_test.go b/integrations/api_wiki_test.go new file mode 100644 index 000000000..3b768b94d --- /dev/null +++ b/integrations/api_wiki_test.go @@ -0,0 +1,251 @@ +// 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 integrations + +import ( + "encoding/base64" + "fmt" + "net/http" + "testing" + + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestAPIGetWikiPage(t *testing.T) { + defer prepareTestEnv(t)() + + username := "user2" + session := loginUser(t, username) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/Home", username, "repo1") + + req := NewRequest(t, "GET", urlStr) + resp := session.MakeRequest(t, req, http.StatusOK) + var page *api.WikiPage + DecodeJSON(t, resp, &page) + + assert.Equal(t, &api.WikiPage{ + WikiPageMetaData: &api.WikiPageMetaData{ + Title: "Home", + HTMLURL: page.HTMLURL, + SubURL: "Home", + LastCommit: &api.WikiCommit{ + ID: "2c54faec6c45d31c1abfaecdab471eac6633738a", + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "Ethan Koenig", + Email: "ethantkoenig@gmail.com", + }, + Date: "2017-11-27T04:31:18Z", + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "Ethan Koenig", + Email: "ethantkoenig@gmail.com", + }, + Date: "2017-11-27T04:31:18Z", + }, + Message: "Add Home.md\n", + }, + }, + ContentBase64: base64.RawStdEncoding.EncodeToString( + []byte("# Home page\n\nThis is the home page!\n"), + ), + CommitCount: 1, + Sidebar: "", + Footer: "", + }, page) +} + +func TestAPIListWikiPages(t *testing.T) { + defer prepareTestEnv(t)() + + username := "user2" + session := loginUser(t, username) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/pages", username, "repo1") + + req := NewRequest(t, "GET", urlStr) + resp := session.MakeRequest(t, req, http.StatusOK) + + var meta []*api.WikiPageMetaData + DecodeJSON(t, resp, &meta) + + dummymeta := []*api.WikiPageMetaData{ + { + Title: "Home", + HTMLURL: meta[0].HTMLURL, + SubURL: "Home", + LastCommit: &api.WikiCommit{ + ID: "2c54faec6c45d31c1abfaecdab471eac6633738a", + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "Ethan Koenig", + Email: "ethantkoenig@gmail.com", + }, + Date: "2017-11-27T04:31:18Z", + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "Ethan Koenig", + Email: "ethantkoenig@gmail.com", + }, + Date: "2017-11-27T04:31:18Z", + }, + Message: "Add Home.md\n", + }, + }, + { + Title: "Page With Image", + HTMLURL: meta[1].HTMLURL, + SubURL: "Page-With-Image", + LastCommit: &api.WikiCommit{ + ID: "0cf15c3f66ec8384480ed9c3cf87c9e97fbb0ec3", + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "Gabriel Silva SimĂ”es", + Email: "simoes.sgabriel@gmail.com", + }, + Date: "2019-01-25T01:41:55Z", + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "Gabriel Silva SimĂ”es", + Email: "simoes.sgabriel@gmail.com", + }, + Date: "2019-01-25T01:41:55Z", + }, + Message: "Add jpeg.jpg and page with image\n", + }, + }, + { + Title: "Page With Spaced Name", + HTMLURL: meta[2].HTMLURL, + SubURL: "Page-With-Spaced-Name", + LastCommit: &api.WikiCommit{ + ID: "c10d10b7e655b3dab1f53176db57c8219a5488d6", + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "Gabriel Silva SimĂ”es", + Email: "simoes.sgabriel@gmail.com", + }, + Date: "2019-01-25T01:39:51Z", + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "Gabriel Silva SimĂ”es", + Email: "simoes.sgabriel@gmail.com", + }, + Date: "2019-01-25T01:39:51Z", + }, + Message: "Add page with spaced name\n", + }, + }, + { + Title: "Unescaped File", + HTMLURL: meta[3].HTMLURL, + SubURL: "Unescaped-File", + LastCommit: &api.WikiCommit{ + ID: "0dca5bd9b5d7ef937710e056f575e86c0184ba85", + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "6543", + Email: "6543@obermui.de", + }, + Date: "2021-07-19T16:42:46Z", + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "6543", + Email: "6543@obermui.de", + }, + Date: "2021-07-19T16:42:46Z", + }, + Message: "add unescaped file\n", + }, + }, + } + + assert.Equal(t, dummymeta, meta) +} + +func TestAPINewWikiPage(t *testing.T) { + for _, title := range []string{ + "New page", + "&&&&", + } { + defer prepareTestEnv(t)() + username := "user2" + session := loginUser(t, username) + token := getTokenForLoggedInUser(t, session) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new?token=%s", username, "repo1", token) + + req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{ + Title: title, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")), + Message: "", + }) + session.MakeRequest(t, req, http.StatusCreated) + } +} + +func TestAPIEditWikiPage(t *testing.T) { + defer prepareTestEnv(t)() + username := "user2" + session := loginUser(t, username) + token := getTokenForLoggedInUser(t, session) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/Page-With-Spaced-Name?token=%s", username, "repo1", token) + + req := NewRequestWithJSON(t, "PATCH", urlStr, &api.CreateWikiPageOptions{ + Title: "edited title", + ContentBase64: base64.StdEncoding.EncodeToString([]byte("Edited wiki page content for API unit tests")), + Message: "", + }) + session.MakeRequest(t, req, http.StatusOK) +} + +func TestAPIListPageRevisions(t *testing.T) { + defer prepareTestEnv(t)() + username := "user2" + session := loginUser(t, username) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/revisions/Home", username, "repo1") + + req := NewRequest(t, "GET", urlStr) + resp := session.MakeRequest(t, req, http.StatusOK) + + var revisions *api.WikiCommitList + DecodeJSON(t, resp, &revisions) + + dummyrevisions := &api.WikiCommitList{ + WikiCommits: []*api.WikiCommit{ + { + ID: "2c54faec6c45d31c1abfaecdab471eac6633738a", + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "Ethan Koenig", + Email: "ethantkoenig@gmail.com", + }, + Date: "2017-11-27T04:31:18Z", + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "Ethan Koenig", + Email: "ethantkoenig@gmail.com", + }, + Date: "2017-11-27T04:31:18Z", + }, + Message: "Add Home.md\n", + }, + }, + Count: 1, + } + + assert.Equal(t, dummyrevisions, revisions) +} diff --git a/modules/convert/wiki.go b/modules/convert/wiki.go new file mode 100644 index 000000000..9563f1544 --- /dev/null +++ b/modules/convert/wiki.go @@ -0,0 +1,60 @@ +// 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 convert + +import ( + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + wiki_service "code.gitea.io/gitea/services/wiki" +) + +// ToWikiCommit convert a git commit into a WikiCommit +func ToWikiCommit(commit *git.Commit) *api.WikiCommit { + return &api.WikiCommit{ + ID: commit.ID.String(), + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Author.Name, + Email: commit.Author.Email, + }, + Date: commit.Author.When.UTC().Format(time.RFC3339), + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: commit.Committer.Name, + Email: commit.Committer.Email, + }, + Date: commit.Committer.When.UTC().Format(time.RFC3339), + }, + Message: commit.CommitMessage, + } +} + +// ToWikiCommitList convert a list of git commits into a WikiCommitList +func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList { + result := make([]*api.WikiCommit, len(commits)) + for i := range commits { + result[i] = ToWikiCommit(commits[i]) + } + return &api.WikiCommitList{ + WikiCommits: result, + Count: total, + } +} + +// ToWikiPageMetaData converts meta information to a WikiPageMetaData +func ToWikiPageMetaData(title string, lastCommit *git.Commit, repo *models.Repository) *api.WikiPageMetaData { + suburl := wiki_service.NameToSubURL(title) + return &api.WikiPageMetaData{ + Title: title, + HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", suburl), + SubURL: suburl, + LastCommit: ToWikiCommit(lastCommit), + } +} diff --git a/modules/structs/repo_wiki.go b/modules/structs/repo_wiki.go new file mode 100644 index 000000000..09e21387d --- /dev/null +++ b/modules/structs/repo_wiki.go @@ -0,0 +1,47 @@ +// 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 structs + +// WikiCommit page commit/revision +type WikiCommit struct { + ID string `json:"sha"` + Author *CommitUser `json:"author"` + Committer *CommitUser `json:"commiter"` + Message string `json:"message"` +} + +// WikiPage a wiki page +type WikiPage struct { + *WikiPageMetaData + // Page content, base64 encoded + ContentBase64 string `json:"content_base64"` + CommitCount int64 `json:"commit_count"` + Sidebar string `json:"sidebar"` + Footer string `json:"footer"` +} + +// WikiPageMetaData wiki page meta information +type WikiPageMetaData struct { + Title string `json:"title"` + HTMLURL string `json:"html_url"` + SubURL string `json:"sub_url"` + LastCommit *WikiCommit `json:"last_commit"` +} + +// CreateWikiPageOptions form for creating wiki +type CreateWikiPageOptions struct { + // page title. leave empty to keep unchanged + Title string `json:"title"` + // content must be base64 encoded + ContentBase64 string `json:"content_base64"` + // optional commit message summarizing the change + Message string `json:"message"` +} + +// WikiCommitList commit/revision list +type WikiCommitList struct { + WikiCommits []*WikiCommit `json:"commits"` + Count int64 `json:"count"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index d915b76f7..70d7cb40f 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 000000000..f8969067b --- /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 3f0c6e2d5..2bd43c618 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 ed5fe5169..40aeca677 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"` +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index afb93c50f..6bbc09348 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -9889,6 +9889,284 @@ } } }, + "/repos/{owner}/{repo}/wiki/new": { + "post": { + "consumes": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a wiki page", + "operationId": "repoCreateWikiPage", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateWikiPageOptions" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/WikiPage" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, + "/repos/{owner}/{repo}/wiki/page/{pageName}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a wiki page", + "operationId": "repoGetWikiPage", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the page", + "name": "pageName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/WikiPage" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "tags": [ + "repository" + ], + "summary": "Delete a wiki page", + "operationId": "repoDeleteWikiPage", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the page", + "name": "pageName", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Edit a wiki page", + "operationId": "repoEditWikiPage", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the page", + "name": "pageName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateWikiPageOptions" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/WikiPage" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } + } + }, + "/repos/{owner}/{repo}/wiki/pages": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get all wiki pages", + "operationId": "repoGetWikiPages", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WikiPageList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/wiki/revisions/{pageName}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get revisions of a wiki page", + "operationId": "repoGetWikiPageRevisions", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the page", + "name": "pageName", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WikiCommitList" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{template_owner}/{template_repo}/generate": { "post": { "consumes": [ @@ -13666,6 +13944,28 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateWikiPageOptions": { + "description": "CreateWikiPageOptions form for creating wiki", + "type": "object", + "properties": { + "content_base64": { + "description": "content must be base64 encoded", + "type": "string", + "x-go-name": "ContentBase64" + }, + "message": { + "description": "optional commit message summarizing the change", + "type": "string", + "x-go-name": "Message" + }, + "title": { + "description": "page title. leave empty to keep unchanged", + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Cron": { "description": "Cron represents a Cron task", "type": "object", @@ -17376,6 +17676,108 @@ } }, "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "WikiCommit": { + "description": "WikiCommit page commit/revision", + "type": "object", + "properties": { + "author": { + "$ref": "#/definitions/CommitUser" + }, + "commiter": { + "$ref": "#/definitions/CommitUser" + }, + "message": { + "type": "string", + "x-go-name": "Message" + }, + "sha": { + "type": "string", + "x-go-name": "ID" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "WikiCommitList": { + "description": "WikiCommitList commit/revision list", + "type": "object", + "properties": { + "commits": { + "type": "array", + "items": { + "$ref": "#/definitions/WikiCommit" + }, + "x-go-name": "WikiCommits" + }, + "count": { + "type": "integer", + "format": "int64", + "x-go-name": "Count" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "WikiPage": { + "description": "WikiPage a wiki page", + "type": "object", + "properties": { + "commit_count": { + "type": "integer", + "format": "int64", + "x-go-name": "CommitCount" + }, + "content_base64": { + "description": "Page content, base64 encoded", + "type": "string", + "x-go-name": "ContentBase64" + }, + "footer": { + "type": "string", + "x-go-name": "Footer" + }, + "html_url": { + "type": "string", + "x-go-name": "HTMLURL" + }, + "last_commit": { + "$ref": "#/definitions/WikiCommit" + }, + "sidebar": { + "type": "string", + "x-go-name": "Sidebar" + }, + "sub_url": { + "type": "string", + "x-go-name": "SubURL" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "WikiPageMetaData": { + "description": "WikiPageMetaData wiki page meta information", + "type": "object", + "properties": { + "html_url": { + "type": "string", + "x-go-name": "HTMLURL" + }, + "last_commit": { + "$ref": "#/definitions/WikiCommit" + }, + "sub_url": { + "type": "string", + "x-go-name": "SubURL" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" } }, "responses": { @@ -18069,6 +18471,27 @@ "$ref": "#/definitions/WatchInfo" } }, + "WikiCommitList": { + "description": "WikiCommitList", + "schema": { + "$ref": "#/definitions/WikiCommitList" + } + }, + "WikiPage": { + "description": "WikiPage", + "schema": { + "$ref": "#/definitions/WikiPage" + } + }, + "WikiPageList": { + "description": "WikiPageList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/WikiPageMetaData" + } + } + }, "conflict": { "description": "APIConflict is a conflict empty response" }, @@ -18117,7 +18540,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/UserSettingsOptions" + "$ref": "#/definitions/CreateWikiPageOptions" } }, "redirect": {