diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d9122c00..8082b25366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,31 @@ The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the produ - IE 10 no longer supported since global share of IE 10 fell below 5% +## Release v1.1.1 (Bug Fix Release) + +Released 2015-10-20 + +### About Bug Fix Releases + +This is a bug fix release (v1.1.1) and recommended only for users needing a fix to the specific issue listed below. All other users should use the most recent major stable build release (v1.1.0). + +[View more information on Mattermost release numbering](https://github.com/mattermost/platform/blob/master/doc/install/release-numbering.md). + +### Release Purpose + +#### Provide option for upgrading database from Mattermost v0.7 to v1.1 + +Upgrading Mattermost v0.7 to Mattermost v1.1 originally required installing Mattermost v1.0 to upgrade from the Mattermost v0.7 database, followed by an install of Mattermost v1.1. + +This was problematic for installing Mattermost with GitLab omnibus since GitLab 8.0 contained Mattermost v0.7 and GitLab 8.1 was to include Mattermost v1.1 + +Therefore Mattermost v1.1.1 was created that can upgrade the database in Mattermost v0.7 to Mattermost v1.1 directly. + +Users who configured Mattermost v0.7 within GitLab via the `config.json` file should consult [documentation on upgrading configurations from Mattermost v0.7 to Mattermost v1.1](https://github.com/mattermost/platform/blob/master/doc/install/Upgrade-Guide.md#upgrading-mattermost-v07-to-v11). + +#### Removes 32-char limit on salts + +Mattermost v1.1 introduced a 32-char limit on salts that broke the salt generating in GitLab and this restriction was removed for 1.1.1. ## Release v1.1.0 diff --git a/api/admin.go b/api/admin.go index 89353d61dd..8e0a03e4b4 100644 --- a/api/admin.go +++ b/api/admin.go @@ -24,7 +24,7 @@ func InitAdmin(r *mux.Router) { sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET") sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST") sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST") - sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET") + sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET") sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST") sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET") } @@ -57,8 +57,8 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.ArrayToJson(lines))) } -func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) { - w.Write([]byte(model.MapToJson(utils.ClientProperties))) +func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) { + w.Write([]byte(model.MapToJson(utils.ClientCfg))) } func logClient(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api/api.go b/api/api.go index 4da1de62d4..6c7eda0a2f 100644 --- a/api/api.go +++ b/api/api.go @@ -20,7 +20,7 @@ func NewServerTemplatePage(templateName string) *ServerTemplatePage { return &ServerTemplatePage{ TemplateName: templateName, Props: make(map[string]string), - ClientProps: utils.ClientProperties, + ClientCfg: utils.ClientCfg, } } diff --git a/api/channel.go b/api/channel.go index 70f7eba4bf..a8c8505e99 100644 --- a/api/channel.go +++ b/api/channel.go @@ -131,16 +131,21 @@ func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model return nil, model.NewAppError("CreateDirectChannel", "Invalid other user id ", otherUserId) } - if sc, err := CreateChannel(c, channel, true); err != nil { - return nil, err + cm1 := &model.ChannelMember{ + UserId: c.Session.UserId, + Roles: model.CHANNEL_ROLE_ADMIN, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + cm2 := &model.ChannelMember{ + UserId: otherUserId, + Roles: "", + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + + if result := <-Srv.Store.Channel().SaveDirectChannel(channel, cm1, cm2); result.Err != nil { + return nil, result.Err } else { - cm := &model.ChannelMember{ChannelId: sc.Id, UserId: otherUserId, Roles: "", NotifyProps: model.GetDefaultChannelNotifyProps()} - - if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil { - return nil, cmresult.Err - } - - return sc, nil + return result.Data.(*model.Channel), nil } } @@ -503,6 +508,8 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { sc := Srv.Store.Channel().Get(id) scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) uc := Srv.Store.User().Get(c.Session.UserId) + ihc := Srv.Store.Webhook().GetIncomingByChannel(id) + ohc := Srv.Store.Webhook().GetOutgoingByChannel(id) if cresult := <-sc; cresult.Err != nil { c.Err = cresult.Err @@ -513,10 +520,18 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { } else if scmresult := <-scm; scmresult.Err != nil { c.Err = scmresult.Err return + } else if ihcresult := <-ihc; ihcresult.Err != nil { + c.Err = ihcresult.Err + return + } else if ohcresult := <-ohc; ohcresult.Err != nil { + c.Err = ohcresult.Err + return } else { channel := cresult.Data.(*model.Channel) user := uresult.Data.(*model.User) channelMember := scmresult.Data.(model.ChannelMember) + incomingHooks := ihcresult.Data.([]*model.IncomingWebhook) + outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook) if !c.HasPermissionsToTeam(channel.TeamId, "deleteChannel") { return @@ -540,6 +555,23 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } + now := model.GetMillis() + for _, hook := range incomingHooks { + go func() { + if result := <-Srv.Store.Webhook().DeleteIncoming(hook.Id, now); result.Err != nil { + l4g.Error("Encountered error deleting incoming webhook, id=" + hook.Id) + } + }() + } + + for _, hook := range outgoingHooks { + go func() { + if result := <-Srv.Store.Webhook().DeleteOutgoing(hook.Id, now); result.Err != nil { + l4g.Error("Encountered error deleting outgoing webhook, id=" + hook.Id) + } + }() + } + if dresult := <-Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); dresult.Err != nil { c.Err = dresult.Err return diff --git a/api/context.go b/api/context.go index bd9744bf84..9be3e85cc3 100644 --- a/api/context.go +++ b/api/context.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/url" + "strconv" "strings" l4g "code.google.com/p/log4go" @@ -19,20 +20,24 @@ import ( var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) type Context struct { - Session model.Session - RequestId string - IpAddress string - Path string - Err *model.AppError - teamURLValid bool - teamURL string - siteURL string + Session model.Session + RequestId string + IpAddress string + Path string + Err *model.AppError + teamURLValid bool + teamURL string + siteURL string + SessionTokenIndex int64 } type Page struct { - TemplateName string - Props map[string]string - ClientProps map[string]string + TemplateName string + Props map[string]string + ClientCfg map[string]string + User *model.User + Team *model.Team + SessionTokenIndex int64 } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -96,8 +101,37 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Attempt to parse the token from the cookie if len(token) == 0 { - if cookie, err := r.Cookie(model.SESSION_TOKEN); err == nil { - token = cookie.Value + tokens := GetMultiSessionCookieTokens(r) + if len(tokens) > 0 { + // If there is only 1 token in the cookie then just use it like normal + if len(tokens) == 1 { + token = tokens[0] + } else { + // If it is a multi-session token then find the correct session + sessionTokenIndexStr := r.URL.Query().Get(model.SESSION_TOKEN_INDEX) + sessionTokenIndex := int64(-1) + if len(sessionTokenIndexStr) > 0 { + if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { + sessionTokenIndex = index + } + } else { + sessionTokenIndexStr := r.Header.Get(model.HEADER_MM_SESSION_TOKEN_INDEX) + if len(sessionTokenIndexStr) > 0 { + if index, err := strconv.ParseInt(sessionTokenIndexStr, 10, 64); err == nil { + sessionTokenIndex = index + } + } + } + + if sessionTokenIndex >= 0 && sessionTokenIndex < int64(len(tokens)) { + token = tokens[sessionTokenIndex] + c.SessionTokenIndex = sessionTokenIndex + } else { + c.SessionTokenIndex = -1 + } + } + } else { + c.SessionTokenIndex = -1 } } @@ -123,18 +157,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if len(token) != 0 { - var session *model.Session - if ts, ok := sessionCache.Get(token); ok { - session = ts.(*model.Session) - } - - if session == nil { - if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil { - c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "token="+token+", err="+sessionResult.Err.DetailedError)) - } else { - session = sessionResult.Data.(*model.Session) - } - } + session := GetSession(token) if session == nil || session.IsExpired() { c.RemoveSessionCookie(w, r) @@ -318,10 +341,23 @@ func (c *Context) IsTeamAdmin() bool { func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { - sessionCache.Remove(c.Session.Token) + // multiToken := "" + // if oldMultiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + // multiToken = oldMultiCookie.Value + // } + + // multiCookie := &http.Cookie{ + // Name: model.SESSION_COOKIE_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) cookie := &http.Cookie{ - Name: model.SESSION_TOKEN, + Name: model.SESSION_COOKIE_TOKEN, Value: "", Path: "/", MaxAge: -1, @@ -329,21 +365,6 @@ func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { } 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) { @@ -479,7 +500,7 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) } w.WriteHeader(err.StatusCode) - ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientProps: utils.ClientProperties}) + ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg}) } func Handle404(w http.ResponseWriter, r *http.Request) { @@ -489,6 +510,46 @@ func Handle404(w http.ResponseWriter, r *http.Request) { RenderWebError(err, w, r) } +func GetSession(token string) *model.Session { + var session *model.Session + if ts, ok := sessionCache.Get(token); ok { + session = ts.(*model.Session) + } + + if session == nil { + if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil { + l4g.Error("Invalid session token=" + token + ", err=" + sessionResult.Err.DetailedError) + } else { + session = sessionResult.Data.(*model.Session) + } + } + + return session +} + +func GetMultiSessionCookieTokens(r *http.Request) []string { + if multiCookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + multiToken := multiCookie.Value + + if len(multiToken) > 0 { + return strings.Split(multiToken, " ") + } + } + + return []string{} +} + +func FindMultiSessionForTeamId(r *http.Request, teamId string) (int64, *model.Session) { + for index, token := range GetMultiSessionCookieTokens(r) { + s := GetSession(token) + if s != nil && !s.IsExpired() && s.TeamId == teamId { + return int64(index), s + } + } + + return -1, nil +} + func AddSessionToCache(session *model.Session) { sessionCache.Add(session.Token, session) } diff --git a/api/post.go b/api/post.go index c5bcd4f5aa..79f84e04d8 100644 --- a/api/post.go +++ b/api/post.go @@ -281,7 +281,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team // copy the context and create a mock session for posting the message mockSession := model.Session{UserId: hook.CreatorId, TeamId: hook.TeamId, IsOAuth: false} - newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL} + newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0} if text, ok := respProps["text"]; ok { if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"]); err != nil { diff --git a/api/team.go b/api/team.go index 2d7b05ff60..d39d8ed60c 100644 --- a/api/team.go +++ b/api/team.go @@ -426,9 +426,9 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { } subjectPage := NewServerTemplatePage("find_teams_subject") - subjectPage.ClientProps["SiteURL"] = c.GetSiteURL() + subjectPage.ClientCfg["SiteURL"] = c.GetSiteURL() bodyPage := NewServerTemplatePage("find_teams_body") - bodyPage.ClientProps["SiteURL"] = c.GetSiteURL() + bodyPage.ClientCfg["SiteURL"] = c.GetSiteURL() if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { c.Err = result.Err diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html index 41fd6e4c3e..df2db87304 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -23,9 +23,9 @@ - Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -38,7 +38,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html index 962ae868ea..4ff8026f14 100644 --- a/api/templates/email_change_subject.html +++ b/api/templates/email_change_subject.html @@ -1 +1 @@ -{{define "email_change_subject"}}[{{.ClientProps.SiteName}}] Your email address has changed for {{.Props.TeamDisplayName}}{{end}} +{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] Your email address has changed for {{.Props.TeamDisplayName}}{{end}} diff --git a/api/templates/email_change_verify_body.html b/api/templates/email_change_verify_body.html index a9b2a0741a..f6bc3bc395 100644 --- a/api/templates/email_change_verify_body.html +++ b/api/templates/email_change_verify_body.html @@ -26,9 +26,9 @@ - Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -41,7 +41,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html index 5e2ac1452a..744aaccfca 100644 --- a/api/templates/email_change_verify_subject.html +++ b/api/templates/email_change_verify_subject.html @@ -1 +1 @@ -{{define "email_change_verify_subject"}}[{{.ClientProps.SiteName}}] Verify new email address for {{.Props.TeamDisplayName}}{{end}} +{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] Verify new email address for {{.Props.TeamDisplayName}}{{end}} diff --git a/api/templates/error.html b/api/templates/error.html index 6b643556e0..6944f6c680 100644 --- a/api/templates/error.html +++ b/api/templates/error.html @@ -1,7 +1,7 @@ - {{ .ClientProps.SiteName }} - Error + {{ .ClientCfg.SiteName }} - Error @@ -22,7 +22,7 @@
-

{{ .ClientProps.SiteName }} needs your help:

+

{{ .ClientCfg.SiteName }} needs your help:

{{ .Props.Message }}

Go back to team site
diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html index 41f9dac017..4669d51c14 100644 --- a/api/templates/find_teams_body.html +++ b/api/templates/find_teams_body.html @@ -9,7 +9,7 @@ @@ -31,9 +31,9 @@
- +
- Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -42,11 +42,11 @@

- +

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html index 3c2bef5891..f3a1437b31 100644 --- a/api/templates/find_teams_subject.html +++ b/api/templates/find_teams_subject.html @@ -1 +1 @@ -{{define "find_teams_subject"}}Your {{ .ClientProps.SiteName }} Teams{{end}} +{{define "find_teams_subject"}}Your {{ .ClientCfg.SiteName }} Teams{{end}} diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html index 57feef5d99..930bc099d3 100644 --- a/api/templates/invite_body.html +++ b/api/templates/invite_body.html @@ -18,7 +18,7 @@

You've been invited

-

{{.Props.TeamDisplayName}} started using {{.ClientProps.SiteName}}.
The team {{.Props.SenderStatus}} {{.Props.SenderName}}, has invited you to join {{.Props.TeamDisplayName}}.

+

{{.Props.TeamDisplayName}} started using {{.ClientCfg.SiteName}}.
The team {{.Props.SenderStatus}} {{.Props.SenderName}}, has invited you to join {{.Props.TeamDisplayName}}.

Join Team

@@ -26,9 +26,9 @@ - Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -41,7 +41,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html index f46bdcfaf2..10f68969f6 100644 --- a/api/templates/invite_subject.html +++ b/api/templates/invite_subject.html @@ -1 +1 @@ -{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.ClientProps.SiteName}}{{end}} +{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.ClientCfg.SiteName}}{{end}} diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html index 542df4b745..47a93fcb93 100644 --- a/api/templates/password_change_body.html +++ b/api/templates/password_change_body.html @@ -23,9 +23,9 @@ - Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -38,7 +38,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html index 283fda1afd..e7a7940908 100644 --- a/api/templates/password_change_subject.html +++ b/api/templates/password_change_subject.html @@ -1 +1 @@ -{{define "password_change_subject"}}You updated your password for {{.Props.TeamDisplayName}} on {{ .ClientProps.SiteName }}{{end}} +{{define "password_change_subject"}}You updated your password for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}} diff --git a/api/templates/post_body.html b/api/templates/post_body.html index 63a53bf3cc..182134b1a5 100644 --- a/api/templates/post_body.html +++ b/api/templates/post_body.html @@ -26,9 +26,9 @@ - Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -41,7 +41,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html index 944cd5a42a..f53353d85f 100644 --- a/api/templates/post_subject.html +++ b/api/templates/post_subject.html @@ -1 +1 @@ -{{define "post_subject"}}[{{.ClientProps.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}} +{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}} diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html index 4bafc57e89..5e5f6cafc3 100644 --- a/api/templates/reset_body.html +++ b/api/templates/reset_body.html @@ -26,9 +26,9 @@ - Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -41,7 +41,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html index dc2cb32ec7..6f3deb28ba 100644 --- a/api/templates/signup_team_body.html +++ b/api/templates/signup_team_body.html @@ -21,14 +21,14 @@

Set up your team

- {{ .ClientProps.SiteName }} is one place for all your team communication, searchable and available anywhere.
You'll get more out of {{ .ClientProps.SiteName }} when your team is in constant communication--let's get them on board.

+ {{ .ClientCfg.SiteName }} is one place for all your team communication, searchable and available anywhere.
You'll get more out of {{ .ClientCfg.SiteName }} when your team is in constant communication--let's get them on board.

- Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -41,7 +41,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html index 7bc0cc640a..236b288fa6 100644 --- a/api/templates/signup_team_subject.html +++ b/api/templates/signup_team_subject.html @@ -1 +1 @@ -{{define "signup_team_subject"}}Invitation to {{ .ClientProps.SiteName }}{{end}} \ No newline at end of file +{{define "signup_team_subject"}}Invitation to {{ .ClientCfg.SiteName }}{{end}} \ No newline at end of file diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html index 0613b5dd59..a93de9a71b 100644 --- a/api/templates/verify_body.html +++ b/api/templates/verify_body.html @@ -26,9 +26,9 @@ - Any questions at all, mail us any time: {{.ClientProps.FeedbackEmail}}.
+ Any questions at all, mail us any time: {{.ClientCfg.FeedbackEmail}}.
Best wishes,
- The {{.ClientProps.SiteName}} Team
+ The {{.ClientCfg.SiteName}} Team
@@ -41,7 +41,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html index 7990df84ae..9a3a112825 100644 --- a/api/templates/verify_subject.html +++ b/api/templates/verify_subject.html @@ -1 +1 @@ -{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .ClientProps.SiteName }}] Email Verification{{end}} +{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .ClientCfg.SiteName }}] Email Verification{{end}} diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html index b7cb3704d1..485bc63511 100644 --- a/api/templates/welcome_body.html +++ b/api/templates/welcome_body.html @@ -43,7 +43,7 @@

(c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.
- If you no longer wish to receive these emails, click on the following link: Unsubscribe + If you no longer wish to receive these emails, click on the following link: Unsubscribe

diff --git a/api/user.go b/api/user.go index 0c7278711a..3071e1b26c 100644 --- a/api/user.go +++ b/api/user.go @@ -428,43 +428,23 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, } w.Header().Set(model.HEADER_TOKEN, session.Token) - sessionCookie := &http.Cookie{ - Name: model.SESSION_TOKEN, - Value: session.Token, - Path: "/", - MaxAge: maxAge, - HttpOnly: true, - } - - http.SetCookie(w, sessionCookie) + tokens := GetMultiSessionCookieTokens(r) 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 - } - } + seen := make(map[string]string) + seen[session.TeamId] = session.TeamId + for _, token := range tokens { + s := GetSession(token) + if s != nil && !s.IsExpired() && seen[s.TeamId] == "" { + multiToken += " " + token + seen[s.TeamId] = s.TeamId } } - multiToken = strings.TrimSpace(session.Token + " " + multiToken) + multiToken = strings.TrimSpace(multiToken + " " + session.Token) multiSessionCookie := &http.Cookie{ - Name: model.MULTI_SESSION_TOKEN, + Name: model.SESSION_COOKIE_TOKEN, Value: multiToken, Path: "/", MaxAge: maxAge, @@ -1241,6 +1221,11 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } + if len(user.AuthData) != 0 { + c.Err = model.NewAppError("sendPasswordReset", "Cannot reset password for SSO accounts", "userId="+user.Id+", teamId="+team.Id) + return + } + newProps := make(map[string]string) newProps["user_id"] = user.Id newProps["time"] = fmt.Sprintf("%v", model.GetMillis()) @@ -1325,6 +1310,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { user = result.Data.(*model.User) } + if len(user.AuthData) != 0 { + c.Err = model.NewAppError("resetPassword", "Cannot reset password for SSO accounts", "userId="+user.Id+", teamId="+team.Id) + return + } + if user.TeamId != team.Id { c.Err = model.NewAppError("resetPassword", "Trying to reset password for user on wrong team.", "userId="+user.Id+", teamId="+team.Id) c.Err.StatusCode = http.StatusForbidden diff --git a/api/user_test.go b/api/user_test.go index 77309e5b2f..b54e030c55 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -817,6 +817,16 @@ func TestSendPasswordReset(t *testing.T) { if _, err := Client.SendPasswordReset(data); err == nil { t.Fatal("Should have errored - bad name") } + + user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + data["email"] = user2.Email + data["name"] = team.Name + if _, err := Client.SendPasswordReset(data); err == nil { + t.Fatal("should have errored - SSO user can't send reset password link") + } } func TestResetPassword(t *testing.T) { @@ -901,6 +911,20 @@ func TestResetPassword(t *testing.T) { if _, err := Client.ResetPassword(data); err == nil { t.Fatal("Should have errored - domain team doesn't match user team") } + + user2 := &model.User{TeamId: team.Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", AuthData: "1", AuthService: "random"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user2.Id)) + + data["new_password"] = "newpwd" + props["user_id"] = user2.Id + props["time"] = fmt.Sprintf("%v", model.GetMillis()) + data["data"] = model.MapToJson(props) + data["hash"] = model.HashPassword(fmt.Sprintf("%v:%v", data["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) + data["name"] = team.Name + if _, err := Client.ResetPassword(data); err == nil { + t.Fatal("should have errored - SSO user can't reset password") + } } func TestUserUpdateNotify(t *testing.T) { diff --git a/doc/developer/API.md b/doc/developer/API.md new file mode 100644 index 0000000000..6327f1173b --- /dev/null +++ b/doc/developer/API.md @@ -0,0 +1,35 @@ +# Mattermost APIs + +Mattermost APIs let you integrate your favorite tools and services withing your Mattermost experience. + +## Slack-compatible integration support + +To offer an alternative to propreitary SaaS services, Mattermost focuses on being "Slack-compatible, but not Slack limited". That means providing support for developers of Slack applications to easily extend their apps to Mattermost, as well as support and capabilities beyond what Slack offers. + +### [Incoming Webhooks](https://github.com/mattermost/platform/blob/master/doc/integrations/webhooks/Incoming-Webhooks.md) + +Incoming webhooks allow external applications to post messages into Mattermost channels and private groups by sending a JSON payload via HTTP POST request to a secret Mattermost URL generated specifically for each application. + +In addition to supporting Slack's incoming webhook formatting, Mattermost webhooks offer full support of industry-standard markdown formatting, including headings, tables and in-line images. + +### Outgoing Webhooks (coming in Mattermost v1.2) + +Outgoing webhooks allow external applications to receive webhook events from events happening within Mattermost channels and private groups via JSON payloads via HTTP POST requests sent to incoming webhook URLs defined by your applications. + +Over time, Mattermost outgoing webhooks will support not only Slack applications using a compatible format, but also offer optional events and triggers beyond Slack's feature set. + +## Mattermost Drivers + +Mattermost is written in Golang and React and designed as a self-hosted system, which differs from Slack's technical platform and focus on SaaS. Therefore the Mattermost drivers will differ from Slack's interfaces. + +Another key difference is that as an open source project, you are welcome to access and use Mattermost's APIs on your installations the same way the core team would use them for buildling new features. + +While detailed documentation of the interfaces is pending, if you want to build deep integrations with Mattermost there are two drivers at the heart of the system: + +### [ReactJS Javascript Driver](https://github.com/mattermost/platform/blob/master/web/react/utils/client.jsx) + +[client.jsx](https://github.com/mattermost/platform/blob/master/web/react/utils/client.jsx) - This Javascript driver connects with the ReactJS components of Mattermost. The web client does the vast majority of its work by connecting to a RESTful JSON web service. There is a very small amount of processing for error checking and set up that happens on the web server. + +### [Golang Driver](https://github.com/mattermost/platform/blob/master/model/client.go) + +[client.go](https://github.com/mattermost/platform/blob/master/model/client.go) - This is a RESTful driver connecting with the Golang-based webservice of Mattermost and is used by unit tests. diff --git a/doc/install/SMTP-Email-Setup.md b/doc/install/SMTP-Email-Setup.md index 4e06d2f992..b958a6a964 100644 --- a/doc/install/SMTP-Email-Setup.md +++ b/doc/install/SMTP-Email-Setup.md @@ -14,6 +14,7 @@ To enable email, configure an SMTP email service as follows: 3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password` for Step 2 below. 4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain. 5. Choose an sender address like `mattermost@example.com` and click `Send a Test Email` to verify setup is working correctly. + 2. **Configure SMTP settings** 1. Open the **System Console** by logging into an existing team and accessing "System Console" from the main menu. 1. Alternatively, if a team doesn't yet exist, go to `http://dockerhost:8065/` in your browser, create a team, then from the main menu click **System Console** @@ -29,15 +30,42 @@ To enable email, configure an SMTP email service as follows: 9. **SMTP Port**: `SMTP Port` from Step 1 10. **Connection Security**: `TLS (Recommended)` 11. Then click **Save** + 12. Then click **Test Connection** + 13. If the test failed please look in **OTHER** > **Logs** for any errors that look like `[EROR] /api/v1/admin/test_email ...` + +### Known Good Sample Settings + +##### Amazon SES +* Set **SMTP Username** to **AKIASKLDSKDIWEOWE** +* Set **SMTP Password** to **AdskfjAKLSDJShflsdfjkakldADkjkjdfKAJDSlkjweiqQIWEOU** +* Set **SMTP Server** to **email-smtp.us-east-1.amazonaws.com** +* Set **SMTP Port** to **465** +* Set **Connection Security** to **TLS** + +##### Postfix +* Make sure Postfix is installed on the machine where Mattermost is installed +* Set **SMTP Username** to **(empty)** +* Set **SMTP Password** to **(empty)** +* Set **SMTP Server** to **localhost** +* Set **SMTP Port** to **25** +* Set **Connection Security** to **(empty)** + +##### Gmail +* Information needed + +##### Office 365 +* Information needed + +##### Hotmail +* Information needed -3. **Restart Mattermost** - 1. Use `ps -A` to find the process ID ("pid") for service named `platform` and stop it using `kill [pid]` - 2. The service should restart automatically. Run `ps -A` to verify the `platform` is running again - 3. Use the reset password page (E.g. _example.com/teamname/reset_password_) to test that email is now working by entering your email and clicking **Reset my password**. - 4. Note: The next time users log out, or when their session tokens expire, each will be required to verify their email address. ### Troubleshooting SMTP +#### Tip 1 +If you fill in **SMTP Username** and **SMTP Password** then you must set **Connection Security** to **TLS** or to **STARTTLS** + +#### Tip 2 If you have issues with your SMTP install, from your Mattermost team site go to the main menu and open **System Console -> Logs** to look for error messages related to your setup. You can do a search for the error code to narrow down the issue. Sometimes ISPs require nuanced setups for SMTP and error codes can hint at how to make the proper adjustments. For example, if **System Console -> Logs** has an error code reading: @@ -48,4 +76,19 @@ Connection unsuccessful: Failed to add to email address - 554 5.7.1 `. If the command works you should see something like +``` +250-mail.example.com NO UCE +250-STARTTLS +250-PIPELINING +250 8BITMIME +``` \ No newline at end of file diff --git a/doc/install/Troubleshooting.md b/doc/install/Troubleshooting.md index 21839c86f4..46efc61fae 100644 --- a/doc/install/Troubleshooting.md +++ b/doc/install/Troubleshooting.md @@ -12,5 +12,6 @@ - This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority). ##### Lost System Administrator account - - If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"` + - If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`. + - After assigning the role the user needs to log out and log back in before the System Administrator role is applied. diff --git a/doc/integrations/Single-Sign-On/Gitlab.md b/doc/integrations/Single-Sign-On/Gitlab.md index e503a158b6..0a8a1bd189 100644 --- a/doc/integrations/Single-Sign-On/Gitlab.md +++ b/doc/integrations/Single-Sign-On/Gitlab.md @@ -16,7 +16,7 @@ The following steps can be used to configure Mattermost to use GitLab as a singl * _TokenEndpoint_: `https:///oauth/token` * _UserApiEndpoint_: `https:///api/v3/user` - Note: Make sure your `HTTPS` or `HTTP` prefix for endpoint URLs matches how your server configuration. + Note: Make sure your `HTTPS` or `HTTP` prefix for endpoint URLs matches your server configuration. 5. (Optional) If you would like to force all users to sign-up with GitLab only, in the _ServiceSettings_ section of config/config.json please set _DisableEmailSignUp_ to `true`. diff --git a/doc/integrations/webhooks/Incoming-Webhooks.md b/doc/integrations/webhooks/Incoming-Webhooks.md index be17d6a8ed..7340282be1 100644 --- a/doc/integrations/webhooks/Incoming-Webhooks.md +++ b/doc/integrations/webhooks/Incoming-Webhooks.md @@ -90,3 +90,7 @@ As mentioned above, Mattermost makes it easy to take integrations written for Sl To see samples and community contributions, please visit . +#### Limitations + +- The `attachments` payload used in Slack is not yet supported +- Overriding of usernames does not yet apply to notifications diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go index 3fbdd5fd77..3c2289626f 100644 --- a/manualtesting/manual_testing.go +++ b/manualtesting/manual_testing.go @@ -111,7 +111,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { // Respond with an auth token this can be overriden by a specific test as required sessionCookie := &http.Cookie{ - Name: model.SESSION_TOKEN, + Name: model.SESSION_COOKIE_TOKEN, Value: client.AuthToken, Path: "/", MaxAge: model.SESSION_TIME_WEB_IN_SECS, diff --git a/model/client.go b/model/client.go index 6361c8e126..4d2c49e708 100644 --- a/model/client.go +++ b/model/client.go @@ -16,17 +16,19 @@ import ( ) const ( - HEADER_REQUEST_ID = "X-Request-ID" - HEADER_VERSION_ID = "X-Version-ID" - HEADER_ETAG_SERVER = "ETag" - HEADER_ETAG_CLIENT = "If-None-Match" - HEADER_FORWARDED = "X-Forwarded-For" - HEADER_REAL_IP = "X-Real-IP" - HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" - HEADER_TOKEN = "token" - HEADER_BEARER = "BEARER" - HEADER_AUTH = "Authorization" - API_URL_SUFFIX = "/api/v1" + HEADER_REQUEST_ID = "X-Request-ID" + HEADER_VERSION_ID = "X-Version-ID" + HEADER_ETAG_SERVER = "ETag" + HEADER_ETAG_CLIENT = "If-None-Match" + HEADER_FORWARDED = "X-Forwarded-For" + HEADER_REAL_IP = "X-Real-IP" + HEADER_FORWARDED_PROTO = "X-Forwarded-Proto" + HEADER_TOKEN = "token" + HEADER_BEARER = "BEARER" + HEADER_AUTH = "Authorization" + HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex" + SESSION_TOKEN_INDEX = "session_token_index" + API_URL_SUFFIX = "/api/v1" ) type Result struct { @@ -293,7 +295,7 @@ func (c *Client) login(m map[string]string) (*Result, *AppError) { } else { c.AuthToken = r.Header.Get(HEADER_TOKEN) c.AuthType = HEADER_BEARER - sessionToken := getCookie(SESSION_TOKEN, r) + sessionToken := getCookie(SESSION_COOKIE_TOKEN, r) if c.AuthToken != sessionToken.Value { NewAppError("/users/login", "Authentication tokens didn't match", "") diff --git a/model/session.go b/model/session.go index e2c1d4c553..5fe74a161a 100644 --- a/model/session.go +++ b/model/session.go @@ -9,8 +9,7 @@ import ( ) const ( - SESSION_TOKEN = "MMSID" - MULTI_SESSION_TOKEN = "MMSIDMU" + SESSION_COOKIE_TOKEN = "MMTOKEN" 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/store/sql_channel_store.go b/store/sql_channel_store.go index 4b30f646eb..dd368f8104 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -5,6 +5,7 @@ package store import ( l4g "code.google.com/p/log4go" + "github.com/go-gorp/gorp" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -97,49 +98,22 @@ func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel { storeChannel := make(StoreChannel) go func() { - result := StoreResult{} - - if len(channel.Id) > 0 { - result.Err = model.NewAppError("SqlChannelStore.Save", - "Must call update for exisiting channel", "id="+channel.Id) - storeChannel <- result - close(storeChannel) - return - } - - channel.PreSave() - if result.Err = channel.IsValid(); result.Err != nil { - storeChannel <- result - close(storeChannel) - return - } - - if count, err := s.GetMaster().SelectInt("SELECT COUNT(0) FROM Channels WHERE TeamId = :TeamId AND DeleteAt = 0 AND (Type = 'O' OR Type = 'P')", map[string]interface{}{"TeamId": channel.TeamId}); err != nil { - result.Err = model.NewAppError("SqlChannelStore.Save", "Failed to get current channel count", "teamId="+channel.TeamId+", "+err.Error()) - storeChannel <- result - close(storeChannel) - return - } else if count > 150 { - result.Err = model.NewAppError("SqlChannelStore.Save", "You've reached the limit of the number of allowed channels.", "teamId="+channel.TeamId) - storeChannel <- result - close(storeChannel) - return - } - - if err := s.GetMaster().Insert(channel); err != nil { - if IsUniqueConstraintError(err.Error(), "Name", "channels_name_teamid_key") { - dupChannel := model.Channel{} - s.GetReplica().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name = :Name AND DeleteAt > 0", map[string]interface{}{"TeamId": channel.TeamId, "Name": channel.Name}) - if dupChannel.DeleteAt > 0 { - result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL was previously created", "id="+channel.Id+", "+err.Error()) - } else { - result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL already exists", "id="+channel.Id+", "+err.Error()) - } - } else { - result.Err = model.NewAppError("SqlChannelStore.Save", "We couldn't save the channel", "id="+channel.Id+", "+err.Error()) - } + var result StoreResult + if channel.Type == model.CHANNEL_DIRECT { + result.Err = model.NewAppError("SqlChannelStore.Save", "Use SaveDirectChannel to create a direct channel", "") } else { - result.Data = channel + if transaction, err := s.GetMaster().Begin(); err != nil { + result.Err = model.NewAppError("SqlChannelStore.Save", "Unable to open transaction", err.Error()) + } else { + result = s.saveChannelT(transaction, channel) + if result.Err != nil { + transaction.Rollback() + } else { + if err := transaction.Commit(); err != nil { + result.Err = model.NewAppError("SqlChannelStore.Save", "Unable to commit transaction", err.Error()) + } + } + } } storeChannel <- result @@ -149,6 +123,100 @@ func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel { return storeChannel } +func (s SqlChannelStore) SaveDirectChannel(directchannel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + var result StoreResult + + if directchannel.Type != model.CHANNEL_DIRECT { + result.Err = model.NewAppError("SqlChannelStore.SaveDirectChannel", "Not a direct channel attempted to be created with SaveDirectChannel", "") + } else { + if transaction, err := s.GetMaster().Begin(); err != nil { + result.Err = model.NewAppError("SqlChannelStore.SaveDirectChannel", "Unable to open transaction", err.Error()) + } else { + channelResult := s.saveChannelT(transaction, directchannel) + + if channelResult.Err != nil { + transaction.Rollback() + result.Err = channelResult.Err + } else { + newChannel := channelResult.Data.(*model.Channel) + // Members need new channel ID + member1.ChannelId = newChannel.Id + member2.ChannelId = newChannel.Id + + member1Result := s.saveMemberT(transaction, member1, newChannel) + member2Result := s.saveMemberT(transaction, member2, newChannel) + + if member1Result.Err != nil || member2Result.Err != nil { + transaction.Rollback() + details := "" + if member1Result.Err != nil { + details += "Member1Err: " + member1Result.Err.Message + } + if member2Result.Err != nil { + details += "Member2Err: " + member2Result.Err.Message + } + result.Err = model.NewAppError("SqlChannelStore.SaveDirectChannel", "Unable to add direct channel members", details) + } else { + if err := transaction.Commit(); err != nil { + result.Err = model.NewAppError("SqlChannelStore.SaveDirectChannel", "Ubable to commit transaction", err.Error()) + } else { + result = channelResult + } + } + } + } + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlChannelStore) saveChannelT(transaction *gorp.Transaction, channel *model.Channel) StoreResult { + result := StoreResult{} + + if len(channel.Id) > 0 { + result.Err = model.NewAppError("SqlChannelStore.Save", "Must call update for exisiting channel", "id="+channel.Id) + return result + } + + channel.PreSave() + if result.Err = channel.IsValid(); result.Err != nil { + return result + } + + if count, err := transaction.SelectInt("SELECT COUNT(0) FROM Channels WHERE TeamId = :TeamId AND DeleteAt = 0 AND (Type = 'O' OR Type = 'P')", map[string]interface{}{"TeamId": channel.TeamId}); err != nil { + result.Err = model.NewAppError("SqlChannelStore.Save", "Failed to get current channel count", "teamId="+channel.TeamId+", "+err.Error()) + return result + } else if count > 150 { + result.Err = model.NewAppError("SqlChannelStore.Save", "You've reached the limit of the number of allowed channels.", "teamId="+channel.TeamId) + return result + } + + if err := transaction.Insert(channel); err != nil { + if IsUniqueConstraintError(err.Error(), "Name", "channels_name_teamid_key") { + dupChannel := model.Channel{} + s.GetReplica().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name = :Name AND DeleteAt > 0", map[string]interface{}{"TeamId": channel.TeamId, "Name": channel.Name}) + if dupChannel.DeleteAt > 0 { + result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL was previously created", "id="+channel.Id+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL already exists", "id="+channel.Id+", "+err.Error()) + } + } else { + result.Err = model.NewAppError("SqlChannelStore.Save", "We couldn't save the channel", "id="+channel.Id+", "+err.Error()) + } + } else { + result.Data = channel + } + + return result +} + func (s SqlChannelStore) Update(channel *model.Channel) StoreChannel { storeChannel := make(StoreChannel) @@ -396,31 +464,27 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel { storeChannel := make(StoreChannel) go func() { - result := StoreResult{} - + var result StoreResult // Grab the channel we are saving this member to if cr := <-s.Get(member.ChannelId); cr.Err != nil { result.Err = cr.Err } else { channel := cr.Data.(*model.Channel) - member.PreSave() - if result.Err = member.IsValid(); result.Err != nil { - storeChannel <- result - return - } - - if err := s.GetMaster().Insert(member); err != nil { - if IsUniqueConstraintError(err.Error(), "ChannelId", "channelmembers_pkey") { - result.Err = model.NewAppError("SqlChannelStore.SaveMember", "A channel member with that id already exists", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error()) - } else { - result.Err = model.NewAppError("SqlChannelStore.SaveMember", "We couldn't save the channel member", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error()) - } + if transaction, err := s.GetMaster().Begin(); err != nil { + result.Err = model.NewAppError("SqlChannelStore.SaveMember", "Unable to open transaction", err.Error()) } else { - result.Data = member - // If sucessfull record members have changed in channel - if mu := <-s.extraUpdated(channel); mu.Err != nil { - result.Err = mu.Err + result = s.saveMemberT(transaction, member, channel) + if result.Err != nil { + transaction.Rollback() + } else { + if err := transaction.Commit(); err != nil { + result.Err = model.NewAppError("SqlChannelStore.SaveMember", "Unable to commit transaction", err.Error()) + } + // If sucessfull record members have changed in channel + if mu := <-s.extraUpdated(channel); mu.Err != nil { + result.Err = mu.Err + } } } } @@ -432,6 +496,27 @@ func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel { return storeChannel } +func (s SqlChannelStore) saveMemberT(transaction *gorp.Transaction, member *model.ChannelMember, channel *model.Channel) StoreResult { + result := StoreResult{} + + member.PreSave() + if result.Err = member.IsValid(); result.Err != nil { + return result + } + + if err := transaction.Insert(member); err != nil { + if IsUniqueConstraintError(err.Error(), "ChannelId", "channelmembers_pkey") { + result.Err = model.NewAppError("SqlChannelStore.SaveMember", "A channel member with that id already exists", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error()) + } else { + result.Err = model.NewAppError("SqlChannelStore.SaveMember", "We couldn't save the channel member", "channel_id="+member.ChannelId+", user_id="+member.UserId+", "+err.Error()) + } + } else { + result.Data = member + } + + return result +} + func (s SqlChannelStore) UpdateMember(member *model.ChannelMember) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index 3bfafff4b9..f6a0fb7139 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -33,6 +33,14 @@ func TestChannelStoreSave(t *testing.T) { t.Fatal("should be unique name") } + o1.Id = "" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + if err := (<-store.Channel().Save(&o1)).Err; err == nil { + t.Fatal("Should not be able to save direct channel") + } + + o1.Type = model.CHANNEL_OPEN for i := 0; i < 150; i++ { o1.Id = "" o1.Name = "a" + model.NewId() + "b" @@ -48,6 +56,61 @@ func TestChannelStoreSave(t *testing.T) { } } +func TestChannelStoreSaveDirectChannel(t *testing.T) { + Setup() + + teamId := model.NewId() + + o1 := model.Channel{} + o1.TeamId = teamId + o1.DisplayName = "Name" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_DIRECT + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + u1.Nickname = model.NewId() + Must(store.User().Save(&u1)) + + u2 := model.User{} + u2.TeamId = model.NewId() + u2.Email = model.NewId() + u2.Nickname = model.NewId() + Must(store.User().Save(&u2)) + + m1 := model.ChannelMember{} + m1.ChannelId = o1.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o1.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + if err := (<-store.Channel().SaveDirectChannel(&o1, &m1, &m2)).Err; err != nil { + t.Fatal("couldn't save direct channel", err) + } + + members := (<-store.Channel().GetMembers(o1.Id)).Data.([]model.ChannelMember) + if len(members) != 2 { + t.Fatal("should have saved 2 members") + } + + if err := (<-store.Channel().SaveDirectChannel(&o1, &m1, &m2)).Err; err == nil { + t.Fatal("shouldn't be able to update from save") + } + + o1.Id = "" + o1.Name = "a" + model.NewId() + "b" + o1.Type = model.CHANNEL_OPEN + if err := (<-store.Channel().SaveDirectChannel(&o1, &m1, &m2)).Err; err == nil { + t.Fatal("Should not be able to save non-direct channel") + } + +} + func TestChannelStoreUpdate(t *testing.T) { Setup() @@ -99,6 +162,44 @@ func TestChannelStoreGet(t *testing.T) { if err := (<-store.Channel().Get("")).Err; err == nil { t.Fatal("Missing id should have failed") } + + u1 := model.User{} + u1.TeamId = model.NewId() + u1.Email = model.NewId() + u1.Nickname = model.NewId() + Must(store.User().Save(&u1)) + + u2 := model.User{} + u2.TeamId = model.NewId() + u2.Email = model.NewId() + u2.Nickname = model.NewId() + Must(store.User().Save(&u2)) + + o2 := model.Channel{} + o2.TeamId = model.NewId() + o2.DisplayName = "Direct Name" + o2.Name = "a" + model.NewId() + "b" + o2.Type = model.CHANNEL_DIRECT + + m1 := model.ChannelMember{} + m1.ChannelId = o2.Id + m1.UserId = u1.Id + m1.NotifyProps = model.GetDefaultChannelNotifyProps() + + m2 := model.ChannelMember{} + m2.ChannelId = o2.Id + m2.UserId = u2.Id + m2.NotifyProps = model.GetDefaultChannelNotifyProps() + + Must(store.Channel().SaveDirectChannel(&o2, &m1, &m2)) + + if r2 := <-store.Channel().Get(o2.Id); r2.Err != nil { + t.Fatal(r2.Err) + } else { + if r2.Data.(*model.Channel).ToJson() != o2.ToJson() { + t.Fatal("invalid returned channel") + } + } } func TestChannelStoreDelete(t *testing.T) { diff --git a/store/sql_store.go b/store/sql_store.go index d4d8fdf73c..0d1bfe41b2 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -73,6 +73,7 @@ func NewSqlStore() Store { } schemaVersion := sqlStore.GetCurrentSchemaVersion() + isSchemaVersion07 := false // If the version is already set then we are potentially in an 'upgrade needed' state if schemaVersion != "" { @@ -81,7 +82,6 @@ func NewSqlStore() Store { // If we are upgrading from the previous version then print a warning and continue // Special case - isSchemaVersion07 := false if schemaVersion == "0.7.1" || schemaVersion == "0.7.0" { isSchemaVersion07 = true } @@ -140,7 +140,7 @@ func NewSqlStore() Store { sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists() sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists() - if model.IsPreviousVersion(schemaVersion) { + if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 { sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion}) l4g.Warn("The database schema has been upgraded to version " + model.CurrentVersion) } diff --git a/store/sql_user_store.go b/store/sql_user_store.go index a2b317afac..5fab38acea 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -125,6 +125,7 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha oldUser := oldUserResult.(*model.User) user.CreateAt = oldUser.CreateAt user.AuthData = oldUser.AuthData + user.AuthService = oldUser.AuthService user.Password = oldUser.Password user.LastPasswordUpdate = oldUser.LastPasswordUpdate user.LastPictureUpdate = oldUser.LastPictureUpdate @@ -265,7 +266,7 @@ func (us SqlUserStore) UpdatePassword(userId, hashedPassword string) StoreChanne updateAt := model.GetMillis() - if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0 WHERE Id = :UserId", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil { + if _, err := us.GetMaster().Exec("UPDATE Users SET Password = :Password, LastPasswordUpdate = :LastPasswordUpdate, UpdateAt = :UpdateAt, FailedAttempts = 0 WHERE Id = :UserId AND AuthData = ''", map[string]interface{}{"Password": hashedPassword, "LastPasswordUpdate": updateAt, "UpdateAt": updateAt, "UserId": userId}); err != nil { result.Err = model.NewAppError("SqlUserStore.UpdatePassword", "We couldn't update the user password", "id="+userId+", "+err.Error()) } else { result.Data = userId diff --git a/store/sql_webhook_store.go b/store/sql_webhook_store.go index 1910984f07..c758e23393 100644 --- a/store/sql_webhook_store.go +++ b/store/sql_webhook_store.go @@ -137,6 +137,27 @@ func (s SqlWebhookStore) GetIncomingByUser(userId string) StoreChannel { return storeChannel } +func (s SqlWebhookStore) GetIncomingByChannel(channelId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var webhooks []*model.IncomingWebhook + + if _, err := s.GetReplica().Select(&webhooks, "SELECT * FROM IncomingWebhooks WHERE ChannelId = :ChannelId AND DeleteAt = 0", map[string]interface{}{"ChannelId": channelId}); err != nil { + result.Err = model.NewAppError("SqlWebhookStore.GetIncomingByChannel", "We couldn't get the webhooks", "channelId="+channelId+", err="+err.Error()) + } + + result.Data = webhooks + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlWebhookStore) SaveOutgoing(webhook *model.OutgoingWebhook) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/store.go b/store/store.go index e52368290c..42329b0366 100644 --- a/store/store.go +++ b/store/store.go @@ -54,6 +54,7 @@ type TeamStore interface { type ChannelStore interface { Save(channel *model.Channel) StoreChannel + SaveDirectChannel(channel *model.Channel, member1 *model.ChannelMember, member2 *model.ChannelMember) StoreChannel Update(channel *model.Channel) StoreChannel Get(id string) StoreChannel Delete(channelId string, time int64) StoreChannel @@ -153,6 +154,7 @@ type WebhookStore interface { SaveIncoming(webhook *model.IncomingWebhook) StoreChannel GetIncoming(id string) StoreChannel GetIncomingByUser(userId string) StoreChannel + GetIncomingByChannel(channelId string) StoreChannel DeleteIncoming(webhookId string, time int64) StoreChannel SaveOutgoing(webhook *model.OutgoingWebhook) StoreChannel GetOutgoing(id string) StoreChannel diff --git a/utils/config.go b/utils/config.go index 15d6b217c8..fd9856a67c 100644 --- a/utils/config.go +++ b/utils/config.go @@ -26,7 +26,7 @@ const ( var Cfg *model.Config = &model.Config{} var CfgLastModified int64 = 0 var CfgFileName string = "" -var ClientProperties map[string]string = map[string]string{} +var ClientCfg map[string]string = map[string]string{} var SanitizeOptions map[string]bool = map[string]bool{} func FindConfigFile(fileName string) string { @@ -161,7 +161,7 @@ func LoadConfig(fileName string) { Cfg = &config SanitizeOptions = getSanitizeOptions(Cfg) - ClientProperties = getClientProperties(Cfg) + ClientCfg = getClientConfig(Cfg) } func getSanitizeOptions(c *model.Config) map[string]bool { @@ -172,7 +172,7 @@ func getSanitizeOptions(c *model.Config) map[string]bool { return options } -func getClientProperties(c *model.Config) map[string]string { +func getClientConfig(c *model.Config) map[string]string { props := make(map[string]string) props["Version"] = model.CurrentVersion diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index e8a46086ad..6962876d47 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -14,7 +14,7 @@ export default class AboutBuildModal extends React.Component { } render() { - const config = global.window.config; + const config = global.window.mm_config; return ( ); } diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 395e22e6ce..f7e92672d1 100644 --- a/web/react/components/admin_console/user_item.jsx +++ b/web/react/components/admin_console/user_item.jsx @@ -215,7 +215,7 @@ export default class UserItem extends React.Component {
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 270631db29..55b4a55c07 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -26,7 +26,6 @@ export default class ChannelLoader extends React.Component { } componentDidMount() { /* Initial aysnc loads */ - AsyncClient.getMe(); AsyncClient.getPosts(ChannelStore.getCurrentId()); AsyncClient.getChannels(true, true); AsyncClient.getChannelExtraInfo(true); diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 0358995929..8b5fc4162b 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -51,7 +51,7 @@ export default class CreatePost extends React.Component { submitting: false, initialText: draft.messageText, windowWidth: Utils.windowWidth(), - windowHeigth: Utils.windowHeight() + windowHeight: Utils.windowHeight() }; } handleResize() { @@ -71,7 +71,7 @@ export default class CreatePost extends React.Component { return; } - if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeigth) { + if (prevState.windowWidth !== this.state.windowWidth || prevState.windowHeight !== this.state.windowHeight) { this.resizePostHolder(); return; } @@ -208,7 +208,7 @@ export default class CreatePost extends React.Component { PostStore.storeCurrentDraft(draft); } resizePostHolder() { - const height = this.state.windowHeigth - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; + const height = this.state.windowHeight - $(ReactDOM.findDOMNode(this.refs.topDiv)).height() - 50; $('.post-list-holder-by-time').css('height', `${height}px`); if (this.state.windowWidth > 960) { $('#post_textbox').focus(); diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 940b01f8d6..9c07853b72 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -19,10 +19,10 @@ export default class EmailVerify extends React.Component { var resend = ''; var resendConfirm = ''; if (this.props.isVerified === 'true') { - title = global.window.config.SiteName + ' Email Verified'; + title = global.window.mm_config.SiteName + ' Email Verified'; body =

Your email has been verified! Click Please verify your email address. Check your inbox for an email.

; resend = (
-

By proceeding to create your account and use {global.window.config.SiteName}, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {global.window.config.SiteName}.

+

By proceeding to create your account and use {global.window.mm_config.SiteName}, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {global.window.mm_config.SiteName}.

Welcome to:

-

{global.window.config.SiteName}

+

{global.window.mm_config.SiteName}

Let's set up your new team

diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 5403316633..c4402ae231 100644 --- a/web/react/components/user_profile.jsx +++ b/web/react/components/user_profile.jsx @@ -67,13 +67,14 @@ export default class UserProfile extends React.Component { dataContent.push( ); - if (!global.window.config.ShowEmailAddress === 'true') { + + if (!global.window.mm_config.ShowEmailAddress === 'true') { dataContent.push(

{ if (channel.type !== Constants.DM_CHANNEL) { - options.push(); + options.push( + + ); } }); @@ -108,26 +115,31 @@ export default class ManageIncomingHooks extends React.Component { const hooks = []; this.state.hooks.forEach((hook) => { const c = ChannelStore.get(hook.channel_id); - hooks.push( -
-
-
- ); + ); + } }); let displayHooks; diff --git a/web/react/components/user_settings/manage_outgoing_hooks.jsx b/web/react/components/user_settings/manage_outgoing_hooks.jsx index e83ae3bd6c..f6d6b515b3 100644 --- a/web/react/components/user_settings/manage_outgoing_hooks.jsx +++ b/web/react/components/user_settings/manage_outgoing_hooks.jsx @@ -128,21 +128,42 @@ export default class ManageOutgoingHooks extends React.Component { } const channels = ChannelStore.getAll(); - const options = []; + const options = []; + options.push( + + ); + channels.forEach((channel) => { if (channel.type === Constants.OPEN_CHANNEL) { - options.push(); + options.push( + + ); } }); const hooks = []; this.state.hooks.forEach((hook) => { const c = ChannelStore.get(hook.channel_id); + + if (!c && hook.channel_id && hook.channel_id.length !== 0) { + return; + } + let channelDiv; if (c) { channelDiv = (
- {'Channel: '}{c.name} + {'Channel: '}{c.display_name}
); } @@ -157,7 +178,10 @@ export default class ManageOutgoingHooks extends React.Component { } hooks.push( -
+
{'URLs: '}{hook.callback_urls.join(', ')} diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 9c03f77a64..70e559c304 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -122,7 +122,7 @@ export default class UserSettingsGeneralTab extends React.Component { () => { this.updateSection(''); AsyncClient.getMe(); - const verificationEnabled = global.window.config.SendEmailNotifications === 'true' && global.window.config.RequireEmailVerification === 'true' && emailUpdated; + const verificationEnabled = global.window.mm_config.SendEmailNotifications === 'true' && global.window.mm_config.RequireEmailVerification === 'true' && emailUpdated; if (verificationEnabled) { ErrorStore.storeLastError({message: 'Check your email at ' + user.email + ' to verify the address.'}); @@ -451,8 +451,8 @@ export default class UserSettingsGeneralTab extends React.Component { } var emailSection; if (this.props.activeSection === 'email') { - const emailEnabled = global.window.config.SendEmailNotifications === 'true'; - const emailVerificationEnabled = global.window.config.RequireEmailVerification === 'true'; + const emailEnabled = global.window.mm_config.SendEmailNotifications === 'true'; + const emailVerificationEnabled = global.window.mm_config.RequireEmailVerification === 'true'; let helpText = 'Email is used for notifications, and requires verification if changed.'; if (!emailEnabled) { @@ -542,7 +542,7 @@ export default class UserSettingsGeneralTab extends React.Component { + ); incomingHooksSection = ( @@ -65,10 +65,10 @@ export default class UserSettingsIntegrationsTab extends React.Component { } } - if (global.window.config.EnableOutgoingWebhooks === 'true') { + if (global.window.mm_config.EnableOutgoingWebhooks === 'true') { if (this.props.activeSection === 'outgoing-hooks') { inputs.push( - + ); outgoingHooksSection = ( diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 44cd423b57..5449ae91e5 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -35,10 +35,11 @@ export default class UserSettingsModal extends React.Component { tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'}); tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'}); tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'}); - if (global.window.config.EnableOAuthServiceProvider === 'true') { + if (global.window.mm_config.EnableOAuthServiceProvider === 'true') { tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'}); } - if (global.window.config.EnableIncomingWebhooks === 'true' || global.window.config.EnableOutgoingWebhooks === 'true') { + + if (global.window.mm_config.EnableIncomingWebhooks === 'true' || global.window.mm_config.EnableOutgoingWebhooks === 'true') { tabs.push({name: 'integrations', uiName: 'Integrations', icon: 'glyphicon glyphicon-transfer'}); } tabs.push({name: 'display', uiName: 'Display', icon: 'glyphicon glyphicon-eye-open'}); diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx index 8693af494f..61d49acb2a 100644 --- a/web/react/components/user_settings/user_settings_notifications.jsx +++ b/web/react/components/user_settings/user_settings_notifications.jsx @@ -413,7 +413,7 @@ export default class NotificationsTab extends React.Component {
-

{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.config.SiteName + ' for more than 5 minutes.'}
+

{'Email notifications are sent for mentions and direct messages after you’ve been offline for more than 60 seconds or away from ' + global.window.mm_config.SiteName + ' for more than 5 minutes.'}
); diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index bea6ce7a55..92d7cd8356 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -197,7 +197,7 @@ export default class ViewImageModal extends React.Component { } fileInfo.path = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; - return fileInfo.path + '_preview.jpg'; + return fileInfo.path + '_preview.jpg' + '?' + Utils.getSessionIndex(); } // only images have proper previews, so just use a placeholder icon for non-images @@ -306,7 +306,7 @@ export default class ViewImageModal extends React.Component { width={width} height={height} > - + ); } else { diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx index 5b3ee540c3..1287f4fba1 100644 --- a/web/react/components/view_image_popover_bar.jsx +++ b/web/react/components/view_image_popover_bar.jsx @@ -7,7 +7,7 @@ export default class ViewImagePopoverBar extends React.Component { } render() { var publicLink = ''; - if (global.window.config.EnablePublicLink === 'true') { + if (global.window.mm_config.EnablePublicLink === 'true') { publicLink = (
{ var action = payload.action; switch (action.type) { - case ActionTypes.CLICK_TEAM: - TeamStore.setCurrentId(action.id); - TeamStore.emitChange(); - break; - case ActionTypes.RECIEVED_TEAM: - TeamStore.storeTeam(action.team); + TeamStore.saveTeam(action.team); TeamStore.emitChange(); break; diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index fa74f812d2..575e6d51ea 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -3,7 +3,6 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var EventEmitter = require('events').EventEmitter; -var client = require('../utils/client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; @@ -38,13 +37,11 @@ class UserStoreClass extends EventEmitter { this.emitToggleImportModal = this.emitToggleImportModal.bind(this); this.addImportModalListener = this.addImportModalListener.bind(this); this.removeImportModalListener = this.removeImportModalListener.bind(this); - this.setCurrentId = this.setCurrentId.bind(this); this.getCurrentId = this.getCurrentId.bind(this); this.getCurrentUser = this.getCurrentUser.bind(this); this.setCurrentUser = this.setCurrentUser.bind(this); this.getLastEmail = this.getLastEmail.bind(this); this.setLastEmail = this.setLastEmail.bind(this); - this.removeCurrentUser = this.removeCurrentUser.bind(this); this.hasProfile = this.hasProfile.bind(this); this.getProfile = this.getProfile.bind(this); this.getProfileByUsername = this.getProfileByUsername.bind(this); @@ -52,9 +49,6 @@ class UserStoreClass extends EventEmitter { this.getProfiles = this.getProfiles.bind(this); this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this); this.saveProfile = this.saveProfile.bind(this); - this.pStoreProfiles = this.pStoreProfiles.bind(this); - this.pGetProfiles = this.pGetProfiles.bind(this); - this.pGetProfilesUsernameMap = this.pGetProfilesUsernameMap.bind(this); this.setSessions = this.setSessions.bind(this); this.getSessions = this.getSessions.bind(this); this.setAudits = this.setAudits.bind(this); @@ -62,138 +56,155 @@ class UserStoreClass extends EventEmitter { this.setTeams = this.setTeams.bind(this); this.getTeams = this.getTeams.bind(this); this.getCurrentMentionKeys = this.getCurrentMentionKeys.bind(this); - this.getLastVersion = this.getLastVersion.bind(this); - this.setLastVersion = this.setLastVersion.bind(this); this.setStatuses = this.setStatuses.bind(this); this.pSetStatuses = this.pSetStatuses.bind(this); this.setStatus = this.setStatus.bind(this); this.getStatuses = this.getStatuses.bind(this); this.getStatus = this.getStatus.bind(this); - - this.gCurrentId = null; } emitChange(userId) { this.emit(CHANGE_EVENT, userId); } + addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + emitSessionsChange() { this.emit(CHANGE_EVENT_SESSIONS); } + addSessionsChangeListener(callback) { this.on(CHANGE_EVENT_SESSIONS, callback); } + removeSessionsChangeListener(callback) { this.removeListener(CHANGE_EVENT_SESSIONS, callback); } + emitAuditsChange() { this.emit(CHANGE_EVENT_AUDITS); } + addAuditsChangeListener(callback) { this.on(CHANGE_EVENT_AUDITS, callback); } + removeAuditsChangeListener(callback) { this.removeListener(CHANGE_EVENT_AUDITS, callback); } + emitTeamsChange() { this.emit(CHANGE_EVENT_TEAMS); } + addTeamsChangeListener(callback) { this.on(CHANGE_EVENT_TEAMS, callback); } + removeTeamsChangeListener(callback) { this.removeListener(CHANGE_EVENT_TEAMS, callback); } + emitStatusesChange() { this.emit(CHANGE_EVENT_STATUSES); } + addStatusesChangeListener(callback) { this.on(CHANGE_EVENT_STATUSES, callback); } + removeStatusesChangeListener(callback) { this.removeListener(CHANGE_EVENT_STATUSES, callback); } + emitToggleImportModal(value) { this.emit(TOGGLE_IMPORT_MODAL_EVENT, value); } + addImportModalListener(callback) { this.on(TOGGLE_IMPORT_MODAL_EVENT, callback); } + removeImportModalListener(callback) { this.removeListener(TOGGLE_IMPORT_MODAL_EVENT, callback); } - setCurrentId(id) { - this.gCurrentId = id; - if (id == null) { - BrowserStore.removeGlobalItem('current_user_id'); + + getCurrentUser() { + if (this.getProfiles()[global.window.mm_user.id] == null) { + this.saveProfile(global.window.mm_user); + } + + return global.window.mm_user; + } + + setCurrentUser(user) { + var oldUser = global.window.mm_user; + + if (oldUser.id === user.id) { + global.window.mm_user = user; + this.saveProfile(user); } else { - BrowserStore.setGlobalItem('current_user_id', id); + throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id); } } - getCurrentId(skipFetch) { - var currentId = this.gCurrentId; - if (currentId == null) { - currentId = BrowserStore.getGlobalItem('current_user_id'); - this.gCurrentId = currentId; + getCurrentId() { + var user = global.window.mm_user; + + if (user) { + return user.id; } - // this is a special case to force fetch the - // current user if it's missing - // it's synchronous to block rendering - if (currentId == null && !skipFetch) { - var me = client.getMeSynchronous(); - if (me != null) { - this.setCurrentUser(me); - currentId = me.id; + return null; + } + + getLastEmail() { + return BrowserStore.getGlobalItem('last_email', ''); + } + + setLastEmail(email) { + BrowserStore.setGlobalItem('last_email', email); + } + + hasProfile(userId) { + return this.getProfiles()[userId] != null; + } + + getProfile(userId) { + return this.getProfiles()[userId]; + } + + getProfileByUsername(username) { + return this.getProfilesUsernameMap()[username]; + } + + getProfilesUsernameMap() { + var profileUsernameMap = {}; + + var profiles = this.getProfiles(); + for (var key in profiles) { + if (profiles.hasOwnProperty(key)) { + var profile = profiles[key]; + profileUsernameMap[profile.username] = profile; } } - return currentId; + return profileUsernameMap; } - getCurrentUser() { - if (this.getCurrentId() == null) { - return null; - } - return this.pGetProfiles()[this.getCurrentId()]; - } - setCurrentUser(user) { - this.setCurrentId(user.id); - this.saveProfile(user); - } - getLastEmail() { - return BrowserStore.getItem('last_email', ''); - } - setLastEmail(email) { - BrowserStore.setItem('last_email', email); - } - removeCurrentUser() { - this.setCurrentId(null); - } - hasProfile(userId) { - return this.pGetProfiles()[userId] != null; - } - getProfile(userId) { - return this.pGetProfiles()[userId]; - } - getProfileByUsername(username) { - return this.pGetProfilesUsernameMap()[username]; - } - getProfilesUsernameMap() { - return this.pGetProfilesUsernameMap(); - } getProfiles() { - return this.pGetProfiles(); + return BrowserStore.getItem('profiles', {}); } + getActiveOnlyProfiles() { var active = {}; - var current = this.pGetProfiles(); + var current = this.getProfiles(); for (var key in current) { if (current[key].delete_at === 0) { @@ -203,45 +214,37 @@ class UserStoreClass extends EventEmitter { return active; } + saveProfile(profile) { - var ps = this.pGetProfiles(); + var ps = this.getProfiles(); ps[profile.id] = profile; - this.pStoreProfiles(ps); - } - pStoreProfiles(profiles) { - BrowserStore.setItem('profiles', profiles); - var profileUsernameMap = {}; - for (var id in profiles) { - if (profiles.hasOwnProperty(id)) { - profileUsernameMap[profiles[id].username] = profiles[id]; - } - } - BrowserStore.setItem('profileUsernameMap', profileUsernameMap); - } - pGetProfiles() { - return BrowserStore.getItem('profiles', {}); - } - pGetProfilesUsernameMap() { - return BrowserStore.getItem('profileUsernameMap', {}); + BrowserStore.setItem('profiles', ps); } + setSessions(sessions) { BrowserStore.setItem('sessions', sessions); } + getSessions() { return BrowserStore.getItem('sessions', {loading: true}); } + setAudits(audits) { BrowserStore.setItem('audits', audits); } + getAudits() { return BrowserStore.getItem('audits', {loading: true}); } + setTeams(teams) { BrowserStore.setItem('teams', teams); } + getTeams() { return BrowserStore.getItem('teams', []); } + getCurrentMentionKeys() { var user = this.getCurrentUser(); @@ -269,28 +272,27 @@ class UserStoreClass extends EventEmitter { return keys; } - getLastVersion() { - return BrowserStore.getItem('last_version', ''); - } - setLastVersion(version) { - BrowserStore.setItem('last_version', version); - } + setStatuses(statuses) { this.pSetStatuses(statuses); this.emitStatusesChange(); } + pSetStatuses(statuses) { BrowserStore.setItem('statuses', statuses); } + setStatus(userId, status) { var statuses = this.getStatuses(); statuses[userId] = status; this.pSetStatuses(statuses); this.emitStatusesChange(); } + getStatuses() { return BrowserStore.getItem('statuses', {}); } + getStatus(id) { return this.getStatuses()[id]; } @@ -299,7 +301,7 @@ class UserStoreClass extends EventEmitter { var UserStore = new UserStoreClass(); UserStore.setMaxListeners(0); -UserStore.dispatchToken = AppDispatcher.register(function registry(payload) { +UserStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index b22d7237e2..fb76311594 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -3,6 +3,7 @@ var client = require('./client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +var BrowserStore = require('../stores/browser_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); @@ -50,18 +51,18 @@ export function getChannels(force, updateLastViewed, checkVersion) { callTracker.getChannels = utils.getTimestamp(); client.getChannels( - function getChannelsSuccess(data, textStatus, xhr) { + (data, textStatus, xhr) => { callTracker.getChannels = 0; if (checkVersion) { var serverVersion = xhr.getResponseHeader('X-Version-ID'); - if (!UserStore.getLastVersion()) { - UserStore.setLastVersion(serverVersion); + if (!BrowserStore.getLastServerVersion()) { + BrowserStore.setLastServerVersion(serverVersion); } - if (serverVersion !== UserStore.getLastVersion()) { - UserStore.setLastVersion(serverVersion); + if (serverVersion !== BrowserStore.getLastServerVersion()) { + BrowserStore.setLastServerVersion(serverVersion); window.location.href = window.location.href; console.log('Detected version update refreshing the page'); //eslint-disable-line no-console } @@ -77,7 +78,7 @@ export function getChannels(force, updateLastViewed, checkVersion) { members: data.members }); }, - function getChannelsFailure(err) { + (err) => { callTracker.getChannels = 0; dispatchError(err, 'getChannels'); } @@ -566,8 +567,8 @@ export function getMe() { } callTracker.getMe = utils.getTimestamp(); - client.getMeSynchronous( - function getMeSyncSuccess(data, textStatus, xhr) { + client.getMe( + (data, textStatus, xhr) => { callTracker.getMe = 0; if (xhr.status === 304 || !data) { @@ -579,7 +580,7 @@ export function getMe() { me: data }); }, - function getMeSyncFailure(err) { + (err) => { callTracker.getMe = 0; dispatchError(err, 'getMe'); } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index eca4f4b3ed..fb6b45a1f7 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -4,8 +4,8 @@ var BrowserStore = require('../stores/browser_store.jsx'); var TeamStore = require('../stores/team_store.jsx'); var ErrorStore = require('../stores/error_store.jsx'); -export function track(category, action, label, prop, val) { - global.window.analytics.track(action, {category: category, label: label, property: prop, value: val}); +export function track(category, action, label, property, value) { + global.window.analytics.track(action, {category, label, property, value}); } export function trackPage() { @@ -232,6 +232,7 @@ export function logout() { track('api', 'api_users_logout'); var currentTeamUrl = TeamStore.getCurrentTeamUrl(); BrowserStore.clear(); + ErrorStore.storeLastError(null); window.location.href = currentTeamUrl + '/logout'; } @@ -399,10 +400,9 @@ export function getAllTeams(success, error) { }); } -export function getMeSynchronous(success, error) { +export function getMe(success, error) { var currentUser = null; $.ajax({ - async: false, cache: false, url: '/api/v1/users/me', dataType: 'json', @@ -416,7 +416,7 @@ export function getMeSynchronous(success, error) { }, error: function onError(xhr, status, err) { if (error) { - var e = handleError('getMeSynchronous', xhr, status, err); + var e = handleError('getMe', xhr, status, err); error(e); } } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 1d856e067c..7d2626fc13 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -33,7 +33,6 @@ module.exports = { RECIEVED_MSG: null, - CLICK_TEAM: null, RECIEVED_TEAM: null, RECIEVED_CONFIG: null, @@ -140,7 +139,7 @@ module.exports = { sidebarText: '#333333', sidebarUnreadText: '#333333', sidebarTextHoverBg: '#e6f2fa', - sidebarTextActiveBg: '#e1e1e1', + sidebarTextActiveBorder: '#378FD2', sidebarTextActiveColor: '#111111', sidebarHeaderBg: '#2389d7', sidebarHeaderTextColor: '#ffffff', @@ -162,7 +161,7 @@ module.exports = { sidebarText: '#fff', sidebarUnreadText: '#fff', sidebarTextHoverBg: '#136197', - sidebarTextActiveBg: '#136197', + sidebarTextActiveBorder: '#7AB0D6', sidebarTextActiveColor: '#FFFFFF', sidebarHeaderBg: '#2f81b7', sidebarHeaderTextColor: '#FFFFFF', @@ -184,7 +183,7 @@ module.exports = { sidebarText: '#fff', sidebarUnreadText: '#fff', sidebarTextHoverBg: '#4A5664', - sidebarTextActiveBg: '#39769C', + sidebarTextActiveBorder: '#39769C', sidebarTextActiveColor: '#FFFFFF', sidebarHeaderBg: '#1B2C3E', sidebarHeaderTextColor: '#FFFFFF', @@ -206,7 +205,7 @@ module.exports = { sidebarText: '#fff', sidebarUnreadText: '#fff', sidebarTextHoverBg: '#302e30', - sidebarTextActiveBg: '#484748', + sidebarTextActiveBorder: '#196CAF', sidebarTextActiveColor: '#FFFFFF', sidebarHeaderBg: '#1f1f1f', sidebarHeaderTextColor: '#FFFFFF', @@ -249,8 +248,8 @@ module.exports = { uiName: 'Sidebar Text Hover BG' }, { - id: 'sidebarTextActiveBg', - uiName: 'Sidebar Text Active BG' + id: 'sidebarTextActiveBorder', + uiName: 'Sidebar Text Active Border' }, { id: 'sidebarTextActiveColor', diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx index aabddcffdc..bb948b6dc8 100644 --- a/web/react/utils/emoticons.jsx +++ b/web/react/utils/emoticons.jsx @@ -2,27 +2,27 @@ // See License.txt for license information. const emoticonPatterns = { - smile: /(^|\s)(:-?\))($|\s)/g, // :) - wink: /(^|\s)(;-?\))($|\s)/g, // ;) - open_mouth: /(^|\s)(:o)($|\s)/gi, // :o - scream: /(^|\s)(:-o)($|\s)/gi, // :-o - smirk: /(^|\s)(:-?])($|\s)/g, // :] - grinning: /(^|\s)(:-?d)($|\s)/gi, // :D - stuck_out_tongue_closed_eyes: /(^|\s)(x-d)($|\s)/gi, // x-d - stuck_out_tongue: /(^|\s)(:-?p)($|\s)/gi, // :p - rage: /(^|\s)(:-?[\[@])($|\s)/g, // :@ - frowning: /(^|\s)(:-?\()($|\s)/g, // :( - sob: /(^|\s)(:['’]-?\(|:'\(|:'\()($|\s)/g, // :`( - kissing_heart: /(^|\s)(:-?\*)($|\s)/g, // :* - pensive: /(^|\s)(:-?\/)($|\s)/g, // :/ - confounded: /(^|\s)(:-?s)($|\s)/gi, // :s - flushed: /(^|\s)(:-?\|)($|\s)/g, // :| - relaxed: /(^|\s)(:-?\$)($|\s)/g, // :$ - mask: /(^|\s)(:-x)($|\s)/gi, // :-x - heart: /(^|\s)(<3|<3)($|\s)/g, // <3 - broken_heart: /(^|\s)(<\/3|</3)($|\s)/g, // `, - originalText: match + value: `${matchText}`, + originalText: fullMatch }); - return prefix + alias + suffix; + return prefix + alias; } - return match; + return fullMatch; } - output = output.replace(/(^|\s):([a-zA-Z0-9_-]+):($|\s)/g, replaceEmoticonWithToken); + output = output.replace(/(^|\s)(:([a-zA-Z0-9_-]+):)(?=$|\s)/g, (fullMatch, prefix, matchText, name) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name)); $.each(emoticonPatterns, (name, pattern) => { // this might look a bit funny, but since the name isn't contained in the actual match // like with the named emoticons, we need to add it in manually - output = output.replace(pattern, (match, prefix, emoticon, suffix) => replaceEmoticonWithToken(match, prefix, name, suffix)); + output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name)); }); return output; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index b9084b26e2..3d0cee562d 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -118,7 +118,7 @@ export function notifyMe(title, body, channel) { } if (permission === 'granted') { - var notification = new Notification(title, {body: body, tag: body, icon: '/static/images/icon50x50.gif'}); + var notification = new Notification(title, {body: body, tag: body, icon: '/static/images/icon50x50.png'}); notification.onclick = function onClick() { window.focus(); if (channel) { @@ -425,18 +425,14 @@ export function applyTheme(theme) { changeCss('@media(max-width: 768px){.settings-modal .settings-table .nav>li:hover a', 'background:' + theme.sidebarTextHoverBg, 1); } - if (theme.sidebarTextActiveBg) { - 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.sidebarTextActiveBorder) { + changeCss('.sidebar--left .nav li.active a:before, .settings-modal .nav-pills>li.active a:before', 'background:' + theme.sidebarTextActiveBorder, 1); } if (theme.sidebarTextActiveColor) { 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.sidebarTextActiveBg === theme.onlineIndicator) { - changeCss('.sidebar--left .nav-pills__container li.active a .status .online--icon', 'fill:' + theme.sidebarTextActiveColor, 1); - } - if (theme.sidebarHeaderBg) { changeCss('.sidebar--left .team__header, .sidebar--menu .team__header', 'background:' + theme.sidebarHeaderBg, 1); changeCss('.modal .modal-header', 'background:' + theme.sidebarHeaderBg, 1); @@ -873,7 +869,7 @@ export function getFileUrl(filename) { if (url.indexOf('/api/v1/files/get') !== -1) { url = filename.split('/api/v1/files/get')[1]; } - url = getWindowLocationOrigin() + '/api/v1/files/get' + url; + url = getWindowLocationOrigin() + '/api/v1/files/get' + url + '?' + getSessionIndex(); return url; } @@ -884,6 +880,14 @@ export function getFileName(path) { return split[split.length - 1]; } +export function getSessionIndex() { + if (global.window.mm_session_token_index >= 0) { + return 'session_token_index=' + global.window.mm_session_token_index; + } + + return ''; +} + // Generates a RFC-4122 version 4 compliant globally unique identifier. export function generateId() { // implementation taken from http://stackoverflow.com/a/2117523 diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index cb5ff67b59..6b2e799334 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -40,6 +40,7 @@ img { } .popover { + @include border-radius(3px); color: #333; &.bottom, &.right, &.top, &.left { >.arrow:after { @@ -118,6 +119,7 @@ a:focus, a:hover { .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control { cursor: auto; + background: rgba(#fff, 0.1); } .form-group { diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss index 126d239ecd..484e63c7cc 100644 --- a/web/sass-files/sass/partials/_popover.scss +++ b/web/sass-files/sass/partials/_popover.scss @@ -20,3 +20,44 @@ display: block; } +.search-help-popover { + visibility: hidden; + max-width: none; + width: 100%; + top: 36px; + @include single-transition(opacity, 0.3s, ease-in); + font-size: em(13px); + + &.bottom > .arrow { + top: -18px; + border-width: 9px; + left: 30px; + } + + .popover-content { + padding: 3px 13px; + } + + h4 { + font-size: 1em; + } + + ul { + padding-left: 17px; + span { + @include opacity(0.8); + } + strong, b { + @include opacity(1); + } + } + + .tooltip-inner { + max-width: 100%; + } + + &.visible { + visibility: visible; + @include opacity(1); + } +} diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 6ecc0d9659..db58d03477 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -54,6 +54,7 @@ body.ios { height: 2em; margin: 0; position: relative; + z-index: 0; &:before, &:after { content: ""; height: 1em; @@ -116,13 +117,25 @@ body.ios { left: 0; width: 100%; height: 100%; - background-color: rgba(0, 0, 0, 0.6); text-align: center; color: #FFF; font-size: em(20px); font-weight: 600; z-index: 6; + .overlay__indent { + background-color: rgba(0, 0, 0, 0.6); + position: relative; + height: 100%; + @include clearfix; + } + + &.center-file-overlay { + .overlay__indent { + margin-left: 220px; + } + } + &.right-file-overlay { font-size: em(18px); .overlay__circle { diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index c8bb24f3aa..f4e57eec58 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -179,6 +179,22 @@ } @media screen and (max-width: 1140px) { + .inner__wrap { + &.move--left { + .file-overlay { + font-size: em(18px); + .overlay__circle { + width: 300px; + height: 300px; + margin: -150px 0 0 -150px; + } + .overlay__files { + margin: 60px auto 15px; + width: 150px; + } + } + } + } .post { .post__content { width: 100%; @@ -277,6 +293,11 @@ } .file-overlay { font-size: em(18px); + &.center-file-overlay { + .overlay__indent { + margin-left: 0; + } + } .overlay__circle { width: 300px; height: 300px; diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index bc53dc0e47..9af045f98b 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -211,6 +211,22 @@ a { color: #111; background-color: #E1E1E1; + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 5px; + height: 100%; + background: #000; + } + } + a, a:hover, a:focus { + padding-right: 10px; + background-color: rgba(black, 0.1); + border-radius: 0; + font-weight: 400; + position: relative; } } } diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 585a51f080..ab13d1b423 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -128,12 +128,23 @@ } } &.active { + a { + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 5px; + height: 100%; + background: #000; + } + } a, a:hover, a:focus { - color: #111; padding-right: 10px; - background-color: #e1e1e1; + background-color: rgba(black, 0.1); border-radius: 0; font-weight: 400; + position: relative; } } } diff --git a/web/sass-files/sass/partials/_videos.scss b/web/sass-files/sass/partials/_videos.scss index 6ae5b488b6..f6999d15ce 100644 --- a/web/sass-files/sass/partials/_videos.scss +++ b/web/sass-files/sass/partials/_videos.scss @@ -50,3 +50,11 @@ border-bottom:36px solid transparent; border-left:60px solid rgba(255,255,255,0.4); } + +.gif-div { + position:relative; + max-width: 450px; + max-height: 500px; + margin-bottom: 8px; + border-radius:5px +} diff --git a/web/static/config/manifest.json b/web/static/config/manifest.json index 6110122c2f..8f29460b3b 100644 --- a/web/static/config/manifest.json +++ b/web/static/config/manifest.json @@ -2,7 +2,7 @@ "name": "Mattermost", "icons": [ { - "src": "../static/iamges/icon50x50.gif", + "src": "../static/iamges/icon50x50.png", "sizes": "50x50", "type": "image/png" } diff --git a/web/static/images/favicon.ico b/web/static/images/favicon.ico index 0e7d36616c..af55053316 100644 Binary files a/web/static/images/favicon.ico and b/web/static/images/favicon.ico differ diff --git a/web/static/images/icon50x50.gif b/web/static/images/icon50x50.gif deleted file mode 100644 index d79991a0fe..0000000000 Binary files a/web/static/images/icon50x50.gif and /dev/null differ diff --git a/web/static/images/icon50x50.png b/web/static/images/icon50x50.png new file mode 100644 index 0000000000..7ac6ce1c97 Binary files /dev/null and b/web/static/images/icon50x50.png differ diff --git a/web/static/images/logo.png b/web/static/images/logo.png index 36c43b94bc..423d4d2704 100644 Binary files a/web/static/images/logo.png and b/web/static/images/logo.png differ diff --git a/web/static/images/redfavicon.ico b/web/static/images/redfavicon.ico index 7f404d1ef3..eefefc6200 100644 Binary files a/web/static/images/redfavicon.ico and b/web/static/images/redfavicon.ico differ diff --git a/web/templates/authorize.html b/web/templates/authorize.html index b0fa3e4750..4302916765 100644 --- a/web/templates/authorize.html +++ b/web/templates/authorize.html @@ -6,7 +6,7 @@
- +
An application would like to connect to your {{.Props.TeamName}} account.
diff --git a/web/templates/footer.html b/web/templates/footer.html index 296e902cf5..dc1a7c9d0c 100644 --- a/web/templates/footer.html +++ b/web/templates/footer.html @@ -1,7 +1,7 @@ {{define "footer"}}