[GH-16199] add IsValid to model OpenDialogRequest (#26526)
* [GH-16199] add IsValid to model OpenDialogRequest * [GH-16199] add IsValid to model OpenDialogRequest, revert remove of translations * [GH-16199] fix tests after revert * [GH-16199] add IsValid to model OpenDialogRequest * [GH-16199] revert validation of icon url * [GH-16199] update go sum * [GH-16199] log warning for invalid dialog * [GH-16199] log error for invalid dialog * [GH-16199] log warning for invalid dialog * [GH-16199] fix golang-ci --------- Co-authored-by: Lukas Eipert <git@leipert.io> Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
@@ -20,11 +20,22 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
PostActionTypeButton = "button"
|
||||
PostActionTypeSelect = "select"
|
||||
PostActionTypeButton = "button"
|
||||
PostActionTypeSelect = "select"
|
||||
DialogTitleMaxLength = 24
|
||||
DialogElementDisplayNameMaxLength = 24
|
||||
DialogElementNameMaxLength = 300
|
||||
DialogElementHelpTextMaxLength = 150
|
||||
DialogElementTextMaxLength = 150
|
||||
DialogElementTextareaMaxLength = 3000
|
||||
DialogElementSelectMaxLength = 3000
|
||||
DialogElementBoolMaxLength = 150
|
||||
)
|
||||
|
||||
var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"}
|
||||
@@ -335,6 +346,148 @@ func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey, timeou
|
||||
return DecodeAndVerifyTriggerId(r.TriggerId, s, timeout)
|
||||
}
|
||||
|
||||
func (r *OpenDialogRequest) IsValid() error {
|
||||
var multiErr *multierror.Error
|
||||
if r.URL == "" {
|
||||
multiErr = multierror.Append(multiErr, errors.New("empty URL"))
|
||||
}
|
||||
|
||||
if r.TriggerId == "" {
|
||||
multiErr = multierror.Append(multiErr, errors.New("empty trigger id"))
|
||||
}
|
||||
|
||||
err := r.Dialog.IsValid()
|
||||
if err != nil {
|
||||
multiErr = multierror.Append(multiErr, err)
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (d *Dialog) IsValid() error {
|
||||
var multiErr *multierror.Error
|
||||
|
||||
if d.Title == "" || len(d.Title) > DialogTitleMaxLength {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("invalid dialog title %q", d.Title))
|
||||
}
|
||||
|
||||
if d.IconURL != "" && !IsValidHTTPURL(d.IconURL) {
|
||||
multiErr = multierror.Append(multiErr, errors.New("invalid icon url"))
|
||||
}
|
||||
|
||||
if len(d.Elements) != 0 {
|
||||
elementMap := make(map[string]bool)
|
||||
|
||||
for _, element := range d.Elements {
|
||||
if elementMap[element.Name] {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("duplicate dialog element %q", element.Name))
|
||||
}
|
||||
elementMap[element.Name] = true
|
||||
|
||||
err := element.IsValid()
|
||||
if err != nil {
|
||||
multiErr = multierror.Append(multiErr, errors.Wrapf(err, "%q field is not valid", element.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (e *DialogElement) IsValid() error {
|
||||
var multiErr *multierror.Error
|
||||
textSubTypes := map[string]bool{
|
||||
"": true,
|
||||
"text": true,
|
||||
"email": true,
|
||||
"number": true,
|
||||
"tel": true,
|
||||
"url": true,
|
||||
"password": true,
|
||||
}
|
||||
|
||||
if e.MinLength < 0 {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("min length cannot be a negative number, got %d", e.MinLength))
|
||||
}
|
||||
if e.MinLength > e.MaxLength {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("min length should be less then max length, got %d > %d", e.MinLength, e.MaxLength))
|
||||
}
|
||||
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("DisplayName", e.DisplayName, DialogElementDisplayNameMaxLength))
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Name", e.Name, DialogElementNameMaxLength))
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("HelpText", e.HelpText, DialogElementHelpTextMaxLength))
|
||||
|
||||
switch e.Type {
|
||||
case "text":
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Default", e.Default, DialogElementTextMaxLength))
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementTextMaxLength))
|
||||
if _, ok := textSubTypes[e.SubType]; !ok {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("invalid subtype %q", e.Type))
|
||||
}
|
||||
|
||||
case "textarea":
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Default", e.Default, DialogElementTextareaMaxLength))
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementTextareaMaxLength))
|
||||
|
||||
if _, ok := textSubTypes[e.SubType]; !ok {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("invalid subtype %q", e.Type))
|
||||
}
|
||||
|
||||
case "select":
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Default", e.Default, DialogElementSelectMaxLength))
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementSelectMaxLength))
|
||||
if e.DataSource != "" && e.DataSource != "users" && e.DataSource != "channels" {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("invalid data source %q, allowed are 'users' or 'channels'", e.DataSource))
|
||||
}
|
||||
if e.DataSource == "" && !isDefaultInOptions(e.Default, e.Options) {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("default value %q doesn't exist in options ", e.Default))
|
||||
}
|
||||
|
||||
case "bool":
|
||||
if e.Default != "" && e.Default != "true" && e.Default != "false" {
|
||||
multiErr = multierror.Append(multiErr, errors.New("invalid default of bool"))
|
||||
}
|
||||
multiErr = multierror.Append(multiErr, checkMaxLength("Placeholder", e.Placeholder, DialogElementBoolMaxLength))
|
||||
|
||||
case "radio":
|
||||
if !isDefaultInOptions(e.Default, e.Options) {
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("default value %q doesn't exist in options ", e.Default))
|
||||
}
|
||||
|
||||
default:
|
||||
multiErr = multierror.Append(multiErr, errors.Errorf("invalid element type: %q", e.Type))
|
||||
}
|
||||
|
||||
return multiErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func isDefaultInOptions(defaultValue string, options []*PostActionOptions) bool {
|
||||
if defaultValue == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
if defaultValue == option.Value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func checkMaxLength(fieldName string, field string, length int) error {
|
||||
var valid bool
|
||||
// DisplayName and Name are required fields
|
||||
if fieldName == "DisplayName" || fieldName == "Name" {
|
||||
valid = len(field) > 0 && len(field) > length
|
||||
} else {
|
||||
valid = len(field) > length
|
||||
}
|
||||
if valid {
|
||||
return errors.Errorf("%v cannot be longer than %d characters", fieldName, length)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Post) StripActionIntegrations() {
|
||||
attachments := o.Attachments()
|
||||
if o.GetProp("attachments") != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -180,3 +181,190 @@ func TestPostActionIntegrationEquals(t *testing.T) {
|
||||
require.True(t, pa1.Equals(pa2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenDialogRequestIsValid(t *testing.T) {
|
||||
getBaseOpenDialogRequest := func() OpenDialogRequest {
|
||||
return OpenDialogRequest{
|
||||
TriggerId: "triggerId",
|
||||
URL: "http://localhost:8065",
|
||||
Dialog: Dialog{
|
||||
CallbackId: "callbackid",
|
||||
Title: "Some Title",
|
||||
Elements: []DialogElement{
|
||||
{
|
||||
DisplayName: "Element Name",
|
||||
Name: "element_name",
|
||||
Type: "text",
|
||||
Placeholder: "Enter a value",
|
||||
},
|
||||
},
|
||||
SubmitLabel: "Submit",
|
||||
NotifyOnCancel: false,
|
||||
State: "somestate",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("should pass validation", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
err := request.IsValid()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should fail on empty url", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.URL = ""
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "empty URL")
|
||||
})
|
||||
|
||||
t.Run("should fail on empty trigger", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.TriggerId = ""
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "empty trigger id")
|
||||
})
|
||||
|
||||
t.Run("should fail on empty dialog title", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Title = ""
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "invalid dialog title")
|
||||
})
|
||||
|
||||
t.Run("should fail on wrong subtype and long dialog title", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements[0].SubType = "wrong SubType"
|
||||
request.Dialog.Title = "Very very long Dialog Name"
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "invalid subtype")
|
||||
assert.ErrorContains(t, err, "invalid dialog title")
|
||||
t.Cleanup(func() {
|
||||
request.Dialog.Elements[0].SubType = ""
|
||||
request.Dialog.Title = "Some Title"
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("should fail on wrong dialog icon url", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.IconURL = "wrong url"
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "invalid icon url")
|
||||
})
|
||||
|
||||
t.Run("should pass on empty dialog icon url", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.IconURL = ""
|
||||
err := request.IsValid()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should fail on wrong minimal and maximal element length", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements[0].MinLength = 10
|
||||
request.Dialog.Elements[0].MaxLength = 9
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "field is not valid")
|
||||
assert.ErrorContains(t, err, "min length should be less then max length")
|
||||
})
|
||||
|
||||
t.Run("should fail on wrong element type", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements[0].Type = "wrong type"
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "invalid element type")
|
||||
})
|
||||
|
||||
t.Run("should fail on duplicate element name", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
||||
DisplayName: "Radio element name",
|
||||
Name: "element_name",
|
||||
Type: "radio",
|
||||
})
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "duplicate dialog element")
|
||||
})
|
||||
|
||||
t.Run("should fail on wrong bool default value", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
||||
DisplayName: "Bool element name",
|
||||
Name: "bool_element_name",
|
||||
Type: "bool",
|
||||
Default: "wrong default",
|
||||
})
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "invalid default of bool")
|
||||
})
|
||||
|
||||
t.Run("should pass on bool default value", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
||||
DisplayName: "Bool element name",
|
||||
Name: "bool_element_name",
|
||||
Type: "bool",
|
||||
Default: "true",
|
||||
})
|
||||
err := request.IsValid()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should fail on wrong select datasource value", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
||||
DisplayName: "Select element name",
|
||||
Name: "select_element_name",
|
||||
Type: "select",
|
||||
DataSource: "wrong DataSource",
|
||||
})
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "invalid data source")
|
||||
})
|
||||
|
||||
t.Run("should fail on wrong radio default value", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
||||
DisplayName: "Radio element name",
|
||||
Name: "radio_element_name",
|
||||
Type: "radio",
|
||||
Default: "default",
|
||||
Options: []*PostActionOptions{
|
||||
{
|
||||
Text: "Text 1",
|
||||
Value: "value 1",
|
||||
},
|
||||
},
|
||||
})
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "default value \"default\" doesn't exist in options")
|
||||
})
|
||||
t.Run("should pass radio default value", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements = append(request.Dialog.Elements, DialogElement{
|
||||
DisplayName: "Radio element name",
|
||||
Name: "radio_element_name",
|
||||
Type: "radio",
|
||||
Default: "default",
|
||||
Options: []*PostActionOptions{
|
||||
{
|
||||
Text: "Text 1",
|
||||
Value: "value 1",
|
||||
},
|
||||
{
|
||||
Text: "Text 2",
|
||||
Value: "default",
|
||||
},
|
||||
},
|
||||
})
|
||||
err := request.IsValid()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should fail on too long text placeholder", func(t *testing.T) {
|
||||
request := getBaseOpenDialogRequest()
|
||||
request.Dialog.Elements[0].Placeholder = strings.Repeat("x", 151)
|
||||
err := request.IsValid()
|
||||
assert.ErrorContains(t, err, "Placeholder cannot be longer than 150 characters")
|
||||
})
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user