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