github.com/gigforks/mattermost-server@v4.9.1-0.20180619094218-800d97fa55d0+incompatible/model/post.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 "encoding/json" 8 "io" 9 "net/http" 10 "regexp" 11 "sort" 12 "strings" 13 "unicode/utf8" 14 15 "github.com/mattermost/mattermost-server/utils/markdown" 16 ) 17 18 const ( 19 POST_SYSTEM_MESSAGE_PREFIX = "system_" 20 POST_DEFAULT = "" 21 POST_SLACK_ATTACHMENT = "slack_attachment" 22 POST_SYSTEM_GENERIC = "system_generic" 23 POST_JOIN_LEAVE = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead 24 POST_JOIN_CHANNEL = "system_join_channel" 25 POST_LEAVE_CHANNEL = "system_leave_channel" 26 POST_JOIN_TEAM = "system_join_team" 27 POST_LEAVE_TEAM = "system_leave_team" 28 POST_AUTO_RESPONDER = "system_auto_responder" 29 POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead 30 POST_ADD_TO_CHANNEL = "system_add_to_channel" 31 POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" 32 POST_MOVE_CHANNEL = "system_move_channel" 33 POST_ADD_TO_TEAM = "system_add_to_team" 34 POST_REMOVE_FROM_TEAM = "system_remove_from_team" 35 POST_HEADER_CHANGE = "system_header_change" 36 POST_DISPLAYNAME_CHANGE = "system_displayname_change" 37 POST_CONVERT_CHANNEL = "system_convert_channel" 38 POST_PURPOSE_CHANGE = "system_purpose_change" 39 POST_CHANNEL_DELETED = "system_channel_deleted" 40 POST_EPHEMERAL = "system_ephemeral" 41 POST_CHANGE_CHANNEL_PRIVACY = "system_change_chan_privacy" 42 POST_FILEIDS_MAX_RUNES = 150 43 POST_FILENAMES_MAX_RUNES = 4000 44 POST_HASHTAGS_MAX_RUNES = 1000 45 POST_MESSAGE_MAX_RUNES_V1 = 4000 46 POST_MESSAGE_MAX_BYTES_V2 = 65535 // Maximum size of a TEXT column in MySQL 47 POST_MESSAGE_MAX_RUNES_V2 = POST_MESSAGE_MAX_BYTES_V2 / 4 // Assume a worst-case representation 48 POST_PROPS_MAX_RUNES = 8000 49 POST_PROPS_MAX_USER_RUNES = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications 50 POST_CUSTOM_TYPE_PREFIX = "custom_" 51 PROPS_ADD_CHANNEL_MEMBER = "add_channel_member" 52 POST_PROPS_ADDED_USER_ID = "addedUserId" 53 ) 54 55 type Post struct { 56 Id string `json:"id"` 57 CreateAt int64 `json:"create_at"` 58 UpdateAt int64 `json:"update_at"` 59 EditAt int64 `json:"edit_at"` 60 DeleteAt int64 `json:"delete_at"` 61 IsPinned bool `json:"is_pinned"` 62 UserId string `json:"user_id"` 63 ChannelId string `json:"channel_id"` 64 RootId string `json:"root_id"` 65 ParentId string `json:"parent_id"` 66 OriginalId string `json:"original_id"` 67 68 Message string `json:"message"` 69 70 // MessageSource will contain the message as submitted by the user if Message has been modified 71 // by Mattermost for presentation (e.g if an image proxy is being used). It should be used to 72 // populate edit boxes if present. 73 MessageSource string `json:"message_source,omitempty" db:"-"` 74 75 Type string `json:"type"` 76 Props StringInterface `json:"props"` 77 Hashtags string `json:"hashtags"` 78 Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more 79 FileIds StringArray `json:"file_ids,omitempty"` 80 PendingPostId string `json:"pending_post_id" db:"-"` 81 HasReactions bool `json:"has_reactions,omitempty"` 82 } 83 84 type PostEphemeral struct { 85 UserID string `json:"user_id"` 86 Post *Post `json:"post"` 87 } 88 89 type PostPatch struct { 90 IsPinned *bool `json:"is_pinned"` 91 Message *string `json:"message"` 92 Props *StringInterface `json:"props"` 93 FileIds *StringArray `json:"file_ids"` 94 HasReactions *bool `json:"has_reactions"` 95 } 96 97 func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { 98 copy := *o 99 if copy.Message != nil { 100 *copy.Message = RewriteImageURLs(*o.Message, f) 101 } 102 return © 103 } 104 105 type PostForIndexing struct { 106 Post 107 TeamId string `json:"team_id"` 108 ParentCreateAt *int64 `json:"parent_create_at"` 109 } 110 111 type PostAction struct { 112 Id string `json:"id"` 113 Name string `json:"name"` 114 Integration *PostActionIntegration `json:"integration,omitempty"` 115 } 116 117 type PostActionIntegration struct { 118 URL string `json:"url,omitempty"` 119 Context StringInterface `json:"context,omitempty"` 120 } 121 122 type PostActionIntegrationRequest struct { 123 UserId string `json:"user_id"` 124 Context StringInterface `json:"context,omitempty"` 125 } 126 127 type PostActionIntegrationResponse struct { 128 Update *Post `json:"update"` 129 EphemeralText string `json:"ephemeral_text"` 130 } 131 132 func (o *Post) ToJson() string { 133 copy := *o 134 copy.StripActionIntegrations() 135 b, _ := json.Marshal(©) 136 return string(b) 137 } 138 139 func (o *Post) ToUnsanitizedJson() string { 140 b, _ := json.Marshal(o) 141 return string(b) 142 } 143 144 func PostFromJson(data io.Reader) *Post { 145 var o *Post 146 json.NewDecoder(data).Decode(&o) 147 return o 148 } 149 150 func (o *Post) Etag() string { 151 return Etag(o.Id, o.UpdateAt) 152 } 153 154 func (o *Post) IsValid(maxPostSize int) *AppError { 155 156 if len(o.Id) != 26 { 157 return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest) 158 } 159 160 if o.CreateAt == 0 { 161 return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 162 } 163 164 if o.UpdateAt == 0 { 165 return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 166 } 167 168 if len(o.UserId) != 26 { 169 return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) 170 } 171 172 if len(o.ChannelId) != 26 { 173 return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) 174 } 175 176 if !(len(o.RootId) == 26 || len(o.RootId) == 0) { 177 return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) 178 } 179 180 if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { 181 return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest) 182 } 183 184 if len(o.ParentId) == 26 && len(o.RootId) == 0 { 185 return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest) 186 } 187 188 if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { 189 return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest) 190 } 191 192 if utf8.RuneCountInString(o.Message) > maxPostSize { 193 return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest) 194 } 195 196 if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES { 197 return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) 198 } 199 200 switch o.Type { 201 case 202 POST_DEFAULT, 203 POST_JOIN_LEAVE, 204 POST_AUTO_RESPONDER, 205 POST_ADD_REMOVE, 206 POST_JOIN_CHANNEL, 207 POST_LEAVE_CHANNEL, 208 POST_JOIN_TEAM, 209 POST_LEAVE_TEAM, 210 POST_ADD_TO_CHANNEL, 211 POST_REMOVE_FROM_CHANNEL, 212 POST_MOVE_CHANNEL, 213 POST_ADD_TO_TEAM, 214 POST_REMOVE_FROM_TEAM, 215 POST_SLACK_ATTACHMENT, 216 POST_HEADER_CHANGE, 217 POST_PURPOSE_CHANGE, 218 POST_DISPLAYNAME_CHANGE, 219 POST_CONVERT_CHANNEL, 220 POST_CHANNEL_DELETED, 221 POST_CHANGE_CHANNEL_PRIVACY: 222 default: 223 if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { 224 return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) 225 } 226 } 227 228 if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES { 229 return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) 230 } 231 232 if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES { 233 return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) 234 } 235 236 if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES { 237 return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) 238 } 239 240 return nil 241 } 242 243 func (o *Post) SanitizeProps() { 244 membersToSanitize := []string{ 245 PROPS_ADD_CHANNEL_MEMBER, 246 } 247 248 for _, member := range membersToSanitize { 249 if _, ok := o.Props[member]; ok { 250 delete(o.Props, member) 251 } 252 } 253 } 254 255 func (o *Post) PreSave() { 256 if o.Id == "" { 257 o.Id = NewId() 258 } 259 260 o.OriginalId = "" 261 262 if o.CreateAt == 0 { 263 o.CreateAt = GetMillis() 264 } 265 266 o.UpdateAt = o.CreateAt 267 o.PreCommit() 268 } 269 270 func (o *Post) PreCommit() { 271 if o.Props == nil { 272 o.Props = make(map[string]interface{}) 273 } 274 275 if o.Filenames == nil { 276 o.Filenames = []string{} 277 } 278 279 if o.FileIds == nil { 280 o.FileIds = []string{} 281 } 282 283 o.GenerateActionIds() 284 } 285 286 func (o *Post) MakeNonNil() { 287 if o.Props == nil { 288 o.Props = make(map[string]interface{}) 289 } 290 } 291 292 func (o *Post) AddProp(key string, value interface{}) { 293 294 o.MakeNonNil() 295 296 o.Props[key] = value 297 } 298 299 func (o *Post) IsSystemMessage() bool { 300 return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX 301 } 302 303 func (p *Post) Patch(patch *PostPatch) { 304 if patch.IsPinned != nil { 305 p.IsPinned = *patch.IsPinned 306 } 307 308 if patch.Message != nil { 309 p.Message = *patch.Message 310 } 311 312 if patch.Props != nil { 313 p.Props = *patch.Props 314 } 315 316 if patch.FileIds != nil { 317 p.FileIds = *patch.FileIds 318 } 319 320 if patch.HasReactions != nil { 321 p.HasReactions = *patch.HasReactions 322 } 323 } 324 325 func (o *PostPatch) ToJson() string { 326 b, err := json.Marshal(o) 327 if err != nil { 328 return "" 329 } 330 331 return string(b) 332 } 333 334 func PostPatchFromJson(data io.Reader) *PostPatch { 335 decoder := json.NewDecoder(data) 336 var post PostPatch 337 err := decoder.Decode(&post) 338 if err != nil { 339 return nil 340 } 341 342 return &post 343 } 344 345 var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`) 346 347 func (o *Post) ChannelMentions() (names []string) { 348 if strings.Contains(o.Message, "~") { 349 alreadyMentioned := make(map[string]bool) 350 for _, match := range channelMentionRegexp.FindAllString(o.Message, -1) { 351 name := match[1:] 352 if !alreadyMentioned[name] { 353 names = append(names, name) 354 alreadyMentioned[name] = true 355 } 356 } 357 } 358 return 359 } 360 361 func (r *PostActionIntegrationRequest) ToJson() string { 362 b, _ := json.Marshal(r) 363 return string(b) 364 } 365 366 func (o *Post) Attachments() []*SlackAttachment { 367 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 368 return attachments 369 } 370 var ret []*SlackAttachment 371 if attachments, ok := o.Props["attachments"].([]interface{}); ok { 372 for _, attachment := range attachments { 373 if enc, err := json.Marshal(attachment); err == nil { 374 var decoded SlackAttachment 375 if json.Unmarshal(enc, &decoded) == nil { 376 ret = append(ret, &decoded) 377 } 378 } 379 } 380 } 381 return ret 382 } 383 384 func (o *Post) StripActionIntegrations() { 385 attachments := o.Attachments() 386 if o.Props["attachments"] != nil { 387 o.Props["attachments"] = attachments 388 } 389 for _, attachment := range attachments { 390 for _, action := range attachment.Actions { 391 action.Integration = nil 392 } 393 } 394 } 395 396 func (o *Post) GetAction(id string) *PostAction { 397 for _, attachment := range o.Attachments() { 398 for _, action := range attachment.Actions { 399 if action.Id == id { 400 return action 401 } 402 } 403 } 404 return nil 405 } 406 407 func (o *Post) GenerateActionIds() { 408 if o.Props["attachments"] != nil { 409 o.Props["attachments"] = o.Attachments() 410 } 411 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 412 for _, attachment := range attachments { 413 for _, action := range attachment.Actions { 414 if action.Id == "" { 415 action.Id = NewId() 416 } 417 } 418 } 419 } 420 } 421 422 var markdownDestinationEscaper = strings.NewReplacer( 423 `\`, `\\`, 424 `<`, `\<`, 425 `>`, `\>`, 426 `(`, `\(`, 427 `)`, `\)`, 428 ) 429 430 // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been 431 // rewritten via RewriteImageURLs. 432 func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post { 433 copy := *o 434 copy.Message = RewriteImageURLs(o.Message, f) 435 if copy.MessageSource == "" && copy.Message != o.Message { 436 copy.MessageSource = o.Message 437 } 438 return © 439 } 440 441 func (o *PostEphemeral) ToUnsanitizedJson() string { 442 b, _ := json.Marshal(o) 443 return string(b) 444 } 445 446 // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced 447 // according to the function f. For each image URL, f will be invoked, and the resulting markdown 448 // will contain the URL returned by that invocation instead. 449 // 450 // Image URLs are destination URLs used in inline images or reference definitions that are used 451 // anywhere in the input markdown as an image. 452 func RewriteImageURLs(message string, f func(string) string) string { 453 if !strings.Contains(message, "![") { 454 return message 455 } 456 457 var ranges []markdown.Range 458 459 markdown.Inspect(message, func(blockOrInline interface{}) bool { 460 switch v := blockOrInline.(type) { 461 case *markdown.ReferenceImage: 462 ranges = append(ranges, v.ReferenceDefinition.RawDestination) 463 case *markdown.InlineImage: 464 ranges = append(ranges, v.RawDestination) 465 default: 466 return true 467 } 468 return true 469 }) 470 471 if ranges == nil { 472 return message 473 } 474 475 sort.Slice(ranges, func(i, j int) bool { 476 return ranges[i].Position < ranges[j].Position 477 }) 478 479 copyRanges := make([]markdown.Range, 0, len(ranges)) 480 urls := make([]string, 0, len(ranges)) 481 resultLength := len(message) 482 483 start := 0 484 for i, r := range ranges { 485 switch { 486 case i == 0: 487 case r.Position != ranges[i-1].Position: 488 start = ranges[i-1].End 489 default: 490 continue 491 } 492 original := message[r.Position:r.End] 493 replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original))) 494 resultLength += len(replacement) - len(original) 495 copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position}) 496 urls = append(urls, replacement) 497 } 498 499 result := make([]byte, resultLength) 500 501 offset := 0 502 for i, r := range copyRanges { 503 offset += copy(result[offset:], message[r.Position:r.End]) 504 offset += copy(result[offset:], urls[i]) 505 } 506 copy(result[offset:], message[ranges[len(ranges)-1].End:]) 507 508 return string(result) 509 }