diff options
Diffstat (limited to 'routers/web/repo/issue_content_history.go')
-rw-r--r-- | routers/web/repo/issue_content_history.go | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go new file mode 100644 index 0000000..16b250a --- /dev/null +++ b/routers/web/repo/issue_content_history.go @@ -0,0 +1,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, + }) +} |