mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 23:54:25 +01:00 
			
		
		
		
	Show missing full names when configured to do so Co-authored-by: zeripath <art27@cantab.net>
		
			
				
	
	
		
			218 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			218 lines
		
	
	
		
			7.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"code.gitea.io/gitea/models"
 | 
						|
	"code.gitea.io/gitea/models/db"
 | 
						|
	issuesModel "code.gitea.io/gitea/models/issues"
 | 
						|
	"code.gitea.io/gitea/models/unit"
 | 
						|
	"code.gitea.io/gitea/modules/context"
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
	"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.Locale.Language()
 | 
						|
	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.Locale.Language()
 | 
						|
	var results []map[string]interface{}
 | 
						|
	for _, item := range items {
 | 
						|
		var actionText string
 | 
						|
		if item.IsDeleted {
 | 
						|
			actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
 | 
						|
			actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
 | 
						|
		} else if item.IsFirstCreated {
 | 
						|
			actionText = ctx.Locale.Tr("repo.issues.content_history.created")
 | 
						|
		} else {
 | 
						|
			actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
 | 
						|
		}
 | 
						|
		timeSinceText := timeutil.TimeSinceUnix(item.EditedUnix, lang)
 | 
						|
 | 
						|
		username := item.UserName
 | 
						|
		if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
 | 
						|
			username = strings.TrimSpace(item.UserFullName)
 | 
						|
		}
 | 
						|
 | 
						|
		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(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,
 | 
						|
	history *issuesModel.ContentHistory,
 | 
						|
) bool {
 | 
						|
	canSoftDelete := false
 | 
						|
	if ctx.Repo.IsOwner() {
 | 
						|
		canSoftDelete = true
 | 
						|
	} else if ctx.Repo.CanWrite(unit.TypeIssues) {
 | 
						|
		if comment == nil {
 | 
						|
			// the issue poster or the history poster can soft-delete
 | 
						|
			canSoftDelete = ctx.User.ID == issue.PosterID || ctx.User.ID == history.PosterID
 | 
						|
			canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
 | 
						|
		} else {
 | 
						|
			// the comment poster or the history poster can soft-delete
 | 
						|
			canSoftDelete = ctx.User.ID == comment.PosterID || ctx.User.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 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()
 | 
						|
	// `checklines=false` makes better diff result
 | 
						|
	diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, false)
 | 
						|
	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(),
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// SoftDeleteContentHistory soft delete
 | 
						|
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,
 | 
						|
	})
 | 
						|
}
 |