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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 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 wiki
  6. import (
  7. "fmt"
  8. "net/url"
  9. "os"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. admin_model "code.gitea.io/gitea/models/admin"
  13. "code.gitea.io/gitea/models/db"
  14. repo_model "code.gitea.io/gitea/models/repo"
  15. "code.gitea.io/gitea/models/unit"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/log"
  19. repo_module "code.gitea.io/gitea/modules/repository"
  20. "code.gitea.io/gitea/modules/sync"
  21. "code.gitea.io/gitea/modules/util"
  22. asymkey_service "code.gitea.io/gitea/services/asymkey"
  23. )
  24. var (
  25. reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
  26. wikiWorkingPool = sync.NewExclusivePool()
  27. )
  28. func nameAllowed(name string) error {
  29. if util.IsStringInSlice(name, reservedWikiNames) {
  30. return models.ErrWikiReservedName{
  31. Title: name,
  32. }
  33. }
  34. return nil
  35. }
  36. // NameToSubURL converts a wiki name to its corresponding sub-URL.
  37. func NameToSubURL(name string) string {
  38. return url.PathEscape(strings.ReplaceAll(name, " ", "-"))
  39. }
  40. // NormalizeWikiName normalizes a wiki name
  41. func NormalizeWikiName(name string) string {
  42. return strings.ReplaceAll(name, "-", " ")
  43. }
  44. // NameToFilename converts a wiki name to its corresponding filename.
  45. func NameToFilename(name string) string {
  46. name = strings.ReplaceAll(name, " ", "-")
  47. return url.QueryEscape(name) + ".md"
  48. }
  49. // FilenameToName converts a wiki filename to its corresponding page name.
  50. func FilenameToName(filename string) (string, error) {
  51. if !strings.HasSuffix(filename, ".md") {
  52. return "", models.ErrWikiInvalidFileName{
  53. FileName: filename,
  54. }
  55. }
  56. basename := filename[:len(filename)-3]
  57. unescaped, err := url.QueryUnescape(basename)
  58. if err != nil {
  59. return "", err
  60. }
  61. return NormalizeWikiName(unescaped), nil
  62. }
  63. // InitWiki initializes a wiki for repository,
  64. // it does nothing when repository already has wiki.
  65. func InitWiki(repo *repo_model.Repository) error {
  66. if repo.HasWiki() {
  67. return nil
  68. }
  69. if err := git.InitRepository(repo.WikiPath(), true); err != nil {
  70. return fmt.Errorf("InitRepository: %v", err)
  71. } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
  72. return fmt.Errorf("createDelegateHooks: %v", err)
  73. } else if _, err = git.NewCommand("symbolic-ref", "HEAD", git.BranchPrefix+"master").RunInDir(repo.WikiPath()); err != nil {
  74. return fmt.Errorf("unable to set default wiki branch to master: %v", err)
  75. }
  76. return nil
  77. }
  78. // prepareWikiFileName try to find a suitable file path with file name by the given raw wiki name.
  79. // return: existence, prepared file path with name, error
  80. func prepareWikiFileName(gitRepo *git.Repository, wikiName string) (bool, string, error) {
  81. unescaped := wikiName + ".md"
  82. escaped := NameToFilename(wikiName)
  83. // Look for both files
  84. filesInIndex, err := gitRepo.LsTree("master", unescaped, escaped)
  85. if err != nil {
  86. if strings.Contains(err.Error(), "Not a valid object name master") {
  87. return false, escaped, nil
  88. }
  89. log.Error("%v", err)
  90. return false, escaped, err
  91. }
  92. foundEscaped := false
  93. for _, filename := range filesInIndex {
  94. switch filename {
  95. case unescaped:
  96. // if we find the unescaped file return it
  97. return true, unescaped, nil
  98. case escaped:
  99. foundEscaped = true
  100. }
  101. }
  102. // If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility.
  103. return foundEscaped, escaped, nil
  104. }
  105. // updateWikiPage adds a new page to the repository wiki.
  106. func updateWikiPage(doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName, content, message string, isNew bool) (err error) {
  107. if err = nameAllowed(newWikiName); err != nil {
  108. return err
  109. }
  110. wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
  111. defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
  112. if err = InitWiki(repo); err != nil {
  113. return fmt.Errorf("InitWiki: %v", err)
  114. }
  115. hasMasterBranch := git.IsBranchExist(git.DefaultContext, repo.WikiPath(), "master")
  116. basePath, err := models.CreateTemporaryPath("update-wiki")
  117. if err != nil {
  118. return err
  119. }
  120. defer func() {
  121. if err := models.RemoveTemporaryPath(basePath); err != nil {
  122. log.Error("Merge: RemoveTemporaryPath: %s", err)
  123. }
  124. }()
  125. cloneOpts := git.CloneRepoOptions{
  126. Bare: true,
  127. Shared: true,
  128. }
  129. if hasMasterBranch {
  130. cloneOpts.Branch = "master"
  131. }
  132. if err := git.Clone(repo.WikiPath(), basePath, cloneOpts); err != nil {
  133. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  134. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  135. }
  136. gitRepo, err := git.OpenRepository(basePath)
  137. if err != nil {
  138. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  139. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  140. }
  141. defer gitRepo.Close()
  142. if hasMasterBranch {
  143. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  144. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  145. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  146. }
  147. }
  148. isWikiExist, newWikiPath, err := prepareWikiFileName(gitRepo, newWikiName)
  149. if err != nil {
  150. return err
  151. }
  152. if isNew {
  153. if isWikiExist {
  154. return models.ErrWikiAlreadyExist{
  155. Title: newWikiPath,
  156. }
  157. }
  158. } else {
  159. // avoid check existence again if wiki name is not changed since gitRepo.LsFiles(...) is not free.
  160. isOldWikiExist := true
  161. oldWikiPath := newWikiPath
  162. if oldWikiName != newWikiName {
  163. isOldWikiExist, oldWikiPath, err = prepareWikiFileName(gitRepo, oldWikiName)
  164. if err != nil {
  165. return err
  166. }
  167. }
  168. if isOldWikiExist {
  169. err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
  170. if err != nil {
  171. log.Error("%v", err)
  172. return err
  173. }
  174. }
  175. }
  176. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  177. objectHash, err := gitRepo.HashObject(strings.NewReader(content))
  178. if err != nil {
  179. log.Error("%v", err)
  180. return err
  181. }
  182. if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
  183. log.Error("%v", err)
  184. return err
  185. }
  186. tree, err := gitRepo.WriteTree()
  187. if err != nil {
  188. log.Error("%v", err)
  189. return err
  190. }
  191. commitTreeOpts := git.CommitTreeOpts{
  192. Message: message,
  193. }
  194. committer := doer.NewGitSig()
  195. sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(repo.WikiPath(), doer)
  196. if sign {
  197. commitTreeOpts.KeyID = signingKey
  198. if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
  199. committer = signer
  200. }
  201. } else {
  202. commitTreeOpts.NoGPGSign = true
  203. }
  204. if hasMasterBranch {
  205. commitTreeOpts.Parents = []string{"HEAD"}
  206. }
  207. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
  208. if err != nil {
  209. log.Error("%v", err)
  210. return err
  211. }
  212. if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
  213. Remote: "origin",
  214. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  215. Env: models.FullPushingEnvironment(
  216. doer,
  217. doer,
  218. repo,
  219. repo.Name+".wiki",
  220. 0,
  221. ),
  222. }); err != nil {
  223. log.Error("%v", err)
  224. if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
  225. return err
  226. }
  227. return fmt.Errorf("Push: %v", err)
  228. }
  229. return nil
  230. }
  231. // AddWikiPage adds a new wiki page with a given wikiPath.
  232. func AddWikiPage(doer *user_model.User, repo *repo_model.Repository, wikiName, content, message string) error {
  233. return updateWikiPage(doer, repo, "", wikiName, content, message, true)
  234. }
  235. // EditWikiPage updates a wiki page identified by its wikiPath,
  236. // optionally also changing wikiPath.
  237. func EditWikiPage(doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName, content, message string) error {
  238. return updateWikiPage(doer, repo, oldWikiName, newWikiName, content, message, false)
  239. }
  240. // DeleteWikiPage deletes a wiki page identified by its path.
  241. func DeleteWikiPage(doer *user_model.User, repo *repo_model.Repository, wikiName string) (err error) {
  242. wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
  243. defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
  244. if err = InitWiki(repo); err != nil {
  245. return fmt.Errorf("InitWiki: %v", err)
  246. }
  247. basePath, err := models.CreateTemporaryPath("update-wiki")
  248. if err != nil {
  249. return err
  250. }
  251. defer func() {
  252. if err := models.RemoveTemporaryPath(basePath); err != nil {
  253. log.Error("Merge: RemoveTemporaryPath: %s", err)
  254. }
  255. }()
  256. if err := git.Clone(repo.WikiPath(), basePath, git.CloneRepoOptions{
  257. Bare: true,
  258. Shared: true,
  259. Branch: "master",
  260. }); err != nil {
  261. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  262. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  263. }
  264. gitRepo, err := git.OpenRepository(basePath)
  265. if err != nil {
  266. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  267. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  268. }
  269. defer gitRepo.Close()
  270. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  271. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  272. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  273. }
  274. found, wikiPath, err := prepareWikiFileName(gitRepo, wikiName)
  275. if err != nil {
  276. return err
  277. }
  278. if found {
  279. err := gitRepo.RemoveFilesFromIndex(wikiPath)
  280. if err != nil {
  281. return err
  282. }
  283. } else {
  284. return os.ErrNotExist
  285. }
  286. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  287. tree, err := gitRepo.WriteTree()
  288. if err != nil {
  289. return err
  290. }
  291. message := "Delete page '" + wikiName + "'"
  292. commitTreeOpts := git.CommitTreeOpts{
  293. Message: message,
  294. Parents: []string{"HEAD"},
  295. }
  296. committer := doer.NewGitSig()
  297. sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(repo.WikiPath(), doer)
  298. if sign {
  299. commitTreeOpts.KeyID = signingKey
  300. if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
  301. committer = signer
  302. }
  303. } else {
  304. commitTreeOpts.NoGPGSign = true
  305. }
  306. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
  307. if err != nil {
  308. return err
  309. }
  310. if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
  311. Remote: "origin",
  312. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  313. Env: models.PushingEnvironment(doer, repo),
  314. }); err != nil {
  315. if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
  316. return err
  317. }
  318. return fmt.Errorf("Push: %v", err)
  319. }
  320. return nil
  321. }
  322. // DeleteWiki removes the actual and local copy of repository wiki.
  323. func DeleteWiki(repo *repo_model.Repository) error {
  324. if err := models.UpdateRepositoryUnits(repo, nil, []unit.Type{unit.TypeWiki}); err != nil {
  325. return err
  326. }
  327. admin_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath())
  328. return nil
  329. }