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 &copy
   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 &copy
   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  }