Merge remote-tracking branch 'origin/fix/2.0.1/chat' into test
This commit is contained in:
+71
-3
@@ -211,9 +211,27 @@ func createMainGoFile(b *buildingMaterial) (err error) {
|
||||
|
||||
// downloadGoModFile run go mod commands to download dependencies
|
||||
func downloadGoModFile(b *buildingMaterial) (err error) {
|
||||
// If user specify a module replacement, use it. Otherwise, use the latest version.
|
||||
if len(b.answerModuleReplacement) > 0 {
|
||||
replacement := fmt.Sprintf("%s=%s", "github.com/apache/answer", b.answerModuleReplacement)
|
||||
answerReplacement := b.answerModuleReplacement
|
||||
|
||||
// If no replacement specified and current binary is v2+, auto-determine replacement.
|
||||
// This is needed because go mod tidy would otherwise resolve github.com/apache/answer
|
||||
// to the latest v1.x version, causing v2+ features (e.g. AI/MCP) to disappear.
|
||||
if len(answerReplacement) == 0 && b.originalAnswerInfo.Version != "" {
|
||||
ver, verErr := semver.NewVersion(strings.TrimPrefix(b.originalAnswerInfo.Version, "v"))
|
||||
if verErr == nil && ver.Major() >= 2 {
|
||||
answerReplacement = fmt.Sprintf("github.com/apache/answer@%s", b.originalAnswerInfo.Version)
|
||||
}
|
||||
}
|
||||
|
||||
if len(answerReplacement) > 0 {
|
||||
// For v2+ versioned module paths (e.g. github.com/apache/answer@v2.0.0),
|
||||
// go mod tidy rejects the version because the module path lacks a /v2 suffix.
|
||||
// Work around this by cloning the repo locally and using a local path replacement.
|
||||
localPath, resolveErr := resolveAnswerModuleReplacement(answerReplacement, b.tmpDir)
|
||||
if resolveErr != nil {
|
||||
return resolveErr
|
||||
}
|
||||
replacement := fmt.Sprintf("%s=%s", "github.com/apache/answer", localPath)
|
||||
err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -232,6 +250,56 @@ func downloadGoModFile(b *buildingMaterial) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// resolveAnswerModuleReplacement resolves the ANSWER_MODULE value to a usable local path or
|
||||
// remote replacement string. For v2+ versioned module paths (e.g. github.com/apache/answer@v2.0.0),
|
||||
// Go module system rejects the version because the module path has no /v2 suffix. In that case
|
||||
// the repository is cloned locally and the local path is returned instead.
|
||||
func resolveAnswerModuleReplacement(replacement, tmpDir string) (string, error) {
|
||||
// Local paths can be used as-is.
|
||||
if strings.HasPrefix(replacement, "/") || strings.HasPrefix(replacement, "./") || strings.HasPrefix(replacement, "../") {
|
||||
return replacement, nil
|
||||
}
|
||||
|
||||
// Parse module@version format.
|
||||
moduleName, version, hasVersion := strings.Cut(replacement, "@")
|
||||
if !hasVersion {
|
||||
return replacement, nil
|
||||
}
|
||||
|
||||
// Only handle v2+ versions on module paths without the /vN suffix.
|
||||
ver, err := semver.StrictNewVersion(strings.TrimPrefix(version, "v"))
|
||||
if err != nil || ver.Major() < 2 {
|
||||
return replacement, nil
|
||||
}
|
||||
if strings.HasSuffix(moduleName, fmt.Sprintf("/v%d", ver.Major())) {
|
||||
return replacement, nil
|
||||
}
|
||||
|
||||
// Clone the repo to a local directory and return its path.
|
||||
gitURL := "https://" + moduleName
|
||||
tag := "v" + strings.TrimPrefix(version, "v")
|
||||
localPath := filepath.Join(filepath.Dir(tmpDir), fmt.Sprintf("answer_src_%s", strings.ReplaceAll(version, ".", "_")))
|
||||
|
||||
if _, statErr := os.Stat(localPath); statErr == nil {
|
||||
fmt.Printf("[build] using cached local clone at %s\n", localPath)
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
fmt.Printf("[build] v2+ module detected, cloning %s@%s to local path %s...\n", moduleName, version, localPath)
|
||||
cloneCmd := exec.Command("git", "clone", "--depth=1", "--branch="+tag, gitURL, localPath)
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
if err = cloneCmd.Run(); err != nil {
|
||||
return "", fmt.Errorf(
|
||||
"failed to clone %s@%s: %w\nTip: set ANSWER_MODULE to a local checkout path instead, e.g. ANSWER_MODULE=/path/to/answer",
|
||||
moduleName, version, err,
|
||||
)
|
||||
}
|
||||
|
||||
fmt.Printf("[build] successfully cloned to %s\n", localPath)
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
// movePluginToVendor move plugin to vendor dir
|
||||
// Traverse the plugins, and if the plugin path is not github.com/apache/answer-plugins, move the contents of the current plugin to the vendor/github.com/apache/answer-plugins/ directory.
|
||||
func movePluginToVendor(b *buildingMaterial) (err error) {
|
||||
|
||||
@@ -164,8 +164,9 @@ func (ac *AnswerController) GetAnswerInfo(ctx *gin.Context) {
|
||||
id := ctx.Query("id")
|
||||
id = uid.DeShortID(id)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
isAdminModerator := middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID)
|
||||
info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID, isAdminModerator)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, gin.H{})
|
||||
return
|
||||
@@ -271,7 +272,7 @@ func (ac *AnswerController) AddAnswer(ctx *gin.Context) {
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionAnswer, req.UserID)
|
||||
}
|
||||
info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID)
|
||||
info, questionInfo, has, err := ac.answerService.Get(ctx, answerID, req.UserID, isAdmin)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
@@ -348,7 +349,7 @@ func (ac *AnswerController) UpdateAnswer(ctx *gin.Context) {
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
ac.actionService.ActionRecordAdd(ctx, entity.CaptchaActionEdit, req.UserID)
|
||||
}
|
||||
_, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID)
|
||||
_, _, _, err = ac.answerService.Get(ctx, req.ID, req.UserID, isAdmin)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
@@ -376,6 +377,7 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
|
||||
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.AnswerEdit,
|
||||
|
||||
@@ -248,6 +248,7 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
|
||||
req.ObjectID = uid.DeShortID(req.ObjectID)
|
||||
req.CommentID = uid.DeShortID(req.CommentID)
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentEdit,
|
||||
permission.CommentDelete,
|
||||
@@ -300,6 +301,7 @@ func (cc *CommentController) GetComment(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
|
||||
permission.CommentEdit,
|
||||
permission.CommentDelete,
|
||||
|
||||
@@ -234,6 +234,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
||||
id = uid.DeShortID(id)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
req := schema.QuestionPermission{}
|
||||
req.IsAdminModerator = middleware.GetUserIsAdminModerator(ctx)
|
||||
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
|
||||
permission.QuestionEdit,
|
||||
permission.QuestionDelete,
|
||||
@@ -590,7 +591,7 @@ func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID)
|
||||
info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID, isAdmin)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
|
||||
@@ -106,15 +106,16 @@ type AnswerUpdateResp struct {
|
||||
}
|
||||
|
||||
type AnswerListReq struct {
|
||||
QuestionID string `json:"question_id" form:"question_id"`
|
||||
Order string `json:"order" form:"order"`
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
UserID string `json:"-"`
|
||||
IsAdmin bool `json:"-"`
|
||||
CanEdit bool `json:"-"`
|
||||
CanDelete bool `json:"-"`
|
||||
CanRecover bool `json:"-"`
|
||||
QuestionID string `json:"question_id" form:"question_id"`
|
||||
Order string `json:"order" form:"order"`
|
||||
Page int `json:"page" form:"page"`
|
||||
PageSize int `json:"page_size" form:"page_size"`
|
||||
UserID string `json:"-"`
|
||||
IsAdmin bool `json:"-"`
|
||||
IsAdminModerator bool `json:"-"`
|
||||
CanEdit bool `json:"-"`
|
||||
CanDelete bool `json:"-"`
|
||||
CanRecover bool `json:"-"`
|
||||
}
|
||||
|
||||
type AnswerInfo struct {
|
||||
|
||||
@@ -150,7 +150,8 @@ type GetCommentWithPageReq struct {
|
||||
// query condition
|
||||
QueryCond string `validate:"omitempty,oneof=vote created_at" form:"query_cond"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
IsAdminModerator bool `json:"-"`
|
||||
// whether user can edit it
|
||||
CanEdit bool `json:"-"`
|
||||
// whether user can delete it
|
||||
@@ -162,7 +163,8 @@ type GetCommentReq struct {
|
||||
// object id
|
||||
ID string `validate:"required" form:"id"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
IsAdminModerator bool `json:"-"`
|
||||
// whether user can edit it
|
||||
CanEdit bool `json:"-"`
|
||||
// whether user can delete it
|
||||
|
||||
@@ -143,6 +143,7 @@ func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField,
|
||||
}
|
||||
|
||||
type QuestionPermission struct {
|
||||
IsAdminModerator bool `json:"-"`
|
||||
// whether user can add it
|
||||
CanAdd bool `json:"-"`
|
||||
// whether user can edit it
|
||||
|
||||
@@ -21,25 +21,28 @@ package schema
|
||||
|
||||
import (
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// SimpleObjectInfo simple object info
|
||||
type SimpleObjectInfo struct {
|
||||
ObjectID string `json:"object_id"`
|
||||
ObjectCreatorUserID string `json:"object_creator_user_id"`
|
||||
QuestionID string `json:"question_id"`
|
||||
QuestionStatus int `json:"question_status"`
|
||||
QuestionShow int `json:"question_show"`
|
||||
AnswerID string `json:"answer_id"`
|
||||
AnswerStatus int `json:"answer_status"`
|
||||
CommentID string `json:"comment_id"`
|
||||
CommentStatus int `json:"comment_status"`
|
||||
TagID string `json:"tag_id"`
|
||||
TagStatus int `json:"tag_status"`
|
||||
ObjectType string `json:"object_type"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
ObjectID string `json:"object_id"`
|
||||
ObjectCreatorUserID string `json:"object_creator_user_id"`
|
||||
QuestionID string `json:"question_id"`
|
||||
QuestionCreatorUserID string `json:"question_creator_user_id"`
|
||||
QuestionStatus int `json:"question_status"`
|
||||
QuestionShow int `json:"question_show"`
|
||||
AnswerID string `json:"answer_id"`
|
||||
AnswerStatus int `json:"answer_status"`
|
||||
CommentID string `json:"comment_id"`
|
||||
CommentStatus int `json:"comment_status"`
|
||||
TagID string `json:"tag_id"`
|
||||
TagStatus int `json:"tag_status"`
|
||||
ObjectType string `json:"object_type"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// IsDeleted is deleted
|
||||
@@ -57,6 +60,88 @@ func (s *SimpleObjectInfo) IsDeleted() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SimpleObjectInfo) CheckVisibility(userID string, isAdminModerator bool) error {
|
||||
if s == nil {
|
||||
return errors.NotFound(reason.ObjectNotFound)
|
||||
}
|
||||
if s.isObjectRestricted() && !s.canViewObject(userID, isAdminModerator) {
|
||||
return errors.NotFound(s.objectNotFoundReason())
|
||||
}
|
||||
if s.hasParentQuestion() && s.isParentQuestionRestricted() &&
|
||||
!s.canViewParentQuestion(userID, isAdminModerator) {
|
||||
return errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SimpleObjectInfo) canViewObject(userID string, isAdminModerator bool) bool {
|
||||
if isAdminModerator {
|
||||
return true
|
||||
}
|
||||
switch s.ObjectType {
|
||||
case constant.QuestionObjectType:
|
||||
return s.QuestionCreatorUserID == userID
|
||||
case constant.AnswerObjectType, constant.CommentObjectType, constant.TagObjectType:
|
||||
return s.ObjectCreatorUserID == userID
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SimpleObjectInfo) canViewParentQuestion(userID string, isAdminModerator bool) bool {
|
||||
if isAdminModerator {
|
||||
return true
|
||||
}
|
||||
return s.QuestionCreatorUserID == userID
|
||||
}
|
||||
|
||||
func (s *SimpleObjectInfo) hasParentQuestion() bool {
|
||||
switch s.ObjectType {
|
||||
case constant.AnswerObjectType, constant.CommentObjectType:
|
||||
return len(s.QuestionID) > 0 && s.QuestionID != "0"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SimpleObjectInfo) isObjectRestricted() bool {
|
||||
switch s.ObjectType {
|
||||
case constant.QuestionObjectType:
|
||||
return s.QuestionStatus == entity.QuestionStatusDeleted ||
|
||||
s.QuestionStatus == entity.QuestionStatusPending ||
|
||||
s.QuestionShow == entity.QuestionHide
|
||||
case constant.AnswerObjectType:
|
||||
return s.AnswerStatus == entity.AnswerStatusDeleted || s.AnswerStatus == entity.AnswerStatusPending
|
||||
case constant.CommentObjectType:
|
||||
return s.CommentStatus == entity.CommentStatusDeleted || s.CommentStatus == entity.CommentStatusPending
|
||||
case constant.TagObjectType:
|
||||
return s.TagStatus == entity.TagStatusDeleted
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SimpleObjectInfo) isParentQuestionRestricted() bool {
|
||||
return s.QuestionStatus == entity.QuestionStatusDeleted ||
|
||||
s.QuestionStatus == entity.QuestionStatusPending ||
|
||||
s.QuestionShow == entity.QuestionHide
|
||||
}
|
||||
|
||||
func (s *SimpleObjectInfo) objectNotFoundReason() string {
|
||||
switch s.ObjectType {
|
||||
case constant.QuestionObjectType:
|
||||
return reason.QuestionNotFound
|
||||
case constant.AnswerObjectType:
|
||||
return reason.AnswerNotFound
|
||||
case constant.CommentObjectType:
|
||||
return reason.CommentNotFound
|
||||
case constant.TagObjectType:
|
||||
return reason.TagNotFound
|
||||
default:
|
||||
return reason.ObjectNotFound
|
||||
}
|
||||
}
|
||||
|
||||
type UnreviewedRevisionInfoInfo struct {
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
ObjectID string `json:"object_id"`
|
||||
|
||||
@@ -348,6 +348,13 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.CommentNotFound)
|
||||
}
|
||||
objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = &schema.GetCommentResp{
|
||||
CommentID: comment.ID,
|
||||
@@ -399,6 +406,13 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
|
||||
// GetCommentWithPage get comment list page
|
||||
func (cs *CommentService) GetCommentWithPage(ctx context.Context, req *schema.GetCommentWithPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
objInfo, err := cs.objectInfoService.GetInfo(ctx, req.ObjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := objInfo.CheckVisibility(req.UserID, req.IsAdminModerator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := &CommentQuery{
|
||||
PageCond: pager.PageCond{Page: req.Page, PageSize: req.PageSize},
|
||||
ObjectID: req.ObjectID,
|
||||
|
||||
@@ -533,11 +533,27 @@ func (as *AnswerService) updateAnswerRank(ctx context.Context, userID string,
|
||||
}
|
||||
}
|
||||
|
||||
func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) {
|
||||
func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string, isAdminModerator bool) (*schema.AnswerInfo, *schema.QuestionInfoResp, bool, error) {
|
||||
answerInfo, has, err := as.answerRepo.GetByID(ctx, answerID)
|
||||
if err != nil {
|
||||
return nil, nil, has, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil, false, nil
|
||||
}
|
||||
question, exist, err := as.questionRepo.GetQuestion(ctx, answerInfo.QuestionID)
|
||||
if err != nil {
|
||||
return nil, nil, has, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
|
||||
}
|
||||
if (question.Status == entity.QuestionStatusDeleted ||
|
||||
question.Status == entity.QuestionStatusPending ||
|
||||
question.Show == entity.QuestionHide) &&
|
||||
!isAdminModerator && question.UserID != loginUserID {
|
||||
return nil, nil, false, errors.NotFound(reason.AnswerNotFound)
|
||||
}
|
||||
info := as.ShowFormat(ctx, answerInfo)
|
||||
// todo questionFunc
|
||||
questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID)
|
||||
@@ -650,6 +666,19 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
|
||||
|
||||
func (as *AnswerService) SearchList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
|
||||
list := make([]*schema.AnswerInfo, 0)
|
||||
questionInfo, exist, err := as.questionRepo.GetQuestion(ctx, req.QuestionID)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
}
|
||||
if !exist {
|
||||
return list, 0, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
if (questionInfo.Status == entity.QuestionStatusDeleted ||
|
||||
questionInfo.Status == entity.QuestionStatusPending ||
|
||||
questionInfo.Show == entity.QuestionHide) &&
|
||||
!req.IsAdminModerator && questionInfo.UserID != req.UserID {
|
||||
return list, 0, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
dbSearch := entity.AnswerSearch{}
|
||||
dbSearch.QuestionID = req.QuestionID
|
||||
dbSearch.Page = req.Page
|
||||
|
||||
@@ -1096,6 +1096,9 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
|
||||
question.Status == entity.QuestionStatusPending) && !per.CanReopen && question.UserID != userID {
|
||||
return nil, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
if question.Show == entity.QuestionHide && !per.IsAdminModerator && question.UserID != userID {
|
||||
return nil, errors.NotFound(reason.QuestionNotFound)
|
||||
}
|
||||
if question.Status != entity.QuestionStatusClosed {
|
||||
per.CanReopen = false
|
||||
}
|
||||
|
||||
@@ -392,17 +392,8 @@ func (rs *RevisionService) GetRevisionList(ctx context.Context, req *schema.GetR
|
||||
if infoErr != nil {
|
||||
return nil, infoErr
|
||||
}
|
||||
if !req.IsAdmin && objInfo.IsDeleted() && objInfo.ObjectCreatorUserID != req.UserID {
|
||||
switch objInfo.ObjectType {
|
||||
case constant.QuestionObjectType:
|
||||
return nil, errors.NotFound(reason.QuestionNotFound)
|
||||
case constant.AnswerObjectType:
|
||||
return nil, errors.NotFound(reason.AnswerNotFound)
|
||||
case constant.TagObjectType:
|
||||
return nil, errors.NotFound(reason.TagNotFound)
|
||||
default:
|
||||
return nil, errors.NotFound(reason.ObjectNotFound)
|
||||
}
|
||||
if err := objInfo.CheckVisibility(req.UserID, req.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = copier.Copy(&rev, req)
|
||||
|
||||
@@ -353,6 +353,10 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
errFields, err = us.validateAvatarInfo(ctx, req.UserID, oldUserInfo.Avatar, req.Avatar)
|
||||
if err != nil {
|
||||
return errFields, err
|
||||
}
|
||||
|
||||
cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req)
|
||||
|
||||
@@ -366,6 +370,41 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (us *UserService) validateAvatarInfo(
|
||||
ctx context.Context,
|
||||
userID string,
|
||||
oldAvatarJSON string,
|
||||
newAvatar schema.AvatarInfo,
|
||||
) (errFields []*validator.FormErrorField, err error) {
|
||||
if newAvatar.Type != constant.AvatarTypeCustom {
|
||||
return nil, nil
|
||||
}
|
||||
if len(newAvatar.Custom) == 0 {
|
||||
return append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "avatar",
|
||||
ErrorMsg: reason.UserSetAvatar,
|
||||
}), errors.BadRequest(reason.UserSetAvatar)
|
||||
}
|
||||
|
||||
var oldAvatar schema.AvatarInfo
|
||||
_ = json.Unmarshal([]byte(oldAvatarJSON), &oldAvatar)
|
||||
if oldAvatar.Type == constant.AvatarTypeCustom && oldAvatar.Custom == newAvatar.Custom {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fileRecord, err := us.fileRecordService.GetFileRecordByURL(ctx, newAvatar.Custom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fileRecord == nil || fileRecord.UserID != userID || fileRecord.Source != string(plugin.UserAvatar) {
|
||||
return append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "avatar",
|
||||
ErrorMsg: reason.UserSetAvatar,
|
||||
}), errors.BadRequest(reason.UserSetAvatar)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (us *UserService) cleanUpRemovedAvatar(
|
||||
ctx context.Context,
|
||||
oldAvatarJSON string,
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"mime"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -224,6 +225,10 @@ func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, e
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
func escapeEmailHTMLText(text string) string {
|
||||
return html.EscapeString(text)
|
||||
}
|
||||
|
||||
// NewAnswerTemplate new answer template
|
||||
func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) (
|
||||
title, body string, err error) {
|
||||
@@ -246,7 +251,14 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewAnswerBody, &schema.NewAnswerTemplateData{
|
||||
SiteName: escapeEmailHTMLText(templateData.SiteName),
|
||||
DisplayName: escapeEmailHTMLText(templateData.DisplayName),
|
||||
QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle),
|
||||
AnswerUrl: templateData.AnswerUrl,
|
||||
AnswerSummary: escapeEmailHTMLText(templateData.AnswerSummary),
|
||||
UnsubscribeUrl: templateData.UnsubscribeUrl,
|
||||
})
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
@@ -271,7 +283,13 @@ func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyInvitedAnswerBody, &schema.NewInviteAnswerTemplateData{
|
||||
SiteName: escapeEmailHTMLText(templateData.SiteName),
|
||||
DisplayName: escapeEmailHTMLText(templateData.DisplayName),
|
||||
QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle),
|
||||
InviteUrl: templateData.InviteUrl,
|
||||
UnsubscribeUrl: templateData.UnsubscribeUrl,
|
||||
})
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
@@ -298,7 +316,14 @@ func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewC
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyNewCommentTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewCommentBody, &schema.NewCommentTemplateData{
|
||||
SiteName: escapeEmailHTMLText(templateData.SiteName),
|
||||
DisplayName: escapeEmailHTMLText(templateData.DisplayName),
|
||||
QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle),
|
||||
CommentUrl: templateData.CommentUrl,
|
||||
CommentSummary: escapeEmailHTMLText(templateData.CommentSummary),
|
||||
UnsubscribeUrl: templateData.UnsubscribeUrl,
|
||||
})
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
@@ -324,7 +349,13 @@ func (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.New
|
||||
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
title = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionTitle, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, templateData)
|
||||
body = translator.TrWithData(lang, constant.EmailTplKeyNewQuestionBody, &schema.NewQuestionTemplateData{
|
||||
SiteName: escapeEmailHTMLText(templateData.SiteName),
|
||||
QuestionTitle: escapeEmailHTMLText(templateData.QuestionTitle),
|
||||
QuestionUrl: templateData.QuestionUrl,
|
||||
Tags: escapeEmailHTMLText(templateData.Tags),
|
||||
UnsubscribeUrl: templateData.UnsubscribeUrl,
|
||||
})
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -200,14 +200,15 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
|
||||
break
|
||||
}
|
||||
objInfo = &schema.SimpleObjectInfo{
|
||||
ObjectID: questionInfo.ID,
|
||||
ObjectCreatorUserID: questionInfo.UserID,
|
||||
QuestionID: questionInfo.ID,
|
||||
QuestionStatus: questionInfo.Status,
|
||||
QuestionShow: questionInfo.Show,
|
||||
ObjectType: objectType,
|
||||
Title: questionInfo.Title,
|
||||
Content: questionInfo.ParsedText, // todo trim
|
||||
ObjectID: questionInfo.ID,
|
||||
ObjectCreatorUserID: questionInfo.UserID,
|
||||
QuestionID: questionInfo.ID,
|
||||
QuestionCreatorUserID: questionInfo.UserID,
|
||||
QuestionStatus: questionInfo.Status,
|
||||
QuestionShow: questionInfo.Show,
|
||||
ObjectType: objectType,
|
||||
Title: questionInfo.Title,
|
||||
Content: questionInfo.ParsedText, // todo trim
|
||||
}
|
||||
case constant.AnswerObjectType:
|
||||
answerInfo, exist, err := os.answerRepo.GetAnswer(ctx, objectID)
|
||||
@@ -225,16 +226,17 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
|
||||
break
|
||||
}
|
||||
objInfo = &schema.SimpleObjectInfo{
|
||||
ObjectID: answerInfo.ID,
|
||||
ObjectCreatorUserID: answerInfo.UserID,
|
||||
QuestionID: answerInfo.QuestionID,
|
||||
QuestionStatus: questionInfo.Status,
|
||||
QuestionShow: questionInfo.Show,
|
||||
AnswerStatus: answerInfo.Status,
|
||||
AnswerID: answerInfo.ID,
|
||||
ObjectType: objectType,
|
||||
Title: questionInfo.Title, // this should be question title
|
||||
Content: answerInfo.ParsedText, // todo trim
|
||||
ObjectID: answerInfo.ID,
|
||||
ObjectCreatorUserID: answerInfo.UserID,
|
||||
QuestionID: answerInfo.QuestionID,
|
||||
QuestionCreatorUserID: questionInfo.UserID,
|
||||
QuestionStatus: questionInfo.Status,
|
||||
QuestionShow: questionInfo.Show,
|
||||
AnswerStatus: answerInfo.Status,
|
||||
AnswerID: answerInfo.ID,
|
||||
ObjectType: objectType,
|
||||
Title: questionInfo.Title, // this should be question title
|
||||
Content: answerInfo.ParsedText, // todo trim
|
||||
}
|
||||
case constant.CommentObjectType:
|
||||
commentInfo, exist, err := os.commentRepo.GetComment(ctx, objectID)
|
||||
@@ -259,6 +261,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
|
||||
}
|
||||
if exist {
|
||||
objInfo.QuestionID = questionInfo.ID
|
||||
objInfo.QuestionCreatorUserID = questionInfo.UserID
|
||||
objInfo.QuestionStatus = questionInfo.Status
|
||||
objInfo.QuestionShow = questionInfo.Show
|
||||
objInfo.Title = questionInfo.Title
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/apache/answer/pkg/checker"
|
||||
"github.com/apache/answer/pkg/gravatar"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
@@ -158,6 +159,10 @@ func (s *siteInfoCommonService) selectedAvatar(
|
||||
email string, userStatus int) *schema.AvatarInfo {
|
||||
avatarInfo := &schema.AvatarInfo{}
|
||||
_ = json.Unmarshal([]byte(originalAvatarData), avatarInfo)
|
||||
if len(avatarInfo.Type) == 0 && checker.IsURL(originalAvatarData) {
|
||||
avatarInfo.Type = constant.AvatarTypeCustom
|
||||
avatarInfo.Custom = originalAvatarData
|
||||
}
|
||||
|
||||
if userStatus == entity.UserStatusDeleted {
|
||||
return &schema.AvatarInfo{
|
||||
|
||||
+43
-16
@@ -22,9 +22,9 @@ package checker
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif" // use init to support decode jpeg,jpg,png,gif
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -47,25 +47,26 @@ func IsUnAuthorizedExtension(fileName string, allowedExtensions []string) bool {
|
||||
func DecodeAndCheckImageFile(localFilePath string, maxImageMegapixel int) bool {
|
||||
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(localFilePath), "."))
|
||||
switch ext {
|
||||
case "jpg", "jpeg", "png", "gif": // only allow for `image/jpeg, image/jpg, image/png, image/gif`
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageConfigCheck) {
|
||||
case "jpg", "jpeg", "png", "gif":
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, formatSpecificConfigCheck) {
|
||||
return false
|
||||
}
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageCheck) {
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, formatSpecificImageCheck) {
|
||||
return false
|
||||
}
|
||||
case "webp":
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageConfigCheck) {
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, webpImageConfigCheck) {
|
||||
return false
|
||||
}
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageCheck) {
|
||||
if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, ext, webpImageCheck) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, checker func(file io.Reader, maxImageMegapixel int) error) bool {
|
||||
func decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, ext string,
|
||||
checker func(file io.Reader, ext string, maxImageMegapixel int) error) bool {
|
||||
file, err := os.Open(localFilePath)
|
||||
if err != nil {
|
||||
log.Errorf("open file error: %v", err)
|
||||
@@ -75,15 +76,30 @@ func decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, checke
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
if err = checker(file, maxImageMegapixel); err != nil {
|
||||
if err = checker(file, ext, maxImageMegapixel); err != nil {
|
||||
log.Errorf("check image format error: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func standardImageConfigCheck(file io.Reader, maxImageMegapixel int) error {
|
||||
config, _, err := image.DecodeConfig(file)
|
||||
// formatSpecificConfigCheck decodes image config using a format-specific decoder
|
||||
// based on the file extension. This avoids calling image.DecodeConfig() which
|
||||
// dispatches by magic bytes and can invoke unintended decoders (e.g., TIFF)
|
||||
// registered by transitive dependencies.
|
||||
func formatSpecificConfigCheck(file io.Reader, ext string, maxImageMegapixel int) error {
|
||||
var config image.Config
|
||||
var err error
|
||||
switch ext {
|
||||
case "jpg", "jpeg":
|
||||
config, err = jpeg.DecodeConfig(file)
|
||||
case "png":
|
||||
config, err = png.DecodeConfig(file)
|
||||
case "gif":
|
||||
config, err = gif.DecodeConfig(file)
|
||||
default:
|
||||
return fmt.Errorf("unsupported image format: %s", ext)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode image config error: %v", err)
|
||||
}
|
||||
@@ -93,15 +109,26 @@ func standardImageConfigCheck(file io.Reader, maxImageMegapixel int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func standardImageCheck(file io.Reader, maxImageMegapixel int) error {
|
||||
_, _, err := image.Decode(file)
|
||||
// formatSpecificImageCheck fully decodes the image using a format-specific decoder.
|
||||
func formatSpecificImageCheck(file io.Reader, ext string, _ int) error {
|
||||
var err error
|
||||
switch ext {
|
||||
case "jpg", "jpeg":
|
||||
_, err = jpeg.Decode(file)
|
||||
case "png":
|
||||
_, err = png.Decode(file)
|
||||
case "gif":
|
||||
_, err = gif.Decode(file)
|
||||
default:
|
||||
return fmt.Errorf("unsupported image format: %s", ext)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode image error: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webpImageConfigCheck(file io.Reader, maxImageMegapixel int) error {
|
||||
func webpImageConfigCheck(file io.Reader, _ string, maxImageMegapixel int) error {
|
||||
config, err := webp.DecodeConfig(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode webp image config error: %v", err)
|
||||
@@ -112,7 +139,7 @@ func webpImageConfigCheck(file io.Reader, maxImageMegapixel int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func webpImageCheck(file io.Reader, maxImageMegapixel int) error {
|
||||
func webpImageCheck(file io.Reader, _ string, _ int) error {
|
||||
_, err := webp.Decode(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode webp image error: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user