github.com/levb/mattermost-server@v5.3.1+incompatible/model/incoming_webhook.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package model 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "io" 10 "net/http" 11 "regexp" 12 ) 13 14 const ( 15 DEFAULT_WEBHOOK_USERNAME = "webhook" 16 ) 17 18 type IncomingWebhook struct { 19 Id string `json:"id"` 20 CreateAt int64 `json:"create_at"` 21 UpdateAt int64 `json:"update_at"` 22 DeleteAt int64 `json:"delete_at"` 23 UserId string `json:"user_id"` 24 ChannelId string `json:"channel_id"` 25 TeamId string `json:"team_id"` 26 DisplayName string `json:"display_name"` 27 Description string `json:"description"` 28 Username string `json:"username"` 29 IconURL string `json:"icon_url"` 30 ChannelLocked bool `json:"channel_locked"` 31 } 32 33 type IncomingWebhookRequest struct { 34 Text string `json:"text"` 35 Username string `json:"username"` 36 IconURL string `json:"icon_url"` 37 ChannelName string `json:"channel"` 38 Props StringInterface `json:"props"` 39 Attachments []*SlackAttachment `json:"attachments"` 40 Type string `json:"type"` 41 } 42 43 func (o *IncomingWebhook) ToJson() string { 44 b, _ := json.Marshal(o) 45 return string(b) 46 } 47 48 func IncomingWebhookFromJson(data io.Reader) *IncomingWebhook { 49 var o *IncomingWebhook 50 json.NewDecoder(data).Decode(&o) 51 return o 52 } 53 54 func IncomingWebhookListToJson(l []*IncomingWebhook) string { 55 b, _ := json.Marshal(l) 56 return string(b) 57 } 58 59 func IncomingWebhookListFromJson(data io.Reader) []*IncomingWebhook { 60 var o []*IncomingWebhook 61 json.NewDecoder(data).Decode(&o) 62 return o 63 } 64 65 func (o *IncomingWebhook) IsValid() *AppError { 66 67 if len(o.Id) != 26 { 68 return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest) 69 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) 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) 78 } 79 80 if len(o.UserId) != 26 { 81 return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.user_id.app_error", nil, "", http.StatusBadRequest) 82 } 83 84 if len(o.ChannelId) != 26 { 85 return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.channel_id.app_error", nil, "", http.StatusBadRequest) 86 } 87 88 if len(o.TeamId) != 26 { 89 return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.team_id.app_error", nil, "", http.StatusBadRequest) 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) > 128 { 97 return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.description.app_error", nil, "", http.StatusBadRequest) 98 } 99 100 if len(o.Username) > 64 { 101 return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.username.app_error", nil, "", http.StatusBadRequest) 102 } 103 104 if len(o.IconURL) > 1024 { 105 return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.icon_url.app_error", nil, "", http.StatusBadRequest) 106 } 107 108 return nil 109 } 110 111 func (o *IncomingWebhook) PreSave() { 112 if o.Id == "" { 113 o.Id = NewId() 114 } 115 116 o.CreateAt = GetMillis() 117 o.UpdateAt = o.CreateAt 118 } 119 120 func (o *IncomingWebhook) PreUpdate() { 121 o.UpdateAt = GetMillis() 122 } 123 124 // escapeControlCharsFromPayload escapes control chars (\n, \t) from a byte slice. 125 // Context: 126 // JSON strings are not supposed to contain control characters such as \n, \t, 127 // ... but some incoming webhooks might still send invalid JSON and we want to 128 // try to handle that. An example invalid JSON string from an incoming webhook 129 // might look like this (strings for both "text" and "fallback" attributes are 130 // invalid JSON strings because they contain unescaped newlines and tabs): 131 // `{ 132 // "text": "this is a test 133 // that contains a newline and tabs", 134 // "attachments": [ 135 // { 136 // "fallback": "Required plain-text summary of the attachment 137 // that contains a newline and tabs", 138 // "color": "#36a64f", 139 // ... 140 // "text": "Optional text that appears within the attachment 141 // that contains a newline and tabs", 142 // ... 143 // "thumb_url": "http://example.com/path/to/thumb.png" 144 // } 145 // ] 146 // }` 147 // This function will search for `"key": "value"` pairs, and escape \n, \t 148 // from the value. 149 func escapeControlCharsFromPayload(by []byte) []byte { 150 // we'll search for `"text": "..."` or `"fallback": "..."`, ... 151 keys := "text|fallback|pretext|author_name|title|value" 152 153 // the regexp reads like this: 154 // (?s): this flag let . match \n (default is false) 155 // "(keys)": we search for the keys defined above 156 // \s*:\s*: followed by 0..n spaces/tabs, a colon then 0..n spaces/tabs 157 // ": a double-quote 158 // (\\"|[^"])*: any number of times the `\"` string or any char but a double-quote 159 // ": a double-quote 160 r := `(?s)"(` + keys + `)"\s*:\s*"(\\"|[^"])*"` 161 re := regexp.MustCompile(r) 162 163 // the function that will escape \n and \t on the regexp matches 164 repl := func(b []byte) []byte { 165 if bytes.Contains(b, []byte("\n")) { 166 b = bytes.Replace(b, []byte("\n"), []byte("\\n"), -1) 167 } 168 if bytes.Contains(b, []byte("\t")) { 169 b = bytes.Replace(b, []byte("\t"), []byte("\\t"), -1) 170 } 171 172 return b 173 } 174 175 return re.ReplaceAllFunc(by, repl) 176 } 177 178 func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) { 179 decoder := json.NewDecoder(bytes.NewReader(by)) 180 var o IncomingWebhookRequest 181 err := decoder.Decode(&o) 182 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 buf := new(bytes.Buffer) 191 buf.ReadFrom(data) 192 by := buf.Bytes() 193 194 // Try to decode the JSON data. Only if it fails, try to escape control 195 // characters from the strings contained in the JSON data. 196 o, err := decodeIncomingWebhookRequest(by) 197 if err != nil { 198 o, err = decodeIncomingWebhookRequest(escapeControlCharsFromPayload(by)) 199 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 func (o *IncomingWebhookRequest) ToJson() string { 210 b, err := json.Marshal(o) 211 if err != nil { 212 return "" 213 } else { 214 return string(b) 215 } 216 }