incoming_webhook.go 6.36 KB
Newer Older
1
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
JoramWilander's avatar
JoramWilander committed
2 3 4 5 6
// See License.txt for license information.

package model

import (
7
	"bytes"
JoramWilander's avatar
JoramWilander committed
8 9
	"encoding/json"
	"io"
10
	"net/http"
11
	"regexp"
JoramWilander's avatar
JoramWilander committed
12 13
)

14 15 16 17
const (
	DEFAULT_WEBHOOK_USERNAME = "webhook"
)

JoramWilander's avatar
JoramWilander committed
18
type IncomingWebhook struct {
19 20 21 22 23 24 25 26 27 28 29 30
	Id            string `json:"id"`
	CreateAt      int64  `json:"create_at"`
	UpdateAt      int64  `json:"update_at"`
	DeleteAt      int64  `json:"delete_at"`
	UserId        string `json:"user_id"`
	ChannelId     string `json:"channel_id"`
	TeamId        string `json:"team_id"`
	DisplayName   string `json:"display_name"`
	Description   string `json:"description"`
	Username      string `json:"username"`
	IconURL       string `json:"icon_url"`
	ChannelLocked bool   `json:"channel_locked"`
JoramWilander's avatar
JoramWilander committed
31 32
}

33
type IncomingWebhookRequest struct {
34 35 36 37 38 39 40
	Text        string             `json:"text"`
	Username    string             `json:"username"`
	IconURL     string             `json:"icon_url"`
	ChannelName string             `json:"channel"`
	Props       StringInterface    `json:"props"`
	Attachments []*SlackAttachment `json:"attachments"`
	Type        string             `json:"type"`
41 42
}

JoramWilander's avatar
JoramWilander committed
43
func (o *IncomingWebhook) ToJson() string {
Chris's avatar
Chris committed
44 45
	b, _ := json.Marshal(o)
	return string(b)
JoramWilander's avatar
JoramWilander committed
46 47 48
}

func IncomingWebhookFromJson(data io.Reader) *IncomingWebhook {
Chris's avatar
Chris committed
49 50 51
	var o *IncomingWebhook
	json.NewDecoder(data).Decode(&o)
	return o
JoramWilander's avatar
JoramWilander committed
52 53 54
}

func IncomingWebhookListToJson(l []*IncomingWebhook) string {
Chris's avatar
Chris committed
55 56
	b, _ := json.Marshal(l)
	return string(b)
JoramWilander's avatar
JoramWilander committed
57 58 59 60
}

func IncomingWebhookListFromJson(data io.Reader) []*IncomingWebhook {
	var o []*IncomingWebhook
Chris's avatar
Chris committed
61 62
	json.NewDecoder(data).Decode(&o)
	return o
JoramWilander's avatar
JoramWilander committed
63 64 65 66 67
}

func (o *IncomingWebhook) IsValid() *AppError {

	if len(o.Id) != 26 {
68 69
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest)

JoramWilander's avatar
JoramWilander committed
70 71 72
	}

	if o.CreateAt == 0 {
73
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
JoramWilander's avatar
JoramWilander committed
74 75 76
	}

	if o.UpdateAt == 0 {
77
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
JoramWilander's avatar
JoramWilander committed
78 79 80
	}

	if len(o.UserId) != 26 {
81
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.user_id.app_error", nil, "", http.StatusBadRequest)
JoramWilander's avatar
JoramWilander committed
82 83 84
	}

	if len(o.ChannelId) != 26 {
85
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.channel_id.app_error", nil, "", http.StatusBadRequest)
JoramWilander's avatar
JoramWilander committed
86 87 88
	}

	if len(o.TeamId) != 26 {
89
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.team_id.app_error", nil, "", http.StatusBadRequest)
JoramWilander's avatar
JoramWilander committed
90 91
	}

92
	if len(o.DisplayName) > 64 {
93
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.display_name.app_error", nil, "", http.StatusBadRequest)
94 95
	}

96
	if len(o.Description) > 500 {
97
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "", http.StatusBadRequest)
98 99
	}

100
	if len(o.Username) > 64 {
101 102 103
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.username.app_error", nil, "", http.StatusBadRequest)
	}

104 105
	if len(o.IconURL) > 1024 {
		return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.icon_url.app_error", nil, "", http.StatusBadRequest)
106 107
	}

JoramWilander's avatar
JoramWilander committed
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
	return nil
}

func (o *IncomingWebhook) PreSave() {
	if o.Id == "" {
		o.Id = NewId()
	}

	o.CreateAt = GetMillis()
	o.UpdateAt = o.CreateAt
}

func (o *IncomingWebhook) PreUpdate() {
	o.UpdateAt = GetMillis()
}
123

124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
// escapeControlCharsFromPayload escapes control chars (\n, \t) from a byte slice.
// Context:
// JSON strings are not supposed to contain control characters such as \n, \t,
// ... but some incoming webhooks might still send invalid JSON and we want to
// try to handle that. An example invalid JSON string from an incoming webhook
// might look like this (strings for both "text" and "fallback" attributes are
// invalid JSON strings because they contain unescaped newlines and tabs):
//  `{
//    "text": "this is a test
//						 that contains a newline and tabs",
//    "attachments": [
//      {
//        "fallback": "Required plain-text summary of the attachment
//										that contains a newline and tabs",
//        "color": "#36a64f",
//  			...
//        "text": "Optional text that appears within the attachment
//								 that contains a newline and tabs",
//  			...
//        "thumb_url": "http://example.com/path/to/thumb.png"
//      }
//    ]
//  }`
// This function will search for `"key": "value"` pairs, and escape \n, \t
// from the value.
func escapeControlCharsFromPayload(by []byte) []byte {
	// we'll search for `"text": "..."` or `"fallback": "..."`, ...
	keys := "text|fallback|pretext|author_name|title|value"

	// the regexp reads like this:
	// (?s): this flag let . match \n (default is false)
	// "(keys)": we search for the keys defined above
	// \s*:\s*: followed by 0..n spaces/tabs, a colon then 0..n spaces/tabs
	// ": a double-quote
	// (\\"|[^"])*: any number of times the `\"` string or any char but a double-quote
	// ": a double-quote
	r := `(?s)"(` + keys + `)"\s*:\s*"(\\"|[^"])*"`
	re := regexp.MustCompile(r)

	// the function that will escape \n and \t on the regexp matches
	repl := func(b []byte) []byte {
		if bytes.Contains(b, []byte("\n")) {
			b = bytes.Replace(b, []byte("\n"), []byte("\\n"), -1)
		}
		if bytes.Contains(b, []byte("\t")) {
			b = bytes.Replace(b, []byte("\t"), []byte("\\t"), -1)
		}

		return b
	}

	return re.ReplaceAllFunc(by, repl)
}

func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) {
	decoder := json.NewDecoder(bytes.NewReader(by))
180 181 182
	var o IncomingWebhookRequest
	err := decoder.Decode(&o)
	if err == nil {
183
		return &o, nil
184
	} else {
185
		return nil, err
186 187
	}
}
188

189
func IncomingWebhookRequestFromJson(data io.Reader) (*IncomingWebhookRequest, *AppError) {
190 191 192 193 194 195 196 197 198 199
	buf := new(bytes.Buffer)
	buf.ReadFrom(data)
	by := buf.Bytes()

	// Try to decode the JSON data. Only if it fails, try to escape control
	// characters from the strings contained in the JSON data.
	o, err := decodeIncomingWebhookRequest(by)
	if err != nil {
		o, err = decodeIncomingWebhookRequest(escapeControlCharsFromPayload(by))
		if err != nil {
200
			return nil, NewAppError("IncomingWebhookRequestFromJson", "model.incoming_hook.parse_data.app_error", nil, err.Error(), http.StatusBadRequest)
201 202 203
		}
	}

204
	o.Attachments = StringifySlackFieldValue(o.Attachments)
205

206
	return o, nil
207
}
208 209 210 211 212 213 214 215 216

func (o *IncomingWebhookRequest) ToJson() string {
	b, err := json.Marshal(o)
	if err != nil {
		return ""
	} else {
		return string(b)
	}
}