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.

437 lines
11 KiB

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