[MM-62427] Add message attachments validation (#30180)
* Add message attachments validation * Add props validation * Validate slack attachment fields * Update tests and library usage * Improve interactive dialog error for length checks * Allow predefined colors for slack attachments * Fix TestPostAction * Use const for data source * Add tests * Cleanup unused props * Add happy path tests * lint fixes * Add validation for PostActionOptions
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
5609489e86
Коммит
9b5d8d52bf
@@ -17,6 +17,7 @@ import (
|
||||
"math/big"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -38,13 +39,18 @@ const (
|
||||
DialogElementBoolMaxLength = 150
|
||||
)
|
||||
|
||||
var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"}
|
||||
var PostActionRetainPropKeys = []string{PostPropsFromWebhook, PostPropsOverrideUsername, PostPropsOverrideIconURL}
|
||||
|
||||
type DoPostActionRequest struct {
|
||||
SelectedOption string `json:"selected_option,omitempty"`
|
||||
Cookie string `json:"cookie,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
PostActionDataSourceUsers = "users"
|
||||
PostActionDataSourceChannels = "channels"
|
||||
)
|
||||
|
||||
type PostAction struct {
|
||||
// A unique Action ID. If not set, generated automatically.
|
||||
Id string `json:"id,omitempty"`
|
||||
@@ -85,6 +91,73 @@ type PostAction struct {
|
||||
Cookie string `json:"cookie,omitempty" db:"-"`
|
||||
}
|
||||
|
||||
// IsValid validates the action and returns an error if it is invalid.
|
||||
func (p *PostAction) IsValid() error {
|
||||
var multiErr *multierror.Error
|
||||
|
||||
if p.Name == "" {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("action must have a name"))
|
||||
}
|
||||
|
||||
if p.Style != "" {
|
||||
validStyles := []string{"default", "primary", "success", "good", "warning", "danger"}
|
||||
// If not a predefined style, check if it's a hex color
|
||||
if !slices.Contains(validStyles, p.Style) && !hexColorRegex.MatchString(p.Style) {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid style '%s' - must be one of [default, primary, success, good, warning, danger] or a hex color", p.Style))
|
||||
}
|
||||
}
|
||||
|
||||
switch p.Type {
|
||||
case PostActionTypeButton:
|
||||
if len(p.Options) > 0 {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("button action must not have options"))
|
||||
}
|
||||
if p.DataSource != "" {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("button action must not have a data source"))
|
||||
}
|
||||
case PostActionTypeSelect:
|
||||
if p.DataSource != "" {
|
||||
validSources := []string{PostActionDataSourceUsers, PostActionDataSourceChannels}
|
||||
if !slices.Contains(validSources, p.DataSource) {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid data_source '%s' for select action", p.DataSource))
|
||||
}
|
||||
|
||||
if len(p.Options) > 0 {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("select action cannot have both DataSource and Options set"))
|
||||
}
|
||||
} else {
|
||||
if len(p.Options) == 0 {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("select action must have either DataSource or Options set"))
|
||||
} else {
|
||||
for i, opt := range p.Options {
|
||||
if opt == nil {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("select action contains nil option"))
|
||||
continue
|
||||
}
|
||||
if err := opt.IsValid(); err != nil {
|
||||
multiErr = multierror.Append(multiErr, multierror.Prefix(err, fmt.Sprintf("option at index %d is invalid:", i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("invalid action type: must be '%s' or '%s'", PostActionTypeButton, PostActionTypeSelect))
|
||||
}
|
||||
|
||||
if p.Integration == nil {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("action must have integration settings"))
|
||||
} else {
|
||||
if p.Integration.URL == "" {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("action must have an integration URL"))
|
||||
}
|
||||
if !(strings.HasPrefix(p.Integration.URL, "/plugins/") || strings.HasPrefix(p.Integration.URL, "plugins/") || IsValidHTTPURL(p.Integration.URL)) {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("action must have an valid integration URL"))
|
||||
}
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (p *PostAction) Equals(input *PostAction) bool {
|
||||
if p.Id != input.Id {
|
||||
return false
|
||||
@@ -189,7 +262,22 @@ type PostActionOptions struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (o *PostActionOptions) IsValid() error {
|
||||
var multiErr *multierror.Error
|
||||
|
||||
if o.Text == "" {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("text is required"))
|
||||
}
|
||||
if o.Value == "" {
|
||||
multiErr = multierror.Append(multiErr, fmt.Errorf("value is required"))
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
type PostActionIntegration struct {
|
||||
// URL is the endpoint that the action will be sent to.
|
||||
// It can be a relative path to a plugin.
|
||||
URL string `json:"url,omitempty"`
|
||||
Context map[string]any `json:"context,omitempty"`
|
||||
}
|
||||
@@ -474,24 +562,25 @@ func isDefaultInOptions(defaultValue string, options []*PostActionOptions) bool
|
||||
return false
|
||||
}
|
||||
|
||||
func checkMaxLength(fieldName string, field string, length int) error {
|
||||
var valid bool
|
||||
func checkMaxLength(fieldName string, field string, maxLength int) error {
|
||||
// DisplayName and Name are required fields
|
||||
if fieldName == "DisplayName" || fieldName == "Name" {
|
||||
valid = len(field) > 0 && len(field) > length
|
||||
} else {
|
||||
valid = len(field) > length
|
||||
if len(field) == 0 {
|
||||
return errors.Errorf("%v cannot be empty", fieldName)
|
||||
}
|
||||
}
|
||||
if valid {
|
||||
return errors.Errorf("%v cannot be longer than %d characters", fieldName, length)
|
||||
|
||||
if len(field) > maxLength {
|
||||
return errors.Errorf("%v cannot be longer than %d characters, got %d", fieldName, maxLength, len(field))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Post) StripActionIntegrations() {
|
||||
attachments := o.Attachments()
|
||||
if o.GetProp("attachments") != nil {
|
||||
o.AddProp("attachments", attachments)
|
||||
if o.GetProp(PostPropsAttachments) != nil {
|
||||
o.AddProp(PostPropsAttachments, attachments)
|
||||
}
|
||||
for _, attachment := range attachments {
|
||||
for _, action := range attachment.Actions {
|
||||
@@ -512,10 +601,10 @@ func (o *Post) GetAction(id string) *PostAction {
|
||||
}
|
||||
|
||||
func (o *Post) GenerateActionIds() {
|
||||
if o.GetProp("attachments") != nil {
|
||||
o.AddProp("attachments", o.Attachments())
|
||||
if o.GetProp(PostPropsAttachments) != nil {
|
||||
o.AddProp(PostPropsAttachments, o.Attachments())
|
||||
}
|
||||
if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
|
||||
if attachments, ok := o.GetProp(PostPropsAttachments).([]*SlackAttachment); ok {
|
||||
for _, attachment := range attachments {
|
||||
for _, action := range attachment.Actions {
|
||||
if action != nil && action.Id == "" {
|
||||
|
||||
Ссылка в новой задаче
Block a user