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

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