diff options
Diffstat (limited to 'model')
-rw-r--r-- | model/client4.go | 68 | ||||
-rw-r--r-- | model/config.go | 144 | ||||
-rw-r--r-- | model/emoji.go | 7 | ||||
-rw-r--r-- | model/emoji_search.go | 34 | ||||
-rw-r--r-- | model/emoji_search_test.go | 19 | ||||
-rw-r--r-- | model/post.go | 184 | ||||
-rw-r--r-- | model/post_list.go | 9 | ||||
-rw-r--r-- | model/post_test.go | 127 | ||||
-rw-r--r-- | model/reaction.go | 2 | ||||
-rw-r--r-- | model/testdata/markdown-sample-with-rewritten-image-urls.md | 245 | ||||
-rw-r--r-- | model/testdata/markdown-sample.md | 245 | ||||
-rw-r--r-- | model/user_access_token_search.go | 35 | ||||
-rw-r--r-- | model/user_access_token_search_test.go | 19 | ||||
-rw-r--r-- | model/utils.go | 12 |
14 files changed, 1047 insertions, 103 deletions
diff --git a/model/client4.go b/model/client4.go index e84a23e5f..151b5a491 100644 --- a/model/client4.go +++ b/model/client4.go @@ -82,6 +82,10 @@ func (c *Client4) GetUserRoute(userId string) string { return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId) } +func (c *Client4) GetUserAccessTokensRoute() string { + return fmt.Sprintf(c.GetUsersRoute() + "/tokens") +} + func (c *Client4) GetUserAccessTokenRoute(tokenId string) string { return fmt.Sprintf(c.GetUsersRoute()+"/tokens/%v", tokenId) } @@ -1035,10 +1039,23 @@ func (c *Client4) CreateUserAccessToken(userId, description string) (*UserAccess } } -// GetUserAccessToken will get a user access token's id, description and the user_id -// of the user it is for. The actual token will not be returned. Must have the -// 'read_user_access_token' permission and if getting for another user, must have the -// 'edit_other_users' permission. +// GetUserAccessTokens will get a page of access tokens' id, description, is_active +// and the user_id in the system. The actual token will not be returned. Must have +// the 'manage_system' permission. +func (c *Client4) GetUserAccessTokens(page int, perPage int) ([]*UserAccessToken, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage) + if r, err := c.DoApiGet(c.GetUserAccessTokensRoute()+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UserAccessTokenListFromJson(r.Body), BuildResponse(r) + } +} + +// GetUserAccessToken will get a user access tokens' id, description, is_active +// and the user_id of the user it is for. The actual token will not be returned. +// Must have the 'read_user_access_token' permission and if getting for another +// user, must have the 'edit_other_users' permission. func (c *Client4) GetUserAccessToken(tokenId string) (*UserAccessToken, *Response) { if r, err := c.DoApiGet(c.GetUserAccessTokenRoute(tokenId), ""); err != nil { return nil, BuildErrorResponse(r, err) @@ -1075,6 +1092,16 @@ func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) { } } +// SearchUserAccessTokens returns user access tokens matching the provided search term. +func (c *Client4) SearchUserAccessTokens(search *UserAccessTokenSearch) ([]*UserAccessToken, *Response) { + if r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/search", search.ToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return UserAccessTokenListFromJson(r.Body), BuildResponse(r) + } +} + // DisableUserAccessToken will disable a user access token by id. Must have the // 'revoke_user_access_token' permission and if disabling for another user, must have the // 'edit_other_users' permission. @@ -2996,6 +3023,18 @@ func (c *Client4) GetEmojiList(page, perPage int) ([]*Emoji, *Response) { } } +// GetSortedEmojiList returns a page of custom emoji on the system sorted based on the sort +// parameter, blank for no sorting and "name" to sort by emoji names. +func (c *Client4) GetSortedEmojiList(page, perPage int, sort string) ([]*Emoji, *Response) { + query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v", page, perPage, sort) + if r, err := c.DoApiGet(c.GetEmojisRoute()+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) + } +} + // DeleteEmoji delete an custom emoji on the provided emoji id string. func (c *Client4) DeleteEmoji(emojiId string) (bool, *Response) { if r, err := c.DoApiDelete(c.GetEmojiRoute(emojiId)); err != nil { @@ -3031,6 +3070,27 @@ func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response) { } } +// SearchEmoji returns a list of emoji matching some search criteria. +func (c *Client4) SearchEmoji(search *EmojiSearch) ([]*Emoji, *Response) { + if r, err := c.DoApiPost(c.GetEmojisRoute()+"/search", search.ToJson()); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) + } +} + +// AutocompleteEmoji returns a list of emoji starting with or matching name. +func (c *Client4) AutocompleteEmoji(name string, etag string) ([]*Emoji, *Response) { + query := fmt.Sprintf("?name=%v", name) + if r, err := c.DoApiGet(c.GetEmojisRoute()+"/autocomplete"+query, ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return EmojiListFromJson(r.Body), BuildResponse(r) + } +} + // Reaction Section // SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned. diff --git a/model/config.go b/model/config.go index fb34d1a04..6e3e8859e 100644 --- a/model/config.go +++ b/model/config.go @@ -157,62 +157,66 @@ const ( ) type ServiceSettings struct { - SiteURL *string - LicenseFileLocation *string - ListenAddress *string - ConnectionSecurity *string - TLSCertFile *string - TLSKeyFile *string - UseLetsEncrypt *bool - LetsEncryptCertificateCacheFile *string - Forward80To443 *bool - ReadTimeout *int - WriteTimeout *int - MaximumLoginAttempts *int - GoroutineHealthThreshold *int - GoogleDeveloperKey string - EnableOAuthServiceProvider bool - EnableIncomingWebhooks bool - EnableOutgoingWebhooks bool - EnableCommands *bool - EnableOnlyAdminIntegrations *bool - EnablePostUsernameOverride bool - EnablePostIconOverride bool - EnableAPIv3 *bool - EnableLinkPreviews *bool - EnableTesting bool - EnableDeveloper *bool - EnableSecurityFixAlert *bool - EnableInsecureOutgoingConnections *bool - AllowedUntrustedInternalConnections *string - EnableMultifactorAuthentication *bool - EnforceMultifactorAuthentication *bool - EnableUserAccessTokens *bool - AllowCorsFrom *string - SessionLengthWebInDays *int - SessionLengthMobileInDays *int - SessionLengthSSOInDays *int - SessionCacheInMinutes *int - SessionIdleTimeoutInMinutes *int - WebsocketSecurePort *int - WebsocketPort *int - WebserverMode *string - EnableCustomEmoji *bool - EnableEmojiPicker *bool - RestrictCustomEmojiCreation *string - RestrictPostDelete *string - AllowEditPost *string - PostEditTimeLimit *int - TimeBetweenUserTypingUpdatesMilliseconds *int64 - EnablePostSearch *bool - EnableUserTypingMessages *bool - EnableChannelViewedMessages *bool - EnableUserStatuses *bool - ExperimentalEnableAuthenticationTransfer *bool - ClusterLogTimeoutMilliseconds *int - CloseUnusedDirectMessages *bool - EnablePreviewFeatures *bool - EnableTutorial *bool + SiteURL *string + LicenseFileLocation *string + ListenAddress *string + ConnectionSecurity *string + TLSCertFile *string + TLSKeyFile *string + UseLetsEncrypt *bool + LetsEncryptCertificateCacheFile *string + Forward80To443 *bool + ReadTimeout *int + WriteTimeout *int + MaximumLoginAttempts *int + GoroutineHealthThreshold *int + GoogleDeveloperKey string + EnableOAuthServiceProvider bool + EnableIncomingWebhooks bool + EnableOutgoingWebhooks bool + EnableCommands *bool + EnableOnlyAdminIntegrations *bool + EnablePostUsernameOverride bool + EnablePostIconOverride bool + EnableAPIv3 *bool + EnableLinkPreviews *bool + EnableTesting bool + EnableDeveloper *bool + EnableSecurityFixAlert *bool + EnableInsecureOutgoingConnections *bool + AllowedUntrustedInternalConnections *string + EnableMultifactorAuthentication *bool + EnforceMultifactorAuthentication *bool + EnableUserAccessTokens *bool + AllowCorsFrom *string + SessionLengthWebInDays *int + SessionLengthMobileInDays *int + SessionLengthSSOInDays *int + SessionCacheInMinutes *int + SessionIdleTimeoutInMinutes *int + WebsocketSecurePort *int + WebsocketPort *int + WebserverMode *string + EnableCustomEmoji *bool + EnableEmojiPicker *bool + RestrictCustomEmojiCreation *string + RestrictPostDelete *string + AllowEditPost *string + PostEditTimeLimit *int + TimeBetweenUserTypingUpdatesMilliseconds *int64 + EnablePostSearch *bool + EnableUserTypingMessages *bool + EnableChannelViewedMessages *bool + EnableUserStatuses *bool + ExperimentalEnableAuthenticationTransfer *bool + ClusterLogTimeoutMilliseconds *int + CloseUnusedDirectMessages *bool + EnablePreviewFeatures *bool + EnableTutorial *bool + ExperimentalEnableDefaultChannelLeaveJoinMessages *bool + ImageProxyType *string + ImageProxyURL *string + ImageProxyOptions *string } func (s *ServiceSettings) SetDefaults() { @@ -249,7 +253,7 @@ func (s *ServiceSettings) SetDefaults() { } if s.AllowedUntrustedInternalConnections == nil { - s.AllowedUntrustedInternalConnections = new(string) + s.AllowedUntrustedInternalConnections = NewString("") } if s.EnableMultifactorAuthentication == nil { @@ -413,6 +417,22 @@ func (s *ServiceSettings) SetDefaults() { if s.EnablePreviewFeatures == nil { s.EnablePreviewFeatures = NewBool(true) } + + if s.ExperimentalEnableDefaultChannelLeaveJoinMessages == nil { + s.ExperimentalEnableDefaultChannelLeaveJoinMessages = NewBool(true) + } + + if s.ImageProxyType == nil { + s.ImageProxyType = NewString("") + } + + if s.ImageProxyURL == nil { + s.ImageProxyURL = NewString("") + } + + if s.ImageProxyOptions == nil { + s.ImageProxyOptions = NewString("") + } } type ClusterSettings struct { @@ -2045,6 +2065,16 @@ func (ss *ServiceSettings) isValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest) } + switch *ss.ImageProxyType { + case "", "willnorris/imageproxy": + case "atmos/camo": + if *ss.ImageProxyOptions == "" { + return NewAppError("Config.IsValid", "model.config.is_valid.atmos_camo_image_proxy_options.app_error", nil, "", http.StatusBadRequest) + } + default: + return NewAppError("Config.IsValid", "model.config.is_valid.image_proxy_type.app_error", nil, "", http.StatusBadRequest) + } + return nil } diff --git a/model/emoji.go b/model/emoji.go index 272616d90..3deff4c5f 100644 --- a/model/emoji.go +++ b/model/emoji.go @@ -9,6 +9,11 @@ import ( "net/http" ) +const ( + EMOJI_NAME_MAX_LENGTH = 64 + EMOJI_SORT_BY_NAME = "name" +) + type Emoji struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` @@ -35,7 +40,7 @@ func (emoji *Emoji) IsValid() *AppError { return NewAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "", http.StatusBadRequest) } - if len(emoji.Name) == 0 || len(emoji.Name) > 64 || !IsValidAlphaNumHyphenUnderscore(emoji.Name, false) { + if len(emoji.Name) == 0 || len(emoji.Name) > EMOJI_NAME_MAX_LENGTH || !IsValidAlphaNumHyphenUnderscore(emoji.Name, false) { return NewAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "", http.StatusBadRequest) } diff --git a/model/emoji_search.go b/model/emoji_search.go new file mode 100644 index 000000000..31931170e --- /dev/null +++ b/model/emoji_search.go @@ -0,0 +1,34 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type EmojiSearch struct { + Term string `json:"term"` + PrefixOnly bool `json:"prefix_only"` +} + +func (es *EmojiSearch) ToJson() string { + b, err := json.Marshal(es) + if err != nil { + return "" + } else { + return string(b) + } +} + +func EmojiSearchFromJson(data io.Reader) *EmojiSearch { + decoder := json.NewDecoder(data) + var es EmojiSearch + err := decoder.Decode(&es) + if err == nil { + return &es + } else { + return nil + } +} diff --git a/model/emoji_search_test.go b/model/emoji_search_test.go new file mode 100644 index 000000000..6e3b01213 --- /dev/null +++ b/model/emoji_search_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestEmojiSearchJson(t *testing.T) { + emojiSearch := EmojiSearch{Term: NewId()} + json := emojiSearch.ToJson() + remojiSearch := EmojiSearchFromJson(strings.NewReader(json)) + + if emojiSearch.Term != remojiSearch.Term { + t.Fatal("Terms do not match") + } +} diff --git a/model/post.go b/model/post.go index 6b282fbfa..f3d990a87 100644 --- a/model/post.go +++ b/model/post.go @@ -8,53 +8,64 @@ import ( "io" "net/http" "regexp" + "sort" "strings" "unicode/utf8" + + "github.com/mattermost/mattermost-server/utils/markdown" ) const ( - POST_SYSTEM_MESSAGE_PREFIX = "system_" - POST_DEFAULT = "" - POST_SLACK_ATTACHMENT = "slack_attachment" - POST_SYSTEM_GENERIC = "system_generic" - POST_JOIN_LEAVE = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead - POST_JOIN_CHANNEL = "system_join_channel" - POST_LEAVE_CHANNEL = "system_leave_channel" - POST_JOIN_TEAM = "system_join_team" - POST_LEAVE_TEAM = "system_leave_team" - POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead - POST_ADD_TO_CHANNEL = "system_add_to_channel" - POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" - POST_ADD_TO_TEAM = "system_add_to_team" - POST_REMOVE_FROM_TEAM = "system_remove_from_team" - POST_HEADER_CHANGE = "system_header_change" - POST_DISPLAYNAME_CHANGE = "system_displayname_change" - POST_PURPOSE_CHANGE = "system_purpose_change" - POST_CHANNEL_DELETED = "system_channel_deleted" - POST_EPHEMERAL = "system_ephemeral" - POST_FILEIDS_MAX_RUNES = 150 - POST_FILENAMES_MAX_RUNES = 4000 - POST_HASHTAGS_MAX_RUNES = 1000 - POST_MESSAGE_MAX_RUNES = 4000 - POST_PROPS_MAX_RUNES = 8000 - POST_PROPS_MAX_USER_RUNES = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications - POST_CUSTOM_TYPE_PREFIX = "custom_" - PROPS_ADD_CHANNEL_MEMBER = "add_channel_member" + POST_SYSTEM_MESSAGE_PREFIX = "system_" + POST_DEFAULT = "" + POST_SLACK_ATTACHMENT = "slack_attachment" + POST_SYSTEM_GENERIC = "system_generic" + POST_JOIN_LEAVE = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead + POST_JOIN_CHANNEL = "system_join_channel" + POST_LEAVE_CHANNEL = "system_leave_channel" + POST_JOIN_TEAM = "system_join_team" + POST_LEAVE_TEAM = "system_leave_team" + POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead + POST_ADD_TO_CHANNEL = "system_add_to_channel" + POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" + POST_ADD_TO_TEAM = "system_add_to_team" + POST_REMOVE_FROM_TEAM = "system_remove_from_team" + POST_HEADER_CHANGE = "system_header_change" + POST_DISPLAYNAME_CHANGE = "system_displayname_change" + POST_PURPOSE_CHANGE = "system_purpose_change" + POST_CHANNEL_DELETED = "system_channel_deleted" + POST_EPHEMERAL = "system_ephemeral" + POST_CHANGE_CHANNEL_PRIVACY = "system_change_chan_privacy" + POST_FILEIDS_MAX_RUNES = 150 + POST_FILENAMES_MAX_RUNES = 4000 + POST_HASHTAGS_MAX_RUNES = 1000 + POST_MESSAGE_MAX_RUNES = 4000 + POST_PROPS_MAX_RUNES = 8000 + POST_PROPS_MAX_USER_RUNES = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications + POST_CUSTOM_TYPE_PREFIX = "custom_" + PROPS_ADD_CHANNEL_MEMBER = "add_channel_member" ) type Post struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - EditAt int64 `json:"edit_at"` - DeleteAt int64 `json:"delete_at"` - IsPinned bool `json:"is_pinned"` - UserId string `json:"user_id"` - ChannelId string `json:"channel_id"` - RootId string `json:"root_id"` - ParentId string `json:"parent_id"` - OriginalId string `json:"original_id"` - Message string `json:"message"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + EditAt int64 `json:"edit_at"` + DeleteAt int64 `json:"delete_at"` + IsPinned bool `json:"is_pinned"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + RootId string `json:"root_id"` + ParentId string `json:"parent_id"` + OriginalId string `json:"original_id"` + + Message string `json:"message"` + + // MessageSource will contain the message as submitted by the user if Message has been modified + // by Mattermost for presentation (e.g if an image proxy is being used). It should be used to + // populate edit boxes if present. + MessageSource string `json:"message_source,omitempty" db:"-"` + Type string `json:"type"` Props StringInterface `json:"props"` Hashtags string `json:"hashtags"` @@ -72,6 +83,14 @@ type PostPatch struct { HasReactions *bool `json:"has_reactions"` } +func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { + copy := *o + if copy.Message != nil { + *copy.Message = RewriteImageURLs(*o.Message, f) + } + return © +} + type PostForIndexing struct { Post TeamId string `json:"team_id"` @@ -188,7 +207,8 @@ func (o *Post) IsValid() *AppError { POST_HEADER_CHANGE, POST_PURPOSE_CHANGE, POST_DISPLAYNAME_CHANGE, - POST_CHANNEL_DELETED: + POST_CHANNEL_DELETED, + POST_CHANGE_CHANNEL_PRIVACY: default: if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) @@ -392,3 +412,87 @@ func (o *Post) GenerateActionIds() { } } } + +var markdownDestinationEscaper = strings.NewReplacer( + `\`, `\\`, + `<`, `\<`, + `>`, `\>`, + `(`, `\(`, + `)`, `\)`, +) + +// WithRewrittenImageURLs returns a new shallow copy of the post where the message has been +// rewritten via RewriteImageURLs. +func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post { + copy := *o + copy.Message = RewriteImageURLs(o.Message, f) + if copy.MessageSource == "" && copy.Message != o.Message { + copy.MessageSource = o.Message + } + return © +} + +// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced +// according to the function f. For each image URL, f will be invoked, and the resulting markdown +// will contain the URL returned by that invocation instead. +// +// Image URLs are destination URLs used in inline images or reference definitions that are used +// anywhere in the input markdown as an image. +func RewriteImageURLs(message string, f func(string) string) string { + if !strings.Contains(message, "![") { + return message + } + + var ranges []markdown.Range + + markdown.Inspect(message, func(blockOrInline interface{}) bool { + switch v := blockOrInline.(type) { + case *markdown.ReferenceImage: + ranges = append(ranges, v.ReferenceDefinition.RawDestination) + case *markdown.InlineImage: + ranges = append(ranges, v.RawDestination) + default: + return true + } + return true + }) + + if ranges == nil { + return message + } + + sort.Slice(ranges, func(i, j int) bool { + return ranges[i].Position < ranges[j].Position + }) + + copyRanges := make([]markdown.Range, 0, len(ranges)) + urls := make([]string, 0, len(ranges)) + resultLength := len(message) + + start := 0 + for i, r := range ranges { + switch { + case i == 0: + case r.Position != ranges[i-1].Position: + start = ranges[i-1].End + default: + continue + } + original := message[r.Position:r.End] + replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original))) + resultLength += len(replacement) - len(original) + copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position}) + urls = append(urls, replacement) + } + + result := make([]byte, resultLength) + + offset := 0 + for i, r := range copyRanges { + offset += copy(result[offset:], message[r.Position:r.End]) + offset += copy(result[offset:], urls[i]) + } + copy(result[offset:], message[ranges[len(ranges)-1].End:]) + + return string(result) +} diff --git a/model/post_list.go b/model/post_list.go index 018f7d14f..09cddfdcf 100644 --- a/model/post_list.go +++ b/model/post_list.go @@ -21,6 +21,15 @@ func NewPostList() *PostList { } } +func (o *PostList) WithRewrittenImageURLs(f func(string) string) *PostList { + copy := *o + copy.Posts = make(map[string]*Post) + for id, post := range o.Posts { + copy.Posts[id] = post.WithRewrittenImageURLs(f) + } + return © +} + func (o *PostList) StripActionIntegrations() { posts := o.Posts o.Posts = make(map[string]*Post) diff --git a/model/post_test.go b/model/post_test.go index 6a908887d..5d5e7c9ec 100644 --- a/model/post_test.go +++ b/model/post_test.go @@ -4,6 +4,7 @@ package model import ( + "io/ioutil" "strings" "testing" @@ -173,3 +174,129 @@ func TestPostSanitizeProps(t *testing.T) { t.Fatal("should not be nil") } } + +var markdownSample, markdownSampleWithRewrittenImageURLs string + +func init() { + bytes, err := ioutil.ReadFile("testdata/markdown-sample.md") + if err != nil { + panic(err) + } + markdownSample = string(bytes) + + bytes, err = ioutil.ReadFile("testdata/markdown-sample-with-rewritten-image-urls.md") + if err != nil { + panic(err) + } + markdownSampleWithRewrittenImageURLs = string(bytes) +} + +func TestRewriteImageURLs(t *testing.T) { + for name, tc := range map[string]struct { + Markdown string + Expected string + }{ + "Empty": { + Markdown: ``, + Expected: ``, + }, + "NoImages": { + Markdown: `foo`, + Expected: `foo`, + }, + "Link": { + Markdown: `[foo](/url)`, + Expected: `[foo](/url)`, + }, + "Image": { + Markdown: `![foo](/url)`, + Expected: `![foo](rewritten:/url)`, + }, + "SpacedURL": { + Markdown: `![foo]( /url )`, + Expected: `![foo]( rewritten:/url )`, + }, + "Title": { + Markdown: `![foo](/url "title")`, + Expected: `![foo](rewritten:/url "title")`, + }, + "Parentheses": { + Markdown: `![foo](/url(1) "title")`, + Expected: `![foo](rewritten:/url\(1\) "title")`, + }, + "AngleBrackets": { + Markdown: `![foo](</url\<1\>\\> "title")`, + Expected: `![foo](<rewritten:/url\<1\>\\> "title")`, + }, + "MultipleLines": { + Markdown: `![foo]( + </url\<1\>\\> + "title" + )`, + Expected: `![foo]( + <rewritten:/url\<1\>\\> + "title" + )`, + }, + "ReferenceLink": { + Markdown: `[foo]: </url\<1\>\\> "title" + [foo]`, + Expected: `[foo]: </url\<1\>\\> "title" + [foo]`, + }, + "ReferenceImage": { + Markdown: `[foo]: </url\<1\>\\> "title" + ![foo]`, + Expected: `[foo]: <rewritten:/url\<1\>\\> "title" + ![foo]`, + }, + "MultipleReferenceImages": { + Markdown: `[foo]: </url1> "title" + [bar]: </url2> + [baz]: /url3 "title" + [qux]: /url4 + ![foo]![qux]`, + Expected: `[foo]: <rewritten:/url1> "title" + [bar]: </url2> + [baz]: /url3 "title" + [qux]: rewritten:/url4 + ![foo]![qux]`, + }, + "DuplicateReferences": { + Markdown: `[foo]: </url1> "title" + [foo]: </url2> + [foo]: /url3 "title" + [foo]: /url4 + ![foo]![foo]![foo]`, + Expected: `[foo]: <rewritten:/url1> "title" + [foo]: </url2> + [foo]: /url3 "title" + [foo]: /url4 + ![foo]![foo]![foo]`, + }, + "TrailingURL": { + Markdown: "![foo]\n\n[foo]: /url", + Expected: "![foo]\n\n[foo]: rewritten:/url", + }, + "Sample": { + Markdown: markdownSample, + Expected: markdownSampleWithRewrittenImageURLs, + }, + } { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.Expected, RewriteImageURLs(tc.Markdown, func(url string) string { + return "rewritten:" + url + })) + }) + } +} + +var rewriteImageURLsSink string + +func BenchmarkRewriteImageURLs(b *testing.B) { + for i := 0; i < b.N; i++ { + rewriteImageURLsSink = RewriteImageURLs(markdownSample, func(url string) string { + return "rewritten:" + url + }) + } +} diff --git a/model/reaction.go b/model/reaction.go index 4b72dd444..8c9b67029 100644 --- a/model/reaction.go +++ b/model/reaction.go @@ -64,7 +64,7 @@ func (o *Reaction) IsValid() *AppError { validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`) - if len(o.EmojiName) == 0 || len(o.EmojiName) > 64 || !validName.MatchString(o.EmojiName) { + if len(o.EmojiName) == 0 || len(o.EmojiName) > EMOJI_NAME_MAX_LENGTH || !validName.MatchString(o.EmojiName) { return NewAppError("Reaction.IsValid", "model.reaction.is_valid.emoji_name.app_error", nil, "emoji_name="+o.EmojiName, http.StatusBadRequest) } diff --git a/model/testdata/markdown-sample-with-rewritten-image-urls.md b/model/testdata/markdown-sample-with-rewritten-image-urls.md new file mode 100644 index 000000000..6683bc459 --- /dev/null +++ b/model/testdata/markdown-sample-with-rewritten-image-urls.md @@ -0,0 +1,245 @@ +--- +__Advertisement :)__ + +- __[pica](https://nodeca.github.io/pica/demo/)__ - high quality and fast image + resize in browser. +- __[babelfish](https://github.com/nodeca/babelfish/)__ - developer friendly + i18n with plurals support and easy syntax. + +You will like those projects! + +--- + +# h1 Heading 8-) +## h2 Heading +### h3 Heading +#### h4 Heading +##### h5 Heading +###### h6 Heading + + +## Horizontal Rules + +___ + +--- + +*** + + +## Typographic replacements + +Enable typographer option to see result. + +(c) (C) (r) (R) (tm) (TM) (p) (P) +- + +test.. test... test..... test?..... test!.... + +!!!!!! ???? ,, -- --- + +"Smartypants, double quotes" and 'single quotes' + + +## Emphasis + +**This is bold text** + +__This is bold text__ + +*This is italic text* + +_This is italic text_ + +~~Strikethrough~~ + + +## Blockquotes + + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs right next to each other... +> > > ...or with spaces between arrows. + + +## Lists + +Unordered + ++ Create a list by starting a line with `+`, `-`, or `*` ++ Sub-lists are made by indenting 2 spaces: + - Marker character change forces new list start: + * Ac tristique libero volutpat at + + Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit ++ Very easy! + +Ordered + +1. Lorem ipsum dolor sit amet +2. Consectetur adipiscing elit +3. Integer molestie lorem at massa + + +1. You can use sequential numbers... +1. ...or keep all the numbers as `1.` + +Start numbering with offset: + +57. foo +1. bar + + +## Code + +Inline `code` + +Indented code + + // Some comments + line 1 of code + line 2 of code + line 3 of code + + +Block code "fences" + +``` +Sample text here... +``` + +Syntax highlighting + +``` js +var foo = function (bar) { + return bar++; +}; + +console.log(foo(5)); +``` + +## Tables + +| Option | Description | +| ------ | ----------- | +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + +Right aligned columns + +| Option | Description | +| ------:| -----------:| +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + + +## Links + +[link text](http://dev.nodeca.com) + +[link with title](http://nodeca.github.io/pica/demo/ "title text!") + +Autoconverted link https://github.com/nodeca/pica (enable linkify to see) + + +## Images + +![Minion](rewritten:https://octodex.github.com/images/minion.png) +![Stormtroopocat](rewritten:https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") + +Like links, Images also have a footnote style syntax + +![Alt text][id] + +With a reference later in the document defining the URL location: + +[id]: rewritten:https://octodex.github.com/images/dojocat.jpg "The Dojocat" + + +## Plugins + +The killer feature of `markdown-it` is very effective support of +[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). + + +### [Emojies](https://github.com/markdown-it/markdown-it-emoji) + +> Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum: +> +> Shortcuts (emoticons): :-) :-( 8-) ;) + +see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji. + + +### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup) + +- 19^th^ +- H~2~O + + +### [\<ins>](https://github.com/markdown-it/markdown-it-ins) + +++Inserted text++ + + +### [\<mark>](https://github.com/markdown-it/markdown-it-mark) + +==Marked text== + + +### [Footnotes](https://github.com/markdown-it/markdown-it-footnote) + +Footnote 1 link[^first]. + +Footnote 2 link[^second]. + +Inline footnote^[Text of inline footnote] definition. + +Duplicated footnote reference[^second]. + +[^first]: Footnote **can have markup** + + and multiple paragraphs. + +[^second]: Footnote text. + + +### [Definition lists](https://github.com/markdown-it/markdown-it-deflist) + +Term 1 + +: Definition 1 +with lazy continuation. + +Term 2 with *inline markup* + +: Definition 2 + + { some code, part of Definition 2 } + + Third paragraph of definition 2. + +_Compact style:_ + +Term 1 + ~ Definition 1 + +Term 2 + ~ Definition 2a + ~ Definition 2b + + +### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr) + +This is HTML abbreviation example. + +It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. + +*[HTML]: Hyper Text Markup Language + +### [Custom containers](https://github.com/markdown-it/markdown-it-container) + +::: warning +*here be dragons* +::: diff --git a/model/testdata/markdown-sample.md b/model/testdata/markdown-sample.md new file mode 100644 index 000000000..f894c1d35 --- /dev/null +++ b/model/testdata/markdown-sample.md @@ -0,0 +1,245 @@ +--- +__Advertisement :)__ + +- __[pica](https://nodeca.github.io/pica/demo/)__ - high quality and fast image + resize in browser. +- __[babelfish](https://github.com/nodeca/babelfish/)__ - developer friendly + i18n with plurals support and easy syntax. + +You will like those projects! + +--- + +# h1 Heading 8-) +## h2 Heading +### h3 Heading +#### h4 Heading +##### h5 Heading +###### h6 Heading + + +## Horizontal Rules + +___ + +--- + +*** + + +## Typographic replacements + +Enable typographer option to see result. + +(c) (C) (r) (R) (tm) (TM) (p) (P) +- + +test.. test... test..... test?..... test!.... + +!!!!!! ???? ,, -- --- + +"Smartypants, double quotes" and 'single quotes' + + +## Emphasis + +**This is bold text** + +__This is bold text__ + +*This is italic text* + +_This is italic text_ + +~~Strikethrough~~ + + +## Blockquotes + + +> Blockquotes can also be nested... +>> ...by using additional greater-than signs right next to each other... +> > > ...or with spaces between arrows. + + +## Lists + +Unordered + ++ Create a list by starting a line with `+`, `-`, or `*` ++ Sub-lists are made by indenting 2 spaces: + - Marker character change forces new list start: + * Ac tristique libero volutpat at + + Facilisis in pretium nisl aliquet + - Nulla volutpat aliquam velit ++ Very easy! + +Ordered + +1. Lorem ipsum dolor sit amet +2. Consectetur adipiscing elit +3. Integer molestie lorem at massa + + +1. You can use sequential numbers... +1. ...or keep all the numbers as `1.` + +Start numbering with offset: + +57. foo +1. bar + + +## Code + +Inline `code` + +Indented code + + // Some comments + line 1 of code + line 2 of code + line 3 of code + + +Block code "fences" + +``` +Sample text here... +``` + +Syntax highlighting + +``` js +var foo = function (bar) { + return bar++; +}; + +console.log(foo(5)); +``` + +## Tables + +| Option | Description | +| ------ | ----------- | +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + +Right aligned columns + +| Option | Description | +| ------:| -----------:| +| data | path to data files to supply the data that will be passed into templates. | +| engine | engine to be used for processing templates. Handlebars is the default. | +| ext | extension to be used for dest files. | + + +## Links + +[link text](http://dev.nodeca.com) + +[link with title](http://nodeca.github.io/pica/demo/ "title text!") + +Autoconverted link https://github.com/nodeca/pica (enable linkify to see) + + +## Images + +![Minion](https://octodex.github.com/images/minion.png) +![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") + +Like links, Images also have a footnote style syntax + +![Alt text][id] + +With a reference later in the document defining the URL location: + +[id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" + + +## Plugins + +The killer feature of `markdown-it` is very effective support of +[syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). + + +### [Emojies](https://github.com/markdown-it/markdown-it-emoji) + +> Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum: +> +> Shortcuts (emoticons): :-) :-( 8-) ;) + +see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji. + + +### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup) + +- 19^th^ +- H~2~O + + +### [\<ins>](https://github.com/markdown-it/markdown-it-ins) + +++Inserted text++ + + +### [\<mark>](https://github.com/markdown-it/markdown-it-mark) + +==Marked text== + + +### [Footnotes](https://github.com/markdown-it/markdown-it-footnote) + +Footnote 1 link[^first]. + +Footnote 2 link[^second]. + +Inline footnote^[Text of inline footnote] definition. + +Duplicated footnote reference[^second]. + +[^first]: Footnote **can have markup** + + and multiple paragraphs. + +[^second]: Footnote text. + + +### [Definition lists](https://github.com/markdown-it/markdown-it-deflist) + +Term 1 + +: Definition 1 +with lazy continuation. + +Term 2 with *inline markup* + +: Definition 2 + + { some code, part of Definition 2 } + + Third paragraph of definition 2. + +_Compact style:_ + +Term 1 + ~ Definition 1 + +Term 2 + ~ Definition 2a + ~ Definition 2b + + +### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr) + +This is HTML abbreviation example. + +It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. + +*[HTML]: Hyper Text Markup Language + +### [Custom containers](https://github.com/markdown-it/markdown-it-container) + +::: warning +*here be dragons* +::: diff --git a/model/user_access_token_search.go b/model/user_access_token_search.go new file mode 100644 index 000000000..1b0146edb --- /dev/null +++ b/model/user_access_token_search.go @@ -0,0 +1,35 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +type UserAccessTokenSearch struct { + Term string `json:"term"` +} + +// ToJson convert a UserAccessTokenSearch to json string +func (c *UserAccessTokenSearch) ToJson() string { + b, err := json.Marshal(c) + if err != nil { + return "" + } + + return string(b) +} + +// UserAccessTokenSearchJson decodes the input and returns a UserAccessTokenSearch +func UserAccessTokenSearchFromJson(data io.Reader) *UserAccessTokenSearch { + decoder := json.NewDecoder(data) + var cs UserAccessTokenSearch + err := decoder.Decode(&cs) + if err == nil { + return &cs + } + + return nil +} diff --git a/model/user_access_token_search_test.go b/model/user_access_token_search_test.go new file mode 100644 index 000000000..15a53f536 --- /dev/null +++ b/model/user_access_token_search_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "strings" + "testing" +) + +func TestUserAccessTokenSearchJson(t *testing.T) { + userAccessTokenSearch := UserAccessTokenSearch{Term: NewId()} + json := userAccessTokenSearch.ToJson() + ruserAccessTokenSearch := UserAccessTokenSearchFromJson(strings.NewReader(json)) + + if userAccessTokenSearch.Term != ruserAccessTokenSearch.Term { + t.Fatal("Terms do not match") + } +} diff --git a/model/utils.go b/model/utils.go index e84d44f72..648e67c75 100644 --- a/model/utils.go +++ b/model/utils.go @@ -36,6 +36,12 @@ type StringInterface map[string]interface{} type StringMap map[string]string type StringArray []string +var translateFunc goi18n.TranslateFunc = nil + +func AppErrorInit(t goi18n.TranslateFunc) { + translateFunc = t +} + type AppError struct { Id string `json:"id"` Message string `json:"message"` // Message to be display to the end user without debugging information @@ -52,6 +58,11 @@ func (er *AppError) Error() string { } func (er *AppError) Translate(T goi18n.TranslateFunc) { + if T == nil { + er.Message = er.Id + return + } + if er.params == nil { er.Message = T(er.Id) } else { @@ -105,6 +116,7 @@ func NewAppError(where string, id string, params map[string]interface{}, details ap.DetailedError = details ap.StatusCode = status ap.IsOAuth = false + ap.Translate(translateFunc) return ap } |