diff options
65 files changed, 620 insertions, 184 deletions
diff --git a/.travis.yml b/.travis.yml index 02e1234d3..1af940d21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ before_install: - sudo sed -i'' 's/log_slow_queries/slow_query_log/' /etc/mysql/my.cnf - sudo sed -i'' 's/basedir[^=]\+=.*$/basedir = \/opt\/mysql\/server-5.6/' /etc/mysql/my.cnf - sudo /etc/init.d/mysql.server start +- sudo pip install mkdocs install: - export PATH=$PATH:$HOME/gopath/bin - go get github.com/tools/godep @@ -37,6 +38,7 @@ addons: hosts: - 127.0.0.1 dockerhost deploy: +# Github releases, builds only on tags - provider: releases api_key: secure: ma8Y0oimU+LB6LTAh8to2E1/ghaDPhcsAFXBrODsHpd4JgxA6HYoEwSEBCJFHSpu/JteclsxSTfp9hcuzw/IOtlwlSAiVoBZ60s24MRKTIAQNtrJ4QrX5wyfAZi+Bcuk/E8NynmoIW5qpaElSAdjgocyjAJIQ5ChMEztglL0cAEBXQRWbWMqSZ0hVLPrKDCIkWIyv3pFxqdLOxktkzxW07r2dlT0hppXR3dCaPJo0nelArS2H3LdN/3Iv6cAddfS27RaZkqDj/PDh6OZr4EguC99TxlVNChIr7nPr3/OiAssbkvEnhlSLeABFO9+7KfutL2WhAjpFXTjtPVq6Qalc8UW0K0gxq//sVfhb1MzjenmdOf06uB2bilQ8kgwHo7dDdRZBqqAtxQ6Q0Ht3SFMj6v/1zVD3s+YX/kWCEbUTHm6r2G/eF794ozcJyU+6j1L8hm6mvf8Mr9XCqBfgpZy6FCLX+9OKdMvX2jY8reo3Xz1PA9R6yzhN08vjku+jW+fsoYrBLd0fY1UGK2uOuvBByCeJzXupd3YpBMjEyRupVxqEj7K0GWOJeml65mkqKSNsHdDSeSjMpb8mwneZyTbdjsxCFQRLcLgpAajFrkk4G2Yz3KfhXSo29XKEGX+EbY5NuP8KmDsBsguPI0zfwv/co0hAY8PIIcehxcdoR9Vb2c= @@ -48,6 +50,7 @@ deploy: go: 1.4.2 condition: $TRAVIS_DB = mysql +# S3 deploy for master docker image. (latest compiled bits) - provider: s3 access_key_id: AKIAJCO3KJYEGWJIKDIQ secret_access_key: @@ -63,3 +66,20 @@ deploy: branch: master go: 1.4.2 condition: $TRAVIS_DB = mysql + +# S3 deploy for documentation + - provider: s3 + access_key_id: AKIAJCO3KJYEGWJIKDIQ + secret_access_key: + secure: p66X2tJBmKgtcVyPtGgkAwW29IiRojqGA39RjCJkIWNTJ0e/9JvBOiMS2c4a7I4aOads38rsthwdaigBWagDWNH7bGsEZN7B0TszZuFAuU+XGjU5A66MIOfFfzbUg8AnByysr+XG5/bknFIrP/XhM2fbRr6gbYrFUK7TNkpgjFs5u3BzUrz2iTAV8uOpSJqKSnaf0pTZk1EywOK/X8W8ViIjc7Di3FzQcqIW9K3D27N+3rVsv8SRT1hWASVlnG6aThqqebiM8FCGCzAYVgQb3h3Wu8JT5fIz7Qo7A6siVRwNBwWwzP8HkGoinEK32Wsj/fDXk27vjpFQO/+9sV0xfcTbIZA6MnuYWF4rHOT59KcshCWCD3V0FopX57p/dtOzM9+6lxIctAT++izxWoZit/5c5A4633iY1d+RMeTko1POix6MSlxPMRHZUFwSXROgFuWWRpyD6TlUTCST9/wTTd0WDPklAAiYcnuEPW3qCnw0r0xkrA4AwWUXqXdAIwDt5bA27KcjRyY4Fofv9NxH09BNuBTXNPrvnYPZMmaKrv+HOX3NFTreuV6+5LJdhYUxYSBvSWo1jeWIQ5Q9RUdTU0PqmKpMhJKbKey/S4gxCXHg2HR8DwLCcbIZcvneF9yPEAT71YA6zpLKoPVSwWwH97huKSzjpic/RUfFXQOcgCQ= + bucket: docs.mattermost.org + local_dir: documentation-html + acl: public_read + region: us-east-1 + skip_cleanup: true + detect_encoding: true + on: + repo: mattermost/platform + branch: master + go: 1.4.2 + condition: $TRAVIS_DB = mysql diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a4335eeb..7357796e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,9 @@ Added new web-based System Console for managing instance level configuration. Th - _monitor operations_, by quickly accessing log files and user roles, and - _manage teams_, with essential functions such as team role assignment and password reset -In addition new command line tools are available for managing Mattermost system roles, creating users, resetting passwords, getting version info and other basic tasks. +In addition new command line tools are available for managing Mattermost system roles, creating users, resetting passwords, getting version info and other basic tasks. + +Run `./platform -h` for documentation using the new command line tool. ### New Features @@ -106,7 +106,12 @@ travis: @sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html rm $(DIST_PATH)/web/templates/*.bak + mv doc/README.md doc/index.md + mkdocs build + cp -r documentation-html $(DIST_PATH)/documentation-html + tar -C dist -czf $(DIST_PATH).tar.gz mattermost + rm -r $(DIST_PATH) build: @$(GO) build $(GOFLAGS) ./... diff --git a/api/channel.go b/api/channel.go index 6494e3528..5e13fa18a 100644 --- a/api/channel.go +++ b/api/channel.go @@ -281,7 +281,7 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) { // lets make sure the user is valid if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } diff --git a/api/context.go b/api/context.go index 02c3dc902..e80582b2a 100644 --- a/api/context.go +++ b/api/context.go @@ -137,7 +137,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if session == nil || session.IsExpired() { - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "token="+token) c.Err.StatusCode = http.StatusUnauthorized } else if !session.IsOAuth && isTokenFromQueryString { @@ -303,7 +303,6 @@ func (c *Context) HasSystemAdminPermissions(where string) bool { } func (c *Context) IsSystemAdmin() bool { - // TODO XXX FIXME && IsPrivateIpAddress(c.IpAddress) if model.IsInRole(c.Session.Roles, model.ROLE_SYSTEM_ADMIN) { return true } @@ -317,7 +316,7 @@ func (c *Context) IsTeamAdmin() bool { return false } -func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { +func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { sessionCache.Remove(c.Session.Token) @@ -330,6 +329,21 @@ func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { } http.SetCookie(w, cookie) + + multiToken := "" + if oldMultiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil { + multiToken = oldMultiCookie.Value + } + + multiCookie := &http.Cookie{ + Name: model.MULTI_SESSION_TOKEN, + Value: strings.TrimSpace(strings.Replace(multiToken, c.Session.Token, "", -1)), + Path: "/", + MaxAge: model.SESSION_TIME_WEB_IN_SECS, + HttpOnly: true, + } + + http.SetCookie(w, multiCookie) } func (c *Context) SetInvalidParam(where string, name string) { @@ -346,7 +360,7 @@ func (c *Context) setTeamURL(url string, valid bool) { c.teamURLValid = valid } -func (c *Context) setTeamURLFromSession() { +func (c *Context) SetTeamURLFromSession() { if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err == nil { c.setTeamURL(c.GetSiteURL()+"/"+result.Data.(*model.Team).Name, true) } @@ -362,7 +376,7 @@ func (c *Context) GetTeamURLFromTeam(team *model.Team) string { func (c *Context) GetTeamURL() string { if !c.teamURLValid { - c.setTeamURLFromSession() + c.SetTeamURLFromSession() if !c.teamURLValid { l4g.Debug("TeamURL accessed when not valid. Team URL should not be used in api functions or those that are team independent") } diff --git a/api/user.go b/api/user.go index ed3576a30..2d7dd9ab1 100644 --- a/api/user.go +++ b/api/user.go @@ -394,6 +394,41 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, http.SetCookie(w, sessionCookie) + multiToken := "" + if originalMultiSessionCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil { + multiToken = originalMultiSessionCookie.Value + } + + // Attempt to clean all the old tokens or duplicate tokens + if len(multiToken) > 0 { + tokens := strings.Split(multiToken, " ") + + multiToken = "" + seen := make(map[string]string) + seen[session.TeamId] = session.TeamId + for _, token := range tokens { + if sr := <-Srv.Store.Session().Get(token); sr.Err == nil { + s := sr.Data.(*model.Session) + if !s.IsExpired() && seen[s.TeamId] == "" { + multiToken += " " + token + seen[s.TeamId] = s.TeamId + } + } + } + } + + multiToken = strings.TrimSpace(session.Token + " " + multiToken) + + multiSessionCookie := &http.Cookie{ + Name: model.MULTI_SESSION_TOKEN, + Value: multiToken, + Path: "/", + MaxAge: maxAge, + HttpOnly: true, + } + + http.SetCookie(w, multiSessionCookie) + c.Session = *session c.LogAuditWithUserId(user.Id, "success") } @@ -514,7 +549,7 @@ func logout(c *Context, w http.ResponseWriter, r *http.Request) { func Logout(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("") - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) if result := <-Srv.Store.Session().Remove(c.Session.Id); result.Err != nil { c.Err = result.Err return @@ -529,7 +564,7 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) { if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } else if HandleEtag(result.Data.(*model.User).Etag(), w, r) { diff --git a/config/config.json b/config/config.json index 38acee85a..b14175372 100644 --- a/config/config.json +++ b/config/config.json @@ -6,6 +6,8 @@ "GoogleDeveloperKey": "", "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": true, + "EnablePostUsernameOverride": false, + "EnablePostIconOverride": false, "EnableTesting": false }, "TeamSettings": { @@ -75,7 +77,8 @@ }, "PrivacySettings": { "ShowEmailAddress": true, - "ShowFullName": true + "ShowFullName": true, + "EnableDiagnostic": false }, "GitLabSettings": { "Enable": false, @@ -86,4 +89,4 @@ "TokenEndpoint": "", "UserApiEndpoint": "" } -}
\ No newline at end of file +} diff --git a/doc/README.md b/doc/README.md index dc1591705..d711b5d53 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,31 +1,26 @@ # Mattermost Documentation -## General Information +## Installation -- [Mattermost Release Numbering Scheme](install/release-numbering.md) +- [AWS Elastic Beanstalk Setup](install/Amazon-Elastic-Beanstalk.md) +- [Docker Single Container Preview Setup](install/Docker-Single-Container.md) +- [Production Ubuntu Setup](install/Production-Ubuntu.md) +- [Mattermost Release Numbering Scheme](install/Release-Numbering.md) +- [Software and Hardware Requirements](install/Requirements.md) +- [SMTP Email Setup](install/SMTP-Email-Setup.md) -## Administrator Documentation +## Integrations -### Installation - -- [Software and Hardware Requirements](install/requirements.md) -- [Production Installation](install/prod-ubuntu.md) -- [Local Machine Setup ](install/single-container-install.md) -- [AWS Elastic Beanstalk Setup](install/aws-ebs-setup.md) -- [Developer Machine Setup](install/dev-setup.md) - -### Configuration - -- [GitLab SSO Configuration](integrations/sso/gitlab-sso.md) -- [SMTP Email Setup](config/smtp-email-setup.md) +- [GitLab SSO Configuration](integrations/Single-Sign-On/Gitlab.md) +- [Incoming Webhooks](integrations/webhooks/Incoming.md) ## Developer Documentation -- [Code Contribution Guidelines](developer/code-contribution.md) -- [Developer Machine Setup](install/dev-setup.md) -- [Mattermost Style Guide](developer/style-guide.md) +- [Code Contribution Guidelines](developer/Code-Contribution-Guidelines.md) +- [Developer Machine Setup](developer/Setup.md) +- [Mattermost Style Guide](developer/Style-Guide.md) -## End User Help +## Usage Help -- [Mattermost Markdown Formatting](help/enduser/markdown.md) -- [Slack Import](https://github.com/mattermost/platform/blob/master/doc/import/slack-import.md) +- [Slack Import](usage/Slack-Import.md) +- [Mattermost Markdown Formatting](usage/Markdown.md) diff --git a/doc/developer/code-contribution.md b/doc/developer/Code-Contribution-Guidelines.md index 51521be1b..80676f107 100644 --- a/doc/developer/code-contribution.md +++ b/doc/developer/Code-Contribution-Guidelines.md @@ -23,7 +23,7 @@ git checkout -b <branch name> ## Programming and Testing -1. Please review the [Mattermost Style Guide](style-guide.md) prior to making changes. +1. Please review the [Mattermost Style Guide](Style-Guide.md) prior to making changes. To keep code clean and well structured, Mattermost uses ESLint to check that pull requests adhere to style guidelines for React. Code will need to follow Mattermost's React style guidelines in order to pass the automated build tests when a pull request is submitted. diff --git a/doc/install/dev-setup.md b/doc/developer/Setup.md index c63bde512..c63bde512 100644 --- a/doc/install/dev-setup.md +++ b/doc/developer/Setup.md diff --git a/doc/developer/style-guide.md b/doc/developer/Style-Guide.md index a06f9e29b..a06f9e29b 100644 --- a/doc/developer/style-guide.md +++ b/doc/developer/Style-Guide.md diff --git a/doc/favicon.ico b/doc/favicon.ico new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/doc/favicon.ico diff --git a/doc/install/aws-ebs-setup.md b/doc/install/Amazon-Elastic-Beanstalk.md index e186fa9c1..0416b67ea 100644 --- a/doc/install/aws-ebs-setup.md +++ b/doc/install/Amazon-Elastic-Beanstalk.md @@ -1,14 +1,14 @@ ## AWS Elastic Beanstalk Setup (Docker) -1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](/docker/0.7/Dockerrun.aws.zip) file provided. +1. Create a new Elastic Beanstalk Docker application using the [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.0/Dockerrun.aws.zip) file provided. 1. From the AWS console select Elastic Beanstalk. 2. Select "Create New Application" from the top right. 3. Name the application and press next. 4. Select "Create a web server" environment. 5. If asked, select create an IAM role and instance profile and press next. 6. For predefined configuration select under Generic: Docker. For environment type select single instance. - 7. For application source, select upload your own and upload Dockerrun.aws.zip from [Dockerrun.aws.zip](/docker/0.7/Dockerrun.aws.zip). Everything else may be left at default. + 7. For application source, select upload your own and upload Dockerrun.aws.zip from [Dockerrun.aws.zip](https://github.com/mattermost/platform/raw/master/docker/1.0/Dockerrun.aws.zip). Everything else may be left at default. 8. Select an environment name, this is how you will refer to your environment. Make sure the URL is available then press next. 9. The options on the additional resources page may be left at default unless you wish to change them. Press Next. 10. On the configuration details place. Select an instance type of t2.small or larger. @@ -24,4 +24,4 @@ ### (Recommended) Enable Email The default single-container Docker instance for Mattermost is designed for product evaluation, and sets `ByPassEmail=true` so the product can run without enabling email, when doing so maybe difficult. - To see the product's full functionality, [enabling SMTP email is recommended](doc/config/smtp-email-setup.md). + To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md). diff --git a/doc/install/single-container-install.md b/doc/install/Docker-Single-Container.md index 304467678..4b952cd71 100644 --- a/doc/install/single-container-install.md +++ b/doc/install/Docker-Single-Container.md @@ -75,7 +75,7 @@ The following install instructions are for single-container installs of Mattermo ## Configuration Settings -There are a few configuration settings you might want to adjust when setting up your instance of Mattermost. You can edit them in [config/config.json](config/config.json) or [docker/0.6/config_docker.json](docker/0.6/config_docker.json) if you're running a Docker instance. +There are a few configuration settings you might want to adjust when setting up your instance of Mattermost. You can edit them in `config.json` or `config_docker.json` if you're running a Docker instance. * *EmailSettings*:*ByPassEmail* - If this is set to true, then users on the system will not need to verify their email addresses when signing up. In addition, no emails will ever be sent. * *ServiceSettings*:*UseLocalStorage* - If this is set to true, then your Mattermost server will store uploaded files in the storage directory specified by *StorageDirectory*. *StorageDirectory* must be set if *UseLocalStorage* is set to true. @@ -86,7 +86,7 @@ There are a few configuration settings you might want to adjust when setting up The default single-container Docker instance for Mattermost is designed for product evaluation, and sets `ByPassEmail=true` so the product can run without enabling email, when doing so maybe difficult. -To see the product's full functionality, [enabling SMTP email is recommended](/doc/config/smtp-email-setup.md). +To see the product's full functionality, [enabling SMTP email is recommended](SMTP-Email-Setup.md). ## Upgrading Mattermost diff --git a/doc/install/prod-ubuntu.md b/doc/install/Production-Ubuntu.md index d2490062d..d2490062d 100644 --- a/doc/install/prod-ubuntu.md +++ b/doc/install/Production-Ubuntu.md diff --git a/doc/install/release-numbering.md b/doc/install/Release-Numbering.md index 71374f7ef..71374f7ef 100644 --- a/doc/install/release-numbering.md +++ b/doc/install/Release-Numbering.md diff --git a/doc/install/requirements.md b/doc/install/Requirements.md index 1e0a12fb9..1e0a12fb9 100644 --- a/doc/install/requirements.md +++ b/doc/install/Requirements.md diff --git a/doc/config/smtp-email-setup.md b/doc/install/SMTP-Email-Setup.md index 86e2bb20e..86e2bb20e 100644 --- a/doc/config/smtp-email-setup.md +++ b/doc/install/SMTP-Email-Setup.md diff --git a/doc/integrations/sso/gitlab-sso.md b/doc/integrations/Single-Sign-On/Gitlab.md index 6110db504..6110db504 100644 --- a/doc/integrations/sso/gitlab-sso.md +++ b/doc/integrations/Single-Sign-On/Gitlab.md diff --git a/doc/integrations/webhook/incoming.md b/doc/integrations/webhooks/Incoming.md index e391cba92..6e25f182e 100644 --- a/doc/integrations/webhook/incoming.md +++ b/doc/integrations/webhooks/Incoming.md @@ -25,7 +25,7 @@ In addition, if `Content-Type` is specified as `application/json` in the headers {"text": "Hello, this is some text."} ``` -It is also possible to post richly formatted messages using [Markdown](../../help/enduser/markdown.md). +It is also possible to post richly formatted messages using [Markdown](../../usage/Markdown.md). ``` payload={"text": "# A Header\nThe _text_ below **the** header."} ``` diff --git a/doc/help/enduser/markdown.md b/doc/usage/Markdown.md index 9e2342a0b..9e2342a0b 100644 --- a/doc/help/enduser/markdown.md +++ b/doc/usage/Markdown.md diff --git a/doc/import/slack-import.md b/doc/usage/Slack-Import.md index c30de0567..c30de0567 100644 --- a/doc/import/slack-import.md +++ b/doc/usage/Slack-Import.md diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index 733267f74..ef91a21ea 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -6,6 +6,8 @@ "GoogleDeveloperKey": "", "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": true, + "EnablePostUsernameOverride": false, + "EnablePostIconOverride": false, "EnableTesting": false }, "TeamSettings": { @@ -75,7 +77,8 @@ }, "PrivacySettings": { "ShowEmailAddress": true, - "ShowFullName": true + "ShowFullName": true, + "EnableDiagnostic": false }, "GitLabSettings": { "Enable": false, diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index 733267f74..ef91a21ea 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -6,6 +6,8 @@ "GoogleDeveloperKey": "", "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": true, + "EnablePostUsernameOverride": false, + "EnablePostIconOverride": false, "EnableTesting": false }, "TeamSettings": { @@ -75,7 +77,8 @@ }, "PrivacySettings": { "ShowEmailAddress": true, - "ShowFullName": true + "ShowFullName": true, + "EnableDiagnostic": false }, "GitLabSettings": { "Enable": false, diff --git a/mattermost.go b/mattermost.go index 94bbe344d..e78e8d04a 100644 --- a/mattermost.go +++ b/mattermost.go @@ -8,6 +8,8 @@ import ( "fmt" "os" "os/signal" + "runtime" + "strconv" "strings" "syscall" "time" @@ -61,6 +63,8 @@ func main() { manualtesting.InitManualTesting() } + diagnosticsJob() + // wait for kill signal before attempting to gracefully shutdown // the running service c := make(chan os.Signal) @@ -71,6 +75,53 @@ func main() { } } +func diagnosticsJob() { + go func() { + for { + if utils.Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() { + if result := <-api.Srv.Store.System().Get(); result.Err == nil { + props := result.Data.(model.StringMap) + lastTime, _ := strconv.ParseInt(props["LastDiagnosticTime"], 10, 0) + currentTime := model.GetMillis() + + if (currentTime - lastTime) > 1000*60*60*24*7 { + l4g.Info("Sending error and diagnostic information to mattermost") + + id := props["DiagnosticId"] + if len(id) == 0 { + id = model.NewId() + systemId := &model.System{Name: "DiagnosticId", Value: id} + <-api.Srv.Store.System().Save(systemId) + } + + systemLastTime := &model.System{Name: "LastDiagnosticTime", Value: strconv.FormatInt(currentTime, 10)} + if lastTime == 0 { + <-api.Srv.Store.System().Save(systemLastTime) + } else { + <-api.Srv.Store.System().Update(systemLastTime) + } + + m := make(map[string]string) + m[utils.PROP_DIAGNOSTIC_ID] = id + m[utils.PROP_DIAGNOSTIC_BUILD] = model.CurrentVersion + "." + model.BuildNumber + m[utils.PROP_DIAGNOSTIC_DATABASE] = utils.Cfg.SqlSettings.DriverName + m[utils.PROP_DIAGNOSTIC_OS] = runtime.GOOS + m[utils.PROP_DIAGNOSTIC_CATEGORY] = utils.VAL_DIAGNOSTIC_CATEGORY_DEFALUT + + if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil { + m[utils.PROP_DIAGNOSTIC_USER_COUNT] = strconv.FormatInt(ucr.Data.(int64), 10) + } + + utils.SendDiagnostic(m) + } + } + } + + time.Sleep(time.Hour * 24) + } + }() +} + func parseCmds() { flag.Usage = func() { fmt.Fprintln(os.Stderr, usage) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..d7113e03e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: Mattermost Documentation +site_url: http://docs.mattermost.org +repo_url: https://github.com/mattermost/platform +repo_name: GitHub +site_favicon: favicon.ico +copyright: "Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved." +strict: true +docs_dir: doc +site_dir: documentation-html +use_directory_urls: false +theme: mkdocs diff --git a/model/config.go b/model/config.go index 5d822e263..c67b36063 100644 --- a/model/config.go +++ b/model/config.go @@ -29,6 +29,8 @@ type ServiceSettings struct { GoogleDeveloperKey string EnableOAuthServiceProvider bool EnableIncomingWebhooks bool + EnablePostUsernameOverride bool + EnablePostIconOverride bool EnableTesting bool } @@ -110,6 +112,7 @@ type RateLimitSettings struct { type PrivacySettings struct { ShowEmailAddress bool ShowFullName bool + EnableDiagnostic bool } type TeamSettings struct { diff --git a/model/session.go b/model/session.go index 3c7c75eb4..bb4d987a7 100644 --- a/model/session.go +++ b/model/session.go @@ -10,6 +10,7 @@ import ( const ( SESSION_TOKEN = "MMSID" + MULTI_SESSION_TOKEN = "MMSIDMU" SESSION_TIME_WEB_IN_DAYS = 30 SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS SESSION_TIME_MOBILE_IN_DAYS = 30 diff --git a/model/utils.go b/model/utils.go index e19cceba5..93b8c4512 100644 --- a/model/utils.go +++ b/model/utils.go @@ -163,6 +163,7 @@ var reservedName = []string{ "post", "cluster", "api", + "oauth", } var wwwStart = regexp.MustCompile(`^www`) diff --git a/model/version.go b/model/version.go index 233fc3747..efa1697db 100644 --- a/model/version.go +++ b/model/version.go @@ -67,6 +67,10 @@ func GetPreviousVersion(currentVersion string) (int64, int64) { return 0, 0 } +func IsOfficalBuild() bool { + return BuildNumber != "_BUILD_NUMBER_" +} + func IsCurrentVersion(versionToCheck string) bool { currentMajor, currentMinor, _ := SplitVersion(CurrentVersion) toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck) diff --git a/utils/config.go b/utils/config.go index 3218211e3..44c4c43af 100644 --- a/utils/config.go +++ b/utils/config.go @@ -184,6 +184,8 @@ func getClientProperties(c *model.Config) map[string]string { props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey props["GoogleDeveloperKey"] = c.ServiceSettings.GoogleDeveloperKey props["EnableIncomingWebhooks"] = strconv.FormatBool(c.ServiceSettings.EnableIncomingWebhooks) + props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride) + props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride) props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications) props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail) diff --git a/utils/diagnostic.go b/utils/diagnostic.go new file mode 100644 index 000000000..9a61ae934 --- /dev/null +++ b/utils/diagnostic.go @@ -0,0 +1,45 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "net/http" + + l4g "code.google.com/p/log4go" + + "github.com/mattermost/platform/model" +) + +const ( + PROP_DIAGNOSTIC_ID = "id" + PROP_DIAGNOSTIC_CATEGORY = "c" + VAL_DIAGNOSTIC_CATEGORY_DEFALUT = "d" + PROP_DIAGNOSTIC_BUILD = "b" + PROP_DIAGNOSTIC_DATABASE = "db" + PROP_DIAGNOSTIC_OS = "os" + PROP_DIAGNOSTIC_USER_COUNT = "uc" +) + +func SendDiagnostic(data model.StringMap) *model.AppError { + if Cfg.PrivacySettings.EnableDiagnostic && !model.IsOfficalBuild() { + + query := "?" + for name, value := range data { + if len(query) > 1 { + query += "&" + } + + query += name + "=" + UrlEncode(value) + } + + res, err := http.Get("http://d7zmvsa9e04kk.cloudfront.net/i" + query) + if err != nil { + l4g.Error("Failed to send diagnostics %v", err.Error()) + } + + res.Body.Close() + } + + return nil +} diff --git a/web/react/components/admin_console/privacy_settings.jsx b/web/react/components/admin_console/privacy_settings.jsx index affd8ae11..c74d321e6 100644 --- a/web/react/components/admin_console/privacy_settings.jsx +++ b/web/react/components/admin_console/privacy_settings.jsx @@ -30,6 +30,7 @@ export default class PrivacySettings extends React.Component { var config = this.props.config; config.PrivacySettings.ShowEmailAddress = React.findDOMNode(this.refs.ShowEmailAddress).checked; config.PrivacySettings.ShowFullName = React.findDOMNode(this.refs.ShowFullName).checked; + config.PrivacySettings.EnableDiagnostic = React.findDOMNode(this.refs.EnableDiagnostic).checked; Client.saveConfig( config, @@ -137,6 +138,39 @@ export default class PrivacySettings extends React.Component { </div> <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnableDiagnostic' + > + {'Send Error and Diagnostic: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableDiagnostic' + value='true' + ref='EnableDiagnostic' + defaultChecked={this.props.config.PrivacySettings.EnableDiagnostic} + onChange={this.handleChange} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableDiagnostic' + value='false' + defaultChecked={!this.props.config.PrivacySettings.EnableDiagnostic} + onChange={this.handleChange} + /> + {'false'} + </label> + <p className='help-text'>{'When true, The server will periodically send error and diagnostic information to Mattermost.'}</p> + </div> + </div> + + <div className='form-group'> <div className='col-sm-12'> {serverError} <button diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx index 245ffa871..b2d1b7b4d 100644 --- a/web/react/components/admin_console/service_settings.jsx +++ b/web/react/components/admin_console/service_settings.jsx @@ -37,6 +37,8 @@ export default class ServiceSettings extends React.Component { config.ServiceSettings.GoogleDeveloperKey = React.findDOMNode(this.refs.GoogleDeveloperKey).value.trim(); //config.ServiceSettings.EnableOAuthServiceProvider = React.findDOMNode(this.refs.EnableOAuthServiceProvider).checked; config.ServiceSettings.EnableIncomingWebhooks = React.findDOMNode(this.refs.EnableIncomingWebhooks).checked; + config.ServiceSettings.EnablePostUsernameOverride = React.findDOMNode(this.refs.EnablePostUsernameOverride).checked; + config.ServiceSettings.EnablePostIconOverride = React.findDOMNode(this.refs.EnablePostIconOverride).checked; config.ServiceSettings.EnableTesting = React.findDOMNode(this.refs.EnableTesting).checked; var MaximumLoginAttempts = 10; @@ -199,7 +201,73 @@ export default class ServiceSettings extends React.Component { /> {'false'} </label> - <p className='help-text'>{'When true, incoming webhooks will be allowed.'}</p> + <p className='help-text'>{'When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnablePostUsernameOverride' + > + {'Enable Overriding Usernames from Webhooks: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnablePostUsernameOverride' + value='true' + ref='EnablePostUsernameOverride' + defaultChecked={this.props.config.ServiceSettings.EnablePostUsernameOverride} + onChange={this.handleChange} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnablePostUsernameOverride' + value='false' + defaultChecked={!this.props.config.ServiceSettings.EnablePostUsernameOverride} + onChange={this.handleChange} + /> + {'false'} + </label> + <p className='help-text'>{'When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'}</p> + </div> + </div> + + <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnablePostIconOverride' + > + {'Enable Overriding Icon from Webhooks: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnablePostIconOverride' + value='true' + ref='EnablePostIconOverride' + defaultChecked={this.props.config.ServiceSettings.EnablePostIconOverride} + onChange={this.handleChange} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnablePostIconOverride' + value='false' + defaultChecked={!this.props.config.ServiceSettings.EnablePostIconOverride} + onChange={this.handleChange} + /> + {'false'} + </label> + <p className='help-text'>{'When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'}</p> </div> </div> diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 57a78a0d4..78057d10b 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -9,7 +9,7 @@ var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); function getStateFromStores() { - return {teams: UserStore.getTeams(), currentTeam: TeamStore.getCurrent()}; + return {teams: UserStore.getTeams()}; } export default class NavbarDropdown extends React.Component { @@ -142,10 +142,10 @@ export default class NavbarDropdown extends React.Component { > </li> ); - if (this.state.teams.length > 1 && this.state.currentTeam) { - var curTeamName = this.state.currentTeam.name; + + if (this.state.teams.length > 1) { this.state.teams.forEach((teamName) => { - if (teamName !== curTeamName) { + if (teamName !== this.props.teamName) { teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>); } }); @@ -234,5 +234,7 @@ NavbarDropdown.defaultProps = { teamType: '' }; NavbarDropdown.propTypes = { - teamType: React.PropTypes.string + teamType: React.PropTypes.string, + teamDisplayName: React.PropTypes.string, + teamName: React.PropTypes.string }; diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 9127f00de..ac9c9252e 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -158,11 +158,18 @@ export default class Post extends React.Component { var profilePic = null; if (!this.props.hideProfilePic) { + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; + if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') { + if (post.props.override_icon_url) { + src = post.props.override_icon_url; + } + } + profilePic = ( <div className='post-profile-img__container'> <img className='post-profile-img' - src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} + src={src} height='36' width='36' /> diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 9dc525e03..dd79b3e36 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -12,9 +12,27 @@ export default class PostHeader extends React.Component { render() { var post = this.props.post; + let userProfile = <UserProfile userId={post.user_id} />; + let botIndicator; + + if (post.props && post.props.from_webhook) { + if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') { + userProfile = ( + <UserProfile + userId={post.user_id} + overwriteName={post.props.override_username} + disablePopover={true} + /> + ); + } + + botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>; + } + return ( <ul className='post-header post-header-post'> - <li className='post-header-col post-header__name'><strong><UserProfile userId={post.user_id} /></strong></li> + <li className='post-header-col post-header__name'><strong>{userProfile}</strong></li> + {botIndicator} <li className='post-info--hidden'> <PostInfo post={post} diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index a31967257..b90197ac4 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -516,8 +516,19 @@ export default class PostList extends React.Component { sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); - // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post - hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post); + // hide the profile pic if: + // the previous post was made by the same user as the current post, + // the previous post is not a comment, + // the current post is not a comment, + // the current post is not from a webhook + // and the previous post is not from a webhook + if ((prevPost.user_id === post.user_id) && + !utils.isComment(prevPost) && + !utils.isComment(post) && + (!post.props || !post.props.from_webhook) && + (!prevPost.props || !prevPost.props.from_webhook)) { + hideProfilePic = true; + } } // check if it's the last comment in a consecutive string of comments on the same post diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index 6d4b56b7b..c0841a508 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -512,6 +512,7 @@ export default class Sidebar extends React.Component { /> <SidebarHeader teamDisplayName={this.props.teamDisplayName} + teamName={this.props.teamName} teamType={this.props.teamType} /> <SearchBox /> @@ -591,5 +592,6 @@ Sidebar.defaultProps = { }; Sidebar.propTypes = { teamType: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + teamDisplayName: React.PropTypes.string, + teamName: React.PropTypes.string }; diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 072c14e0a..33de35064 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -52,6 +52,8 @@ export default class SidebarHeader extends React.Component { <NavbarDropdown ref='dropdown' teamType={this.props.teamType} + teamDisplayName={this.props.teamDisplayName} + teamName={this.props.teamName} /> </div> ); @@ -64,5 +66,6 @@ SidebarHeader.defaultProps = { }; SidebarHeader.propTypes = { teamDisplayName: React.PropTypes.string, + teamName: React.PropTypes.string, teamType: React.PropTypes.string }; diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index c5d028d31..ceb8f52a7 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -31,8 +31,10 @@ export default class UserProfile extends React.Component { } componentDidMount() { UserStore.addChangeListener(this.onChange); - $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}}); - $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); + if (!this.props.disablePopover) { + $('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}}); + $('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'}); + } } componentWillUnmount() { UserStore.removeChangeListener(this.onChange); @@ -56,6 +58,10 @@ export default class UserProfile extends React.Component { name = this.props.overwriteName; } + if (this.props.disablePopover) { + return <div>{name}</div>; + } + var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />'; if (!global.window.config.ShowEmailAddress === 'true') { dataContent += '<div class="text-nowrap">Email not shared</div>'; @@ -79,9 +85,11 @@ export default class UserProfile extends React.Component { UserProfile.defaultProps = { userId: '', - overwriteName: '' + overwriteName: '', + disablePopover: false }; UserProfile.propTypes = { userId: React.PropTypes.string, - overwriteName: React.PropTypes.string + overwriteName: React.PropTypes.string, + disablePopover: React.PropTypes.bool }; diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx index f8f916bd0..8116bffcc 100644 --- a/web/react/components/user_settings/premade_theme_chooser.jsx +++ b/web/react/components/user_settings/premade_theme_chooser.jsx @@ -24,7 +24,7 @@ export default class PremadeThemeChooser extends React.Component { premadeThemes.push( <div - className='col-sm-3 premade-themes' + className='col-xs-6 col-sm-3 premade-themes' key={'premade-theme-key' + k} > <div diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx index db42c8266..8ea8b13eb 100644 --- a/web/react/pages/authorize.jsx +++ b/web/react/pages/authorize.jsx @@ -3,16 +3,16 @@ var Authorize = require('../components/authorize.jsx'); -function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) { +function setupAuthorizePage(props) { React.render( <Authorize - teamName={teamName} - appName={appName} - responseType={responseType} - clientId={clientId} - redirectUri={redirectUri} - scope={scope} - state={state} + teamName={props.TeamName} + appName={props.AppName} + responseType={props.ResponseType} + clientId={props.ClientId} + redirectUri={props.RedirectUri} + scope={props.Scope} + state={props.State} />, document.getElementById('authorize') ); diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 74259194a..c333fd57d 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -36,11 +36,14 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal. var FileUploadOverlay = require('../components/file_upload_overlay.jsx'); var RegisterAppModal = require('../components/register_app_modal.jsx'); var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function setupChannelPage(props) { + TeamStore.setCurrentId(props.TeamId); + AppDispatcher.handleViewAction({ type: ActionTypes.CLICK_CHANNEL, name: props.ChannelName, @@ -71,6 +74,7 @@ function setupChannelPage(props) { React.render( <Sidebar teamDisplayName={props.TeamDisplayName} + teamName={props.TeamName} teamType={props.TeamType} />, document.getElementById('sidebar-left') diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index da59f8e5a..67414dc3b 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -179,7 +179,7 @@ module.exports = { centerChannelColor: '#DDDDDD', newMessageSeparator: '#5de5da', linkColor: '#A4FFEB', - buttonBg: '#1dacfc', + buttonBg: '#4CBBA4', buttonColor: '#FFFFFF' }, windows10: { diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 8b20e2adf..1bc082175 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -395,35 +395,39 @@ export function toTitleCase(str) { export function applyTheme(theme) { if (theme.sidebarBg) { - changeCss('.sidebar--left', 'background:' + theme.sidebarBg, 1); + changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } if (theme.sidebarText) { - changeCss('.sidebar--left .nav li>a, .sidebar--right', 'color:' + theme.sidebarText, 1); - changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1); + changeCss('.sidebar--left .nav-pills__container li>a, .sidebar--right, .settings-modal .nav-pills>li a, .sidebar--menu', 'color:' + theme.sidebarText, 1); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'color:' + theme.sidebarText, 1); + changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1); changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1); changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1); changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li>a', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2); } if (theme.sidebarUnreadText) { - changeCss('.sidebar--left .nav li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 1); + changeCss('.sidebar--left .nav-pills__container li>a.unread-title', 'color:' + theme.sidebarUnreadText + '!important;', 2); } if (theme.sidebarTextHoverBg) { - changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'background:' + theme.sidebarTextHoverBg, 1); + changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'background:' + theme.sidebarTextHoverBg, 1); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1); } if (theme.sidebarTextHoverColor) { - changeCss('.sidebar--left .nav li>a:hover, .sidebar--left .nav li>a:focus', 'color:' + theme.sidebarTextHoverColor, 2); + changeCss('.sidebar--left .nav-pills__container li>a:hover, .sidebar--left .nav-pills__container li>a:focus, .settings-modal .nav-pills>li:hover a, .settings-modal .nav-pills>li:focus a', 'color:' + theme.sidebarTextHoverColor, 2); + changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'color:' + theme.sidebarTextHoverColor, 2); } if (theme.sidebarTextActiveBg) { - changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + theme.sidebarTextActiveBg, 1); + changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'background:' + theme.sidebarTextActiveBg, 1); } if (theme.sidebarTextActiveColor) { - changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'color:' + theme.sidebarTextActiveColor, 2); + changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2); } if (theme.sidebarHeaderBg) { @@ -458,45 +462,51 @@ export function applyTheme(theme) { } if (theme.centerChannelBg) { - changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box', 'background:' + theme.centerChannelBg, 1); + changeCss('.app__content, .markdown__table, .markdown__table tbody tr, .command-box, .modal .modal-content, .mentions-name, .mentions--top .mentions-box', 'background:' + theme.centerChannelBg, 1); changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1); changeCss('#post-create', 'background:' + theme.centerChannelBg, 1); changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1); changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1); - changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1); + changeCss('.sidebar--right, .dropdown-menu, .popover', 'background:' + theme.centerChannelBg, 1); } if (theme.centerChannelColor) { - changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round, .command-name', 'color:' + theme.centerChannelColor, 1); + changeCss('.app__content, .post-create__container .post-create-body .btn-file, .post-create__container .post-create-footer .msg-typing, .loading-screen .loading__content .round, .command-name, .modal .modal-content, .dropdown-menu, .popover, .mentions-name', 'color:' + theme.centerChannelColor, 1); changeCss('#post-create', 'color:' + theme.centerChannelColor, 2); changeCss('.mentions--top, .command-box', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 3); changeCss('.mentions--top, .command-box', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 2); changeCss('.mentions--top, .command-box', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px', 1); + changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3); + changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2); + changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1); changeCss('.post-body hr', 'background:' + theme.centerChannelColor, 1); changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1); changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); - changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); - changeCss('.command-name', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.command-name, .popover .popover-title', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.dropdown-menu .divider', 'background:' + theme.centerChannelColor, 1); changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1); changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2); changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2); changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1); - changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.search-bar__container .search__form .search-bar', 'background: transparent; color:' + theme.centerChannelColor, 1); + changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1); changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1); changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2); - changeCss('.date-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); - changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); + changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1); + changeCss('.channel-intro, .settings-modal .settings-table .settings-content .divider-dark, hr, .settings-modal .settings-table .settings-links', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2); - changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.date-separator.hovered--before:after, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); - changeCss('.date-separator.hovered--after:before, .new-separator.hovered--after:before, .command-name:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.post:hover, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); + changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1); changeCss('.post.current--user:hover .post-body ', 'background: none;', 1); changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2); } @@ -507,7 +517,7 @@ export function applyTheme(theme) { } if (theme.linkColor) { - changeCss('a, a:focus, a:hover', 'color:' + theme.linkColor, 1); + changeCss('a, a:focus, a:hover, .btn, .btn:focus, .btn:hover', 'color:' + theme.linkColor, 1); changeCss('.post .comment-icon__container', 'fill:' + theme.linkColor, 1); } diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index 11794a269..5037da415 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -144,7 +144,6 @@ } .popover { border-radius: 3px; - border: 1px solid #ccc; width: 100%; font-size: 0.95em; } diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 87d9b8200..fa465ff91 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -42,6 +42,21 @@ body { color: $primary-color; } } + .popover-title { + background: rgba(black, 0.1); + } +} + +.dropdown-menu { + .divider { + @include opacity(0.15); + } + > li > a { + color: inherit; + &:focus, &:hover { + color: inherit; + } + } } .word-break--all { @@ -68,6 +83,19 @@ a:focus, a:hover { margin: 0; } +.text-danger { + color: #E05F5D; +} + +.btn { + &.btn-danger { + color: #fff; + &:hover, &:active { + color: #fff; + } + } +} + .form-control { @include border-radius(2px); &.no-resize { diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index b5fcb6145..9b9e5f573 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -114,23 +114,6 @@ } } } - &.theme--black { - &:hover { - &:before { - background: rgba(white, 0.2); - } - } - } - &.theme--gray { - &:hover { - &:before { - background: rgba(white, 0.1); - } - } - } - a { - color: #fff; - } .navbar-right { font-size: 0.85em; position: absolute; @@ -145,7 +128,6 @@ .dropdown-menu { li a { padding: 3px 20px; - color: #555; text-overflow: ellipsis; overflow: hidden; } diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss index 48746ba01..aff31e418 100644 --- a/web/sass-files/sass/partials/_mentions.scss +++ b/web/sass-files/sass/partials/_mentions.scss @@ -33,13 +33,6 @@ line-height: 36px; font-size: 13px; cursor: pointer; - &:hover { - background-color: #E6F2FA; - } -} - -.mentions-focus { - background-color: #E6F2FA; } .mentions-text { @@ -54,7 +47,6 @@ display: block; font-size: 20px; text-align: center; - color: #555; @include border-radius(32px); } diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index e4e8b20b6..96b26f251 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -9,12 +9,6 @@ @include opacity(0.7); } } - a, a:focus, a:hover { - color: #2389D7; - &.text-danger { - color: #a94442; - } - } .custom-textarea { color: inherit; border-color: #ccc; @@ -25,12 +19,9 @@ } .btn { font-size: 13px; - &.btn-primary { - background: #4285f4; - &:hover, &:focus, &:active { - background: $primary-color--hover; - color: #fff; - } + &.btn-default { + border: none; + background: transparent; } } .info__label { @@ -70,7 +61,7 @@ background: $primary-color; color: #FFF; padding: 15px 15px 11px; - border: none; + border: 1px solid #ddd; min-height: 56px; @include clearfix; .modal-title { diff --git a/web/sass-files/sass/partials/_oauth.scss b/web/sass-files/sass/partials/_oauth.scss new file mode 100644 index 000000000..35d3a95e3 --- /dev/null +++ b/web/sass-files/sass/partials/_oauth.scss @@ -0,0 +1,31 @@ +.oauth-prompt { + background: #fff; + border: 1px solid #ddd; + padding: 1em 2em 0; + margin: 50px auto; + max-width: 90%; + width: 600px; + .prompt__heading { + font-size: em(20px); + line-height: normal; + margin: 1em 0; + display: table; + width: 100%; + > div { + display: table-cell; + vertical-align: top; + } + img { + margin-right: 15px; + } + } + .prompt__allow { + margin: 1em 0; + font-size: em(24px); + } + .prompt__buttons { + text-align: right; + border-top: 1px solid #ddd; + padding: 1.5em 0; + } +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 7532875d6..8bf4b0534 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -509,3 +509,11 @@ body.ios { } } } + +.bot-indicator { + background-color: lightgrey; + border-radius:2px; + padding-left:2px; + padding-right:2px; + font-family:"Courier New" +} diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 9e8d0dc7d..82ec1811a 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -402,12 +402,6 @@ margin-left: 7px; } } - &.active, &:hover { - a { - color: #555; - background: #fff; - } - } } } } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 3aab05d70..1f785f63c 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -2,7 +2,6 @@ @import "activity-log"; .user-settings { - background: #fff; min-height:300px; .table-responsive { max-width: 560px; @@ -71,7 +70,7 @@ } .section-max { - background: #f2f2f2; + background: rgba(black, 0.05); padding: 1em 0 1.3em; margin-bottom: 0; @include clearfix; @@ -121,7 +120,7 @@ .fa { margin-right: 7px; font-size: 12px; - color: #aaa; + @include opacity(0.5); display: none; } } @@ -131,7 +130,7 @@ } .section-describe { - color:grey; + @include opacity(0.7); } .divider-dark { @@ -167,15 +166,11 @@ } } .control-label { - color: #555; font-weight: 600; &.text-left { text-align: left; } } - hr { - border-color: #ccc; - } } .file-status { diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 6a418e270..73d702fef 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -16,12 +16,6 @@ overflow-y: auto; max-width: 200px; width: 200px; - a { - color: #262626 !important; - &:hover, &:focus { - background: #f5f5f5 !important; - } - } } .search__form { margin: 0; @@ -75,7 +69,7 @@ top: 66px; } .nav-pills__unread-indicator-bottom { - bottom: 0px; + bottom: 10px; } .nav { @@ -98,7 +92,6 @@ padding: 3px 10px 3px 25px; line-height: 1.5; border-radius: 0; - color: #999; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -109,12 +102,8 @@ text-decoration: underline; } &.unread-title { - color: #333; font-weight: 600; } - &:hover, &:focus { - background: #e6f2fa; - } } &.active { a, a:hover, a:focus { diff --git a/web/sass-files/sass/partials/_sidebar--menu.scss b/web/sass-files/sass/partials/_sidebar--menu.scss index 4366b1a6c..6f4a0cc38 100644 --- a/web/sass-files/sass/partials/_sidebar--menu.scss +++ b/web/sass-files/sass/partials/_sidebar--menu.scss @@ -54,7 +54,7 @@ > a { font-size: 15px; background: none !important; - color: #444; + color: inherit; line-height: 40px; padding: 0 15px; .glyphicon { diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index e704536a8..c614052da 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -41,3 +41,6 @@ // Responsive Css @import "partials/responsive"; + +// Standalone Css +@import "partials/oauth"; diff --git a/web/static/images/themes/mattermost dark.png b/web/static/images/themes/mattermost dark.png Binary files differindex 832f64d2e..1ab44e104 100644 --- a/web/static/images/themes/mattermost dark.png +++ b/web/static/images/themes/mattermost dark.png diff --git a/web/static/images/themes/mattermost.png b/web/static/images/themes/mattermost.png Binary files differindex 4a321adcb..8dea37ff6 100644 --- a/web/static/images/themes/mattermost.png +++ b/web/static/images/themes/mattermost.png diff --git a/web/static/images/themes/organization.png b/web/static/images/themes/organization.png Binary files differindex 1a38bfb34..3066cca0b 100644 --- a/web/static/images/themes/organization.png +++ b/web/static/images/themes/organization.png diff --git a/web/static/images/themes/slack.png b/web/static/images/themes/slack.png Binary files differdeleted file mode 100644 index dc70c7dc2..000000000 --- a/web/static/images/themes/slack.png +++ /dev/null diff --git a/web/static/images/themes/windows dark.png b/web/static/images/themes/windows dark.png Binary files differindex d65304820..1f665d882 100644 --- a/web/static/images/themes/windows dark.png +++ b/web/static/images/themes/windows dark.png diff --git a/web/templates/authorize.html b/web/templates/authorize.html index 3392c1b1e..b0fa3e475 100644 --- a/web/templates/authorize.html +++ b/web/templates/authorize.html @@ -1,26 +1,26 @@ {{define "authorize"}} <html> {{template "head" . }} -<body class="white"> - <div class="container-fluid"> - <div class="inner__wrap"> - <div class="row content"> - <div class="signup-header"> - {{.Props.TeamName}} - </div> - <div class="col-sm-12"> - <div id="authorize"></div> - </div> - <div class="footer-push"></div> - </div> - <div class="row footer"> - {{template "footer" . }} - </div> - </div> - </div> - <script> - window.setup_authorize_page('{{ .Props.TeamName }}', '{{ .Props.AppName }}', '{{ .Props.ResponseType }}', '{{ .Props.ClientId }}', '{{ .Props.RedirectUri }}', '{{ .Props.Scope }}', '{{ .Props.State }}' ); - </script> +<body> + <div class="container-fluid"> + <div class="oauth-prompt"> + <div class="prompt__heading"> + <div class="prompt__app-icon"> + <img src="/static/images/icon50x50.gif" width="50" height="50" alt=""> + </div> + <div class="text">An application would like to connect to your {{.Props.TeamName}} account.</div> + </div> + <p>The app <strong>{{.Props.AppName}}</strong> would like the ability to access Mattermost on your behalf.</p> + <h2 class="prompt__allow">Allow <strong>{{.Props.AppName}}</strong> access?</h2> + <div class="prompt__buttons"> + <input type="button" class="btn btn-link" value="Deny"> + <input type="button" class="btn btn-primary" value="Allow"> + </div> + </div> + </div> + <script> + window.setup_authorize_page('{{.Props}}'); + </script> </body> </html> {{end}} diff --git a/web/web.go b/web/web.go index bf985a5a0..a1bbf5a81 100644 --- a/web/web.go +++ b/web/web.go @@ -189,9 +189,40 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { return } + // We still might be able to switch to this team because we've logged in before + if multiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil { + multiToken := multiCookie.Value + + if len(multiToken) > 0 { + tokens := strings.Split(multiToken, " ") + + for _, token := range tokens { + if sr := <-api.Srv.Store.Session().Get(token); sr.Err == nil { + s := sr.Data.(*model.Session) + + if !s.IsExpired() && s.TeamId == team.Id { + w.Header().Set(model.HEADER_TOKEN, s.Token) + sessionCookie := &http.Cookie{ + Name: model.SESSION_TOKEN, + Value: s.Token, + Path: "/", + MaxAge: model.SESSION_TIME_WEB_IN_SECS, + HttpOnly: true, + } + + http.SetCookie(w, sessionCookie) + + http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusTemporaryRedirect) + return + } + } + } + } + } + page := NewHtmlTemplatePage("login", "Login") page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = teamName + page.Props["TeamName"] = team.Name page.Render(c, w) } @@ -288,6 +319,10 @@ func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) name := params["channelname"] + teamName := params["team"] + + var team *model.Team + teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) var channelId string if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { @@ -297,6 +332,19 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { channelId = result.Data.(string) } + if tResult := <-teamChan; tResult.Err != nil { + c.Err = tResult.Err + return + } else { + team = tResult.Data.(*model.Team) + } + + if team.Name != teamName { + l4g.Error("It appears you are logged into " + team.Name + ", but are trying to access " + teamName) + http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusFound) + return + } + if len(channelId) == 0 { if strings.Index(name, "__") > 0 { // It's a direct message channel that doesn't exist yet so let's create it @@ -319,7 +367,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { // lets make sure the user is valid if result := <-api.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err - c.RemoveSessionCookie(w) + c.RemoveSessionCookie(w, r) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } @@ -332,18 +380,10 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { } } - var team *model.Team - - if tResult := <-api.Srv.Store.Team().Get(c.Session.TeamId); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } - page := NewHtmlTemplatePage("channel", "") page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = team.DisplayName + page.Props["TeamName"] = team.Name page.Props["TeamType"] = team.Type page.Props["TeamId"] = team.Id page.Props["ChannelName"] = name @@ -451,6 +491,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("password_reset", "") page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = teamDisplayName + page.Props["TeamName"] = teamName page.Props["Hash"] = hash page.Props["Data"] = data page.Props["TeamName"] = teamName @@ -843,6 +884,12 @@ func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { } func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { + c.Err = model.NewAppError("incomingWebhook", "Incoming webhooks have been disabled by the system admin.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + params := mux.Vars(r) id := params["id"] @@ -865,6 +912,9 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { channelName := props["channel"] + overrideUsername := props["username"] + overrideIconUrl := props["icon_url"] + var hook *model.IncomingWebhook if result := <-hchan; result.Err != nil { c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message) @@ -910,6 +960,15 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text} + post.AddProp("from_webhook", "true") + + if len(overrideUsername) != 0 && utils.Cfg.ServiceSettings.EnablePostUsernameOverride { + post.AddProp("override_username", overrideUsername) + } + + if len(overrideIconUrl) != 0 && utils.Cfg.ServiceSettings.EnablePostIconOverride { + post.AddProp("override_icon_url", overrideIconUrl) + } if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "") |