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  }