summaryrefslogtreecommitdiffstats
path: root/routers/web/repo/issue_content_history.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /routers/web/repo/issue_content_history.go
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'routers/web/repo/issue_content_history.go')
-rw-r--r--routers/web/repo/issue_content_history.go237
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,
+ })
+}