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