gitea源码

issue_content_history.go 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "bytes"
  6. "html"
  7. "net/http"
  8. "strings"
  9. "code.gitea.io/gitea/models/avatars"
  10. issues_model "code.gitea.io/gitea/models/issues"
  11. "code.gitea.io/gitea/modules/log"
  12. "code.gitea.io/gitea/modules/setting"
  13. "code.gitea.io/gitea/modules/templates"
  14. "code.gitea.io/gitea/services/context"
  15. "github.com/sergi/go-diff/diffmatchpatch"
  16. )
  17. // GetContentHistoryOverview get overview
  18. func GetContentHistoryOverview(ctx *context.Context) {
  19. issue := GetActionIssue(ctx)
  20. if ctx.Written() {
  21. return
  22. }
  23. editedHistoryCountMap, _ := issues_model.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
  24. ctx.JSON(http.StatusOK, map[string]any{
  25. "i18n": map[string]any{
  26. "textEdited": ctx.Tr("repo.issues.content_history.edited"),
  27. "textDeleteFromHistory": ctx.Tr("repo.issues.content_history.delete_from_history"),
  28. "textDeleteFromHistoryConfirm": ctx.Tr("repo.issues.content_history.delete_from_history_confirm"),
  29. "textOptions": ctx.Tr("repo.issues.content_history.options"),
  30. },
  31. "editedHistoryCountMap": editedHistoryCountMap,
  32. })
  33. }
  34. // GetContentHistoryList get list
  35. func GetContentHistoryList(ctx *context.Context) {
  36. issue := GetActionIssue(ctx)
  37. if ctx.Written() {
  38. return
  39. }
  40. commentID := ctx.FormInt64("comment_id")
  41. items, _ := issues_model.FetchIssueContentHistoryList(ctx, issue.ID, commentID)
  42. // render history list to HTML for frontend dropdown items: (name, value)
  43. // name is HTML of "avatar + userName + userAction + timeSince"
  44. // value is historyId
  45. var results []map[string]any
  46. for _, item := range items {
  47. var actionText string
  48. if item.IsDeleted {
  49. actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
  50. actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
  51. } else if item.IsFirstCreated {
  52. actionText = ctx.Locale.TrString("repo.issues.content_history.created")
  53. } else {
  54. actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
  55. }
  56. username := item.UserName
  57. if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
  58. username = strings.TrimSpace(item.UserFullName)
  59. }
  60. src := html.EscapeString(item.UserAvatarLink)
  61. class := avatars.DefaultAvatarClass + " tw-mr-2"
  62. name := html.EscapeString(username)
  63. avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
  64. timeSinceHTML := string(templates.TimeSince(item.EditedUnix))
  65. results = append(results, map[string]any{
  66. "name": avatarHTML + "<strong>" + name + "</strong> " + actionText + " " + timeSinceHTML,
  67. "value": item.HistoryID,
  68. })
  69. }
  70. ctx.JSON(http.StatusOK, map[string]any{
  71. "results": results,
  72. })
  73. }
  74. // canSoftDeleteContentHistory checks whether current user can soft-delete a history revision
  75. // Admins or owners can always delete history revisions. Normal users can only delete own history revisions.
  76. func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue, comment *issues_model.Comment,
  77. history *issues_model.ContentHistory,
  78. ) (canSoftDelete bool) {
  79. // CanWrite means the doer can manage the issue/PR list
  80. if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  81. canSoftDelete = true
  82. } else if ctx.Doer != nil {
  83. // for read-only users, they could still post issues or comments,
  84. // they should be able to delete the history related to their own issue/comment, a case is:
  85. // 1. the user posts some sensitive data
  86. // 2. then the repo owner edits the post but didn't remove the sensitive data
  87. // 3. the poster wants to delete the edited history revision
  88. if comment == nil {
  89. // the issue poster or the history poster can soft-delete
  90. canSoftDelete = ctx.Doer.ID == issue.PosterID || ctx.Doer.ID == history.PosterID
  91. canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
  92. } else {
  93. // the comment poster or the history poster can soft-delete
  94. canSoftDelete = ctx.Doer.ID == comment.PosterID || ctx.Doer.ID == history.PosterID
  95. canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
  96. canSoftDelete = canSoftDelete && (history.CommentID == comment.ID)
  97. }
  98. }
  99. return canSoftDelete
  100. }
  101. // GetContentHistoryDetail get detail
  102. func GetContentHistoryDetail(ctx *context.Context) {
  103. issue := GetActionIssue(ctx)
  104. if ctx.Written() {
  105. return
  106. }
  107. historyID := ctx.FormInt64("history_id")
  108. history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, issue.ID, historyID)
  109. if err != nil {
  110. ctx.JSON(http.StatusNotFound, map[string]any{
  111. "message": "Can not find the content history",
  112. })
  113. return
  114. }
  115. // get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
  116. var comment *issues_model.Comment
  117. if history.CommentID != 0 {
  118. var err error
  119. if comment, err = issues_model.GetCommentByID(ctx, history.CommentID); err != nil {
  120. log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
  121. return
  122. }
  123. }
  124. // get the previous history revision (if exists)
  125. var prevHistoryID int64
  126. var prevHistoryContentText string
  127. if prevHistory != nil {
  128. prevHistoryID = prevHistory.ID
  129. prevHistoryContentText = prevHistory.ContentText
  130. }
  131. // compare the current history revision with the previous one
  132. dmp := diffmatchpatch.New()
  133. // `checklines=false` makes better diff result
  134. diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, false)
  135. diff = dmp.DiffCleanupEfficiency(diff)
  136. // use chroma to render the diff html
  137. diffHTMLBuf := bytes.Buffer{}
  138. diffHTMLBuf.WriteString("<pre class='chroma'>")
  139. for _, it := range diff {
  140. switch it.Type {
  141. case diffmatchpatch.DiffInsert:
  142. diffHTMLBuf.WriteString("<span class='gi'>")
  143. diffHTMLBuf.WriteString(html.EscapeString(it.Text))
  144. diffHTMLBuf.WriteString("</span>")
  145. case diffmatchpatch.DiffDelete:
  146. diffHTMLBuf.WriteString("<span class='gd'>")
  147. diffHTMLBuf.WriteString(html.EscapeString(it.Text))
  148. diffHTMLBuf.WriteString("</span>")
  149. default:
  150. diffHTMLBuf.WriteString(html.EscapeString(it.Text))
  151. }
  152. }
  153. diffHTMLBuf.WriteString("</pre>")
  154. ctx.JSON(http.StatusOK, map[string]any{
  155. "canSoftDelete": canSoftDeleteContentHistory(ctx, issue, comment, history),
  156. "historyId": historyID,
  157. "prevHistoryId": prevHistoryID,
  158. "diffHtml": diffHTMLBuf.String(),
  159. })
  160. }
  161. // SoftDeleteContentHistory soft delete
  162. func SoftDeleteContentHistory(ctx *context.Context) {
  163. issue := GetActionIssue(ctx)
  164. if ctx.Written() {
  165. return
  166. }
  167. if ctx.Doer == nil {
  168. ctx.NotFound(nil)
  169. return
  170. }
  171. commentID := ctx.FormInt64("comment_id")
  172. historyID := ctx.FormInt64("history_id")
  173. var comment *issues_model.Comment
  174. var history *issues_model.ContentHistory
  175. var err error
  176. if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
  177. log.Error("can not get issue content history %v. err=%v", historyID, err)
  178. return
  179. }
  180. if history.IssueID != issue.ID {
  181. ctx.NotFound(issues_model.ErrCommentNotExist{})
  182. return
  183. }
  184. if commentID != 0 {
  185. if history.CommentID != commentID {
  186. ctx.NotFound(issues_model.ErrCommentNotExist{})
  187. return
  188. }
  189. if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
  190. log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
  191. return
  192. }
  193. if comment.IssueID != issue.ID {
  194. ctx.NotFound(issues_model.ErrCommentNotExist{})
  195. return
  196. }
  197. }
  198. canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
  199. if !canSoftDelete {
  200. ctx.JSON(http.StatusForbidden, map[string]any{
  201. "message": "Can not delete the content history",
  202. })
  203. return
  204. }
  205. err = issues_model.SoftDeleteIssueContentHistory(ctx, historyID)
  206. log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
  207. ctx.JSON(http.StatusOK, map[string]any{
  208. "ok": err == nil,
  209. })
  210. }