diff options
99 files changed, 1122 insertions, 589 deletions
diff --git a/api/admin.go b/api/admin.go index cd1e5d2de..7a5616ede 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 4da1de62d..6c7eda0a2 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 70f7eba4b..9c7ac053b 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 - } 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 - } + cm1 := &model.ChannelMember{ + UserId: c.Session.UserId, + Roles: model.CHANNEL_ROLE_ADMIN, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + cm2 := &model.ChannelMember{ + UserId: otherUserId, + Roles: "", + NotifyProps: model.GetDefaultChannelNotifyProps(), + } - return sc, nil + if result := <-Srv.Store.Channel().SaveDirectChannel(channel, cm1, cm2); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.Channel), nil } } diff --git a/api/context.go b/api/context.go index bd9744bf8..9be3e85cc 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 c5bcd4f5a..79f84e04d 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 2d7b05ff6..d39d8ed60 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 41fd6e4c3..df2db8730 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -23,9 +23,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -38,7 +38,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html index 962ae868e..4ff8026f1 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 a9b2a0741..f6bc3bc39 100644 --- a/api/templates/email_change_verify_body.html +++ b/api/templates/email_change_verify_body.html @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -41,7 +41,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html index 5e2ac1452..744aaccfc 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 6b643556e..6944f6c68 100644 --- a/api/templates/error.html +++ b/api/templates/error.html @@ -1,7 +1,7 @@ <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> - <title>{{ .ClientProps.SiteName }} - Error</title> + <title>{{ .ClientCfg.SiteName }} - Error</title> <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css"> <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet"> @@ -22,7 +22,7 @@ <div class="container-fluid"> <div class="error__container"> <div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div> - <h2>{{ .ClientProps.SiteName }} needs your help:</h2> + <h2>{{ .ClientCfg.SiteName }} needs your help:</h2> <p>{{ .Props.Message }}</p> <a href="{{.Props.SiteURL}}">Go back to team site</a> </div> diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html index 41f9dac01..4669d51c1 100644 --- a/api/templates/find_teams_body.html +++ b/api/templates/find_teams_body.html @@ -9,7 +9,7 @@ <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <tr> <td style="padding: 20px 20px 10px; text-align:left;"> - <img src="{{.ClientProps.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt=""> + <img src="{{.ClientCfg.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt=""> </td> </tr> <tr> @@ -31,9 +31,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -42,11 +42,11 @@ <tr> <td style="text-align: center;color: #AAA; font-size: 11px; padding-bottom: 10px;"> <p style="margin: 25px 0;"> - <img width="65" src="{{.ClientProps.SiteURL}}/static/images/circles.png" alt=""> + <img width="65" src="{{.ClientCfg.SiteURL}}/static/images/circles.png" alt=""> </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html index 3c2bef589..f3a1437b3 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 57feef5d9..930bc099d 100644 --- a/api/templates/invite_body.html +++ b/api/templates/invite_body.html @@ -18,7 +18,7 @@ <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2> - <p>{{.Props.TeamDisplayName}} started using {{.ClientProps.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> + <p>{{.Props.TeamDisplayName}} started using {{.ClientCfg.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> <p style="margin: 20px 0 15px"> <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a> </p> @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -41,7 +41,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html index f46bdcfaf..10f68969f 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 542df4b74..47a93fcb9 100644 --- a/api/templates/password_change_body.html +++ b/api/templates/password_change_body.html @@ -23,9 +23,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -38,7 +38,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html index 283fda1af..e7a794090 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 63a53bf3c..182134b1a 100644 --- a/api/templates/post_body.html +++ b/api/templates/post_body.html @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -41,7 +41,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html index 944cd5a42..f53353d85 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 4bafc57e8..5e5f6cafc 100644 --- a/api/templates/reset_body.html +++ b/api/templates/reset_body.html @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -41,7 +41,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html index dc2cb32ec..6f3deb28b 100644 --- a/api/templates/signup_team_body.html +++ b/api/templates/signup_team_body.html @@ -21,14 +21,14 @@ <p style="margin: 20px 0 25px"> <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a> </p> - {{ .ClientProps.SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .ClientProps.SiteName }} when your team is in constant communication--let's get them on board.<br></p> + {{ .ClientCfg.SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .ClientCfg.SiteName }} when your team is in constant communication--let's get them on board.<br></p> </td> </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -41,7 +41,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html index 7bc0cc640..236b288fa 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 0613b5dd5..a93de9a71 100644 --- a/api/templates/verify_body.html +++ b/api/templates/verify_body.html @@ -26,9 +26,9 @@ </tr> <tr> <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientProps.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientProps.FeedbackEmail}}</a>.<br> + Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> Best wishes,<br> - The {{.ClientProps.SiteName}} Team<br> + The {{.ClientCfg.SiteName}} Team<br> </td> </tr> </table> @@ -41,7 +41,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html index 7990df84a..9a3a11282 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 b7cb3704d..485bc6351 100644 --- a/api/templates/welcome_body.html +++ b/api/templates/welcome_body.html @@ -43,7 +43,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientProps.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> + If you no longer wish to receive these emails, click on the following link: <a href="mailto:{{.ClientCfg.FeedbackEmail}}?subject=Unsubscribe&body=Unsubscribe" style="text-decoration: none; color:#2389D7;">Unsubscribe</a> </p> </td> </tr> diff --git a/api/user.go b/api/user.go index 0c7278711..3071e1b26 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 77309e5b2..b54e030c5 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 000000000..6327f1173 --- /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 4e06d2f99..b958a6a96 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 <unknown[IP- Search for `554 5.7.1 error` and `Client host rejected: Access denied`. - +#### Tip 3 +* Attempt to telnet to the email service to make sure the server is reachable. +* You must run the following commands from the same machine or virtual instance where `mattermost/bin/platform` is located. So if you're running Mattermost from docker you need to `docker exec -ti mattermost-dev /bin/bash` +* Telnet to the email server with `telnet mail.example.com 25`. If the command works you should see something like +``` +Trying 24.121.12.143... +Connected to mail.example.com. +220 mail.example.com NO UCE ESMTP +``` +* Then type something like `HELO <your mail server domain>`. 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 3fbdd5fd7..3c2289626 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 9183dcacb..48a560838 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 e2c1d4c55..5fe74a161 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 56e190fee..8bedf0632 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,76 @@ 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 + var result StoreResult + if channel.Type == model.CHANNEL_DIRECT { + result.Err = model.NewAppError("SqlChannelStore.Save", "Use SaveDirectChannel to create a direct channel", "") + } else { + 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()) + } + } + } } - channel.PreSave() - if result.Err = channel.IsValid(); result.Err != nil { - storeChannel <- result - close(storeChannel) - return - } + storeChannel <- result + close(storeChannel) + }() - 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 - } + return storeChannel +} - 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()) +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 { - result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL already exists", "id="+channel.Id+", "+err.Error()) + 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 + } + } } - } else { - result.Err = model.NewAppError("SqlChannelStore.Save", "We couldn't save the channel", "id="+channel.Id+", "+err.Error()) } - } else { - result.Data = channel } storeChannel <- result @@ -149,6 +177,46 @@ func (s SqlChannelStore) Save(channel *model.Channel) 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 b4e0f7593..60d3de56a 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 a2b317afa..5fab38ace 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 27731cee1..1cf686a70 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 15d6b217c..fd9856a67 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 e8a46086a..6962876d4 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 ( <Modal diff --git a/web/react/components/admin_console/admin_sidebar_header.jsx b/web/react/components/admin_console/admin_sidebar_header.jsx index c80811bcd..e66beaf35 100644 --- a/web/react/components/admin_console/admin_sidebar_header.jsx +++ b/web/react/components/admin_console/admin_sidebar_header.jsx @@ -36,7 +36,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( <img className='user__picture' - src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} + src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()} /> ); } diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx index 395e22e6c..f7e92672d 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 { <div className='row member-div'> <img className='post-profile-img pull-left' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} height='36' width='36' /> diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 270631db2..55b4a55c0 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 035899592..8b5fc4162 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 940b01f8d..9c07853b7 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 = <p>Your email has been verified! Click <a href={this.props.teamURL + '?email=' + this.props.userEmail}>here</a> to log in.</p>; } else { - title = global.window.config.SiteName + ': You are almost done'; + title = global.window.mm_config.SiteName + ': You are almost done'; body = <p>Please verify your email address. Check your inbox for an email.</p>; resend = ( <button diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx index 57cccc4e0..4d4e8390c 100644 --- a/web/react/components/file_attachment.jsx +++ b/web/react/components/file_attachment.jsx @@ -36,7 +36,7 @@ export default class FileAttachment extends React.Component { if (type === 'image') { var self = this; // Need this reference since we use the given "this" - $('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) { + $('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) { return function loader() { $(this).remove(); if (name in self.refs) { @@ -147,7 +147,7 @@ export default class FileAttachment extends React.Component { var re3 = new RegExp('\\)', 'g'); var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)'); + $(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')'); } } removeBackgroundImage(name) { diff --git a/web/react/components/file_preview.jsx b/web/react/components/file_preview.jsx index a40ed1dcf..df5deb8bc 100644 --- a/web/react/components/file_preview.jsx +++ b/web/react/components/file_preview.jsx @@ -34,7 +34,7 @@ export default class FilePreview extends React.Component { if (filename.indexOf('/api/v1/files/get') !== -1) { filename = filename.split('/api/v1/files/get')[1]; } - filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename; + filename = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + Utils.getSessionIndex(); if (type === 'image') { previews.push( diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 90290099d..4986f88b6 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -21,7 +21,7 @@ export default class InviteMemberModal extends React.Component { emailErrors: {}, firstNameErrors: {}, lastNameErrors: {}, - emailEnabled: global.window.config.SendEmailNotifications === 'true' + emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' }; } diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index c982d57ca..108735caf 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -16,7 +16,7 @@ export default class Login extends React.Component { } handleSubmit(e) { e.preventDefault(); - let state = {}; + var state = {}; const name = this.props.teamName; if (!name) { @@ -49,8 +49,7 @@ export default class Login extends React.Component { this.setState(state); Client.loginByEmail(name, email, password, - function loggedIn(data) { - UserStore.setCurrentUser(data); + () => { UserStore.setLastEmail(email); const redirect = Utils.getUrlParameter('redirect'); @@ -60,7 +59,7 @@ export default class Login extends React.Component { window.location.href = '/' + name + '/channels/town-square'; } }, - function loginFailed(err) { + (err) => { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email); return; @@ -68,7 +67,7 @@ export default class Login extends React.Component { state.serverError = err.message; this.valid = false; this.setState(state); - }.bind(this) + } ); } render() { @@ -95,7 +94,7 @@ export default class Login extends React.Component { } let loginMessage = []; - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { loginMessage.push( <a className='btn btn-custom-login gitlab' @@ -124,7 +123,7 @@ export default class Login extends React.Component { } let emailSignup; - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { emailSignup = ( <div> <div className={'form-group' + errorClass}> @@ -186,7 +185,7 @@ export default class Login extends React.Component { <div className='signup-team__container'> <h5 className='margin--less'>Sign in to:</h5> <h2 className='signup-team__name'>{teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2> + <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2> <form onSubmit={this.handleSubmit}> {verifiedBox} <div className={'form-group' + errorClass}> diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx index 5c3695ad4..8ed94680e 100644 --- a/web/react/components/member_list_item.jsx +++ b/web/react/components/member_list_item.jsx @@ -105,7 +105,7 @@ export default class MemberListItem extends React.Component { <div className='row member-div'> <img className='post-profile-img pull-left' - src={'/api/v1/users/' + member.id + '/image?time=' + timestamp} + src={'/api/v1/users/' + member.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()} height='36' width='36' /> diff --git a/web/react/components/member_list_team_item.jsx b/web/react/components/member_list_team_item.jsx index 3af1d3800..14db05cdb 100644 --- a/web/react/components/member_list_team_item.jsx +++ b/web/react/components/member_list_team_item.jsx @@ -169,7 +169,7 @@ export default class MemberListTeamItem extends React.Component { <div className='row member-div'> <img className='post-profile-img pull-left' - src={`/api/v1/users/${user.id}/image?time=${timestamp}`} + src={`/api/v1/users/${user.id}/image?time=${timestamp}&${Utils.getSessionIndex()}`} height='36' width='36' /> diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx index aeed724a8..050887c6f 100644 --- a/web/react/components/mention.jsx +++ b/web/react/components/mention.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); export default class Mention extends React.Component { constructor(props) { @@ -25,7 +26,7 @@ export default class Mention extends React.Component { <span> <img className='mention-img' - src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp} + src={'/api/v1/users/' + this.props.id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()} /> </span> ); diff --git a/web/react/components/more_direct_channels.jsx b/web/react/components/more_direct_channels.jsx index da125ae3f..e0aeb3051 100644 --- a/web/react/components/more_direct_channels.jsx +++ b/web/react/components/more_direct_channels.jsx @@ -178,7 +178,7 @@ export default class MoreDirectChannels extends React.Component { className='profile-img pull-left' width='38' height='38' - src={`/api/v1/users/${user.id}/image?time=${user.update_at}`} + src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`} /> <div className='more-name'> {user.username} diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 1cb13bbe5..2b68645e5 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -152,7 +152,7 @@ export default class NavbarDropdown extends React.Component { sysAdminLink = ( <li> <a - href='/admin_console' + href={'/admin_console?' + Utils.getSessionIndex()} > {'System Console'} </a> @@ -178,7 +178,7 @@ export default class NavbarDropdown extends React.Component { }); } - if (global.window.config.EnableTeamCreation === 'true') { + if (global.window.mm_config.EnableTeamCreation === 'true') { teams.push( <li key='newTeam_li'> <a diff --git a/web/react/components/password_reset_form.jsx b/web/react/components/password_reset_form.jsx index 217f1b393..b452c40b7 100644 --- a/web/react/components/password_reset_form.jsx +++ b/web/react/components/password_reset_form.jsx @@ -61,7 +61,7 @@ export default class PasswordResetForm extends React.Component { <div className='signup-team__container'> <h3>Password Reset</h3> <form onSubmit={this.handlePasswordReset}> - <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.config.SiteName + ' account.'}</p> + <p>{'Enter a new password for your ' + this.props.teamDisplayName + ' ' + global.window.mm_config.SiteName + ' account.'}</p> <div className={formClass}> <input type='password' diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index 64d6776b4..bc3144dbc 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -158,8 +158,8 @@ export default class Post extends React.Component { var profilePic = null; if (!this.props.hideProfilePic) { - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; - if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') { + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); + if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; } diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index fb838b736..ae94bd42e 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -119,12 +119,12 @@ export default class PostBody extends React.Component { this.setState({youtubeTitle: metadata.title}); } - if (global.window.config.GoogleDeveloperKey && !this.receivedYoutubeData) { + if (global.window.mm_config.GoogleDeveloperKey && !this.receivedYoutubeData) { $.ajax({ async: true, url: 'https://www.googleapis.com/youtube/v3/videos', type: 'GET', - data: {part: 'snippet', id: youtubeId, key: global.window.config.GoogleDeveloperKey}, + data: {part: 'snippet', id: youtubeId, key: global.window.mm_config.GoogleDeveloperKey}, success: success.bind(this) }); } diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 0ba5ce6b5..45e60c767 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -16,7 +16,7 @@ export default class PostHeader extends React.Component { let botIndicator; if (post.props && post.props.from_webhook) { - if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') { + if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( <UserProfile userId={post.user_id} diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 4402745e1..fd29a303c 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -12,7 +12,7 @@ const UserStore = require('../stores/user_store.jsx'); const SocketStore = require('../stores/socket_store.jsx'); const PreferenceStore = require('../stores/preference_store.jsx'); -const utils = require('../utils/utils.jsx'); +const Utils = require('../utils/utils.jsx'); const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -40,11 +40,13 @@ export default class PostList extends React.Component { this.loadFirstPosts = this.loadFirstPosts.bind(this); this.activate = this.activate.bind(this); this.deactivate = this.deactivate.bind(this); - this.resize = this.resize.bind(this); + this.handleResize = this.handleResize.bind(this); + this.resizePostList = this.resizePostList.bind(this); const state = this.getStateFromStores(props.channelId); state.numToDisplay = Constants.POST_CHUNK_SIZE; state.isFirstLoadComplete = false; + state.windowHeight = Utils.windowHeight(); this.state = state; } @@ -115,12 +117,7 @@ export default class PostList extends React.Component { const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); - $(window).resize(() => { - this.resize(); - if (!this.scrolled) { - this.scrollToBottom(); - } - }); + window.addEventListener('resize', this.handleResize); postHolder.on('scroll', () => { const position = postHolder.scrollTop() + postHolder.height() + 14; @@ -154,7 +151,7 @@ export default class PostList extends React.Component { this.loadFirstPosts(this.props.channelId); } - this.resize(); + this.resizePostList(); this.onChange(); this.scrollToBottom(); } @@ -164,7 +161,9 @@ export default class PostList extends React.Component { SocketStore.removeChangeListener(this.onSocketChange); PreferenceStore.removeChangeListener(this.onTimeChange); $('body').off('click.userpopover'); - $(window).off('resize'); + + window.removeEventListener('resize', this.handleResize); + var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); postHolder.off('scroll'); } @@ -173,6 +172,13 @@ export default class PostList extends React.Component { return; } + if (prevState.windowHeight !== this.state.windowHeight) { + this.resizePostList(); + if (!this.scrolled) { + this.scrollToBottom(); + } + } + $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); @@ -202,7 +208,7 @@ export default class PostList extends React.Component { // it's by the user and not a comment } else if (isNewPost && userId === firstPost.user_id && - !utils.isComment(firstPost)) { + !Utils.isComment(firstPost)) { this.scrollToBottom(true); // the user clicked 'load more messages' @@ -231,10 +237,15 @@ export default class PostList extends React.Component { this.deactivate(); } } - resize() { + handleResize() { + this.setState({ + windowHeight: Utils.windowHeight() + }); + } + resizePostList() { const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); if ($('#create_post').length > 0) { - const height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; + const height = this.state.windowHeight - $('#create_post').height() - $('#error_bar').outerHeight() - 50; postHolder.css('height', height + 'px'); } } @@ -280,7 +291,7 @@ export default class PostList extends React.Component { onChange() { var newState = this.getStateFromStores(this.props.channelId); - if (!utils.areStatesEqual(newState.postList, this.state.postList)) { + if (!Utils.areStatesEqual(newState.postList, this.state.postList)) { this.setState(newState); } } @@ -310,7 +321,7 @@ export default class PostList extends React.Component { } } createDMIntroMessage(channel) { - var teammate = utils.getDirectTeammate(channel.id); + var teammate = Utils.getDirectTeammate(channel.id); if (teammate) { var teammateName = teammate.username; @@ -323,7 +334,7 @@ export default class PostList extends React.Component { <div className='post-profile-img__container channel-intro-img'> <img className='post-profile-img' - src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at} + src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at + '&' + Utils.getSessionIndex()} height='50' width='50' /> @@ -370,13 +381,13 @@ export default class PostList extends React.Component { createDefaultIntroMessage(channel) { return ( <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4> + <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4> <p className='channel-intro__content'> - Welcome to {channel.display_name}! + {'Welcome to ' + channel.display_name + '!'} <br/><br/> - This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know. + {'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'} <br/><br/> - To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”. + {'To create a new channel or join an existing one, go to the Left Sidebar under “Channels” and click “More…”.'} <br/> </p> </div> @@ -385,7 +396,7 @@ export default class PostList extends React.Component { createOffTopicIntroMessage(channel) { return ( <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {channel.display_name}</h4> + <h4 className='channel-intro__title'>{'Beginning of ' + channel.display_name}</h4> <p className='channel-intro__content'> {'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'} <br/> @@ -399,7 +410,7 @@ export default class PostList extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>Set a description + <i className='fa fa-pencil'></i>{'Set a description'} </a> <a className='intro-links' @@ -407,7 +418,7 @@ export default class PostList extends React.Component { data-toggle='modal' data-target='#channel_invite' > - <i className='fa fa-user-plus'></i>Invite others to this channel + <i className='fa fa-user-plus'></i>{'Invite others to this channel'} </a> </div> ); @@ -422,7 +433,7 @@ export default class PostList extends React.Component { var members = ChannelStore.getExtraInfo(channel.id).members; for (var i = 0; i < members.length; i++) { - if (utils.isAdmin(members[i].roles)) { + if (Utils.isAdmin(members[i].roles)) { return members[i].username; } } @@ -443,14 +454,14 @@ export default class PostList extends React.Component { var createMessage; if (creatorName === '') { - createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.'; + createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + Utils.displayDate(channel.create_at) + '.'; } else { - createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{utils.displayDate(channel.create_at)}</strong></span>); + createMessage = (<span>This is the start of the <strong>{uiName}</strong> {uiType}, created by <strong>{creatorName}</strong> on <strong>{Utils.displayDate(channel.create_at)}</strong></span>); } return ( <div className='channel-intro'> - <h4 className='channel-intro__title'>Beginning of {uiName}</h4> + <h4 className='channel-intro__title'>{'Beginning of ' + uiName}</h4> <p className='channel-intro__content'> {createMessage} {memberMessage} @@ -465,7 +476,7 @@ export default class PostList extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - <i className='fa fa-pencil'></i>Set a description + <i className='fa fa-pencil'></i>{'Set a description'} </a> <a className='intro-links' @@ -473,7 +484,7 @@ export default class PostList extends React.Component { data-toggle='modal' data-target='#channel_invite' > - <i className='fa fa-user-plus'></i>Invite others to this {uiType} + <i className='fa fa-user-plus'></i>{'Invite others to this ' + uiType} </a> </div> ); @@ -507,7 +518,7 @@ export default class PostList extends React.Component { if (prevPost) { sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5; - sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); + sameRoot = Utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); // hide the profile pic if: // the previous post was made by the same user as the current post, @@ -516,8 +527,8 @@ export default class PostList extends React.Component { // the current post is not from a webhook // and the previous post is not from a webhook if ((prevPost.user_id === post.user_id) && - !utils.isComment(prevPost) && - !utils.isComment(post) && + !Utils.isComment(prevPost) && + !Utils.isComment(post) && (!post.props || !post.props.from_webhook) && (!prevPost.props || !prevPost.props.from_webhook)) { hideProfilePic = true; @@ -526,7 +537,7 @@ export default class PostList extends React.Component { // check if it's the last comment in a consecutive string of comments on the same post // it is the last comment if it is last post in the channel or the next post has a different root post - var isLastComment = utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); + var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); var postCtl = ( <Post @@ -542,7 +553,7 @@ export default class PostList extends React.Component { /> ); - let currentPostDay = utils.getDateForUnixTicks(post.create_at); + const currentPostDay = Utils.getDateForUnixTicks(post.create_at); if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { postCtls.push( <div @@ -558,9 +569,9 @@ export default class PostList extends React.Component { if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) { renderedLastViewed = true; - // Temporary fix to solve ie10/11 rendering issue + // Temporary fix to solve ie11 rendering issue let newSeparatorId = ''; - if (!utils.isBrowserIE()) { + if (!Utils.isBrowserIE()) { newSeparatorId = 'new_message_' + this.props.channelId; } postCtls.push( @@ -572,7 +583,7 @@ export default class PostList extends React.Component { <hr className='separator__hr' /> - <div className='separator__text'>New Messages</div> + <div className='separator__text'>{'New Messages'}</div> </div> ); } @@ -638,7 +649,7 @@ export default class PostList extends React.Component { order = this.state.postList.order; } - var moreMessages = <p className='beginning-messages-text'>Beginning of Channel</p>; + var moreMessages = <p className='beginning-messages-text'>{'Beginning of Channel'}</p>; if (channel != null) { if (order.length >= this.state.numToDisplay) { moreMessages = ( @@ -648,7 +659,7 @@ export default class PostList extends React.Component { href='#' onClick={this.loadMorePosts} > - Load more messages + {'Load more messages'} </a> ); } else { diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index d3a4cfaeb..cfff04fa2 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -199,7 +199,7 @@ export default class RhsComment extends React.Component { <div className='post-profile-img__container'> <img className='post-profile-img' - src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} + src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + Utils.getSessionIndex()} height='36' width='36' /> diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index a9f1fcd30..deef389e2 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -121,7 +121,7 @@ export default class RhsRootPost extends React.Component { let botIndicator; if (post.props && post.props.from_webhook) { - if (post.props.override_username && global.window.config.EnablePostUsernameOverride === 'true') { + if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( <UserProfile userId={post.user_id} @@ -134,8 +134,8 @@ export default class RhsRootPost extends React.Component { botIndicator = <li className='post-header-col post-header__name bot-indicator'>{'BOT'}</li>; } - let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp; - if (post.props && post.props.from_webhook && global.window.config.EnablePostIconOverride === 'true') { + let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex(); + if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') { if (post.props.override_icon_url) { src = post.props.override_icon_url; } diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index 2e9764bd9..bf12ca160 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -8,6 +8,7 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; +var Tooltip = ReactBootstrap.Tooltip; export default class SearchBar extends React.Component { constructor() { @@ -16,10 +17,14 @@ export default class SearchBar extends React.Component { this.onListenerChange = this.onListenerChange.bind(this); this.handleUserInput = this.handleUserInput.bind(this); + this.handleUserFocus = this.handleUserFocus.bind(this); + this.handleUserBlur = this.handleUserBlur.bind(this); this.performSearch = this.performSearch.bind(this); this.handleSubmit = this.handleSubmit.bind(this); - this.state = this.getSearchTermStateFromStores(); + const state = this.getSearchTermStateFromStores(); + state.focused = false; + this.state = state; } getSearchTermStateFromStores() { var term = PostStore.getSearchTerm() || ''; @@ -78,9 +83,14 @@ export default class SearchBar extends React.Component { handleMouseInput(e) { e.preventDefault(); } + handleUserBlur() { + this.setState({focused: false}); + } handleUserFocus(e) { e.target.select(); $('.search-bar__container').addClass('focused'); + + this.setState({focused: true}); } performSearch(terms, isMentionSearch) { if (terms.length) { @@ -115,6 +125,12 @@ export default class SearchBar extends React.Component { if (this.state.isSearching) { isSearching = <span className={'glyphicon glyphicon-refresh glyphicon-refresh-animate'}></span>; } + + let helpClass = 'search-help-popover'; + if (!this.state.searchTerm && this.state.focused) { + helpClass += ' visible'; + } + return ( <div> <div @@ -142,10 +158,25 @@ export default class SearchBar extends React.Component { placeholder='Search' value={this.state.searchTerm} onFocus={this.handleUserFocus} + onBlur={this.handleUserBlur} onChange={this.handleUserInput} onMouseUp={this.handleMouseInput} /> {isSearching} + <Tooltip + placement='bottom' + className={helpClass} + > + <h4>{'Search Options'}</h4> + <ul> + <li> + <span>{'Use '}</span><b>{'"quotation marks"'}</b><span>{' to search for phrases'}</span> + </li> + <li> + <span>{'Use '}</span><b>{'from:'}</b><span>{' to find posts from specific users and '}</span><b>{'in:'}</b><span>{' to find posts in specific channels'}</span> + </li> + </ul> + </Tooltip> </form> </div> ); diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 75d2e7a45..d212e47a3 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -77,7 +77,7 @@ export default class SearchResultsItem extends React.Component { <div className='post-profile-img__container'> <img className='post-profile-img' - src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp} + src={'/api/v1/users/' + this.props.post.user_id + '/image?time=' + timestamp + '&' + utils.getSessionIndex()} height='36' width='36' /> diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index 2f577fe39..b6bcb13a6 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -79,7 +79,7 @@ export default class SettingPicture extends React.Component { >Save</a> ); } - var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.config.ProfileWidth + 'px in width and ' + global.window.config.ProfileHeight + 'px height.'; + var helpText = 'Upload a profile picture in either JPG or PNG format, at least ' + global.window.mm_config.ProfileWidth + 'px in width and ' + global.window.mm_config.ProfileHeight + 'px height.'; var self = this; return ( diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index d1fe37300..ed2c84057 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -183,8 +183,8 @@ export default class Sidebar extends React.Component { const channel = ChannelStore.getCurrent(); if (channel) { let currentSiteName = ''; - if (global.window.config.SiteName != null) { - currentSiteName = global.window.config.SiteName; + if (global.window.mm_config.SiteName != null) { + currentSiteName = global.window.mm_config.SiteName; } let currentChannelName = channel.display_name; diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index c3709bc0a..de28a8374 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -3,6 +3,7 @@ var NavbarDropdown = require('./navbar_dropdown.jsx'); var UserStore = require('../stores/user_store.jsx'); +const Utils = require('../utils/utils.jsx'); export default class SidebarHeader extends React.Component { constructor(props) { @@ -32,7 +33,7 @@ export default class SidebarHeader extends React.Component { profilePicture = ( <img className='user__picture' - src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at} + src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()} /> ); } @@ -61,7 +62,7 @@ export default class SidebarHeader extends React.Component { } SidebarHeader.defaultProps = { - teamDisplayName: global.window.config.SiteName, + teamDisplayName: global.window.mm_config.SiteName, teamType: '' }; SidebarHeader.propTypes = { diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index ac101d631..fddc98c9d 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -84,7 +84,7 @@ export default class SidebarRightMenu extends React.Component { consoleLink = ( <li> <a - href='/admin_console' + href={'/admin_console?' + utils.getSessionIndex()} > <i className='glyphicon glyphicon-wrench'></i>System Console</a> </li> @@ -92,8 +92,8 @@ export default class SidebarRightMenu extends React.Component { } var siteName = ''; - if (global.window.config.SiteName != null) { - siteName = global.window.config.SiteName; + if (global.window.mm_config.SiteName != null) { + siteName = global.window.mm_config.SiteName; } var teamDisplayName = siteName; if (this.props.teamDisplayName) { diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 48cf2c73c..1858703ef 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -14,19 +14,19 @@ export default class TeamSignUp extends React.Component { var count = 0; - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { count = count + 1; } - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { count = count + 1; } if (count > 1) { this.state = {page: 'choose'}; - } else if (global.window.config.EnableSignUpWithEmail === 'true') { + } else if (global.window.mm_config.EnableSignUpWithEmail === 'true') { this.state = {page: 'email'}; - } else if (global.window.config.EnableSignUpWithGitLab === 'true') { + } else if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { this.state = {page: 'gitlab'}; } } diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index f74c29d27..d70ea5065 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -82,30 +82,29 @@ export default class SignupUserComplete extends React.Component { }); client.createUser(user, this.props.data, this.props.hash, - function createUserSuccess() { + () => { client.track('signup', 'signup_user_02_complete'); client.loginByEmail(this.props.teamName, user.email, user.password, - function emailLoginSuccess(data) { + () => { UserStore.setLastEmail(user.email); - UserStore.setCurrentUser(data); if (this.props.hash > 0) { BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); } window.location.href = '/' + this.props.teamName + '/channels/town-square'; - }.bind(this), - function emailLoginFailure(err) { + }, + (err) => { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.props.teamName); } else { this.setState({serverError: err.message}); } - }.bind(this) + } ); - }.bind(this), - function createUserFailure(err) { + }, + (err) => { this.setState({serverError: err.message}); - }.bind(this) + } ); } render() { @@ -149,7 +148,7 @@ export default class SignupUserComplete extends React.Component { // set up the email entry and hide it if an email was provided var yourEmailIs = ''; if (this.state.user.email) { - yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.config.SiteName}.</span>; + yourEmailIs = <span>Your email address is <strong>{this.state.user.email}</strong>. You'll use this address to sign in to {global.window.mm_config.SiteName}.</span>; } var emailContainerStyle = 'margin--extra'; @@ -177,7 +176,7 @@ export default class SignupUserComplete extends React.Component { ); var signupMessage = []; - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { signupMessage.push( <a className='btn btn-custom-login gitlab' @@ -190,7 +189,7 @@ export default class SignupUserComplete extends React.Component { } var emailSignup; - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { emailSignup = ( <div> <div className='inner__content'> @@ -259,7 +258,7 @@ export default class SignupUserComplete extends React.Component { /> <h5 className='margin--less'>Welcome to:</h5> <h2 className='signup-team__name'>{this.props.teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {global.window.config.SiteName}</h2> + <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2> <h4 className='color--light'>Let's create your account</h4> {signupMessage} {emailSignup} diff --git a/web/react/components/team_signup_choose_auth.jsx b/web/react/components/team_signup_choose_auth.jsx index fa898f63c..0254c8b4e 100644 --- a/web/react/components/team_signup_choose_auth.jsx +++ b/web/react/components/team_signup_choose_auth.jsx @@ -8,7 +8,7 @@ export default class ChooseAuthPage extends React.Component { } render() { var buttons = []; - if (global.window.config.EnableSignUpWithGitLab === 'true') { + if (global.window.mm_config.EnableSignUpWithGitLab === 'true') { buttons.push( <a className='btn btn-custom-login gitlab btn-full' @@ -26,7 +26,7 @@ export default class ChooseAuthPage extends React.Component { ); } - if (global.window.config.EnableSignUpWithEmail === 'true') { + if (global.window.mm_config.EnableSignUpWithEmail === 'true') { buttons.push( <a className='btn btn-custom-login email btn-full' diff --git a/web/react/components/team_signup_password_page.jsx b/web/react/components/team_signup_password_page.jsx index daa898b53..67fd686bc 100644 --- a/web/react/components/team_signup_password_page.jsx +++ b/web/react/components/team_signup_password_page.jsx @@ -36,15 +36,14 @@ export default class TeamSignupPasswordPage extends React.Component { delete teamSignup.wizard; Client.createTeamFromSignup(teamSignup, - function success() { + () => { Client.track('signup', 'signup_team_08_complete'); var props = this.props; Client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, - function loginSuccess(data) { + () => { UserStore.setLastEmail(teamSignup.team.email); - UserStore.setCurrentUser(data); if (this.props.hash > 0) { BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: 'finished'})); } @@ -54,21 +53,21 @@ export default class TeamSignupPasswordPage extends React.Component { props.updateParent(props.state, true); window.location.href = '/' + teamSignup.team.name + '/channels/town-square'; - }.bind(this), - function loginFail(err) { + }, + (err) => { if (err.message === 'Login failed because email address has not been verified') { window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name); } else { this.setState({serverError: err.message}); $('#finish-button').button('reset'); } - }.bind(this) + } ); - }.bind(this), - function error(err) { + }, + (err) => { this.setState({serverError: err.message}); $('#finish-button').button('reset'); - }.bind(this) + } ); } render() { @@ -129,7 +128,7 @@ export default class TeamSignupPasswordPage extends React.Component { Finish </button> </div> - <p>By proceeding to create your account and use {global.window.config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.config.SiteName}.</p> + <p>By proceeding to create your account and use {global.window.mm_config.SiteName}, you agree to our <a href='/static/help/terms.html'>Terms of Service</a> and <a href='/static/help/privacy.html'>Privacy Policy</a>. If you do not agree, you cannot use {global.window.mm_config.SiteName}.</p> <div className='margin--extra'> <a href='#' diff --git a/web/react/components/team_signup_send_invites_page.jsx b/web/react/components/team_signup_send_invites_page.jsx index e7bc0272d..b42343da0 100644 --- a/web/react/components/team_signup_send_invites_page.jsx +++ b/web/react/components/team_signup_send_invites_page.jsx @@ -13,7 +13,7 @@ export default class TeamSignupSendInvitesPage extends React.Component { this.submitSkip = this.submitSkip.bind(this); this.keySubmit = this.keySubmit.bind(this); this.state = { - emailEnabled: global.window.config.SendEmailNotifications === 'true' + emailEnabled: global.window.mm_config.SendEmailNotifications === 'true' }; if (!this.state.emailEnabled) { diff --git a/web/react/components/team_signup_url_page.jsx b/web/react/components/team_signup_url_page.jsx index 75ec2dfd9..6d333aed6 100644 --- a/web/react/components/team_signup_url_page.jsx +++ b/web/react/components/team_signup_url_page.jsx @@ -40,7 +40,7 @@ export default class TeamSignupUrlPage extends React.Component { return; } - if (global.window.config.RestrictTeamNames === 'true') { + if (global.window.mm_config.RestrictTeamNames === 'true') { for (let index = 0; index < Constants.RESERVED_TEAM_NAMES.length; index++) { if (cleanedName.indexOf(Constants.RESERVED_TEAM_NAMES[index]) === 0) { this.setState({nameError: 'URL is taken or contains a reserved word'}); diff --git a/web/react/components/team_signup_username_page.jsx b/web/react/components/team_signup_username_page.jsx index 21e76e2b8..d8d0dbf2c 100644 --- a/web/react/components/team_signup_username_page.jsx +++ b/web/react/components/team_signup_username_page.jsx @@ -15,7 +15,7 @@ export default class TeamSignupUsernamePage extends React.Component { } submitBack(e) { e.preventDefault(); - if (global.window.config.SendEmailNotifications === 'true') { + if (global.window.mm_config.SendEmailNotifications === 'true') { this.props.state.wizard = 'send_invites'; } else { this.props.state.wizard = 'team_url'; diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index 1e9d8df0a..ee325fcaf 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -110,7 +110,7 @@ export default class TeamSignupWelcomePage extends React.Component { src='/static/images/logo.png' /> <h3 className='sub-heading'>Welcome to:</h3> - <h1 className='margin--top-none'>{global.window.config.SiteName}</h1> + <h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1> </p> <p className='margin--less'>Let's set up your new team</p> <p> diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx index 540331663..c4402ae23 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( <img className='user-popover__image' - src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at} + src={'/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '&' + Utils.getSessionIndex()} height='128' width='128' key='user-popover-image' /> ); - if (!global.window.config.ShowEmailAddress === 'true') { + + if (!global.window.mm_config.ShowEmailAddress === 'true') { dataContent.push( <div className='text-nowrap' diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index 9c03f77a6..70e559c30 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 { <SettingPicture title='Profile Picture' submit={this.submitPicture} - src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update} + src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update + '&' + utils.getSessionIndex()} server_error={serverError} client_error={clientError} updateSection={function clearSection(e) { diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx index 231580cc3..1d9ea0ad5 100644 --- a/web/react/components/user_settings/user_settings_integrations.jsx +++ b/web/react/components/user_settings/user_settings_integrations.jsx @@ -34,7 +34,7 @@ export default class UserSettingsIntegrationsTab extends React.Component { let outgoingHooksSection; var inputs = []; - if (global.window.config.EnableIncomingWebhooks === 'true') { + if (global.window.mm_config.EnableIncomingWebhooks === 'true') { if (this.props.activeSection === 'incoming-hooks') { inputs.push( <ManageIncomingHooks /> @@ -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( <ManageOutgoingHooks /> diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index 44cd423b5..5449ae91e 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 8693af494..61d49acb2 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 { </label> <br/> </div> - <div><br/>{'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.'}</div> + <div><br/>{'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.'}</div> </div> ); diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index bea6ce7a5..92d7cd835 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} > - <source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename} /> + <source src={Utils.getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + Utils.getSessionIndex()} /> </video> ); } else { diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx index 5b3ee540c..1287f4fba 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 = ( <div> <a diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 20ed1bf0a..03e049db0 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -35,26 +35,18 @@ var RemovedFromChannelModal = require('../components/removed_from_channel_modal. var FileUploadOverlay = require('../components/file_upload_overlay.jsx'); var RegisterAppModal = require('../components/register_app_modal.jsx'); var ImportThemeModal = require('../components/user_settings/import_theme_modal.jsx'); -var TeamStore = require('../stores/team_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function setupChannelPage(props) { - TeamStore.setCurrentId(props.TeamId); - AppDispatcher.handleViewAction({ type: ActionTypes.CLICK_CHANNEL, name: props.ChannelName, id: props.ChannelId }); - AppDispatcher.handleViewAction({ - type: ActionTypes.CLICK_TEAM, - id: props.TeamId - }); - AsyncClient.getAllPreferences(); // ChannelLoader must be rendered first @@ -237,7 +229,7 @@ function setupChannelPage(props) { document.getElementById('register_app_modal') ); - if (global.window.config.SendEmailNotifications === 'false') { + if (global.window.mm_config.SendEmailNotifications === 'false') { ErrorStore.storeLastError({message: 'Preview Mode: Email notifications have not been configured'}); ErrorStore.emitChange(); } diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx index 5f0fa9d96..a59f2afd0 100644 --- a/web/react/pages/home.jsx +++ b/web/react/pages/home.jsx @@ -2,14 +2,15 @@ // See License.txt for license information. var ChannelStore = require('../stores/channel_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var Constants = require('../utils/constants.jsx'); -function setupHomePage(props) { +function setupHomePage() { var last = ChannelStore.getLastVisitedName(); if (last == null || last.length === 0) { - window.location = props.TeamURL + '/channels/' + Constants.DEFAULT_CHANNEL; + window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; } else { - window.location = props.TeamURL + '/channels/' + last; + window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last; } } diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index c2e7df58e..adaca44ee 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -1,12 +1,12 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore; function getPrefix() { - if (!UserStore) { - UserStore = require('./user_store.jsx'); //eslint-disable-line global-require + if (global.window.mm_user) { + return global.window.mm_user.id + '_'; } - return UserStore.getCurrentId() + '_'; + + return 'unknown_'; } class BrowserStoreClass { @@ -17,35 +17,55 @@ class BrowserStoreClass { this.setGlobalItem = this.setGlobalItem.bind(this); this.getGlobalItem = this.getGlobalItem.bind(this); this.removeGlobalItem = this.removeGlobalItem.bind(this); - this.clear = this.clear.bind(this); this.actionOnItemsWithPrefix = this.actionOnItemsWithPrefix.bind(this); + this.actionOnGlobalItemsWithPrefix = this.actionOnGlobalItemsWithPrefix.bind(this); this.isLocalStorageSupported = this.isLocalStorageSupported.bind(this); + this.getLastServerVersion = this.getLastServerVersion.bind(this); + this.setLastServerVersion = this.setLastServerVersion.bind(this); + this.clear = this.clear.bind(this); + this.clearAll = this.clearAll.bind(this); - var currentVersion = localStorage.getItem('local_storage_version'); - if (currentVersion !== global.window.config.Version) { - this.clear(); - localStorage.setItem('local_storage_version', global.window.config.Version); + var currentVersion = sessionStorage.getItem('storage_version'); + if (currentVersion !== global.window.mm_config.Version) { + sessionStorage.clear(); + sessionStorage.setItem('storage_version', global.window.mm_config.Version); } } getItem(name, defaultValue) { - return this.getGlobalItem(getPrefix() + name, defaultValue); + var result = null; + try { + result = JSON.parse(sessionStorage.getItem(getPrefix() + name)); + } catch (err) { + result = null; + } + + if (result === null && typeof defaultValue !== 'undefined') { + result = defaultValue; + } + + return result; } setItem(name, value) { - this.setGlobalItem(getPrefix() + name, value); + sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); } removeItem(name) { - localStorage.removeItem(getPrefix() + name); + sessionStorage.removeItem(getPrefix() + name); } setGlobalItem(name, value) { try { - localStorage.setItem(name, JSON.stringify(value)); + if (this.isLocalStorageSupported()) { + localStorage.setItem(getPrefix() + name, JSON.stringify(value)); + } else { + sessionStorage.setItem(getPrefix() + name, JSON.stringify(value)); + } } catch (err) { console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console localStorage.clear(); + sessionStorage.clear(); window.location.href = window.location.href; } } @@ -53,7 +73,11 @@ class BrowserStoreClass { getGlobalItem(name, defaultValue) { var result = null; try { - result = JSON.parse(localStorage.getItem(name)); + if (this.isLocalStorageSupported()) { + result = JSON.parse(getPrefix() + localStorage.getItem(name)); + } else { + result = JSON.parse(getPrefix() + sessionStorage.getItem(name)); + } } catch (err) { result = null; } @@ -66,22 +90,46 @@ class BrowserStoreClass { } removeGlobalItem(name) { - localStorage.removeItem(name); + if (this.isLocalStorageSupported()) { + localStorage.removeItem(getPrefix() + name); + } else { + sessionStorage.removeItem(getPrefix() + name); + } } - clear() { - localStorage.clear(); - sessionStorage.clear(); + getLastServerVersion() { + return sessionStorage.getItem('last_server_version'); + } + + setLastServerVersion(version) { + sessionStorage.setItem('last_server_version', version); } /** * Preforms the given action on each item that has the given prefix * Signature for action is action(key, value) */ + actionOnGlobalItemsWithPrefix(prefix, action) { + var globalPrefix = getPrefix(); + var globalPrefixiLen = globalPrefix.length; + + var storage = sessionStorage; + if (this.isLocalStorageSupported()) { + storage = localStorage; + } + + for (var key in storage) { + if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { + var userkey = key.substring(globalPrefixiLen); + action(userkey, this.getGlobalItem(key)); + } + } + } + actionOnItemsWithPrefix(prefix, action) { var globalPrefix = getPrefix(); var globalPrefixiLen = globalPrefix.length; - for (var key in localStorage) { + for (var key in sessionStorage) { if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) { var userkey = key.substring(globalPrefixiLen); action(userkey, this.getGlobalItem(key)); @@ -89,6 +137,15 @@ class BrowserStoreClass { } } + clear() { + sessionStorage.clear(); + } + + clearAll() { + sessionStorage.clear(); + localStorage.clear(); + } + isLocalStorageSupported() { try { sessionStorage.setItem('testSession', '1'); diff --git a/web/react/stores/error_store.jsx b/web/react/stores/error_store.jsx index a4c42dcb7..775b8e006 100644 --- a/web/react/stores/error_store.jsx +++ b/web/react/stores/error_store.jsx @@ -34,9 +34,11 @@ class ErrorStoreClass extends EventEmitter { removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + handledError() { BrowserStore.removeItem('last_error'); } + getLastError() { return BrowserStore.getItem('last_error'); } diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 0ace956d2..4a9314b31 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -324,10 +324,10 @@ class PostStoreClass extends EventEmitter { return 0; }); - BrowserStore.setItem('pending_posts_' + channelId, postList); + BrowserStore.setGlobalItem('pending_posts_' + channelId, postList); } getPendingPosts(channelId) { - return BrowserStore.getItem('pending_posts_' + channelId); + return BrowserStore.getGlobalItem('pending_posts_' + channelId); } storeUnseenDeletedPost(post) { var posts = this.getUnseenDeletedPosts(post.channel_id); @@ -371,7 +371,7 @@ class PostStoreClass extends EventEmitter { this.pStorePendingPosts(channelId, postList); } clearPendingPosts() { - BrowserStore.actionOnItemsWithPrefix('pending_posts_', function clearPending(key) { + BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) { BrowserStore.removeItem(key); }); } @@ -414,26 +414,26 @@ class PostStoreClass extends EventEmitter { } storeCurrentDraft(draft) { var channelId = ChannelStore.getCurrentId(); - BrowserStore.setItem('draft_' + channelId, draft); + BrowserStore.setGlobalItem('draft_' + channelId, draft); } getCurrentDraft() { var channelId = ChannelStore.getCurrentId(); return this.getDraft(channelId); } storeDraft(channelId, draft) { - BrowserStore.setItem('draft_' + channelId, draft); + BrowserStore.setGlobalItem('draft_' + channelId, draft); } getDraft(channelId) { - return BrowserStore.getItem('draft_' + channelId, this.getEmptyDraft()); + return BrowserStore.getGlobalItem('draft_' + channelId, this.getEmptyDraft()); } storeCommentDraft(parentPostId, draft) { - BrowserStore.setItem('comment_draft_' + parentPostId, draft); + BrowserStore.setGlobalItem('comment_draft_' + parentPostId, draft); } getCommentDraft(parentPostId) { - return BrowserStore.getItem('comment_draft_' + parentPostId, this.getEmptyDraft()); + return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft()); } clearDraftUploads() { - BrowserStore.actionOnItemsWithPrefix('draft_', function clearUploads(key, value) { + BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) { if (value) { value.uploadsInProgress = []; BrowserStore.setItem(key, value); @@ -441,7 +441,7 @@ class PostStoreClass extends EventEmitter { }); } clearCommentDraftUploads() { - BrowserStore.actionOnItemsWithPrefix('comment_draft_', function clearUploads(key, value) { + BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) { if (value) { value.uploadsInProgress = []; BrowserStore.setItem(key, value); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 77951f214..33cdc79fb 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -38,6 +38,10 @@ class SocketStoreClass extends EventEmitter { return; } + if (!global.window.mm_session_token_index) { + return; + } + this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -45,7 +49,9 @@ class SocketStoreClass extends EventEmitter { if (window.location.protocol === 'https:') { protocol = 'wss://'; } - var connUrl = protocol + location.host + '/api/v1/websocket'; + + var connUrl = protocol + location.host + '/api/v1/websocket?' + Utils.getSessionIndex(); + if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console } diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 7001acdb1..22114ae85 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -28,29 +28,31 @@ class TeamStoreClass extends EventEmitter { this.get = this.get.bind(this); this.getByName = this.getByName.bind(this); this.getAll = this.getAll.bind(this); - this.setCurrentId = this.setCurrentId.bind(this); this.getCurrentId = this.getCurrentId.bind(this); this.getCurrent = this.getCurrent.bind(this); this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); - this.storeTeam = this.storeTeam.bind(this); - this.pStoreTeams = this.pStoreTeams.bind(this); - this.pGetTeams = this.pGetTeams.bind(this); + this.saveTeam = this.saveTeam.bind(this); } + emitChange() { this.emit(CHANGE_EVENT); } + addChangeListener(callback) { this.on(CHANGE_EVENT, callback); } + removeChangeListener(callback) { this.removeListener(CHANGE_EVENT, callback); } + get(id) { - var c = this.pGetTeams(); + var c = this.getAll(); return c[id]; } + getByName(name) { - var t = this.pGetTeams(); + var t = this.getAll(); for (var id in t) { if (t[id].name === name) { @@ -60,59 +62,51 @@ class TeamStoreClass extends EventEmitter { return null; } + getAll() { - return this.pGetTeams(); - } - setCurrentId(id) { - if (id === null) { - BrowserStore.removeItem('current_team_id'); - } else { - BrowserStore.setItem('current_team_id', id); - } + return BrowserStore.getItem('user_teams', {}); } + getCurrentId() { - return BrowserStore.getItem('current_team_id'); - } - getCurrent() { - var currentId = this.getCurrentId(); + var team = global.window.mm_team; - if (currentId !== null) { - return this.get(currentId); + if (team) { + return team.id; } + return null; } + + getCurrent() { + if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) { + this.saveTeam(global.window.mm_team); + } + + return global.window.mm_team; + } + getCurrentTeamUrl() { if (this.getCurrent()) { return getWindowLocationOrigin() + '/' + this.getCurrent().name; } return null; } - storeTeam(team) { - var teams = this.pGetTeams(); + + saveTeam(team) { + var teams = this.getAll(); teams[team.id] = team; - this.pStoreTeams(teams); - } - pStoreTeams(teams) { BrowserStore.setItem('user_teams', teams); } - pGetTeams() { - return BrowserStore.getItem('user_teams', {}); - } } var TeamStore = new TeamStoreClass(); -TeamStore.dispatchToken = AppDispatcher.register(function registry(payload) { +TeamStore.dispatchToken = AppDispatcher.register((payload) => { 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 fa74f812d..575e6d51e 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'); - } else { - BrowserStore.setGlobalItem('current_user_id', id); + + getCurrentUser() { + if (this.getProfiles()[global.window.mm_user.id] == null) { + this.saveProfile(global.window.mm_user); } + + return global.window.mm_user; } - getCurrentId(skipFetch) { - var currentId = this.gCurrentId; - if (currentId == null) { - currentId = BrowserStore.getGlobalItem('current_user_id'); - this.gCurrentId = currentId; - } + setCurrentUser(user) { + var oldUser = global.window.mm_user; - // 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; - } + if (oldUser.id === user.id) { + global.window.mm_user = user; + this.saveProfile(user); + } else { + throw new Error('Problem with setCurrentUser old_user_id=' + oldUser.id + ' new_user_id=' + user.id); } - - return currentId; } - getCurrentUser() { - if (this.getCurrentId() == null) { - return null; + + getCurrentId() { + var user = global.window.mm_user; + + if (user) { + return user.id; } - return this.pGetProfiles()[this.getCurrentId()]; - } - setCurrentUser(user) { - this.setCurrentId(user.id); - this.saveProfile(user); + return null; } + getLastEmail() { - return BrowserStore.getItem('last_email', ''); + return BrowserStore.getGlobalItem('last_email', ''); } + setLastEmail(email) { - BrowserStore.setItem('last_email', email); - } - removeCurrentUser() { - this.setCurrentId(null); + BrowserStore.setGlobalItem('last_email', email); } + hasProfile(userId) { - return this.pGetProfiles()[userId] != null; + return this.getProfiles()[userId] != null; } + getProfile(userId) { - return this.pGetProfiles()[userId]; + return this.getProfiles()[userId]; } + getProfileByUsername(username) { - return this.pGetProfilesUsernameMap()[username]; + return this.getProfilesUsernameMap()[username]; } + getProfilesUsernameMap() { - return this.pGetProfilesUsernameMap(); + var profileUsernameMap = {}; + + var profiles = this.getProfiles(); + for (var key in profiles) { + if (profiles.hasOwnProperty(key)) { + var profile = profiles[key]; + profileUsernameMap[profile.username] = profile; + } + } + + return profileUsernameMap; } + 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 b22d7237e..fb7631159 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 f92633439..bc73f3c64 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 1d856e067..d891cc48b 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/utils.jsx b/web/react/utils/utils.jsx index b9084b26e..c72c3f32c 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -873,7 +873,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 +884,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 126d239ec..c389ddf7d 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 296e902cf..dc1a7c9d0 100644 --- a/web/templates/footer.html +++ b/web/templates/footer.html @@ -1,7 +1,7 @@ {{define "footer"}} <div class="footer-pane col-xs-12"> <div class="col-xs-12"> - <span class="pull-right footer-site-name">{{ .ClientProps.SiteName }}</span> + <span class="pull-right footer-site-name">{{ .ClientCfg.SiteName }}</span> </div> <div class="col-xs-12"> <span class="pull-right footer-link copyright">© 2015 Mattermost, Inc.</span> diff --git a/web/templates/head.html b/web/templates/head.html index 3466510d4..041831ed7 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -18,10 +18,6 @@ <link rel="manifest" href="/static/config/manifest.json"> <!-- Android add to homescreen --> - <script> - window.config = {{ .ClientProps }}; - </script> - <!-- CSS Should always go first --> <link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css"> <link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css"> @@ -44,6 +40,19 @@ <style id="antiClickjack">body{display:none !important;}</style> <script> + window.mm_config = {{ .ClientCfg }}; + window.mm_team = {{ .Team }}; + window.mm_user = {{ .User }}; + + if ({{.SessionTokenIndex}} >= 0) { + window.mm_session_token_index = {{.SessionTokenIndex}}; + $.ajaxSetup({ + headers: { 'X-MM-TokenIndex': mm_session_token_index } + }); + } + </script> + + <script> window.onerror = function(msg, url, line, column, stack) { var l = {}; l.level = 'ERROR'; @@ -70,9 +79,9 @@ </script> <script type="text/javascript"> - if (window.config.SegmentDeveloperKey != null && window.config.SegmentDeveloperKey !== "") { + if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") { !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1"; - analytics.load(window.config.SegmentDeveloperKey); + analytics.load(window.mm_config.SegmentDeveloperKey); var user = window.UserStore.getCurrentUser(true); if (user) { analytics.identify(user.id, { diff --git a/web/templates/home.html b/web/templates/home.html index 0d8b89061..08876d41d 100644 --- a/web/templates/home.html +++ b/web/templates/home.html @@ -17,7 +17,7 @@ </div> </div> <script> - window.setup_home_page({{ .Props }}); + window.setup_home_page(); </script> </body> </html> diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html index a6000696e..39fd3791b 100644 --- a/web/templates/signup_team.html +++ b/web/templates/signup_team.html @@ -9,7 +9,7 @@ <div class="col-sm-12"> <div class="signup-team__container"> <img class="signup-team-logo" src="/static/images/logo.png" /> - <h1>{{ .ClientProps.SiteName }}</h1> + <h1>{{ .ClientCfg.SiteName }}</h1> <h4 class="color--light">All team communication in one place, searchable and accessible anywhere</h4> <div id="signup-team"></div> </div> diff --git a/web/templates/welcome.html b/web/templates/welcome.html index e7eeb5648..15c072226 100644 --- a/web/templates/welcome.html +++ b/web/templates/welcome.html @@ -11,7 +11,7 @@ <div class="row main"> <div class="app__content"> <div class="welcome-info"> - <h1>Welcome to {{ .ClientProps.SiteName }}!</h1> + <h1>Welcome to {{ .ClientCfg.SiteName }}!</h1> <p> You do not appear to be part of any teams. Please contact your administrator to have him send you an invitation to a private team. diff --git a/web/web.go b/web/web.go index 3bfed371b..5f290ec99 100644 --- a/web/web.go +++ b/web/web.go @@ -15,6 +15,7 @@ import ( "gopkg.in/fsnotify.v1" "html/template" "net/http" + "net/url" "strconv" "strings" ) @@ -31,10 +32,20 @@ func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage { props := make(map[string]string) props["Title"] = title - return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientProps: utils.ClientProperties} + return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientCfg: utils.ClientCfg} } func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { + if me.Team != nil { + me.Team.Sanitize() + } + + if me.User != nil { + me.User.Sanitize(map[string]bool{}) + } + + me.SessionTokenIndex = c.SessionTokenIndex + if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil { c.SetUnknownError(me.TemplateName, err.Error()) } @@ -78,9 +89,9 @@ func InitWeb() { mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") - mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/channels/{channelname}", api.UserRequired(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. + mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. + mainrouter.Handle("/{team}/channels/{channelname}", api.AppHandler(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. + mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. watchAndParseTemplates() } @@ -141,6 +152,20 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool { } +// func getTeamAndUser(c *api.Context) (*model.Team, *model.User) { +// if tr := <-api.Srv.Store.Team().Get(c.Session.TeamId); tr.Err != nil { +// c.Err = tr.Err +// return nil, nil +// } else { +// if ur := <-api.Srv.Store.User().Get(c.Session.UserId); ur.Err != nil { +// c.Err = ur.Err +// return nil, nil +// } else { +// return tr.Data.(*model.Team), ur.Data.(*model.User) +// } +// } +// } + func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if !CheckBrowserCompatability(c, r) { @@ -151,8 +176,29 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("signup_team", "Signup") page.Render(c, w) } else { + teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) + userChan := api.Srv.Store.User().Get(c.Session.UserId) + + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return + } else { + team = tr.Data.(*model.Team) + + } + + var user *model.User + if ur := <-userChan; ur.Err != nil { + c.Err = ur.Err + return + } else { + user = ur.Data.(*model.User) + } + page := NewHtmlTemplatePage("home", "Home") - page.Props["TeamURL"] = c.GetTeamURL() + page.Team = team + page.User = user page.Render(c, w) } } @@ -176,50 +222,19 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { var team *model.Team if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error("Couldn't find team name=%v, teamURL=%v, err=%v", teamName, c.GetTeamURL(), tResult.Err.Message) + l4g.Error("Couldn't find team name=%v, err=%v", teamName, tResult.Err.Message) http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) return } else { team = tResult.Data.(*model.Team) } - // If we are already logged into this team then go to home - if len(c.Session.UserId) != 0 && c.Session.TeamId == team.Id { - page := NewHtmlTemplatePage("home", "Home") - page.Props["TeamURL"] = c.GetTeamURL() - page.Render(c, w) - return - } - // We still might be able to switch to this team because we've logged in before - if multiCookie, err := r.Cookie(model.MULTI_SESSION_TOKEN); err == nil { - multiToken := multiCookie.Value - - if len(multiToken) > 0 { - tokens := strings.Split(multiToken, " ") - - for _, token := range tokens { - if sr := <-api.Srv.Store.Session().Get(token); sr.Err == nil { - s := sr.Data.(*model.Session) - - if !s.IsExpired() && s.TeamId == team.Id { - w.Header().Set(model.HEADER_TOKEN, s.Token) - sessionCookie := &http.Cookie{ - Name: model.SESSION_TOKEN, - Value: s.Token, - Path: "/", - MaxAge: model.SESSION_TIME_WEB_IN_SECS, - HttpOnly: true, - } - - http.SetCookie(w, sessionCookie) - - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusTemporaryRedirect) - return - } - } - } - } + _, session := api.FindMultiSessionForTeamId(r, team.Id) + if session != nil { + w.Header().Set(model.HEADER_TOKEN, session.Token) + http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusTemporaryRedirect) + return } page := NewHtmlTemplatePage("login", "Login") @@ -315,7 +330,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { api.Logout(c, w, r) - http.Redirect(w, r, c.GetTeamURL(), http.StatusFound) + http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect) } func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { @@ -324,7 +339,27 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { teamName := params["team"] var team *model.Team - teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) + if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + // We are logged into a different team. Lets see if we have another + // session in the cookie that will give us access. + if c.Session.TeamId != team.Id { + index, session := api.FindMultiSessionForTeamId(r, team.Id) + if session == nil { + // redirect to login + http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/?redirect="+url.QueryEscape(r.URL.Path), http.StatusTemporaryRedirect) + } else { + c.Session = *session + c.SessionTokenIndex = index + } + } + + userChan := api.Srv.Store.User().Get(c.Session.UserId) var channelId string if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { @@ -334,17 +369,14 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { channelId = result.Data.(string) } - if tResult := <-teamChan; tResult.Err != nil { - c.Err = tResult.Err + var user *model.User + if ur := <-userChan; ur.Err != nil { + c.Err = ur.Err + c.RemoveSessionCookie(w, r) + l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } else { - team = tResult.Data.(*model.Team) - } - - if team.Name != teamName { - l4g.Error("It appears you are logged into " + team.Name + ", but are trying to access " + teamName) - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/town-square", http.StatusFound) - return + user = ur.Data.(*model.User) } if len(channelId) == 0 { @@ -365,15 +397,6 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { channelId = sc.Id } } else { - - // lets make sure the user is valid - if result := <-api.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { - c.Err = result.Err - c.RemoveSessionCookie(w, r) - l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) - return - } - // We will attempt to auto-join open channels if cr := <-api.Srv.Store.Channel().GetByName(c.Session.TeamId, name); cr.Err != nil { http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) @@ -394,7 +417,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("channel", "") - page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"] + page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"] page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamName"] = team.Name page.Props["TeamType"] = team.Type @@ -402,6 +425,8 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { page.Props["ChannelName"] = name page.Props["ChannelId"] = channelId page.Props["UserId"] = c.Session.UserId + page.Team = team + page.User = user page.Render(c, w) } @@ -500,7 +525,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("password_reset", "") - page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"] + page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"] page.Props["TeamDisplayName"] = teamDisplayName page.Props["TeamName"] = teamName page.Props["Hash"] = hash @@ -627,7 +652,10 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) return } - root(c, w, r) + page := NewHtmlTemplatePage("home", "Home") + page.Team = team + page.User = ruser + page.Render(c, w) } } @@ -690,6 +718,11 @@ func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) return } + page := NewHtmlTemplatePage("home", "Home") + page.Team = team + page.User = user + page.Render(c, w) + root(c, w, r) } } @@ -701,12 +734,33 @@ func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { return } + teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) + userChan := api.Srv.Store.User().Get(c.Session.UserId) + + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return + } else { + team = tr.Data.(*model.Team) + + } + + var user *model.User + if ur := <-userChan; ur.Err != nil { + c.Err = ur.Err + return + } else { + user = ur.Data.(*model.User) + } + params := mux.Vars(r) activeTab := params["tab"] teamId := params["team"] page := NewHtmlTemplatePage("admin_console", "Admin Console") - + page.User = user + page.Team = team page.Props["ActiveTab"] = activeTab page.Props["TeamId"] = teamId page.Render(c, w) |