You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

wiki.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "fmt"
  8. "io/ioutil"
  9. "path/filepath"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/auth"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/context"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/markup"
  18. "code.gitea.io/gitea/modules/markup/markdown"
  19. "code.gitea.io/gitea/modules/timeutil"
  20. "code.gitea.io/gitea/modules/util"
  21. )
  22. const (
  23. tplWikiStart base.TplName = "repo/wiki/start"
  24. tplWikiView base.TplName = "repo/wiki/view"
  25. tplWikiRevision base.TplName = "repo/wiki/revision"
  26. tplWikiNew base.TplName = "repo/wiki/new"
  27. tplWikiPages base.TplName = "repo/wiki/pages"
  28. )
  29. // MustEnableWiki check if wiki is enabled, if external then redirect
  30. func MustEnableWiki(ctx *context.Context) {
  31. if !ctx.Repo.CanRead(models.UnitTypeWiki) &&
  32. !ctx.Repo.CanRead(models.UnitTypeExternalWiki) {
  33. if log.IsTrace() {
  34. log.Trace("Permission Denied: User %-v cannot read %-v or %-v of repo %-v\n"+
  35. "User in repo has Permissions: %-+v",
  36. ctx.User,
  37. models.UnitTypeWiki,
  38. models.UnitTypeExternalWiki,
  39. ctx.Repo.Repository,
  40. ctx.Repo.Permission)
  41. }
  42. ctx.NotFound("MustEnableWiki", nil)
  43. return
  44. }
  45. unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalWiki)
  46. if err == nil {
  47. ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
  48. return
  49. }
  50. }
  51. // PageMeta wiki page meat information
  52. type PageMeta struct {
  53. Name string
  54. SubURL string
  55. UpdatedUnix timeutil.TimeStamp
  56. }
  57. // findEntryForFile finds the tree entry for a target filepath.
  58. func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
  59. entries, err := commit.ListEntries()
  60. if err != nil {
  61. return nil, err
  62. }
  63. for _, entry := range entries {
  64. if entry.IsRegular() && entry.Name() == target {
  65. return entry, nil
  66. }
  67. }
  68. return nil, nil
  69. }
  70. func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
  71. wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath())
  72. if err != nil {
  73. ctx.ServerError("OpenRepository", err)
  74. return nil, nil, err
  75. }
  76. commit, err := wikiRepo.GetBranchCommit("master")
  77. if err != nil {
  78. return wikiRepo, nil, err
  79. }
  80. return wikiRepo, commit, nil
  81. }
  82. // wikiContentsByEntry returns the contents of the wiki page referenced by the
  83. // given tree entry. Writes to ctx if an error occurs.
  84. func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
  85. reader, err := entry.Blob().DataAsync()
  86. if err != nil {
  87. ctx.ServerError("Blob.Data", err)
  88. return nil
  89. }
  90. defer reader.Close()
  91. content, err := ioutil.ReadAll(reader)
  92. if err != nil {
  93. ctx.ServerError("ReadAll", err)
  94. return nil
  95. }
  96. return content
  97. }
  98. // wikiContentsByName returns the contents of a wiki page, along with a boolean
  99. // indicating whether the page exists. Writes to ctx if an error occurs.
  100. func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) {
  101. var entry *git.TreeEntry
  102. var err error
  103. pageFilename := models.WikiNameToFilename(wikiName)
  104. if entry, err = findEntryForFile(commit, pageFilename); err != nil {
  105. ctx.ServerError("findEntryForFile", err)
  106. return nil, nil, "", false
  107. } else if entry == nil {
  108. return nil, nil, "", true
  109. }
  110. return wikiContentsByEntry(ctx, entry), entry, pageFilename, false
  111. }
  112. func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
  113. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  114. if err != nil {
  115. if !git.IsErrNotExist(err) {
  116. ctx.ServerError("GetBranchCommit", err)
  117. }
  118. return nil, nil
  119. }
  120. // Get page list.
  121. entries, err := commit.ListEntries()
  122. if err != nil {
  123. ctx.ServerError("ListEntries", err)
  124. return nil, nil
  125. }
  126. pages := make([]PageMeta, 0, len(entries))
  127. for _, entry := range entries {
  128. if !entry.IsRegular() {
  129. continue
  130. }
  131. wikiName, err := models.WikiFilenameToName(entry.Name())
  132. if err != nil {
  133. if models.IsErrWikiInvalidFileName(err) {
  134. continue
  135. }
  136. ctx.ServerError("WikiFilenameToName", err)
  137. return nil, nil
  138. } else if wikiName == "_Sidebar" || wikiName == "_Footer" {
  139. continue
  140. }
  141. pages = append(pages, PageMeta{
  142. Name: wikiName,
  143. SubURL: models.WikiNameToSubURL(wikiName),
  144. })
  145. }
  146. ctx.Data["Pages"] = pages
  147. // get requested pagename
  148. pageName := models.NormalizeWikiName(ctx.Params(":page"))
  149. if len(pageName) == 0 {
  150. pageName = "Home"
  151. }
  152. ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
  153. ctx.Data["old_title"] = pageName
  154. ctx.Data["Title"] = pageName
  155. ctx.Data["title"] = pageName
  156. ctx.Data["RequireHighlightJS"] = true
  157. //lookup filename in wiki - get filecontent, gitTree entry , real filename
  158. data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
  159. if noEntry {
  160. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
  161. }
  162. if entry == nil || ctx.Written() {
  163. return nil, nil
  164. }
  165. sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
  166. if ctx.Written() {
  167. return nil, nil
  168. }
  169. footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
  170. if ctx.Written() {
  171. return nil, nil
  172. }
  173. metas := ctx.Repo.Repository.ComposeMetas()
  174. ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas)
  175. ctx.Data["sidebarPresent"] = sidebarContent != nil
  176. ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas)
  177. ctx.Data["footerPresent"] = footerContent != nil
  178. ctx.Data["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas)
  179. // get commit count - wiki revisions
  180. commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
  181. ctx.Data["CommitCount"] = commitsCount
  182. return wikiRepo, entry
  183. }
  184. func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
  185. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  186. if err != nil {
  187. if !git.IsErrNotExist(err) {
  188. ctx.ServerError("GetBranchCommit", err)
  189. }
  190. return nil, nil
  191. }
  192. // get requested pagename
  193. pageName := models.NormalizeWikiName(ctx.Params(":page"))
  194. if len(pageName) == 0 {
  195. pageName = "Home"
  196. }
  197. ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
  198. ctx.Data["old_title"] = pageName
  199. ctx.Data["Title"] = pageName
  200. ctx.Data["title"] = pageName
  201. ctx.Data["RequireHighlightJS"] = true
  202. //lookup filename in wiki - get filecontent, gitTree entry , real filename
  203. data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
  204. if noEntry {
  205. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
  206. }
  207. if entry == nil || ctx.Written() {
  208. return nil, nil
  209. }
  210. ctx.Data["content"] = string(data)
  211. ctx.Data["sidebarPresent"] = false
  212. ctx.Data["sidebarContent"] = ""
  213. ctx.Data["footerPresent"] = false
  214. ctx.Data["footerContent"] = ""
  215. // get commit count - wiki revisions
  216. commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
  217. ctx.Data["CommitCount"] = commitsCount
  218. // get page
  219. page := ctx.QueryInt("page")
  220. if page <= 1 {
  221. page = 1
  222. }
  223. // get Commit Count
  224. commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page)
  225. if err != nil {
  226. ctx.ServerError("CommitsByFileAndRangeNoFollow", err)
  227. return nil, nil
  228. }
  229. commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
  230. commitsHistory = models.ParseCommitsWithSignature(commitsHistory)
  231. ctx.Data["Commits"] = commitsHistory
  232. pager := context.NewPagination(int(commitsCount), git.CommitsRangeSize, page, 5)
  233. pager.SetDefaultParams(ctx)
  234. ctx.Data["Page"] = pager
  235. return wikiRepo, entry
  236. }
  237. func renderEditPage(ctx *context.Context) {
  238. _, commit, err := findWikiRepoCommit(ctx)
  239. if err != nil {
  240. if !git.IsErrNotExist(err) {
  241. ctx.ServerError("GetBranchCommit", err)
  242. }
  243. return
  244. }
  245. // get requested pagename
  246. pageName := models.NormalizeWikiName(ctx.Params(":page"))
  247. if len(pageName) == 0 {
  248. pageName = "Home"
  249. }
  250. ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
  251. ctx.Data["old_title"] = pageName
  252. ctx.Data["Title"] = pageName
  253. ctx.Data["title"] = pageName
  254. ctx.Data["RequireHighlightJS"] = true
  255. //lookup filename in wiki - get filecontent, gitTree entry , real filename
  256. data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName)
  257. if noEntry {
  258. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
  259. }
  260. if entry == nil || ctx.Written() {
  261. return
  262. }
  263. ctx.Data["content"] = string(data)
  264. ctx.Data["sidebarPresent"] = false
  265. ctx.Data["sidebarContent"] = ""
  266. ctx.Data["footerPresent"] = false
  267. ctx.Data["footerContent"] = ""
  268. }
  269. // Wiki renders single wiki page
  270. func Wiki(ctx *context.Context) {
  271. ctx.Data["PageIsWiki"] = true
  272. ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
  273. if !ctx.Repo.Repository.HasWiki() {
  274. ctx.Data["Title"] = ctx.Tr("repo.wiki")
  275. ctx.HTML(200, tplWikiStart)
  276. return
  277. }
  278. wikiRepo, entry := renderViewPage(ctx)
  279. if ctx.Written() {
  280. return
  281. }
  282. if entry == nil {
  283. ctx.Data["Title"] = ctx.Tr("repo.wiki")
  284. ctx.HTML(200, tplWikiStart)
  285. return
  286. }
  287. wikiPath := entry.Name()
  288. if markup.Type(wikiPath) != markdown.MarkupName {
  289. ext := strings.ToUpper(filepath.Ext(wikiPath))
  290. ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
  291. }
  292. // Get last change information.
  293. lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
  294. if err != nil {
  295. ctx.ServerError("GetCommitByPath", err)
  296. return
  297. }
  298. ctx.Data["Author"] = lastCommit.Author
  299. ctx.HTML(200, tplWikiView)
  300. }
  301. // WikiRevision renders file revision list of wiki page
  302. func WikiRevision(ctx *context.Context) {
  303. ctx.Data["PageIsWiki"] = true
  304. ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
  305. if !ctx.Repo.Repository.HasWiki() {
  306. ctx.Data["Title"] = ctx.Tr("repo.wiki")
  307. ctx.HTML(200, tplWikiStart)
  308. return
  309. }
  310. wikiRepo, entry := renderRevisionPage(ctx)
  311. if ctx.Written() {
  312. return
  313. }
  314. if entry == nil {
  315. ctx.Data["Title"] = ctx.Tr("repo.wiki")
  316. ctx.HTML(200, tplWikiStart)
  317. return
  318. }
  319. // Get last change information.
  320. wikiPath := entry.Name()
  321. lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
  322. if err != nil {
  323. ctx.ServerError("GetCommitByPath", err)
  324. return
  325. }
  326. ctx.Data["Author"] = lastCommit.Author
  327. ctx.HTML(200, tplWikiRevision)
  328. }
  329. // WikiPages render wiki pages list page
  330. func WikiPages(ctx *context.Context) {
  331. if !ctx.Repo.Repository.HasWiki() {
  332. ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
  333. return
  334. }
  335. ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
  336. ctx.Data["PageIsWiki"] = true
  337. ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(models.UnitTypeWiki) && !ctx.Repo.Repository.IsArchived
  338. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  339. if err != nil {
  340. return
  341. }
  342. entries, err := commit.ListEntries()
  343. if err != nil {
  344. ctx.ServerError("ListEntries", err)
  345. return
  346. }
  347. pages := make([]PageMeta, 0, len(entries))
  348. for _, entry := range entries {
  349. if !entry.IsRegular() {
  350. continue
  351. }
  352. c, err := wikiRepo.GetCommitByPath(entry.Name())
  353. if err != nil {
  354. ctx.ServerError("GetCommit", err)
  355. return
  356. }
  357. wikiName, err := models.WikiFilenameToName(entry.Name())
  358. if err != nil {
  359. if models.IsErrWikiInvalidFileName(err) {
  360. continue
  361. }
  362. ctx.ServerError("WikiFilenameToName", err)
  363. return
  364. }
  365. pages = append(pages, PageMeta{
  366. Name: wikiName,
  367. SubURL: models.WikiNameToSubURL(wikiName),
  368. UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()),
  369. })
  370. }
  371. ctx.Data["Pages"] = pages
  372. ctx.HTML(200, tplWikiPages)
  373. }
  374. // WikiRaw outputs raw blob requested by user (image for example)
  375. func WikiRaw(ctx *context.Context) {
  376. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  377. if err != nil {
  378. if wikiRepo != nil {
  379. return
  380. }
  381. }
  382. providedPath := ctx.Params("*")
  383. var entry *git.TreeEntry
  384. if commit != nil {
  385. // Try to find a file with that name
  386. entry, err = findEntryForFile(commit, providedPath)
  387. if err != nil {
  388. ctx.ServerError("findFile", err)
  389. return
  390. }
  391. if entry == nil {
  392. // Try to find a wiki page with that name
  393. if strings.HasSuffix(providedPath, ".md") {
  394. providedPath = providedPath[:len(providedPath)-3]
  395. }
  396. wikiPath := models.WikiNameToFilename(providedPath)
  397. entry, err = findEntryForFile(commit, wikiPath)
  398. if err != nil {
  399. ctx.ServerError("findFile", err)
  400. return
  401. }
  402. }
  403. }
  404. if entry != nil {
  405. if err = ServeBlob(ctx, entry.Blob()); err != nil {
  406. ctx.ServerError("ServeBlob", err)
  407. }
  408. return
  409. }
  410. ctx.NotFound("findEntryForFile", nil)
  411. }
  412. // NewWiki render wiki create page
  413. func NewWiki(ctx *context.Context) {
  414. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  415. ctx.Data["PageIsWiki"] = true
  416. ctx.Data["RequireSimpleMDE"] = true
  417. if !ctx.Repo.Repository.HasWiki() {
  418. ctx.Data["title"] = "Home"
  419. }
  420. ctx.HTML(200, tplWikiNew)
  421. }
  422. // NewWikiPost response for wiki create request
  423. func NewWikiPost(ctx *context.Context, form auth.NewWikiForm) {
  424. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  425. ctx.Data["PageIsWiki"] = true
  426. ctx.Data["RequireSimpleMDE"] = true
  427. if ctx.HasError() {
  428. ctx.HTML(200, tplWikiNew)
  429. return
  430. }
  431. if util.IsEmptyString(form.Title) {
  432. ctx.RenderWithErr(ctx.Tr("repo.issues.new.title_empty"), tplWikiNew, form)
  433. return
  434. }
  435. wikiName := models.NormalizeWikiName(form.Title)
  436. if err := ctx.Repo.Repository.AddWikiPage(ctx.User, wikiName, form.Content, form.Message); err != nil {
  437. if models.IsErrWikiReservedName(err) {
  438. ctx.Data["Err_Title"] = true
  439. ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
  440. } else if models.IsErrWikiAlreadyExist(err) {
  441. ctx.Data["Err_Title"] = true
  442. ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
  443. } else {
  444. ctx.ServerError("AddWikiPage", err)
  445. }
  446. return
  447. }
  448. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.WikiNameToSubURL(wikiName))
  449. }
  450. // EditWiki render wiki modify page
  451. func EditWiki(ctx *context.Context) {
  452. ctx.Data["PageIsWiki"] = true
  453. ctx.Data["PageIsWikiEdit"] = true
  454. ctx.Data["RequireSimpleMDE"] = true
  455. if !ctx.Repo.Repository.HasWiki() {
  456. ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
  457. return
  458. }
  459. renderEditPage(ctx)
  460. if ctx.Written() {
  461. return
  462. }
  463. ctx.HTML(200, tplWikiNew)
  464. }
  465. // EditWikiPost response for wiki modify request
  466. func EditWikiPost(ctx *context.Context, form auth.NewWikiForm) {
  467. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  468. ctx.Data["PageIsWiki"] = true
  469. ctx.Data["RequireSimpleMDE"] = true
  470. if ctx.HasError() {
  471. ctx.HTML(200, tplWikiNew)
  472. return
  473. }
  474. oldWikiName := models.NormalizeWikiName(ctx.Params(":page"))
  475. newWikiName := models.NormalizeWikiName(form.Title)
  476. if err := ctx.Repo.Repository.EditWikiPage(ctx.User, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
  477. ctx.ServerError("EditWikiPage", err)
  478. return
  479. }
  480. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.WikiNameToSubURL(newWikiName))
  481. }
  482. // DeleteWikiPagePost delete wiki page
  483. func DeleteWikiPagePost(ctx *context.Context) {
  484. wikiName := models.NormalizeWikiName(ctx.Params(":page"))
  485. if len(wikiName) == 0 {
  486. wikiName = "Home"
  487. }
  488. if err := ctx.Repo.Repository.DeleteWikiPage(ctx.User, wikiName); err != nil {
  489. ctx.ServerError("DeleteWikiPage", err)
  490. return
  491. }
  492. ctx.JSON(200, map[string]interface{}{
  493. "redirect": ctx.Repo.RepoLink + "/wiki/",
  494. })
  495. }