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.

issue_content_history.go 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. // Copyright 2021 The Gitea 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. "bytes"
  7. "fmt"
  8. "html"
  9. "net/http"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/models/db"
  12. issuesModel "code.gitea.io/gitea/models/issues"
  13. "code.gitea.io/gitea/models/unit"
  14. "code.gitea.io/gitea/modules/context"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/timeutil"
  17. "github.com/sergi/go-diff/diffmatchpatch"
  18. "github.com/unknwon/i18n"
  19. )
  20. // GetContentHistoryOverview get overview
  21. func GetContentHistoryOverview(ctx *context.Context) {
  22. issue := GetActionIssue(ctx)
  23. if issue == nil {
  24. return
  25. }
  26. lang := ctx.Locale.Language()
  27. editedHistoryCountMap, _ := issuesModel.QueryIssueContentHistoryEditedCountMap(db.DefaultContext, issue.ID)
  28. ctx.JSON(http.StatusOK, map[string]interface{}{
  29. "i18n": map[string]interface{}{
  30. "textEdited": i18n.Tr(lang, "repo.issues.content_history.edited"),
  31. "textDeleteFromHistory": i18n.Tr(lang, "repo.issues.content_history.delete_from_history"),
  32. "textDeleteFromHistoryConfirm": i18n.Tr(lang, "repo.issues.content_history.delete_from_history_confirm"),
  33. "textOptions": i18n.Tr(lang, "repo.issues.content_history.options"),
  34. },
  35. "editedHistoryCountMap": editedHistoryCountMap,
  36. })
  37. }
  38. // GetContentHistoryList get list
  39. func GetContentHistoryList(ctx *context.Context) {
  40. issue := GetActionIssue(ctx)
  41. commentID := ctx.FormInt64("comment_id")
  42. if issue == nil {
  43. return
  44. }
  45. items, _ := issuesModel.FetchIssueContentHistoryList(db.DefaultContext, issue.ID, commentID)
  46. // render history list to HTML for frontend dropdown items: (name, value)
  47. // name is HTML of "avatar + userName + userAction + timeSince"
  48. // value is historyId
  49. lang := ctx.Locale.Language()
  50. var results []map[string]interface{}
  51. for _, item := range items {
  52. var actionText string
  53. if item.IsDeleted {
  54. actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
  55. actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
  56. } else if item.IsFirstCreated {
  57. actionText = ctx.Locale.Tr("repo.issues.content_history.created")
  58. } else {
  59. actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
  60. }
  61. timeSinceText := timeutil.TimeSinceUnix(item.EditedUnix, lang)
  62. results = append(results, map[string]interface{}{
  63. "name": fmt.Sprintf("<img class='ui avatar image' src='%s'><strong>%s</strong> %s %s",
  64. html.EscapeString(item.UserAvatarLink), html.EscapeString(item.UserName), actionText, timeSinceText),
  65. "value": item.HistoryID,
  66. })
  67. }
  68. ctx.JSON(http.StatusOK, map[string]interface{}{
  69. "results": results,
  70. })
  71. }
  72. // canSoftDeleteContentHistory checks whether current user can soft-delete a history revision
  73. // Admins or owners can always delete history revisions. Normal users can only delete own history revisions.
  74. func canSoftDeleteContentHistory(ctx *context.Context, issue *models.Issue, comment *models.Comment,
  75. history *issuesModel.ContentHistory,
  76. ) bool {
  77. canSoftDelete := false
  78. if ctx.Repo.IsOwner() {
  79. canSoftDelete = true
  80. } else if ctx.Repo.CanWrite(unit.TypeIssues) {
  81. if comment == nil {
  82. // the issue poster or the history poster can soft-delete
  83. canSoftDelete = ctx.User.ID == issue.PosterID || ctx.User.ID == history.PosterID
  84. canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
  85. } else {
  86. // the comment poster or the history poster can soft-delete
  87. canSoftDelete = ctx.User.ID == comment.PosterID || ctx.User.ID == history.PosterID
  88. canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
  89. canSoftDelete = canSoftDelete && (history.CommentID == comment.ID)
  90. }
  91. }
  92. return canSoftDelete
  93. }
  94. // GetContentHistoryDetail get detail
  95. func GetContentHistoryDetail(ctx *context.Context) {
  96. issue := GetActionIssue(ctx)
  97. if issue == nil {
  98. return
  99. }
  100. historyID := ctx.FormInt64("history_id")
  101. history, prevHistory, err := issuesModel.GetIssueContentHistoryAndPrev(db.DefaultContext, historyID)
  102. if err != nil {
  103. ctx.JSON(http.StatusNotFound, map[string]interface{}{
  104. "message": "Can not find the content history",
  105. })
  106. return
  107. }
  108. // get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
  109. var comment *models.Comment
  110. if history.CommentID != 0 {
  111. var err error
  112. if comment, err = models.GetCommentByID(history.CommentID); err != nil {
  113. log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
  114. return
  115. }
  116. }
  117. // get the previous history revision (if exists)
  118. var prevHistoryID int64
  119. var prevHistoryContentText string
  120. if prevHistory != nil {
  121. prevHistoryID = prevHistory.ID
  122. prevHistoryContentText = prevHistory.ContentText
  123. }
  124. // compare the current history revision with the previous one
  125. dmp := diffmatchpatch.New()
  126. // `checklines=false` makes better diff result
  127. diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, false)
  128. diff = dmp.DiffCleanupEfficiency(diff)
  129. // use chroma to render the diff html
  130. diffHTMLBuf := bytes.Buffer{}
  131. diffHTMLBuf.WriteString("<pre class='chroma' style='tab-size: 4'>")
  132. for _, it := range diff {
  133. if it.Type == diffmatchpatch.DiffInsert {
  134. diffHTMLBuf.WriteString("<span class='gi'>")
  135. diffHTMLBuf.WriteString(html.EscapeString(it.Text))
  136. diffHTMLBuf.WriteString("</span>")
  137. } else if it.Type == diffmatchpatch.DiffDelete {
  138. diffHTMLBuf.WriteString("<span class='gd'>")
  139. diffHTMLBuf.WriteString(html.EscapeString(it.Text))
  140. diffHTMLBuf.WriteString("</span>")
  141. } else {
  142. diffHTMLBuf.WriteString(html.EscapeString(it.Text))
  143. }
  144. }
  145. diffHTMLBuf.WriteString("</pre>")
  146. ctx.JSON(http.StatusOK, map[string]interface{}{
  147. "canSoftDelete": canSoftDeleteContentHistory(ctx, issue, comment, history),
  148. "historyId": historyID,
  149. "prevHistoryId": prevHistoryID,
  150. "diffHtml": diffHTMLBuf.String(),
  151. })
  152. }
  153. // SoftDeleteContentHistory soft delete
  154. func SoftDeleteContentHistory(ctx *context.Context) {
  155. issue := GetActionIssue(ctx)
  156. if issue == nil {
  157. return
  158. }
  159. commentID := ctx.FormInt64("comment_id")
  160. historyID := ctx.FormInt64("history_id")
  161. var comment *models.Comment
  162. var history *issuesModel.ContentHistory
  163. var err error
  164. if commentID != 0 {
  165. if comment, err = models.GetCommentByID(commentID); err != nil {
  166. log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
  167. return
  168. }
  169. }
  170. if history, err = issuesModel.GetIssueContentHistoryByID(db.DefaultContext, historyID); err != nil {
  171. log.Error("can not get issue content history %v. err=%v", historyID, err)
  172. return
  173. }
  174. canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
  175. if !canSoftDelete {
  176. ctx.JSON(http.StatusForbidden, map[string]interface{}{
  177. "message": "Can not delete the content history",
  178. })
  179. return
  180. }
  181. err = issuesModel.SoftDeleteIssueContentHistory(db.DefaultContext, historyID)
  182. log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
  183. ctx.JSON(http.StatusOK, map[string]interface{}{
  184. "ok": err == nil,
  185. })
  186. }