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 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repo
  5. import (
  6. "fmt"
  7. "io/ioutil"
  8. "path/filepath"
  9. "strings"
  10. "code.gitea.io/git"
  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/markup"
  16. "code.gitea.io/gitea/modules/markup/markdown"
  17. "code.gitea.io/gitea/modules/util"
  18. )
  19. const (
  20. tplWikiStart base.TplName = "repo/wiki/start"
  21. tplWikiView base.TplName = "repo/wiki/view"
  22. tplWikiNew base.TplName = "repo/wiki/new"
  23. tplWikiPages base.TplName = "repo/wiki/pages"
  24. )
  25. // MustEnableWiki check if wiki is enabled, if external then redirect
  26. func MustEnableWiki(ctx *context.Context) {
  27. if !ctx.Repo.Repository.UnitEnabled(models.UnitTypeWiki) &&
  28. !ctx.Repo.Repository.UnitEnabled(models.UnitTypeExternalWiki) {
  29. ctx.NotFound("MustEnableWiki", nil)
  30. return
  31. }
  32. unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalWiki)
  33. if err == nil {
  34. ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
  35. return
  36. }
  37. }
  38. // PageMeta wiki page meat information
  39. type PageMeta struct {
  40. Name string
  41. SubURL string
  42. UpdatedUnix util.TimeStamp
  43. }
  44. // findEntryForFile finds the tree entry for a target filepath.
  45. func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
  46. entries, err := commit.ListEntries()
  47. if err != nil {
  48. return nil, err
  49. }
  50. for _, entry := range entries {
  51. if entry.Type == git.ObjectBlob && entry.Name() == target {
  52. return entry, nil
  53. }
  54. }
  55. return nil, nil
  56. }
  57. func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
  58. wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath())
  59. if err != nil {
  60. ctx.ServerError("OpenRepository", err)
  61. return nil, nil, err
  62. }
  63. commit, err := wikiRepo.GetBranchCommit("master")
  64. if err != nil {
  65. ctx.ServerError("GetBranchCommit", err)
  66. return wikiRepo, nil, err
  67. }
  68. return wikiRepo, commit, nil
  69. }
  70. // wikiContentsByEntry returns the contents of the wiki page referenced by the
  71. // given tree entry. Writes to ctx if an error occurs.
  72. func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
  73. reader, err := entry.Blob().Data()
  74. if err != nil {
  75. ctx.ServerError("Blob.Data", err)
  76. return nil
  77. }
  78. content, err := ioutil.ReadAll(reader)
  79. if err != nil {
  80. ctx.ServerError("ReadAll", err)
  81. return nil
  82. }
  83. return content
  84. }
  85. // wikiContentsByName returns the contents of a wiki page, along with a boolean
  86. // indicating whether the page exists. Writes to ctx if an error occurs.
  87. func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, bool) {
  88. entry, err := findEntryForFile(commit, models.WikiNameToFilename(wikiName))
  89. if err != nil {
  90. ctx.ServerError("findEntryForFile", err)
  91. return nil, false
  92. } else if entry == nil {
  93. return nil, false
  94. }
  95. return wikiContentsByEntry(ctx, entry), true
  96. }
  97. func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *git.TreeEntry) {
  98. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  99. if err != nil {
  100. return nil, nil
  101. }
  102. // Get page list.
  103. if isViewPage {
  104. entries, err := commit.ListEntries()
  105. if err != nil {
  106. ctx.ServerError("ListEntries", err)
  107. return nil, nil
  108. }
  109. pages := make([]PageMeta, 0, len(entries))
  110. for _, entry := range entries {
  111. if entry.Type != git.ObjectBlob {
  112. continue
  113. }
  114. wikiName, err := models.WikiFilenameToName(entry.Name())
  115. if err != nil {
  116. if models.IsErrWikiInvalidFileName(err) {
  117. continue
  118. }
  119. ctx.ServerError("WikiFilenameToName", err)
  120. return nil, nil
  121. } else if wikiName == "_Sidebar" || wikiName == "_Footer" {
  122. continue
  123. }
  124. pages = append(pages, PageMeta{
  125. Name: wikiName,
  126. SubURL: models.WikiNameToSubURL(wikiName),
  127. })
  128. }
  129. ctx.Data["Pages"] = pages
  130. }
  131. pageName := models.NormalizeWikiName(ctx.Params(":page"))
  132. if len(pageName) == 0 {
  133. pageName = "Home"
  134. }
  135. ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
  136. ctx.Data["old_title"] = pageName
  137. ctx.Data["Title"] = pageName
  138. ctx.Data["title"] = pageName
  139. ctx.Data["RequireHighlightJS"] = true
  140. pageFilename := models.WikiNameToFilename(pageName)
  141. var entry *git.TreeEntry
  142. if entry, err = findEntryForFile(commit, pageFilename); err != nil {
  143. ctx.ServerError("findEntryForFile", err)
  144. return nil, nil
  145. } else if entry == nil {
  146. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
  147. return nil, nil
  148. }
  149. data := wikiContentsByEntry(ctx, entry)
  150. if ctx.Written() {
  151. return nil, nil
  152. }
  153. if isViewPage {
  154. sidebarContent, sidebarPresent := wikiContentsByName(ctx, commit, "_Sidebar")
  155. if ctx.Written() {
  156. return nil, nil
  157. }
  158. footerContent, footerPresent := wikiContentsByName(ctx, commit, "_Footer")
  159. if ctx.Written() {
  160. return nil, nil
  161. }
  162. metas := ctx.Repo.Repository.ComposeMetas()
  163. ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas)
  164. ctx.Data["sidebarPresent"] = sidebarPresent
  165. ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas)
  166. ctx.Data["footerPresent"] = footerPresent
  167. ctx.Data["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas)
  168. } else {
  169. ctx.Data["content"] = string(data)
  170. ctx.Data["sidebarPresent"] = false
  171. ctx.Data["sidebarContent"] = ""
  172. ctx.Data["footerPresent"] = false
  173. ctx.Data["footerContent"] = ""
  174. }
  175. return wikiRepo, entry
  176. }
  177. // Wiki renders single wiki page
  178. func Wiki(ctx *context.Context) {
  179. ctx.Data["PageIsWiki"] = true
  180. if !ctx.Repo.Repository.HasWiki() {
  181. ctx.Data["Title"] = ctx.Tr("repo.wiki")
  182. ctx.HTML(200, tplWikiStart)
  183. return
  184. }
  185. wikiRepo, entry := renderWikiPage(ctx, true)
  186. if ctx.Written() {
  187. return
  188. }
  189. if entry == nil {
  190. ctx.Data["Title"] = ctx.Tr("repo.wiki")
  191. ctx.HTML(200, tplWikiStart)
  192. return
  193. }
  194. wikiPath := entry.Name()
  195. if markup.Type(wikiPath) != markdown.MarkupName {
  196. ext := strings.ToUpper(filepath.Ext(wikiPath))
  197. ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
  198. }
  199. // Get last change information.
  200. lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
  201. if err != nil {
  202. ctx.ServerError("GetCommitByPath", err)
  203. return
  204. }
  205. ctx.Data["Author"] = lastCommit.Author
  206. ctx.HTML(200, tplWikiView)
  207. }
  208. // WikiPages render wiki pages list page
  209. func WikiPages(ctx *context.Context) {
  210. ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
  211. ctx.Data["PageIsWiki"] = true
  212. if !ctx.Repo.Repository.HasWiki() {
  213. ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
  214. return
  215. }
  216. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  217. if err != nil {
  218. return
  219. }
  220. entries, err := commit.ListEntries()
  221. if err != nil {
  222. ctx.ServerError("ListEntries", err)
  223. return
  224. }
  225. pages := make([]PageMeta, 0, len(entries))
  226. for _, entry := range entries {
  227. if entry.Type != git.ObjectBlob {
  228. continue
  229. }
  230. c, err := wikiRepo.GetCommitByPath(entry.Name())
  231. if err != nil {
  232. ctx.ServerError("GetCommit", err)
  233. return
  234. }
  235. wikiName, err := models.WikiFilenameToName(entry.Name())
  236. if err != nil {
  237. if models.IsErrWikiInvalidFileName(err) {
  238. continue
  239. }
  240. ctx.ServerError("WikiFilenameToName", err)
  241. return
  242. }
  243. pages = append(pages, PageMeta{
  244. Name: wikiName,
  245. SubURL: models.WikiNameToSubURL(wikiName),
  246. UpdatedUnix: util.TimeStamp(c.Author.When.Unix()),
  247. })
  248. }
  249. ctx.Data["Pages"] = pages
  250. ctx.HTML(200, tplWikiPages)
  251. }
  252. // WikiRaw outputs raw blob requested by user (image for example)
  253. func WikiRaw(ctx *context.Context) {
  254. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  255. if err != nil {
  256. if wikiRepo != nil {
  257. return
  258. }
  259. }
  260. providedPath := ctx.Params("*")
  261. if strings.HasSuffix(providedPath, ".md") {
  262. providedPath = providedPath[:len(providedPath)-3]
  263. }
  264. wikiPath := models.WikiNameToFilename(providedPath)
  265. var entry *git.TreeEntry
  266. if commit != nil {
  267. entry, err = findEntryForFile(commit, wikiPath)
  268. }
  269. if err != nil {
  270. ctx.ServerError("findFile", err)
  271. return
  272. } else if entry == nil {
  273. ctx.NotFound("findEntryForFile", nil)
  274. return
  275. }
  276. if err = ServeBlob(ctx, entry.Blob()); err != nil {
  277. ctx.ServerError("ServeBlob", err)
  278. }
  279. }
  280. // NewWiki render wiki create page
  281. func NewWiki(ctx *context.Context) {
  282. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  283. ctx.Data["PageIsWiki"] = true
  284. ctx.Data["RequireSimpleMDE"] = true
  285. if !ctx.Repo.Repository.HasWiki() {
  286. ctx.Data["title"] = "Home"
  287. }
  288. ctx.HTML(200, tplWikiNew)
  289. }
  290. // NewWikiPost response for wiki create request
  291. func NewWikiPost(ctx *context.Context, form auth.NewWikiForm) {
  292. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  293. ctx.Data["PageIsWiki"] = true
  294. ctx.Data["RequireSimpleMDE"] = true
  295. if ctx.HasError() {
  296. ctx.HTML(200, tplWikiNew)
  297. return
  298. }
  299. wikiName := models.NormalizeWikiName(form.Title)
  300. if err := ctx.Repo.Repository.AddWikiPage(ctx.User, wikiName, form.Content, form.Message); err != nil {
  301. if models.IsErrWikiReservedName(err) {
  302. ctx.Data["Err_Title"] = true
  303. ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
  304. } else if models.IsErrWikiAlreadyExist(err) {
  305. ctx.Data["Err_Title"] = true
  306. ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
  307. } else {
  308. ctx.ServerError("AddWikiPage", err)
  309. }
  310. return
  311. }
  312. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.WikiNameToSubURL(wikiName))
  313. }
  314. // EditWiki render wiki modify page
  315. func EditWiki(ctx *context.Context) {
  316. ctx.Data["PageIsWiki"] = true
  317. ctx.Data["PageIsWikiEdit"] = true
  318. ctx.Data["RequireSimpleMDE"] = true
  319. if !ctx.Repo.Repository.HasWiki() {
  320. ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
  321. return
  322. }
  323. renderWikiPage(ctx, false)
  324. if ctx.Written() {
  325. return
  326. }
  327. ctx.HTML(200, tplWikiNew)
  328. }
  329. // EditWikiPost response for wiki modify request
  330. func EditWikiPost(ctx *context.Context, form auth.NewWikiForm) {
  331. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  332. ctx.Data["PageIsWiki"] = true
  333. ctx.Data["RequireSimpleMDE"] = true
  334. if ctx.HasError() {
  335. ctx.HTML(200, tplWikiNew)
  336. return
  337. }
  338. oldWikiName := models.NormalizeWikiName(ctx.Params(":page"))
  339. newWikiName := models.NormalizeWikiName(form.Title)
  340. if err := ctx.Repo.Repository.EditWikiPage(ctx.User, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
  341. ctx.ServerError("EditWikiPage", err)
  342. return
  343. }
  344. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.WikiNameToSubURL(newWikiName))
  345. }
  346. // DeleteWikiPagePost delete wiki page
  347. func DeleteWikiPagePost(ctx *context.Context) {
  348. wikiName := models.NormalizeWikiName(ctx.Params(":page"))
  349. if len(wikiName) == 0 {
  350. wikiName = "Home"
  351. }
  352. if err := ctx.Repo.Repository.DeleteWikiPage(ctx.User, wikiName); err != nil {
  353. ctx.ServerError("DeleteWikiPage", err)
  354. return
  355. }
  356. ctx.JSON(200, map[string]interface{}{
  357. "redirect": ctx.Repo.RepoLink + "/wiki/",
  358. })
  359. }