diff --git a/api/admin.go b/api/admin.go index cd1e5d2dee..7a5616edec 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") } @@ -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..9c7ac053b0 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 } } 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/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 9183dcacb1..48a560838e 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 56e190fee3..8bedf0632b 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 b4e0f75930..60d3de56a3 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_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/store.go b/store/store.go index 27731cee1b..1cf686a702 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 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(

{ 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 { @@ -65,7 +65,7 @@ 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( 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 f926334392..bc73f3c64a 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'; } @@ -385,10 +386,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', @@ -402,7 +402,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 e13f8228e3..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, 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 9fc98e3d5f..3d0cee562d 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -869,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; } @@ -880,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/_popover.scss b/web/sass-files/sass/partials/_popover.scss index 126d239ecd..c389ddf7d3 100644 --- a/web/sass-files/sass/partials/_popover.scss +++ b/web/sass-files/sass/partials/_popover.scss @@ -20,3 +20,22 @@ display: block; } +.search-help-popover { + transition: opacity 0.3s; + + h4 { + text-align: left; + } + ul { + padding-left: 2em; + text-align: left; + } + .tooltip-inner { + max-width: 100%; + } +} + +.search-help-popover.visible { + opacity: 100; + transition: opacity 0.3s; +} 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"}}