summaryrefslogtreecommitdiffstats
path: root/model
diff options
context:
space:
mode:
Diffstat (limited to 'model')
-rw-r--r--model/client4.go68
-rw-r--r--model/config.go144
-rw-r--r--model/emoji.go7
-rw-r--r--model/emoji_search.go34
-rw-r--r--model/emoji_search_test.go19
-rw-r--r--model/post.go184
-rw-r--r--model/post_list.go9
-rw-r--r--model/post_test.go127
-rw-r--r--model/reaction.go2
-rw-r--r--model/testdata/markdown-sample-with-rewritten-image-urls.md245
-rw-r--r--model/testdata/markdown-sample.md245
-rw-r--r--model/user_access_token_search.go35
-rw-r--r--model/user_access_token_search_test.go19
-rw-r--r--model/utils.go12
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 &copy
+}
+
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 &copy
+}
+
+// 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 &copy
+}
+
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
}