summaryrefslogtreecommitdiffstats
path: root/model
diff options
context:
space:
mode:
Diffstat (limited to 'model')
-rw-r--r--model/client.go128
-rw-r--r--model/command.go5
-rw-r--r--model/config.go10
-rw-r--r--model/file.go4
-rw-r--r--model/incoming_webhook.go134
-rw-r--r--model/incoming_webhook_test.go131
-rw-r--r--model/user.go1
-rw-r--r--model/version.go1
8 files changed, 390 insertions, 24 deletions
diff --git a/model/client.go b/model/client.go
index 152aaa706..eabedefa5 100644
--- a/model/client.go
+++ b/model/client.go
@@ -28,6 +28,8 @@ const (
HEADER_AUTH = "Authorization"
HEADER_REQUESTED_WITH = "X-Requested-With"
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
+ STATUS = "status"
+ STATUS_OK = "OK"
API_URL_SUFFIX_V1 = "/api/v1"
API_URL_SUFFIX_V3 = "/api/v3"
@@ -41,18 +43,21 @@ type Result struct {
}
type Client struct {
- Url string // The location of the server like "http://localhost:8065"
- ApiUrl string // The api location of the server like "http://localhost:8065/api/v3"
- HttpClient *http.Client // The http client
- AuthToken string
- AuthType string
- TeamId string
+ Url string // The location of the server like "http://localhost:8065"
+ ApiUrl string // The api location of the server like "http://localhost:8065/api/v3"
+ HttpClient *http.Client // The http client
+ AuthToken string
+ AuthType string
+ TeamId string
+ RequestId string
+ Etag string
+ ServerVersion string
}
// NewClient constructs a new client with convienence methods for talking to
// the server.
func NewClient(url string) *Client {
- return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", "", ""}
+ return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", "", "", "", "", ""}
}
func (c *Client) SetOAuthToken(token string) {
@@ -94,6 +99,10 @@ func (c *Client) GetChannelNameRoute(channelName string) string {
return fmt.Sprintf("/teams/%v/channels/name/%v", c.GetTeamId(), channelName)
}
+func (c *Client) GetGeneralRoute() string {
+ return "/general"
+}
+
func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data))
rq.Header.Set("Content-Type", contentType)
@@ -155,6 +164,7 @@ func getCookie(name string, resp *http.Response) *http.Cookie {
return nil
}
+// Must is a convenience function used for testing.
func (c *Client) Must(result *Result, err *AppError) *Result {
if err != nil {
l4g.Close()
@@ -165,6 +175,76 @@ func (c *Client) Must(result *Result, err *AppError) *Result {
return result
}
+// CheckStatusOK is a convenience function for checking the return of Web Service
+// call that return the a map of status=OK.
+func (c *Client) CheckStatusOK(r *http.Response) bool {
+ m := MapFromJson(r.Body)
+ if m != nil && m[STATUS] == STATUS_OK {
+ return true
+ }
+
+ return false
+}
+
+func (c *Client) fillInExtraProperties(r *http.Response) {
+ c.RequestId = r.Header.Get(HEADER_REQUEST_ID)
+ c.Etag = r.Header.Get(HEADER_ETAG_SERVER)
+ c.ServerVersion = r.Header.Get(HEADER_VERSION_ID)
+}
+
+func (c *Client) clearExtraProperties() {
+ c.RequestId = ""
+ c.Etag = ""
+ c.ServerVersion = ""
+}
+
+// General Routes Section
+
+// GetClientProperties returns properties needed by the client to show/hide
+// certian features. It returns a map of strings.
+func (c *Client) GetClientProperties() (map[string]string, *AppError) {
+ c.clearExtraProperties()
+ if r, err := c.DoApiGet(c.GetGeneralRoute()+"/client_props", "", ""); err != nil {
+ return nil, err
+ } else {
+ c.fillInExtraProperties(r)
+ return MapFromJson(r.Body), nil
+ }
+}
+
+// LogClient is a convenience Web Service call so clients can log messages into
+// the server-side logs. For example we typically log javascript error messages
+// into the server-side. It returns true if the logging was successful.
+func (c *Client) LogClient(message string) (bool, *AppError) {
+ c.clearExtraProperties()
+ m := make(map[string]string)
+ m["level"] = "ERROR"
+ m["message"] = message
+
+ if r, err := c.DoApiPost(c.GetGeneralRoute()+"/log_client", MapToJson(m)); err != nil {
+ return false, err
+ } else {
+ c.fillInExtraProperties(r)
+ return c.CheckStatusOK(r), nil
+ }
+}
+
+// GetPing returns a map of strings with server time, server version, and node Id.
+// Systems that want to check on health status of the server should check the
+// url /api/v3/ping for a 200 status response.
+func (c *Client) GetPing() (map[string]string, *AppError) {
+ c.clearExtraProperties()
+ if r, err := c.DoApiGet(c.GetGeneralRoute()+"/ping", "", ""); err != nil {
+ return nil, err
+ } else {
+ c.fillInExtraProperties(r)
+ return MapFromJson(r.Body), nil
+
+ }
+}
+
+// Team Routes Section
+
func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppError) {
m := make(map[string]string)
m["email"] = email
@@ -596,21 +676,26 @@ func (c *Client) GetAllAudits() (*Result, *AppError) {
}
}
-func (c *Client) GetClientProperties() (*Result, *AppError) {
- if r, err := c.DoApiGet("/admin/client_props", "", ""); err != nil {
+func (c *Client) GetConfig() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/admin/config", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
+ r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
}
}
-func (c *Client) GetConfig() (*Result, *AppError) {
- if r, err := c.DoApiGet("/admin/config", "", ""); err != nil {
- return nil, err
+// ReloadConfig will reload the config.json file from disk. Properties
+// requiring a server restart will still need a server restart. You must
+// have the system admin role to call this method. It will return status=OK
+// if it's successfully reloaded the config file, otherwise check the returned error.
+func (c *Client) ReloadConfig() (bool, *AppError) {
+ c.clearExtraProperties()
+ if r, err := c.DoApiGet("/admin/reload_config", "", ""); err != nil {
+ return false, err
} else {
- return &Result{r.Header.Get(HEADER_REQUEST_ID),
- r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
+ c.fillInExtraProperties(r)
+ return c.CheckStatusOK(r), nil
}
}
@@ -623,6 +708,19 @@ func (c *Client) SaveConfig(config *Config) (*Result, *AppError) {
}
}
+// RecycleDatabaseConnection will attempt to recycle the database connections.
+// You must have the system admin role to call this method. It will return status=OK
+// if it's successfully recycled the connections, otherwise check the returned error.
+func (c *Client) RecycleDatabaseConnection() (bool, *AppError) {
+ c.clearExtraProperties()
+ if r, err := c.DoApiGet("/admin/recycle_db_conn", "", ""); err != nil {
+ return false, err
+ } else {
+ c.fillInExtraProperties(r)
+ return c.CheckStatusOK(r), nil
+ }
+}
+
func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
if r, err := c.DoApiPost("/admin/test_email", config.ToJson()); err != nil {
return nil, err
diff --git a/model/command.go b/model/command.go
index 4d5f7ace9..decb647b7 100644
--- a/model/command.go
+++ b/model/command.go
@@ -6,11 +6,14 @@ package model
import (
"encoding/json"
"io"
+ "strings"
)
const (
COMMAND_METHOD_POST = "P"
COMMAND_METHOD_GET = "G"
+ MIN_TRIGGER_LENGTH = 1
+ MAX_TRIGGER_LENGTH = 128
)
type Command struct {
@@ -99,7 +102,7 @@ func (o *Command) IsValid() *AppError {
return NewLocAppError("Command.IsValid", "model.command.is_valid.team_id.app_error", nil, "")
}
- if len(o.Trigger) == 0 || len(o.Trigger) > 128 {
+ if len(o.Trigger) < MIN_TRIGGER_LENGTH || len(o.Trigger) > MAX_TRIGGER_LENGTH || strings.Index(o.Trigger, "/") == 0 || strings.Contains(o.Trigger, " ") {
return NewLocAppError("Command.IsValid", "model.command.is_valid.trigger.app_error", nil, "")
}
diff --git a/model/config.go b/model/config.go
index 9a7e3b7c5..674a352f0 100644
--- a/model/config.go
+++ b/model/config.go
@@ -92,6 +92,7 @@ type LogSettings struct {
}
type FileSettings struct {
+ MaxFileSize *int64
DriverName string
Directory string
EnablePublicLink bool
@@ -256,6 +257,11 @@ func (o *Config) SetDefaults() {
o.SqlSettings.AtRestEncryptKey = NewRandomString(32)
}
+ if o.FileSettings.MaxFileSize == nil {
+ o.FileSettings.MaxFileSize = new(int64)
+ *o.FileSettings.MaxFileSize = 52428800 // 50 MB
+ }
+
if len(o.FileSettings.PublicLinkSalt) == 0 {
o.FileSettings.PublicLinkSalt = NewRandomString(32)
}
@@ -547,6 +553,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_max_conn.app_error", nil, "")
}
+ if *o.FileSettings.MaxFileSize <= 0 {
+ return NewLocAppError("Config.IsValid", "model.config.is_valid.max_file_size.app_error", nil, "")
+ }
+
if !(o.FileSettings.DriverName == IMAGE_DRIVER_LOCAL || o.FileSettings.DriverName == IMAGE_DRIVER_S3) {
return NewLocAppError("Config.IsValid", "model.config.is_valid.file_driver.app_error", nil, "")
}
diff --git a/model/file.go b/model/file.go
index b7806b3b4..fa98a3b3a 100644
--- a/model/file.go
+++ b/model/file.go
@@ -8,10 +8,6 @@ import (
"io"
)
-const (
- MAX_FILE_SIZE = 50000000 // 50 MB
-)
-
var (
IMAGE_EXTENSIONS = [5]string{".jpg", ".jpeg", ".gif", ".bmp", ".png"}
IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff"}
diff --git a/model/incoming_webhook.go b/model/incoming_webhook.go
index 0763b443e..fabf799cd 100644
--- a/model/incoming_webhook.go
+++ b/model/incoming_webhook.go
@@ -4,8 +4,11 @@
package model
import (
+ "bytes"
"encoding/json"
"io"
+ "regexp"
+ "strings"
)
const (
@@ -125,13 +128,136 @@ func (o *IncomingWebhook) PreUpdate() {
o.UpdateAt = GetMillis()
}
-func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
- decoder := json.NewDecoder(data)
+// escapeControlCharsFromPayload escapes control chars (\n, \t) from a byte slice.
+// Context:
+// JSON strings are not supposed to contain control characters such as \n, \t,
+// ... but some incoming webhooks might still send invalid JSON and we want to
+// try to handle that. An example invalid JSON string from an incoming webhook
+// might look like this (strings for both "text" and "fallback" attributes are
+// invalid JSON strings because they contain unescaped newlines and tabs):
+// `{
+// "text": "this is a test
+// that contains a newline and tabs",
+// "attachments": [
+// {
+// "fallback": "Required plain-text summary of the attachment
+// that contains a newline and tabs",
+// "color": "#36a64f",
+// ...
+// "text": "Optional text that appears within the attachment
+// that contains a newline and tabs",
+// ...
+// "thumb_url": "http://example.com/path/to/thumb.png"
+// }
+// ]
+// }`
+// This function will search for `"key": "value"` pairs, and escape \n, \t
+// from the value.
+func escapeControlCharsFromPayload(by []byte) []byte {
+ // we'll search for `"text": "..."` or `"fallback": "..."`, ...
+ keys := "text|fallback|pretext|author_name|title|value"
+
+ // the regexp reads like this:
+ // (?s): this flag let . match \n (default is false)
+ // "(keys)": we search for the keys defined above
+ // \s*:\s*: followed by 0..n spaces/tabs, a colon then 0..n spaces/tabs
+ // ": a double-quote
+ // (\\"|[^"])*: any number of times the `\"` string or any char but a double-quote
+ // ": a double-quote
+ r := `(?s)"(` + keys + `)"\s*:\s*"(\\"|[^"])*"`
+ re := regexp.MustCompile(r)
+
+ // the function that will escape \n and \t on the regexp matches
+ repl := func(b []byte) []byte {
+ if bytes.Contains(b, []byte("\n")) {
+ b = bytes.Replace(b, []byte("\n"), []byte("\\n"), -1)
+ }
+ if bytes.Contains(b, []byte("\t")) {
+ b = bytes.Replace(b, []byte("\t"), []byte("\\t"), -1)
+ }
+
+ return b
+ }
+
+ return re.ReplaceAllFunc(by, repl)
+}
+
+func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) {
+ decoder := json.NewDecoder(bytes.NewReader(by))
var o IncomingWebhookRequest
err := decoder.Decode(&o)
if err == nil {
- return &o
+ return &o, nil
} else {
- return nil
+ return nil, err
+ }
+}
+
+// To mention @channel via a webhook in Slack, the message should contain
+// <!channel>, as explained at the bottom of this article:
+// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements
+func expandAnnouncement(text string) string {
+ c1 := "<!channel>"
+ c2 := "@channel"
+ if strings.Contains(text, c1) {
+ return strings.Replace(text, c1, c2, -1)
+ }
+ return text
+}
+
+// Expand announcements in incoming webhooks from Slack. Those announcements
+// can be found in the text attribute, or in the pretext, text, title and value
+// attributes of the attachment structure. The Slack attachment structure is
+// documented here: https://api.slack.com/docs/attachments
+func expandAnnouncements(i *IncomingWebhookRequest) {
+ i.Text = expandAnnouncement(i.Text)
+
+ if i.Attachments != nil {
+ attachments := i.Attachments.([]interface{})
+ for _, attachment := range attachments {
+ a := attachment.(map[string]interface{})
+
+ if a["pretext"] != nil {
+ a["pretext"] = expandAnnouncement(a["pretext"].(string))
+ }
+
+ if a["text"] != nil {
+ a["text"] = expandAnnouncement(a["text"].(string))
+ }
+
+ if a["title"] != nil {
+ a["title"] = expandAnnouncement(a["title"].(string))
+ }
+
+ if a["fields"] != nil {
+ fields := a["fields"].([]interface{})
+ for _, field := range fields {
+ f := field.(map[string]interface{})
+ if f["value"] != nil {
+ f["value"] = expandAnnouncement(f["value"].(string))
+ }
+ }
+ }
+ }
}
}
+
+func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(data)
+ by := buf.Bytes()
+
+ // Try to decode the JSON data. Only if it fails, try to escape control
+ // characters from the strings contained in the JSON data.
+ o, err := decodeIncomingWebhookRequest(by)
+ if err != nil {
+ o, err = decodeIncomingWebhookRequest(escapeControlCharsFromPayload(by))
+ if err != nil {
+ return nil
+ }
+ }
+
+ expandAnnouncements(o)
+
+ return o
+}
diff --git a/model/incoming_webhook_test.go b/model/incoming_webhook_test.go
index 3f1754244..8246b6c0a 100644
--- a/model/incoming_webhook_test.go
+++ b/model/incoming_webhook_test.go
@@ -100,3 +100,134 @@ func TestIncomingWebhookPreUpdate(t *testing.T) {
o := IncomingWebhook{}
o.PreUpdate()
}
+
+func TestIncomingWebhookRequestFromJson_Announcements(t *testing.T) {
+ text := "This message will send a notification to all team members in the channel where you post the message, because it contains: <!channel>"
+ expected := "This message will send a notification to all team members in the channel where you post the message, because it contains: @channel"
+
+ // simple payload
+ payload := `{"text": "` + text + `"}`
+ data := strings.NewReader(payload)
+ iwr := IncomingWebhookRequestFromJson(data)
+
+ if iwr == nil {
+ t.Fatal("IncomingWebhookRequest should not be nil")
+ }
+ if iwr.Text != expected {
+ t.Fatalf("Sample text should be: %s, got: %s", expected, iwr.Text)
+ }
+
+ // payload with attachment (pretext, title, text, value)
+ payload = `{
+ "attachments": [
+ {
+ "pretext": "` + text + `",
+ "title": "` + text + `",
+ "text": "` + text + `",
+ "fields": [
+ {
+ "title": "A title",
+ "value": "` + text + `",
+ "short": false
+ }
+ ]
+ }
+ ]
+ }`
+
+ data = strings.NewReader(payload)
+ iwr = IncomingWebhookRequestFromJson(data)
+
+ if iwr == nil {
+ t.Fatal("IncomingWebhookRequest should not be nil")
+ }
+
+ attachments := iwr.Attachments.([]interface{})
+ attachment := attachments[0].(map[string]interface{})
+ if attachment["pretext"] != expected {
+ t.Fatalf("Sample attachment pretext should be: %s, got: %s", expected, attachment["pretext"])
+ }
+ if attachment["text"] != expected {
+ t.Fatalf("Sample attachment text should be: %s, got: %s", expected, attachment["text"])
+ }
+ if attachment["title"] != expected {
+ t.Fatalf("Sample attachment title should be: %s, got: %s", expected, attachment["title"])
+ }
+ fields := attachment["fields"].([]interface{})
+ field := fields[0].(map[string]interface{})
+ if field["value"] != expected {
+ t.Fatalf("Sample attachment field value should be: %s, got: %s", expected, field["value"])
+ }
+}
+
+func TestIncomingWebhookRequestFromJson(t *testing.T) {
+ texts := []string{
+ `this is a test`,
+ `this is a test
+ that contains a newline and tabs`,
+ `this is a test \"foo
+ that contains a newline and tabs`,
+ `this is a test \"foo\"
+ that contains a newline and tabs`,
+ `this is a test \"foo\"
+ \" that contains a newline and tabs`,
+ `this is a test \"foo\"
+
+ \" that contains a newline and tabs
+ `,
+ }
+
+ for i, text := range texts {
+ // build a sample payload with the text
+ payload := `{
+ "text": "` + text + `",
+ "attachments": [
+ {
+ "fallback": "` + text + `",
+
+ "color": "#36a64f",
+
+ "pretext": "` + text + `",
+
+ "author_name": "` + text + `",
+ "author_link": "http://flickr.com/bobby/",
+ "author_icon": "http://flickr.com/icons/bobby.jpg",
+
+ "title": "` + text + `",
+ "title_link": "https://api.slack.com/",
+
+ "text": "` + text + `",
+
+ "fields": [
+ {
+ "title": "` + text + `",
+ "value": "` + text + `",
+ "short": false
+ }
+ ],
+
+ "image_url": "http://my-website.com/path/to/image.jpg",
+ "thumb_url": "http://example.com/path/to/thumb.png"
+ }
+ ]
+ }`
+
+ // try to create an IncomingWebhookRequest from the payload
+ data := strings.NewReader(payload)
+ iwr := IncomingWebhookRequestFromJson(data)
+
+ // After it has been decoded, the JSON string won't contain the escape char anymore
+ expected := strings.Replace(text, `\"`, `"`, -1)
+ if iwr == nil {
+ t.Fatal("IncomingWebhookRequest should not be nil")
+ }
+ if iwr.Text != expected {
+ t.Fatalf("Sample %d text should be: %s, got: %s", i, expected, iwr.Text)
+ }
+ attachments := iwr.Attachments.([]interface{})
+ attachment := attachments[0].(map[string]interface{})
+ if attachment["text"] != expected {
+ t.Fatalf("Sample %d attachment text should be: %s, got: %s", i, expected, attachment["text"])
+ }
+ }
+}
diff --git a/model/user.go b/model/user.go
index 15c281401..7dee67381 100644
--- a/model/user.go
+++ b/model/user.go
@@ -191,6 +191,7 @@ func (u *User) PreUpdate() {
func (u *User) SetDefaultNotifications() {
u.NotifyProps = make(map[string]string)
u.NotifyProps["email"] = "true"
+ u.NotifyProps["push"] = USER_NOTIFY_MENTION
u.NotifyProps["desktop"] = USER_NOTIFY_ALL
u.NotifyProps["desktop_sound"] = "true"
u.NotifyProps["mention_keys"] = u.Username + ",@" + u.Username
diff --git a/model/version.go b/model/version.go
index 4a47f06ef..dde9eccd7 100644
--- a/model/version.go
+++ b/model/version.go
@@ -33,6 +33,7 @@ var CurrentVersion string = versions[0]
var BuildNumber string
var BuildDate string
var BuildHash string
+var BuildHashEnterprise string
var BuildEnterpriseReady string
var versionsWithoutHotFixes []string