2021-10-10 16:40:03 -06:00
|
|
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a MIT-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package repo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"html"
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
"code.gitea.io/gitea/models"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
|
|
issuesModel "code.gitea.io/gitea/models/issues"
|
2021-11-09 12:57:58 -07:00
|
|
|
"code.gitea.io/gitea/models/unit"
|
2021-10-10 16:40:03 -06:00
|
|
|
"code.gitea.io/gitea/modules/context"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
|
|
|
|
|
|
"github.com/sergi/go-diff/diffmatchpatch"
|
|
|
|
"github.com/unknwon/i18n"
|
|
|
|
)
|
|
|
|
|
|
|
|
// GetContentHistoryOverview get overview
|
|
|
|
func GetContentHistoryOverview(ctx *context.Context) {
|
|
|
|
issue := GetActionIssue(ctx)
|
|
|
|
if issue == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
lang := ctx.Data["Lang"].(string)
|
|
|
|
editedHistoryCountMap, _ := issuesModel.QueryIssueContentHistoryEditedCountMap(db.DefaultContext, issue.ID)
|
|
|
|
ctx.JSON(http.StatusOK, map[string]interface{}{
|
|
|
|
"i18n": map[string]interface{}{
|
|
|
|
"textEdited": i18n.Tr(lang, "repo.issues.content_history.edited"),
|
|
|
|
"textDeleteFromHistory": i18n.Tr(lang, "repo.issues.content_history.delete_from_history"),
|
|
|
|
"textDeleteFromHistoryConfirm": i18n.Tr(lang, "repo.issues.content_history.delete_from_history_confirm"),
|
|
|
|
"textOptions": i18n.Tr(lang, "repo.issues.content_history.options"),
|
|
|
|
},
|
|
|
|
"editedHistoryCountMap": editedHistoryCountMap,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetContentHistoryList get list
|
|
|
|
func GetContentHistoryList(ctx *context.Context) {
|
|
|
|
issue := GetActionIssue(ctx)
|
|
|
|
commentID := ctx.FormInt64("comment_id")
|
|
|
|
if issue == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
items, _ := issuesModel.FetchIssueContentHistoryList(db.DefaultContext, 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
|
|
|
|
lang := ctx.Data["Lang"].(string)
|
|
|
|
var results []map[string]interface{}
|
|
|
|
for _, item := range items {
|
|
|
|
var actionText string
|
|
|
|
if item.IsDeleted {
|
|
|
|
actionTextDeleted := i18n.Tr(lang, "repo.issues.content_history.deleted")
|
|
|
|
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
|
|
|
|
} else if item.IsFirstCreated {
|
|
|
|
actionText = i18n.Tr(lang, "repo.issues.content_history.created")
|
|
|
|
} else {
|
|
|
|
actionText = i18n.Tr(lang, "repo.issues.content_history.edited")
|
|
|
|
}
|
|
|
|
timeSinceText := timeutil.TimeSinceUnix(item.EditedUnix, lang)
|
|
|
|
results = append(results, map[string]interface{}{
|
|
|
|
"name": fmt.Sprintf("<img class='ui avatar image' src='%s'><strong>%s</strong> %s %s",
|
|
|
|
html.EscapeString(item.UserAvatarLink), html.EscapeString(item.UserName), actionText, timeSinceText),
|
|
|
|
"value": item.HistoryID,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.JSON(http.StatusOK, map[string]interface{}{
|
|
|
|
"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 *models.Issue, comment *models.Comment,
|
2022-01-20 10:46:10 -07:00
|
|
|
history *issuesModel.ContentHistory,
|
|
|
|
) bool {
|
2021-10-10 16:40:03 -06:00
|
|
|
canSoftDelete := false
|
|
|
|
if ctx.Repo.IsOwner() {
|
|
|
|
canSoftDelete = true
|
2021-11-09 12:57:58 -07:00
|
|
|
} else if ctx.Repo.CanWrite(unit.TypeIssues) {
|
2021-10-10 16:40:03 -06:00
|
|
|
if comment == nil {
|
2021-10-23 08:47:38 -06:00
|
|
|
// the issue poster or the history poster can soft-delete
|
|
|
|
canSoftDelete = ctx.User.ID == issue.PosterID || ctx.User.ID == history.PosterID
|
2021-10-10 16:40:03 -06:00
|
|
|
canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
|
|
|
|
} else {
|
2021-10-23 08:47:38 -06:00
|
|
|
// the comment poster or the history poster can soft-delete
|
|
|
|
canSoftDelete = ctx.User.ID == comment.PosterID || ctx.User.ID == history.PosterID
|
2021-10-10 16:40:03 -06:00
|
|
|
canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
|
|
|
|
canSoftDelete = canSoftDelete && (history.CommentID == comment.ID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return canSoftDelete
|
|
|
|
}
|
|
|
|
|
2022-01-20 10:46:10 -07:00
|
|
|
// GetContentHistoryDetail get detail
|
2021-10-10 16:40:03 -06:00
|
|
|
func GetContentHistoryDetail(ctx *context.Context) {
|
|
|
|
issue := GetActionIssue(ctx)
|
|
|
|
if issue == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
historyID := ctx.FormInt64("history_id")
|
|
|
|
history, prevHistory, err := issuesModel.GetIssueContentHistoryAndPrev(db.DefaultContext, historyID)
|
|
|
|
if err != nil {
|
|
|
|
ctx.JSON(http.StatusNotFound, map[string]interface{}{
|
|
|
|
"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 *models.Comment
|
|
|
|
if history.CommentID != 0 {
|
|
|
|
var err error
|
|
|
|
if comment, err = models.GetCommentByID(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()
|
2021-10-23 08:47:38 -06:00
|
|
|
// `checklines=false` makes better diff result
|
|
|
|
diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, false)
|
2021-10-10 16:40:03 -06:00
|
|
|
diff = dmp.DiffCleanupEfficiency(diff)
|
|
|
|
|
|
|
|
// use chroma to render the diff html
|
|
|
|
diffHTMLBuf := bytes.Buffer{}
|
|
|
|
diffHTMLBuf.WriteString("<pre class='chroma' style='tab-size: 4'>")
|
|
|
|
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]interface{}{
|
|
|
|
"canSoftDelete": canSoftDeleteContentHistory(ctx, issue, comment, history),
|
|
|
|
"historyId": historyID,
|
|
|
|
"prevHistoryId": prevHistoryID,
|
|
|
|
"diffHtml": diffHTMLBuf.String(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-01-20 10:46:10 -07:00
|
|
|
// SoftDeleteContentHistory soft delete
|
2021-10-10 16:40:03 -06:00
|
|
|
func SoftDeleteContentHistory(ctx *context.Context) {
|
|
|
|
issue := GetActionIssue(ctx)
|
|
|
|
if issue == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
commentID := ctx.FormInt64("comment_id")
|
|
|
|
historyID := ctx.FormInt64("history_id")
|
|
|
|
|
|
|
|
var comment *models.Comment
|
|
|
|
var history *issuesModel.ContentHistory
|
|
|
|
var err error
|
|
|
|
if commentID != 0 {
|
|
|
|
if comment, err = models.GetCommentByID(commentID); err != nil {
|
|
|
|
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if history, err = issuesModel.GetIssueContentHistoryByID(db.DefaultContext, historyID); err != nil {
|
|
|
|
log.Error("can not get issue content history %v. err=%v", historyID, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
|
|
|
|
if !canSoftDelete {
|
|
|
|
ctx.JSON(http.StatusForbidden, map[string]interface{}{
|
|
|
|
"message": "Can not delete the content history",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = issuesModel.SoftDeleteIssueContentHistory(db.DefaultContext, historyID)
|
|
|
|
log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
|
|
|
|
ctx.JSON(http.StatusOK, map[string]interface{}{
|
|
|
|
"ok": err == nil,
|
|
|
|
})
|
|
|
|
}
|