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