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.

509 lines
13 KiB

  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. "net/url"
  9. "path/filepath"
  10. "strings"
  11. "time"
  12. "code.gitea.io/git"
  13. "code.gitea.io/gitea/models"
  14. "code.gitea.io/gitea/modules/auth"
  15. "code.gitea.io/gitea/modules/base"
  16. "code.gitea.io/gitea/modules/context"
  17. "code.gitea.io/gitea/modules/markdown"
  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.EnableUnit(models.UnitTypeWiki) &&
  28. !ctx.Repo.Repository.EnableUnit(models.UnitTypeExternalWiki) {
  29. ctx.Handle(404, "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. URL string
  42. Updated time.Time
  43. }
  44. func urlEncoded(str string) string {
  45. u, err := url.Parse(str)
  46. if err != nil {
  47. return str
  48. }
  49. return u.String()
  50. }
  51. func urlDecoded(str string) string {
  52. res, err := url.QueryUnescape(str)
  53. if err != nil {
  54. return str
  55. }
  56. return res
  57. }
  58. // commitTreeBlobEntry processes found file and checks if it matches search target
  59. func commitTreeBlobEntry(entry *git.TreeEntry, path string, targets []string, textOnly bool) *git.TreeEntry {
  60. name := entry.Name()
  61. ext := filepath.Ext(name)
  62. if !textOnly || markdown.IsMarkdownFile(name) || ext == ".textile" {
  63. for _, target := range targets {
  64. if matchName(path, target) || matchName(urlEncoded(path), target) || matchName(urlDecoded(path), target) {
  65. return entry
  66. }
  67. pathNoExt := strings.TrimSuffix(path, ext)
  68. if matchName(pathNoExt, target) || matchName(urlEncoded(pathNoExt), target) || matchName(urlDecoded(pathNoExt), target) {
  69. return entry
  70. }
  71. }
  72. }
  73. return nil
  74. }
  75. // commitTreeDirEntry is a recursive file tree traversal function
  76. func commitTreeDirEntry(repo *git.Repository, commit *git.Commit, entries []*git.TreeEntry, prevPath string, targets []string, textOnly bool) (*git.TreeEntry, error) {
  77. for i := range entries {
  78. entry := entries[i]
  79. var path string
  80. if len(prevPath) == 0 {
  81. path = entry.Name()
  82. } else {
  83. path = prevPath + "/" + entry.Name()
  84. }
  85. if entry.Type == git.ObjectBlob {
  86. // File
  87. if res := commitTreeBlobEntry(entry, path, targets, textOnly); res != nil {
  88. return res, nil
  89. }
  90. } else if entry.IsDir() {
  91. // Directory
  92. // Get our tree entry, handling all possible errors
  93. var err error
  94. var tree *git.Tree
  95. if tree, err = repo.GetTree(entry.ID.String()); tree == nil || err != nil {
  96. if err == nil {
  97. err = fmt.Errorf("repo.GetTree(%s) => nil", entry.ID.String())
  98. }
  99. return nil, err
  100. }
  101. // Found us, get children entries
  102. var ls git.Entries
  103. if ls, err = tree.ListEntries(); err != nil {
  104. return nil, err
  105. }
  106. // Call itself recursively to find needed entry
  107. var te *git.TreeEntry
  108. if te, err = commitTreeDirEntry(repo, commit, ls, path, targets, textOnly); err != nil {
  109. return nil, err
  110. }
  111. if te != nil {
  112. return te, nil
  113. }
  114. }
  115. }
  116. return nil, nil
  117. }
  118. // commitTreeEntry is a first step of commitTreeDirEntry, which should be never called directly
  119. func commitTreeEntry(repo *git.Repository, commit *git.Commit, targets []string, textOnly bool) (*git.TreeEntry, error) {
  120. entries, err := commit.ListEntries()
  121. if err != nil {
  122. return nil, err
  123. }
  124. return commitTreeDirEntry(repo, commit, entries, "", targets, textOnly)
  125. }
  126. // findFile finds the best match for given filename in repo file tree
  127. func findFile(repo *git.Repository, commit *git.Commit, target string, textOnly bool) (*git.TreeEntry, error) {
  128. targets := []string{target, urlEncoded(target), urlDecoded(target)}
  129. var entry *git.TreeEntry
  130. var err error
  131. if entry, err = commitTreeEntry(repo, commit, targets, textOnly); err != nil {
  132. return nil, err
  133. }
  134. return entry, nil
  135. }
  136. // matchName matches generic name representation of the file with required one
  137. func matchName(target, name string) bool {
  138. if len(target) != len(name) {
  139. return false
  140. }
  141. name = strings.ToLower(name)
  142. target = strings.ToLower(target)
  143. if name == target {
  144. return true
  145. }
  146. target = strings.Replace(target, " ", "?", -1)
  147. target = strings.Replace(target, "-", "?", -1)
  148. for i := range name {
  149. ch := name[i]
  150. reqCh := target[i]
  151. if ch != reqCh {
  152. if string(reqCh) != "?" {
  153. return false
  154. }
  155. }
  156. }
  157. return true
  158. }
  159. func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
  160. wikiRepo, err := git.OpenRepository(ctx.Repo.Repository.WikiPath())
  161. if err != nil {
  162. // ctx.Handle(500, "OpenRepository", err)
  163. return nil, nil, err
  164. }
  165. commit, err := wikiRepo.GetBranchCommit("master")
  166. if err != nil {
  167. ctx.Handle(500, "GetBranchCommit", err)
  168. return wikiRepo, nil, err
  169. }
  170. return wikiRepo, commit, nil
  171. }
  172. func renderWikiPage(ctx *context.Context, isViewPage bool) (*git.Repository, *git.TreeEntry) {
  173. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  174. if err != nil {
  175. return nil, nil
  176. }
  177. // Get page list.
  178. if isViewPage {
  179. entries, err := commit.ListEntries()
  180. if err != nil {
  181. ctx.Handle(500, "ListEntries", err)
  182. return nil, nil
  183. }
  184. pages := []PageMeta{}
  185. for i := range entries {
  186. if entries[i].Type == git.ObjectBlob {
  187. name := entries[i].Name()
  188. ext := filepath.Ext(name)
  189. if markdown.IsMarkdownFile(name) || ext == ".textile" {
  190. name = strings.TrimSuffix(name, ext)
  191. if name == "" || name == "_Sidebar" || name == "_Footer" || name == "_Header" {
  192. continue
  193. }
  194. pages = append(pages, PageMeta{
  195. Name: strings.Replace(name, "-", " ", -1),
  196. URL: models.ToWikiPageURL(name),
  197. })
  198. }
  199. }
  200. }
  201. ctx.Data["Pages"] = pages
  202. }
  203. pageURL := ctx.Params(":page")
  204. if len(pageURL) == 0 {
  205. pageURL = "Home"
  206. }
  207. ctx.Data["PageURL"] = pageURL
  208. pageName := models.ToWikiPageName(pageURL)
  209. ctx.Data["old_title"] = pageName
  210. ctx.Data["Title"] = pageName
  211. ctx.Data["title"] = pageName
  212. ctx.Data["RequireHighlightJS"] = true
  213. var entry *git.TreeEntry
  214. if entry, err = findFile(wikiRepo, commit, pageName, true); err != nil {
  215. ctx.Handle(500, "findFile", err)
  216. return nil, nil
  217. }
  218. if entry == nil {
  219. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages")
  220. return nil, nil
  221. }
  222. blob := entry.Blob()
  223. r, err := blob.Data()
  224. if err != nil {
  225. ctx.Handle(500, "Data", err)
  226. return nil, nil
  227. }
  228. data, err := ioutil.ReadAll(r)
  229. if err != nil {
  230. ctx.Handle(500, "ReadAll", err)
  231. return nil, nil
  232. }
  233. sidebarPresent := false
  234. sidebarContent := []byte{}
  235. sentry, err := findFile(wikiRepo, commit, "_Sidebar", true)
  236. if err == nil && sentry != nil {
  237. r, err = sentry.Blob().Data()
  238. if err == nil {
  239. dataSB, err := ioutil.ReadAll(r)
  240. if err == nil {
  241. sidebarPresent = true
  242. sidebarContent = dataSB
  243. }
  244. }
  245. }
  246. footerPresent := false
  247. footerContent := []byte{}
  248. sentry, err = findFile(wikiRepo, commit, "_Footer", true)
  249. if err == nil && sentry != nil {
  250. r, err = sentry.Blob().Data()
  251. if err == nil {
  252. dataSB, err := ioutil.ReadAll(r)
  253. if err == nil {
  254. footerPresent = true
  255. footerContent = dataSB
  256. }
  257. }
  258. }
  259. if isViewPage {
  260. metas := ctx.Repo.Repository.ComposeMetas()
  261. ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas)
  262. ctx.Data["sidebarPresent"] = sidebarPresent
  263. ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas)
  264. ctx.Data["footerPresent"] = footerPresent
  265. ctx.Data["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas)
  266. } else {
  267. ctx.Data["content"] = string(data)
  268. ctx.Data["sidebarPresent"] = false
  269. ctx.Data["sidebarContent"] = ""
  270. ctx.Data["footerPresent"] = false
  271. ctx.Data["footerContent"] = ""
  272. }
  273. return wikiRepo, entry
  274. }
  275. // Wiki renders single wiki page
  276. func Wiki(ctx *context.Context) {
  277. ctx.Data["PageIsWiki"] = true
  278. if !ctx.Repo.Repository.HasWiki() {
  279. ctx.Data["Title"] = ctx.Tr("repo.wiki")
  280. ctx.HTML(200, tplWikiStart)
  281. return
  282. }
  283. wikiRepo, entry := renderWikiPage(ctx, true)
  284. if ctx.Written() {
  285. return
  286. }
  287. ename := entry.Name()
  288. if !markdown.IsMarkdownFile(ename) {
  289. ext := strings.ToUpper(filepath.Ext(ename))
  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(ename)
  294. if err != nil {
  295. ctx.Handle(500, "GetCommitByPath", err)
  296. return
  297. }
  298. ctx.Data["Author"] = lastCommit.Author
  299. ctx.HTML(200, tplWikiView)
  300. }
  301. // WikiPages render wiki pages list page
  302. func WikiPages(ctx *context.Context) {
  303. ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
  304. ctx.Data["PageIsWiki"] = true
  305. if !ctx.Repo.Repository.HasWiki() {
  306. ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
  307. return
  308. }
  309. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  310. if err != nil {
  311. return
  312. }
  313. entries, err := commit.ListEntries()
  314. if err != nil {
  315. ctx.Handle(500, "ListEntries", err)
  316. return
  317. }
  318. pages := make([]PageMeta, 0, len(entries))
  319. for i := range entries {
  320. if entries[i].Type == git.ObjectBlob {
  321. c, err := wikiRepo.GetCommitByPath(entries[i].Name())
  322. if err != nil {
  323. ctx.Handle(500, "GetCommit", err)
  324. return
  325. }
  326. name := entries[i].Name()
  327. ext := filepath.Ext(name)
  328. if markdown.IsMarkdownFile(name) || ext == ".textile" {
  329. name = strings.TrimSuffix(name, ext)
  330. if name == "" {
  331. continue
  332. }
  333. pages = append(pages, PageMeta{
  334. Name: name,
  335. URL: models.ToWikiPageURL(name),
  336. Updated: c.Author.When,
  337. })
  338. }
  339. }
  340. }
  341. ctx.Data["Pages"] = pages
  342. ctx.HTML(200, tplWikiPages)
  343. }
  344. // WikiRaw outputs raw blob requested by user (image for example)
  345. func WikiRaw(ctx *context.Context) {
  346. wikiRepo, commit, err := findWikiRepoCommit(ctx)
  347. if err != nil {
  348. if wikiRepo != nil {
  349. return
  350. }
  351. }
  352. uri := ctx.Params("*")
  353. var entry *git.TreeEntry
  354. if commit != nil {
  355. entry, err = findFile(wikiRepo, commit, uri, false)
  356. }
  357. if err != nil || entry == nil {
  358. if entry == nil || commit == nil {
  359. defBranch := ctx.Repo.Repository.DefaultBranch
  360. if commit, err = ctx.Repo.GitRepo.GetBranchCommit(defBranch); commit == nil || err != nil {
  361. ctx.Handle(500, "GetBranchCommit", err)
  362. return
  363. }
  364. if entry, err = findFile(ctx.Repo.GitRepo, commit, uri, false); err != nil {
  365. ctx.Handle(500, "findFile", err)
  366. return
  367. }
  368. if entry == nil {
  369. ctx.Handle(404, "findFile", nil)
  370. return
  371. }
  372. } else {
  373. ctx.Handle(500, "findFile", err)
  374. return
  375. }
  376. }
  377. if err = ServeBlob(ctx, entry.Blob()); err != nil {
  378. ctx.Handle(500, "ServeBlob", err)
  379. }
  380. }
  381. // NewWiki render wiki create page
  382. func NewWiki(ctx *context.Context) {
  383. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  384. ctx.Data["PageIsWiki"] = true
  385. ctx.Data["RequireSimpleMDE"] = true
  386. if !ctx.Repo.Repository.HasWiki() {
  387. ctx.Data["title"] = "Home"
  388. }
  389. ctx.HTML(200, tplWikiNew)
  390. }
  391. // NewWikiPost response fro wiki create request
  392. func NewWikiPost(ctx *context.Context, form auth.NewWikiForm) {
  393. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  394. ctx.Data["PageIsWiki"] = true
  395. ctx.Data["RequireSimpleMDE"] = true
  396. if ctx.HasError() {
  397. ctx.HTML(200, tplWikiNew)
  398. return
  399. }
  400. wikiPath := models.ToWikiPageURL(form.Title)
  401. if err := ctx.Repo.Repository.AddWikiPage(ctx.User, wikiPath, form.Content, form.Message); err != nil {
  402. if models.IsErrWikiAlreadyExist(err) {
  403. ctx.Data["Err_Title"] = true
  404. ctx.RenderWithErr(ctx.Tr("repo.wiki.page_already_exists"), tplWikiNew, &form)
  405. } else {
  406. ctx.Handle(500, "AddWikiPage", err)
  407. }
  408. return
  409. }
  410. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wikiPath)
  411. }
  412. // EditWiki render wiki modify page
  413. func EditWiki(ctx *context.Context) {
  414. ctx.Data["PageIsWiki"] = true
  415. ctx.Data["PageIsWikiEdit"] = true
  416. ctx.Data["RequireSimpleMDE"] = true
  417. if !ctx.Repo.Repository.HasWiki() {
  418. ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
  419. return
  420. }
  421. renderWikiPage(ctx, false)
  422. if ctx.Written() {
  423. return
  424. }
  425. ctx.HTML(200, tplWikiNew)
  426. }
  427. // EditWikiPost response fro wiki modify request
  428. func EditWikiPost(ctx *context.Context, form auth.NewWikiForm) {
  429. ctx.Data["Title"] = ctx.Tr("repo.wiki.new_page")
  430. ctx.Data["PageIsWiki"] = true
  431. ctx.Data["RequireSimpleMDE"] = true
  432. if ctx.HasError() {
  433. ctx.HTML(200, tplWikiNew)
  434. return
  435. }
  436. oldWikiPath := ctx.Params(":page")
  437. newWikiPath := models.ToWikiPageURL(form.Title)
  438. if err := ctx.Repo.Repository.EditWikiPage(ctx.User, oldWikiPath, newWikiPath, form.Content, form.Message); err != nil {
  439. ctx.Handle(500, "EditWikiPage", err)
  440. return
  441. }
  442. ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + newWikiPath)
  443. }
  444. // DeleteWikiPagePost delete wiki page
  445. func DeleteWikiPagePost(ctx *context.Context) {
  446. pageURL := ctx.Params(":page")
  447. if len(pageURL) == 0 {
  448. pageURL = "Home"
  449. }
  450. if err := ctx.Repo.Repository.DeleteWikiPage(ctx.User, pageURL); err != nil {
  451. ctx.Handle(500, "DeleteWikiPage", err)
  452. return
  453. }
  454. ctx.JSON(200, map[string]interface{}{
  455. "redirect": ctx.Repo.RepoLink + "/wiki/",
  456. })
  457. }