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

wiki.go 14 kB

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