code.gitea.io/gitea@v1.22.3/routers/api/v1/repo/wiki.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  
    12  	repo_model "code.gitea.io/gitea/models/repo"
    13  	"code.gitea.io/gitea/modules/git"
    14  	"code.gitea.io/gitea/modules/gitrepo"
    15  	"code.gitea.io/gitea/modules/setting"
    16  	api "code.gitea.io/gitea/modules/structs"
    17  	"code.gitea.io/gitea/modules/util"
    18  	"code.gitea.io/gitea/modules/web"
    19  	"code.gitea.io/gitea/services/context"
    20  	"code.gitea.io/gitea/services/convert"
    21  	notify_service "code.gitea.io/gitea/services/notify"
    22  	wiki_service "code.gitea.io/gitea/services/wiki"
    23  )
    24  
    25  // NewWikiPage response for wiki create request
    26  func NewWikiPage(ctx *context.APIContext) {
    27  	// swagger:operation POST /repos/{owner}/{repo}/wiki/new repository repoCreateWikiPage
    28  	// ---
    29  	// summary: Create a wiki page
    30  	// consumes:
    31  	// - application/json
    32  	// parameters:
    33  	// - name: owner
    34  	//   in: path
    35  	//   description: owner of the repo
    36  	//   type: string
    37  	//   required: true
    38  	// - name: repo
    39  	//   in: path
    40  	//   description: name of the repo
    41  	//   type: string
    42  	//   required: true
    43  	// - name: body
    44  	//   in: body
    45  	//   schema:
    46  	//     "$ref": "#/definitions/CreateWikiPageOptions"
    47  	// responses:
    48  	//   "201":
    49  	//     "$ref": "#/responses/WikiPage"
    50  	//   "400":
    51  	//     "$ref": "#/responses/error"
    52  	//   "403":
    53  	//     "$ref": "#/responses/forbidden"
    54  	//   "404":
    55  	//     "$ref": "#/responses/notFound"
    56  	//   "423":
    57  	//     "$ref": "#/responses/repoArchivedError"
    58  
    59  	form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
    60  
    61  	if util.IsEmptyString(form.Title) {
    62  		ctx.Error(http.StatusBadRequest, "emptyTitle", nil)
    63  		return
    64  	}
    65  
    66  	wikiName := wiki_service.UserTitleToWebPath("", form.Title)
    67  
    68  	if len(form.Message) == 0 {
    69  		form.Message = fmt.Sprintf("Add %q", form.Title)
    70  	}
    71  
    72  	content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
    73  	if err != nil {
    74  		ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err)
    75  		return
    76  	}
    77  	form.ContentBase64 = string(content)
    78  
    79  	if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.ContentBase64, form.Message); err != nil {
    80  		if repo_model.IsErrWikiReservedName(err) {
    81  			ctx.Error(http.StatusBadRequest, "IsErrWikiReservedName", err)
    82  		} else if repo_model.IsErrWikiAlreadyExist(err) {
    83  			ctx.Error(http.StatusBadRequest, "IsErrWikiAlreadyExists", err)
    84  		} else {
    85  			ctx.Error(http.StatusInternalServerError, "AddWikiPage", err)
    86  		}
    87  		return
    88  	}
    89  
    90  	wikiPage := getWikiPage(ctx, wikiName)
    91  
    92  	if !ctx.Written() {
    93  		notify_service.NewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName), form.Message)
    94  		ctx.JSON(http.StatusCreated, wikiPage)
    95  	}
    96  }
    97  
    98  // EditWikiPage response for wiki modify request
    99  func EditWikiPage(ctx *context.APIContext) {
   100  	// swagger:operation PATCH /repos/{owner}/{repo}/wiki/page/{pageName} repository repoEditWikiPage
   101  	// ---
   102  	// summary: Edit a wiki page
   103  	// consumes:
   104  	// - application/json
   105  	// parameters:
   106  	// - name: owner
   107  	//   in: path
   108  	//   description: owner of the repo
   109  	//   type: string
   110  	//   required: true
   111  	// - name: repo
   112  	//   in: path
   113  	//   description: name of the repo
   114  	//   type: string
   115  	//   required: true
   116  	// - name: pageName
   117  	//   in: path
   118  	//   description: name of the page
   119  	//   type: string
   120  	//   required: true
   121  	// - name: body
   122  	//   in: body
   123  	//   schema:
   124  	//     "$ref": "#/definitions/CreateWikiPageOptions"
   125  	// responses:
   126  	//   "200":
   127  	//     "$ref": "#/responses/WikiPage"
   128  	//   "400":
   129  	//     "$ref": "#/responses/error"
   130  	//   "403":
   131  	//     "$ref": "#/responses/forbidden"
   132  	//   "404":
   133  	//     "$ref": "#/responses/notFound"
   134  	//   "423":
   135  	//     "$ref": "#/responses/repoArchivedError"
   136  
   137  	form := web.GetForm(ctx).(*api.CreateWikiPageOptions)
   138  
   139  	oldWikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
   140  	newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
   141  
   142  	if len(newWikiName) == 0 {
   143  		newWikiName = oldWikiName
   144  	}
   145  
   146  	if len(form.Message) == 0 {
   147  		form.Message = fmt.Sprintf("Update %q", newWikiName)
   148  	}
   149  
   150  	content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
   151  	if err != nil {
   152  		ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err)
   153  		return
   154  	}
   155  	form.ContentBase64 = string(content)
   156  
   157  	if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil {
   158  		ctx.Error(http.StatusInternalServerError, "EditWikiPage", err)
   159  		return
   160  	}
   161  
   162  	wikiPage := getWikiPage(ctx, newWikiName)
   163  
   164  	if !ctx.Written() {
   165  		notify_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message)
   166  		ctx.JSON(http.StatusOK, wikiPage)
   167  	}
   168  }
   169  
   170  func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.WikiPage {
   171  	wikiRepo, commit := findWikiRepoCommit(ctx)
   172  	if wikiRepo != nil {
   173  		defer wikiRepo.Close()
   174  	}
   175  	if ctx.Written() {
   176  		return nil
   177  	}
   178  
   179  	// lookup filename in wiki - get filecontent, real filename
   180  	content, pageFilename := wikiContentsByName(ctx, commit, wikiName, false)
   181  	if ctx.Written() {
   182  		return nil
   183  	}
   184  
   185  	sidebarContent, _ := wikiContentsByName(ctx, commit, "_Sidebar", true)
   186  	if ctx.Written() {
   187  		return nil
   188  	}
   189  
   190  	footerContent, _ := wikiContentsByName(ctx, commit, "_Footer", true)
   191  	if ctx.Written() {
   192  		return nil
   193  	}
   194  
   195  	// get commit count - wiki revisions
   196  	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
   197  
   198  	// Get last change information.
   199  	lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
   200  	if err != nil {
   201  		ctx.Error(http.StatusInternalServerError, "GetCommitByPath", err)
   202  		return nil
   203  	}
   204  
   205  	return &api.WikiPage{
   206  		WikiPageMetaData: wiki_service.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository),
   207  		ContentBase64:    content,
   208  		CommitCount:      commitsCount,
   209  		Sidebar:          sidebarContent,
   210  		Footer:           footerContent,
   211  	}
   212  }
   213  
   214  // DeleteWikiPage delete wiki page
   215  func DeleteWikiPage(ctx *context.APIContext) {
   216  	// swagger:operation DELETE /repos/{owner}/{repo}/wiki/page/{pageName} repository repoDeleteWikiPage
   217  	// ---
   218  	// summary: Delete a wiki page
   219  	// parameters:
   220  	// - name: owner
   221  	//   in: path
   222  	//   description: owner of the repo
   223  	//   type: string
   224  	//   required: true
   225  	// - name: repo
   226  	//   in: path
   227  	//   description: name of the repo
   228  	//   type: string
   229  	//   required: true
   230  	// - name: pageName
   231  	//   in: path
   232  	//   description: name of the page
   233  	//   type: string
   234  	//   required: true
   235  	// responses:
   236  	//   "204":
   237  	//     "$ref": "#/responses/empty"
   238  	//   "403":
   239  	//     "$ref": "#/responses/forbidden"
   240  	//   "404":
   241  	//     "$ref": "#/responses/notFound"
   242  	//   "423":
   243  	//     "$ref": "#/responses/repoArchivedError"
   244  
   245  	wikiName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
   246  
   247  	if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
   248  		if err.Error() == "file does not exist" {
   249  			ctx.NotFound(err)
   250  			return
   251  		}
   252  		ctx.Error(http.StatusInternalServerError, "DeleteWikiPage", err)
   253  		return
   254  	}
   255  
   256  	notify_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName))
   257  
   258  	ctx.Status(http.StatusNoContent)
   259  }
   260  
   261  // ListWikiPages get wiki pages list
   262  func ListWikiPages(ctx *context.APIContext) {
   263  	// swagger:operation GET /repos/{owner}/{repo}/wiki/pages repository repoGetWikiPages
   264  	// ---
   265  	// summary: Get all wiki pages
   266  	// produces:
   267  	// - application/json
   268  	// parameters:
   269  	// - name: owner
   270  	//   in: path
   271  	//   description: owner of the repo
   272  	//   type: string
   273  	//   required: true
   274  	// - name: repo
   275  	//   in: path
   276  	//   description: name of the repo
   277  	//   type: string
   278  	//   required: true
   279  	// - name: page
   280  	//   in: query
   281  	//   description: page number of results to return (1-based)
   282  	//   type: integer
   283  	// - name: limit
   284  	//   in: query
   285  	//   description: page size of results
   286  	//   type: integer
   287  	// responses:
   288  	//   "200":
   289  	//     "$ref": "#/responses/WikiPageList"
   290  	//   "404":
   291  	//     "$ref": "#/responses/notFound"
   292  
   293  	wikiRepo, commit := findWikiRepoCommit(ctx)
   294  	if wikiRepo != nil {
   295  		defer wikiRepo.Close()
   296  	}
   297  	if ctx.Written() {
   298  		return
   299  	}
   300  
   301  	page := ctx.FormInt("page")
   302  	if page <= 1 {
   303  		page = 1
   304  	}
   305  	limit := ctx.FormInt("limit")
   306  	if limit <= 1 {
   307  		limit = setting.API.DefaultPagingNum
   308  	}
   309  
   310  	skip := (page - 1) * limit
   311  	max := page * limit
   312  
   313  	entries, err := commit.ListEntries()
   314  	if err != nil {
   315  		ctx.ServerError("ListEntries", err)
   316  		return
   317  	}
   318  	pages := make([]*api.WikiPageMetaData, 0, len(entries))
   319  	for i, entry := range entries {
   320  		if i < skip || i >= max || !entry.IsRegular() {
   321  			continue
   322  		}
   323  		c, err := wikiRepo.GetCommitByPath(entry.Name())
   324  		if err != nil {
   325  			ctx.Error(http.StatusInternalServerError, "GetCommit", err)
   326  			return
   327  		}
   328  		wikiName, err := wiki_service.GitPathToWebPath(entry.Name())
   329  		if err != nil {
   330  			if repo_model.IsErrWikiInvalidFileName(err) {
   331  				continue
   332  			}
   333  			ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err)
   334  			return
   335  		}
   336  		pages = append(pages, wiki_service.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
   337  	}
   338  
   339  	ctx.SetTotalCountHeader(int64(len(entries)))
   340  	ctx.JSON(http.StatusOK, pages)
   341  }
   342  
   343  // GetWikiPage get single wiki page
   344  func GetWikiPage(ctx *context.APIContext) {
   345  	// swagger:operation GET /repos/{owner}/{repo}/wiki/page/{pageName} repository repoGetWikiPage
   346  	// ---
   347  	// summary: Get a wiki page
   348  	// produces:
   349  	// - application/json
   350  	// parameters:
   351  	// - name: owner
   352  	//   in: path
   353  	//   description: owner of the repo
   354  	//   type: string
   355  	//   required: true
   356  	// - name: repo
   357  	//   in: path
   358  	//   description: name of the repo
   359  	//   type: string
   360  	//   required: true
   361  	// - name: pageName
   362  	//   in: path
   363  	//   description: name of the page
   364  	//   type: string
   365  	//   required: true
   366  	// responses:
   367  	//   "200":
   368  	//     "$ref": "#/responses/WikiPage"
   369  	//   "404":
   370  	//     "$ref": "#/responses/notFound"
   371  
   372  	// get requested pagename
   373  	pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
   374  
   375  	wikiPage := getWikiPage(ctx, pageName)
   376  	if !ctx.Written() {
   377  		ctx.JSON(http.StatusOK, wikiPage)
   378  	}
   379  }
   380  
   381  // ListPageRevisions renders file revision list of wiki page
   382  func ListPageRevisions(ctx *context.APIContext) {
   383  	// swagger:operation GET /repos/{owner}/{repo}/wiki/revisions/{pageName} repository repoGetWikiPageRevisions
   384  	// ---
   385  	// summary: Get revisions of a wiki page
   386  	// produces:
   387  	// - application/json
   388  	// parameters:
   389  	// - name: owner
   390  	//   in: path
   391  	//   description: owner of the repo
   392  	//   type: string
   393  	//   required: true
   394  	// - name: repo
   395  	//   in: path
   396  	//   description: name of the repo
   397  	//   type: string
   398  	//   required: true
   399  	// - name: pageName
   400  	//   in: path
   401  	//   description: name of the page
   402  	//   type: string
   403  	//   required: true
   404  	// - name: page
   405  	//   in: query
   406  	//   description: page number of results to return (1-based)
   407  	//   type: integer
   408  	// responses:
   409  	//   "200":
   410  	//     "$ref": "#/responses/WikiCommitList"
   411  	//   "404":
   412  	//     "$ref": "#/responses/notFound"
   413  
   414  	wikiRepo, commit := findWikiRepoCommit(ctx)
   415  	if wikiRepo != nil {
   416  		defer wikiRepo.Close()
   417  	}
   418  	if ctx.Written() {
   419  		return
   420  	}
   421  
   422  	// get requested pagename
   423  	pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw(":pageName"))
   424  	if len(pageName) == 0 {
   425  		pageName = "Home"
   426  	}
   427  
   428  	// lookup filename in wiki - get filecontent, gitTree entry , real filename
   429  	_, pageFilename := wikiContentsByName(ctx, commit, pageName, false)
   430  	if ctx.Written() {
   431  		return
   432  	}
   433  
   434  	// get commit count - wiki revisions
   435  	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
   436  
   437  	page := ctx.FormInt("page")
   438  	if page <= 1 {
   439  		page = 1
   440  	}
   441  
   442  	// get Commit Count
   443  	commitsHistory, err := wikiRepo.CommitsByFileAndRange(
   444  		git.CommitsByFileAndRangeOptions{
   445  			Revision: "master",
   446  			File:     pageFilename,
   447  			Page:     page,
   448  		})
   449  	if err != nil {
   450  		ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRange", err)
   451  		return
   452  	}
   453  
   454  	ctx.SetTotalCountHeader(commitsCount)
   455  	ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
   456  }
   457  
   458  // findEntryForFile finds the tree entry for a target filepath.
   459  func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
   460  	entry, err := commit.GetTreeEntryByPath(target)
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	if entry != nil {
   465  		return entry, nil
   466  	}
   467  
   468  	// Then the unescaped, shortest alternative
   469  	var unescapedTarget string
   470  	if unescapedTarget, err = url.QueryUnescape(target); err != nil {
   471  		return nil, err
   472  	}
   473  	return commit.GetTreeEntryByPath(unescapedTarget)
   474  }
   475  
   476  // findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error.
   477  // The caller is responsible for closing the returned repo again
   478  func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) {
   479  	wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
   480  	if err != nil {
   481  		if git.IsErrNotExist(err) || err.Error() == "no such file or directory" {
   482  			ctx.NotFound(err)
   483  		} else {
   484  			ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
   485  		}
   486  		return nil, nil
   487  	}
   488  
   489  	commit, err := wikiRepo.GetBranchCommit("master")
   490  	if err != nil {
   491  		if git.IsErrNotExist(err) {
   492  			ctx.NotFound(err)
   493  		} else {
   494  			ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
   495  		}
   496  		return wikiRepo, nil
   497  	}
   498  	return wikiRepo, commit
   499  }
   500  
   501  // wikiContentsByEntry returns the contents of the wiki page referenced by the
   502  // given tree entry, encoded with base64. Writes to ctx if an error occurs.
   503  func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
   504  	blob := entry.Blob()
   505  	if blob.Size() > setting.API.DefaultMaxBlobSize {
   506  		return ""
   507  	}
   508  	content, err := blob.GetBlobContentBase64()
   509  	if err != nil {
   510  		ctx.Error(http.StatusInternalServerError, "GetBlobContentBase64", err)
   511  		return ""
   512  	}
   513  	return content
   514  }
   515  
   516  // wikiContentsByName returns the contents of a wiki page, along with a boolean
   517  // indicating whether the page exists. Writes to ctx if an error occurs.
   518  func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) {
   519  	gitFilename := wiki_service.WebPathToGitPath(wikiName)
   520  	entry, err := findEntryForFile(commit, gitFilename)
   521  	if err != nil {
   522  		if git.IsErrNotExist(err) {
   523  			if !isSidebarOrFooter {
   524  				ctx.NotFound()
   525  			}
   526  		} else {
   527  			ctx.ServerError("findEntryForFile", err)
   528  		}
   529  		return "", ""
   530  	}
   531  	return wikiContentsByEntry(ctx, entry), gitFilename
   532  }