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 }