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