Enhance Ghost comment mitigation Settings (#14392)
* refactor models.DeleteComment and delete related reactions too * use deleteComment for UserDeleteWithCommentsMaxDays in DeleteUser * nits * Use time.Duration as other time settings have * docs * Resolve Fixme & fix potential deadlock * Disabled by Default * Update Config Value Description * switch args * Update models/issue_comment.go Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
		
							parent
							
								
									0e2e73410e
								
							
						
					
					
						commit
						a0e424da85
					
				
					 14 changed files with 72 additions and 41 deletions
				
			
		|  | @ -688,9 +688,8 @@ AUTO_WATCH_NEW_REPOS = true | ||||||
| ; Default value for AutoWatchOnChanges | ; Default value for AutoWatchOnChanges | ||||||
| ; Make the user watch a repository When they commit for the first time | ; Make the user watch a repository When they commit for the first time | ||||||
| AUTO_WATCH_ON_CHANGES = false | AUTO_WATCH_ON_CHANGES = false | ||||||
| ; Default value for the minimum age a user has to exist before deletion to keep issue comments. | ; Minimum amount of time a user must exist before comments are kept when the user is deleted. | ||||||
| ; If a user deletes his account before that amount of days, his comments will be deleted as well. | USER_DELETE_WITH_COMMENTS_MAX_TIME = 0 | ||||||
| USER_DELETE_WITH_COMMENTS_MAX_DAYS = 0 |  | ||||||
| 
 | 
 | ||||||
| [webhook] | [webhook] | ||||||
| ; Hook task queue length, increase if webhook shooting starts hanging | ; Hook task queue length, increase if webhook shooting starts hanging | ||||||
|  |  | ||||||
|  | @ -474,7 +474,7 @@ relation to port exhaustion. | ||||||
| - `ALLOW_ONLY_EXTERNAL_REGISTRATION`: **false** Set to true to force registration only using third-party services. | - `ALLOW_ONLY_EXTERNAL_REGISTRATION`: **false** Set to true to force registration only using third-party services. | ||||||
| - `NO_REPLY_ADDRESS`: **DOMAIN** Default value for the domain part of the user's email address in the git log if he has set KeepEmailPrivate to true. | - `NO_REPLY_ADDRESS`: **DOMAIN** Default value for the domain part of the user's email address in the git log if he has set KeepEmailPrivate to true. | ||||||
|   The user's email will be replaced with a concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. |   The user's email will be replaced with a concatenation of the user name in lower case, "@" and NO_REPLY_ADDRESS. | ||||||
| - `USER_DELETE_WITH_COMMENTS_MAX_DAYS`: **0** If a user deletes his account before that amount of days, his comments will be deleted as well. | - `USER_DELETE_WITH_COMMENTS_MAX_TIME`: **0** Minimum amount of time a user must exist before comments are kept when the user is deleted. | ||||||
| 
 | 
 | ||||||
| ## SSH Minimum Key Sizes (`ssh.minimum_key_sizes`) | ## SSH Minimum Key Sizes (`ssh.minimum_key_sizes`) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -75,7 +75,7 @@ func removeStorageWithNotice(e Engine, bucket storage.ObjectStorage, title, path | ||||||
| 	if err := bucket.Delete(path); err != nil { | 	if err := bucket.Delete(path); err != nil { | ||||||
| 		desc := fmt.Sprintf("%s [%s]: %v", title, path, err) | 		desc := fmt.Sprintf("%s [%s]: %v", title, path, err) | ||||||
| 		log.Warn(title+" [%s]: %v", path, err) | 		log.Warn(title+" [%s]: %v", path, err) | ||||||
| 		if err = createNotice(x, NoticeRepository, desc); err != nil { | 		if err = createNotice(e, NoticeRepository, desc); err != nil { | ||||||
| 			log.Error("CreateRepositoryNotice: %v", err) | 			log.Error("CreateRepositoryNotice: %v", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -82,7 +82,7 @@ func isUserAssignedToIssue(e Engine, issue *Issue, user *User) (isAssigned bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ClearAssigneeByUserID deletes all assignments of an user
 | // ClearAssigneeByUserID deletes all assignments of an user
 | ||||||
| func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) { | func clearAssigneeByUserID(sess Engine, userID int64) (err error) { | ||||||
| 	_, err = sess.Delete(&IssueAssignees{AssigneeID: userID}) | 	_, err = sess.Delete(&IssueAssignees{AssigneeID: userID}) | ||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1037,33 +1037,41 @@ func UpdateComment(c *Comment, doer *User) error { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeleteComment deletes the comment
 | // DeleteComment deletes the comment
 | ||||||
| func DeleteComment(comment *Comment, doer *User) error { | func DeleteComment(comment *Comment) error { | ||||||
| 	sess := x.NewSession() | 	sess := x.NewSession() | ||||||
| 	defer sess.Close() | 	defer sess.Close() | ||||||
| 	if err := sess.Begin(); err != nil { | 	if err := sess.Begin(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := sess.Delete(&Comment{ | 	if err := deleteComment(sess, comment); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return sess.Commit() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func deleteComment(e Engine, comment *Comment) error { | ||||||
|  | 	if _, err := e.Delete(&Comment{ | ||||||
| 		ID: comment.ID, | 		ID: comment.ID, | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if comment.Type == CommentTypeComment { | 	if comment.Type == CommentTypeComment { | ||||||
| 		if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil { | 		if _, err := e.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil { | 	if _, err := e.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := comment.neuterCrossReferences(sess); err != nil { | 	if err := comment.neuterCrossReferences(e); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return sess.Commit() | 	return deleteReaction(e, &ReactionOptions{Comment: comment}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
 | // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
 | ||||||
|  |  | ||||||
|  | @ -178,11 +178,15 @@ func CreateCommentReaction(doer *User, issue *Issue, comment *Comment, content s | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func deleteReaction(e *xorm.Session, opts *ReactionOptions) error { | func deleteReaction(e Engine, opts *ReactionOptions) error { | ||||||
| 	reaction := &Reaction{ | 	reaction := &Reaction{ | ||||||
| 		Type: opts.Type, | 		Type: opts.Type, | ||||||
| 		UserID:  opts.Doer.ID, | 	} | ||||||
| 		IssueID: opts.Issue.ID, | 	if opts.Doer != nil { | ||||||
|  | 		reaction.UserID = opts.Doer.ID | ||||||
|  | 	} | ||||||
|  | 	if opts.Issue != nil { | ||||||
|  | 		reaction.IssueID = opts.Issue.ID | ||||||
| 	} | 	} | ||||||
| 	if opts.Comment != nil { | 	if opts.Comment != nil { | ||||||
| 		reaction.CommentID = opts.Comment.ID | 		reaction.CommentID = opts.Comment.ID | ||||||
|  |  | ||||||
|  | @ -38,7 +38,6 @@ import ( | ||||||
| 	"golang.org/x/crypto/scrypt" | 	"golang.org/x/crypto/scrypt" | ||||||
| 	"golang.org/x/crypto/ssh" | 	"golang.org/x/crypto/ssh" | ||||||
| 	"xorm.io/builder" | 	"xorm.io/builder" | ||||||
| 	"xorm.io/xorm" |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // UserType defines the user type
 | // UserType defines the user type
 | ||||||
|  | @ -1071,8 +1070,7 @@ func deleteBeans(e Engine, beans ...interface{}) (err error) { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FIXME: need some kind of mechanism to record failure. HINT: system notice
 | func deleteUser(e Engine, u *User) error { | ||||||
| func deleteUser(e *xorm.Session, u *User) error { |  | ||||||
| 	// Note: A user owns any repository or belongs to any organization
 | 	// Note: A user owns any repository or belongs to any organization
 | ||||||
| 	//	cannot perform delete operation.
 | 	//	cannot perform delete operation.
 | ||||||
| 
 | 
 | ||||||
|  | @ -1151,12 +1149,30 @@ func deleteUser(e *xorm.Session, u *User) error { | ||||||
| 		return fmt.Errorf("deleteBeans: %v", err) | 		return fmt.Errorf("deleteBeans: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if setting.Service.UserDeleteWithCommentsMaxDays != 0 && | 	if setting.Service.UserDeleteWithCommentsMaxTime != 0 && | ||||||
| 		u.CreatedUnix.AsTime().Add(time.Duration(setting.Service.UserDeleteWithCommentsMaxDays)*24*time.Hour).After(time.Now()) { | 		u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now()) { | ||||||
| 		if err = deleteBeans(e, | 
 | ||||||
| 			&Comment{PosterID: u.ID}, | 		// Delete Comments
 | ||||||
| 		); err != nil { | 		const batchSize = 50 | ||||||
| 			return fmt.Errorf("deleteBeans: %v", err) | 		for start := 0; ; start += batchSize { | ||||||
|  | 			comments := make([]*Comment, 0, batchSize) | ||||||
|  | 			if err = e.Where("type=? AND poster_id=?", CommentTypeComment, u.ID).Limit(batchSize, start).Find(&comments); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			if len(comments) == 0 { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			for _, comment := range comments { | ||||||
|  | 				if err = deleteComment(e, comment); err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Delete Reactions
 | ||||||
|  | 		if err = deleteReaction(e, &ReactionOptions{Doer: u}); err != nil { | ||||||
|  | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -1195,18 +1211,21 @@ func deleteUser(e *xorm.Session, u *User) error { | ||||||
| 		return fmt.Errorf("Delete: %v", err) | 		return fmt.Errorf("Delete: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// FIXME: system notice
 |  | ||||||
| 	// Note: There are something just cannot be roll back,
 | 	// Note: There are something just cannot be roll back,
 | ||||||
| 	//	so just keep error logs of those operations.
 | 	//	so just keep error logs of those operations.
 | ||||||
| 	path := UserPath(u.Name) | 	path := UserPath(u.Name) | ||||||
| 	if err := util.RemoveAll(path); err != nil { | 	if err = util.RemoveAll(path); err != nil { | ||||||
| 		return fmt.Errorf("Failed to RemoveAll %s: %v", path, err) | 		err = fmt.Errorf("Failed to RemoveAll %s: %v", path, err) | ||||||
|  | 		_ = createNotice(e, NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) | ||||||
|  | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(u.Avatar) > 0 { | 	if len(u.Avatar) > 0 { | ||||||
| 		avatarPath := u.CustomAvatarRelativePath() | 		avatarPath := u.CustomAvatarRelativePath() | ||||||
| 		if err := storage.Avatars.Delete(avatarPath); err != nil { | 		if err = storage.Avatars.Delete(avatarPath); err != nil { | ||||||
| 			return fmt.Errorf("Failed to remove %s: %v", avatarPath, err) | 			err = fmt.Errorf("Failed to remove %s: %v", avatarPath, err) | ||||||
|  | 			_ = createNotice(e, NoticeTask, fmt.Sprintf("delete user '%s': %v", u.Name, err)) | ||||||
|  | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ package setting | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"regexp" | 	"regexp" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
| ) | ) | ||||||
|  | @ -50,7 +51,7 @@ var Service struct { | ||||||
| 	AutoWatchNewRepos                       bool | 	AutoWatchNewRepos                       bool | ||||||
| 	AutoWatchOnChanges                      bool | 	AutoWatchOnChanges                      bool | ||||||
| 	DefaultOrgMemberVisible                 bool | 	DefaultOrgMemberVisible                 bool | ||||||
| 	UserDeleteWithCommentsMaxDays           int | 	UserDeleteWithCommentsMaxTime           time.Duration | ||||||
| 
 | 
 | ||||||
| 	// OpenID settings
 | 	// OpenID settings
 | ||||||
| 	EnableOpenIDSignIn bool | 	EnableOpenIDSignIn bool | ||||||
|  | @ -103,7 +104,7 @@ func newService() { | ||||||
| 	Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) | 	Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) | ||||||
| 	Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] | 	Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] | ||||||
| 	Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() | 	Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() | ||||||
| 	Service.UserDeleteWithCommentsMaxDays = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_DAYS").MustInt(0) | 	Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0) | ||||||
| 
 | 
 | ||||||
| 	sec = Cfg.Section("openid") | 	sec = Cfg.Section("openid") | ||||||
| 	Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock) | 	Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock) | ||||||
|  |  | ||||||
|  | @ -648,7 +648,7 @@ repos_none = You do not own any repositories | ||||||
| 
 | 
 | ||||||
| delete_account = Delete Your Account | delete_account = Delete Your Account | ||||||
| delete_prompt = This operation will permanently delete your user account. It <strong>CAN NOT</strong> be undone. | delete_prompt = This operation will permanently delete your user account. It <strong>CAN NOT</strong> be undone. | ||||||
| delete_with_all_comments = Your account is younger than %d days. To avoid ghost comments, all issue/PR comments will be deleted with it. | delete_with_all_comments = Your account is younger than %s. To avoid ghost comments, all issue/PR comments will be deleted with it. | ||||||
| confirm_delete_account = Confirm Deletion | confirm_delete_account = Confirm Deletion | ||||||
| delete_account_title = Delete User Account | delete_account_title = Delete User Account | ||||||
| delete_account_desc = Are you sure you want to permanently delete this user account? | delete_account_desc = Are you sure you want to permanently delete this user account? | ||||||
|  |  | ||||||
|  | @ -509,7 +509,7 @@ func deleteIssueComment(ctx *context.APIContext) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = comment_service.DeleteComment(comment, ctx.User); err != nil { | 	if err = comment_service.DeleteComment(ctx.User, comment); err != nil { | ||||||
| 		ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) | 		ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -2125,7 +2125,7 @@ func DeleteComment(ctx *context.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = comment_service.DeleteComment(comment, ctx.User); err != nil { | 	if err = comment_service.DeleteComment(ctx.User, comment); err != nil { | ||||||
| 		ctx.ServerError("DeleteCommentByID", err) | 		ctx.ServerError("DeleteCommentByID", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -302,8 +302,8 @@ func loadAccountData(ctx *context.Context) { | ||||||
| 	ctx.Data["ActivationsPending"] = pendingActivation | 	ctx.Data["ActivationsPending"] = pendingActivation | ||||||
| 	ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm | 	ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm | ||||||
| 
 | 
 | ||||||
| 	if setting.Service.UserDeleteWithCommentsMaxDays != 0 { | 	if setting.Service.UserDeleteWithCommentsMaxTime != 0 { | ||||||
| 		ctx.Data["UserDeleteWithCommentsMaxDays"] = setting.Service.UserDeleteWithCommentsMaxDays | 		ctx.Data["UserDeleteWithCommentsMaxTime"] = setting.Service.UserDeleteWithCommentsMaxTime.String() | ||||||
| 		ctx.Data["UserDeleteWithComments"] = ctx.User.CreatedUnix.AsTime().Add(time.Duration(setting.Service.UserDeleteWithCommentsMaxDays) * 24 * time.Hour).After(time.Now()) | 		ctx.Data["UserDeleteWithComments"] = ctx.User.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now()) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -43,8 +43,8 @@ func UpdateComment(c *models.Comment, doer *models.User, oldContent string) erro | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DeleteComment deletes the comment
 | // DeleteComment deletes the comment
 | ||||||
| func DeleteComment(comment *models.Comment, doer *models.User) error { | func DeleteComment(doer *models.User, comment *models.Comment) error { | ||||||
| 	if err := models.DeleteComment(comment, doer); err != nil { | 	if err := models.DeleteComment(comment); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -174,7 +174,7 @@ | ||||||
| 			<div class="ui red message"> | 			<div class="ui red message"> | ||||||
| 				<p class="text left">{{svg "octicon-alert"}} {{.i18n.Tr "settings.delete_prompt" | Str2html}}</p> | 				<p class="text left">{{svg "octicon-alert"}} {{.i18n.Tr "settings.delete_prompt" | Str2html}}</p> | ||||||
| 				{{ if .UserDeleteWithComments }} | 				{{ if .UserDeleteWithComments }} | ||||||
| 				<p class="text left" style="font-weight: bold;">{{.i18n.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxDays | Str2html}}</p> | 				<p class="text left" style="font-weight: bold;">{{.i18n.Tr "settings.delete_with_all_comments" .UserDeleteWithCommentsMaxTime | Str2html}}</p> | ||||||
| 				{{ end }} | 				{{ end }} | ||||||
| 			</div> | 			</div> | ||||||
| 			<form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post"> | 			<form class="ui form ignore-dirty" id="delete-form" action="{{AppSubUrl}}/user/settings/account/delete" method="post"> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue