diff --git a/CHANGELOG.md b/CHANGELOG.md index 8606fc72c0..08b5ddbf07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,130 @@ # Mattermost Changelog +## Release v2.1.0 + +Expected release date: 2016-03-16 + +### Highlights + +- New Android application now available. +- New desktop applications for Windows, Mac and Linux now in beta. +- Brazilian Portuguese translation added. + +### New Features + +Android Application + +- New [Mattermost Android App](https://github.com/mattermost/android) supporting push notifications available for devices running Android 4.4.2+. Requires Mattermost server 2.1 and higher. See [list of tested devices](https://github.com/mattermost/android/blob/master/DEVICES.md). + +Desktop Application + +- New [Desktop Application](https://github.com/mattermost/desktop) for Windows, Mac, and Linux now available as a beta release. + +Languages + +- Added Portuguese language translation (Beta) available from **Account Settings** > **Display**. + +### Improvements + +System Console + +- Removed unused “Disable File Storage” option from the System Console as it is no longer relevant. +- Added a warning message if a system admin demotes themselves. +- System Console statistics now use a client store instead of fetching data and storing it in state. + +Messaging + +- Custom slash commands now support temporary messages that appear only to the user that issued the command. +- Username autocomplete list no longer suggests inactive users. + +Mobile + +- Significant responsiveness and speed improvements using [fastclick](https://github.com/ftlabs/fastclick). +- Team name and username are now shown in the LHS header. +- Added a button to go back to the team URL page from the login page. + +Files and Images + +- Increased the maximum size of image uploads to 24 megapixels. + +User Interface + +- Custom theme color selectors are now organized into categories. +- Add Members and Manage Members dialogs can now be filtered using a search bar. +- Deactivated members no longer appear in the channel members list. +- Keyboard focus is set to the text input box in the RHS if a user clicks the reply icon. +- Permalinks are now displayed in a Copy Permalink dialog instead of a popover. +- Permalink option is now available from the [...] menu on messages and comments in the RHS. +- Reply icon now only appears on-hover for messages that don’t have replies. +- Scroll bar now appears in the center channel. + +#### Bug Fixes + +- System console user management tab now shows username and email on different lines. +- Yellow text box error no longer appears when the system is connected. +- Wildcard search on MySQL databases is now fixed. +- Usernames in the center channel no longer appear as “...” on login. +- Deleted messages now delete in the RHS and center channel without requiring a page refresh. +- Contact us email address in the footer of notification emails now uses the SupportEmail config setting instead of FeedbackEmail. +- Email addresses are now required to have at least one letter before and after the @ sign. +- Firefox desktop notifications are now fixed for some users experiencing missed notifications. +- “User is typing” message containing long usernames no longer causes text wrapping. +- Usernames appearing as “...” in the RHS when performing a search is fixed. +- Links that end in image extensions but do not actually link to raw images no longer generate a blank image preview. +- Channel handle field in the Rename Channel dialog is now visible on themes with dark backgrounds. +- Autolinked images no longer persist after the post containing the link is deleted. +- Code theme selector on IE11 now only shows one dropdown arrow and clicking directly on the arrow opens the dropdown. +- Save/Cancel buttons for language selection in Account Settings are now formatted the same as other settings. +- Inconsistent field spacing in the Channel Info dialog is fixed. +- Recent mentions icon no longer jumps to the left of the search bar when the RHS is opened. +- Custom slash command hints now show up in the autocomplete list. +- GIF links inside code blocks no longer auto-post the GIFs. +- Changing usernames no longer adds the old username to “words that trigger mentions”. +- Notification email footer is now translated based on the sender’s language setting. +- Slash command `/me` now posts as the user instead of a webhook message. +- Logout slash command now forces logout. +- Public links to file attachments on deleted posts no longer work. +- Error message is now shown in IE11 when uploading more than 5 files or a file over 50 MB. + + +### Compatibility +Changes from v2.0 to v2.1: + +**Android** +Mattermost Android Application is for use with Mattermost platform v2.1 and higher. + +#### Known Issues + +- File name tooltip stays open after clicking to download. +- Unable to paste images into the text box on Firefox, Safari, and IE11. +- Archived channels are not removed from the "More" menu for the person that archived the channel until after refresh. +- First load of an empty channel does not display the introduction message. +- Search results don't highlight searches for @username, non-latin characters, or terms inside Markdown code blocks. +- Searching for a username or hashtag containing a dot returns a search where the dot is replaced with the "or" operator. +- Hashtags containing a dash incorrectly highlight in the search results. +- Emoji smileys ending with a letter at the end of a message do not auto-complete as expected. +- Incorrect formatting when a new line is added directly after a list. +- Timestamps are displayed in 12-hour format when set to 24-hour format. +- Syntax highlighting code block is missing the label for Latex documents. +- Posts from webhooks do not fire notifications to the user who created the webhook. +- Theme color vector is not updated after making custom changes to a default theme. +- Search term highlighting doesn't update on IE11 when search terms change but return the same posts. +- Team creation via SSO fails when email domain is restricted. + +#### Contributors + +Many thanks to all our external contributors. In no particular order: + +- [rodrigocorsi2](https://github.com/rodrigocorsi2) +- [enahum](https://github.com/enahum) +- [khoa-le](https://github.com/khoa-le) +- [alanmoo](https://github.com/alanmoo) +- [daizenberg](https://github.com/daizenberg) +- [GuillaumeAmat](https://github.com/GuillaumeAmat) +- [kernicPanel](https://github.com/kernicPanel) +- [timlyo](https://github.com/timlyo) +- [ttyniwa](https://github.com/ttyniwa) + ## Release v2.0.0 Expected Release date: 2016-02-16 diff --git a/Makefile b/Makefile index a7c277e4c0..57a28bf3ae 100644 --- a/Makefile +++ b/Makefile @@ -127,10 +127,9 @@ package: cp -RL web/static/help $(DIST_PATH)/web/static cp -RL web/static/images $(DIST_PATH)/web/static cp -RL web/static/js/jquery-dragster $(DIST_PATH)/web/static/js/ - cp -RL web/templates $(DIST_PATH)/web + cp -RL templates $(DIST_PATH) mkdir -p $(DIST_PATH)/api - cp -RL api/templates $(DIST_PATH)/api cp -RL i18n $(DIST_PATH) cp build/MIT-COMPILED-LICENSE.md $(DIST_PATH) @@ -140,17 +139,17 @@ package: mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js mv $(DIST_PATH)/web/static/js/libs.min.js $(DIST_PATH)/web/static/js/libs-$(BUILD_NUMBER).min.js - sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html - rm $(DIST_PATH)/web/templates/*.bak + sed -i'.bak' 's|react-0.14.3.js|react-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-dom-0.14.3.js|react-dom-0.14.3.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|Intl.js|Intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-intl.js|react-intl.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|jquery-2.1.4.js|jquery-2.1.4.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|react-bootstrap-0.28.1.js|react-bootstrap-0.28.1.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|perfect-scrollbar-0.6.7.jquery.js|perfect-scrollbar-0.6.7.jquery.min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/templates/head.html + rm $(DIST_PATH)/templates/*.bak sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv' diff --git a/api/api.go b/api/api.go index 4fecd3dd4a..20f77e558d 100644 --- a/api/api.go +++ b/api/api.go @@ -4,47 +4,15 @@ package api import ( - "bytes" - l4g "github.com/alecthomas/log4go" + "net/http" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "html/template" - "net/http" _ "github.com/cloudfoundry/jibber_jabber" _ "github.com/nicksnyder/go-i18n/i18n" ) -var ServerTemplates *template.Template - -type ServerTemplatePage Page - -func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage { - return &ServerTemplatePage{ - TemplateName: templateName, - Props: make(map[string]string), - Extra: make(map[string]string), - Html: make(map[string]template.HTML), - ClientCfg: utils.ClientCfg, - Locale: locale, - } -} - -func (me *ServerTemplatePage) Render() string { - var text bytes.Buffer - - T := utils.GetUserTranslations(me.Locale) - me.Props["Footer"] = T("api.templates.email_footer") - me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", - map[string]interface{}{"SupportEmail": me.ClientCfg["SupportEmail"], "SiteName": me.ClientCfg["SiteName"]})) - - if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil { - l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err) - } - - return text.String() -} - func InitApi() { r := Srv.Router.PathPrefix("/api/v1").Subrouter() InitUser(r) @@ -60,12 +28,7 @@ func InitApi() { InitPreference(r) InitLicense(r) - templatesDir := utils.FindDir("api/templates") - l4g.Debug(utils.T("api.api.init.parsing_templates.debug"), templatesDir) - var err error - if ServerTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("api.api.init.parsing_templates.error"), err) - } + utils.InitHTML() } func HandleEtag(etag string, w http.ResponseWriter, r *http.Request) bool { diff --git a/api/context.go b/api/context.go index edcdcbfeff..eed035dafb 100644 --- a/api/context.go +++ b/api/context.go @@ -5,11 +5,9 @@ package api import ( "fmt" - "html/template" "net" "net/http" "net/url" - "strconv" "strings" l4g "github.com/alecthomas/log4go" @@ -31,33 +29,16 @@ var allowedMethods []string = []string{ } type Context struct { - Session model.Session - RequestId string - IpAddress string - Path string - Err *model.AppError - teamURLValid bool - teamURL string - siteURL string - SessionTokenIndex int64 - T goi18n.TranslateFunc - Locale string -} - -type Page struct { - TemplateName string - Props map[string]string - Extra map[string]string - Html map[string]template.HTML - ClientCfg map[string]string - ClientLicense map[string]string - User *model.User - Team *model.Team - Channel *model.Channel - Preferences *model.Preferences - PostID string - SessionTokenIndex int64 - Locale string + Session model.Session + RequestId string + IpAddress string + Path string + Err *model.AppError + teamURLValid bool + teamURL string + siteURL string + T goi18n.TranslateFunc + Locale string } func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { @@ -121,37 +102,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Attempt to parse the token from the cookie if len(token) == 0 { - 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 + if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + token = cookie.Value } } @@ -185,8 +137,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if session == nil || session.IsExpired() { c.RemoveSessionCookie(w, r) - c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) - c.Err.StatusCode = http.StatusUnauthorized + if h.requireUser || h.requireSystemAdmin { + c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } } else if !session.IsOAuth && isTokenFromQueryString { c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token) c.Err.StatusCode = http.StatusUnauthorized @@ -390,22 +344,6 @@ func (c *Context) IsTeamAdmin() bool { } func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { - - // 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_COOKIE_TOKEN, Value: "", @@ -538,23 +476,25 @@ func IsPrivateIpAddress(ipAddress string) bool { } func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { - props := make(map[string]string) - props["Message"] = err.Message - props["Details"] = err.DetailedError + T, locale := utils.GetTranslationsAndLocale(w, r) + page := utils.NewHTMLTemplate("error", locale) + page.Props["Message"] = err.Message + page.Props["Details"] = err.DetailedError pathParts := strings.Split(r.URL.Path, "/") if len(pathParts) > 1 { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host + "/" + pathParts[1] } else { - props["SiteURL"] = GetProtocol(r) + "://" + r.Host + page.Props["SiteURL"] = GetProtocol(r) + "://" + r.Host } - T, _ := utils.GetTranslationsAndLocale(w, r) - props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - props["Link"] = T("api.templates.error.link") + page.Props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + page.Props["Link"] = T("api.templates.error.link") w.WriteHeader(err.StatusCode) - ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg}) + if rErr := page.RenderToWriter(w); rErr != nil { + l4g.Error("Failed to create error page: " + rErr.Error() + ", Original error: " + err.Error()) + } } func Handle404(w http.ResponseWriter, r *http.Request) { @@ -588,29 +528,6 @@ func GetSession(token string) *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.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60)) } diff --git a/api/file.go b/api/file.go index 0011afd5b8..9150e4bfed 100644 --- a/api/file.go +++ b/api/file.go @@ -547,6 +547,41 @@ func writeFile(f []byte, path string) *model.AppError { return nil } +func moveFile(oldPath, newPath string) *model.AppError { + if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { + fileData := make(chan []byte) + getFileAndForget(oldPath, fileData) + fileBytes := <-fileData + + if fileBytes == nil { + return model.NewLocAppError("moveFile", "api.file.move_file.get_from_s3.app_error", nil, "") + } + + var auth aws.Auth + auth.AccessKey = utils.Cfg.FileSettings.AmazonS3AccessKeyId + auth.SecretKey = utils.Cfg.FileSettings.AmazonS3SecretAccessKey + + s := s3.New(auth, awsRegion()) + bucket := s.Bucket(utils.Cfg.FileSettings.AmazonS3Bucket) + + if err := bucket.Del(oldPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error()) + } + + if err := writeFile(fileBytes, newPath); err != nil { + return err + } + } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { + if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil { + return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error()) + } + } else { + return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "") + } + + return nil +} + func writeFileLocally(f []byte, path string) *model.AppError { if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil { return model.NewLocAppError("writeFile", "api.file.write_file_locally.create_dir.app_error", nil, err.Error()) diff --git a/api/license.go b/api/license.go index 23e7946c8e..542b45e266 100644 --- a/api/license.go +++ b/api/license.go @@ -20,6 +20,7 @@ func InitLicense(r *mux.Router) { sr := r.PathPrefix("/license").Subrouter() sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST") sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST") + sr.Handle("/client_config", ApiAppHandler(getClientLicenceConfig)).Methods("GET") } func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { @@ -130,3 +131,22 @@ func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) { rdata["status"] = "ok" w.Write([]byte(model.MapToJson(rdata))) } + +func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) { + config := utils.ClientLicense + + var etag string + if config["IsLicensed"] == "false" { + etag = model.Etag(config["IsLicensed"]) + } else { + etag = model.Etag(config["IsLicensed"], config["IssuedAt"]) + } + + if HandleEtag(etag, w, r) { + return + } + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + + w.Write([]byte(model.MapToJson(config))) +} diff --git a/api/license_test.go b/api/license_test.go new file mode 100644 index 0000000000..b34aeb7a63 --- /dev/null +++ b/api/license_test.go @@ -0,0 +1,22 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api + +import ( + "testing" +) + +func TestGetLicenceConfig(t *testing.T) { + Setup() + + if result, err := Client.GetClientLicenceConfig(); err != nil { + t.Fatal(err) + } else { + cfg := result.Data.(map[string]string) + + if _, ok := cfg["IsLicensed"]; !ok { + t.Fatal(cfg) + } + } +} diff --git a/api/oauth.go b/api/oauth.go index 1ae3dbf78e..9b7f3699d5 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -5,12 +5,15 @@ package api import ( "fmt" + "net/http" + "net/url" + "strconv" + "strings" + l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "net/http" - "net/url" ) func InitOAuth(r *mux.Router) { @@ -20,6 +23,17 @@ func InitOAuth(r *mux.Router) { sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST") sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET") + sr.Handle("/{service:[A-Za-z]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET") + sr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + sr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + + // Also handle this a the old routes remove soon apiv2? + mr := Srv.Router + mr.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET") + mr.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST") + mr.Handle("/{service:[A-Za-z]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET") } func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) { @@ -163,3 +177,328 @@ func GetAuthData(code string) *model.AuthData { return result.Data.(*model.AuthData) } } + +func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + uri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" + + if body, team, props, err := AuthorizeOAuthUser(service, code, state, uri); err != nil { + c.Err = err + return + } else { + action := props["action"] + switch action { + case model.OAUTH_ACTION_SIGNUP: + CreateOAuthUser(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_LOGIN: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_EMAIL_TO_SSO: + CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) + } + break + case model.OAUTH_ACTION_SSO_TO_EMAIL: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) + } + break + default: + LoginByOAuth(c, w, r, service, body, team) + if c.Err == nil { + http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/"+team.Name, http.StatusTemporaryRedirect) + } + break + } + } +} + +func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + responseType := r.URL.Query().Get("response_type") + clientId := r.URL.Query().Get("client_id") + redirect := r.URL.Query().Get("redirect_uri") + scope := r.URL.Query().Get("scope") + state := r.URL.Query().Get("state") + + if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { + c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + } + + var team *model.Team + if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + page := utils.NewHTMLTemplate("authorize", c.Locale) + page.Props["Title"] = c.T("web.authorize_oauth.title") + page.Props["TeamName"] = team.Name + page.Props["AppName"] = app.Name + page.Props["ResponseType"] = responseType + page.Props["ClientId"] = clientId + page.Props["RedirectUri"] = redirect + page.Props["Scope"] = scope + page.Props["State"] = state + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) + } +} + +func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + c.LogAudit("attempt") + + r.ParseForm() + + grantType := r.FormValue("grant_type") + if grantType != model.ACCESS_TOKEN_GRANT_TYPE { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") + return + } + + clientId := r.FormValue("client_id") + if len(clientId) != 26 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") + return + } + + secret := r.FormValue("client_secret") + if len(secret) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") + return + } + + code := r.FormValue("code") + if len(code) == 0 { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") + return + } + + redirectUri := r.FormValue("redirect_uri") + + achan := Srv.Store.OAuth().GetApp(clientId) + tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code) + + authData := GetAuthData(code) + + if authData == nil { + c.LogAudit("fail - invalid auth code") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + uchan := Srv.Store.User().Get(authData.UserId) + + if authData.IsExpired() { + c.LogAudit("fail - auth code expired") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + if authData.RedirectUri != redirectUri { + c.LogAudit("fail - redirect uri provided did not match previous redirect uri") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") + return + } + + if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { + c.LogAudit("fail - auth code is invalid") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") + return + } + + var app *model.OAuthApp + if result := <-achan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !model.ComparePassword(app.ClientSecret, secret) { + c.LogAudit("fail - invalid client credentials") + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") + return + } + + callback := redirectUri + if len(callback) == 0 { + callback = app.CallbackUrls[0] + } + + if result := <-tchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") + return + } else if result.Data != nil { + c.LogAudit("fail - auth code has been used previously") + accessData := result.Data.(*model.AccessData) + + // Revoke access token, related auth code, and session from DB as well as from cache + if err := RevokeAccessToken(accessData.Token); err != nil { + l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) + } + + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") + return + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") + return + } else { + user = result.Data.(*model.User) + } + + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + + if result := <-Srv.Store.Session().Save(session); result.Err != nil { + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") + return + } else { + session = result.Data.(*model.Session) + AddSessionToCache(session) + } + + accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} + + if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") + return + } + + accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + + c.LogAuditWithUserId(user.Id, "success") + + w.Write([]byte(accessRsp.ToJson())) +} + +func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + loginHint := r.URL.Query().Get("login_hint") + teamName := r.URL.Query().Get("team") + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_LOGIN + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} + +func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + teamName := r.URL.Query().Get("team") + + if !utils.Cfg.TeamSettings.EnableUserCreation { + c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if len(teamName) == 0 { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + hash := r.URL.Query().Get("h") + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if IsVerifyHashRequired(nil, team, hash) { + data := r.URL.Query().Get("d") + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") + return + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") + return + } + + if team.Id != props["id"] { + c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) + return + } + } + + stateProps := map[string]string{} + stateProps["action"] = model.OAUTH_ACTION_SIGNUP + + if authUrl, err := GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { + c.Err = err + return + } else { + http.Redirect(w, r, authUrl, http.StatusFound) + } +} diff --git a/api/post.go b/api/post.go index e6560a8e88..d0ec5826ab 100644 --- a/api/post.go +++ b/api/post.go @@ -419,7 +419,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, 0, c.T, c.Locale} + newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, c.T, c.Locale} if text, ok := respProps["text"]; ok { if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil { @@ -604,12 +604,13 @@ func sendNotifications(c *Context, post *model.Post, team *model.Team, channel * year := fmt.Sprintf("%d", tm.Year()) zone, _ := tm.Zone() - subjectPage := NewServerTemplatePage("post_subject", profileMap[id].Locale) + subjectPage := utils.NewHTMLTemplate("post_subject", profileMap[id].Locale) subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, "Month": month[:3], "Day": day, "Year": year}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("post_body", profileMap[id].Locale) + bodyPage := utils.NewHTMLTemplate("post_body", profileMap[id].Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name @@ -1094,6 +1095,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { message.Add("post", post.ToJson()) PublishAndForget(message) + DeletePostFilesAndForget(c.Session.TeamId, post) result := make(map[string]string) result["id"] = postId @@ -1101,6 +1103,23 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { } } +func DeletePostFilesAndForget(teamId string, post *model.Post) { + go func() { + if len(post.Filenames) == 0 { + return + } + + prefix := "teams/" + teamId + "/channels/" + post.ChannelId + "/users/" + post.UserId + "/" + for _, filename := range post.Filenames { + splitUrl := strings.Split(filename, "/") + oldPath := prefix + splitUrl[len(splitUrl)-2] + "/" + splitUrl[len(splitUrl)-1] + newPath := prefix + splitUrl[len(splitUrl)-2] + "/deleted_" + splitUrl[len(splitUrl)-1] + moveFile(oldPath, newPath) + } + + }() +} + func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) { getPostsBeforeOrAfter(c, w, r, true) } diff --git a/api/team.go b/api/team.go index 2f680dc768..2559825226 100644 --- a/api/team.go +++ b/api/team.go @@ -29,13 +29,12 @@ func InitTeam(r *mux.Router) { sr.Handle("/create_with_ldap", ApiAppHandler(createTeamWithLdap)).Methods("POST") sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") - sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET") + sr.Handle("/all", ApiAppHandler(getAll)).Methods("GET") sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") - sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") - sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST") sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") + sr.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST") // These should be moved to the global admain console sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") sr.Handle("/export_team", ApiUserRequired(exportTeam)).Methods("GET") @@ -60,11 +59,11 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signup_team_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signup_team_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signup_team_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title") bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button") @@ -86,7 +85,7 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { } if !utils.Cfg.EmailSettings.RequireEmailVerification { - m["follow_link"] = bodyPage.Props["Link"] + m["follow_link"] = fmt.Sprintf("/signup_team_complete/?d=%s&h=%s", url.QueryEscape(data), url.QueryEscape(hash)) } w.Header().Set("Access-Control-Allow-Origin", " *") @@ -147,7 +146,7 @@ func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { return } - data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service} + data := map[string]string{"follow_link": c.GetSiteURL() + "/api/v1/oauth/" + service + "/signup?team=" + rteam.Name} w.Write([]byte(model.MapToJson(data))) } @@ -391,10 +390,6 @@ func isTeamCreationAllowed(c *Context, email string) bool { } func getAll(c *Context, w http.ResponseWriter, r *http.Request) { - if !c.HasSystemAdminPermissions("getLogs") { - return - } - if result := <-Srv.Store.Team().GetAll(); result.Err != nil { c.Err = result.Err return @@ -403,6 +398,9 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) { m := make(map[string]*model.Team) for _, v := range teams { m[v.Id] = v + if !c.IsSystemAdmin() { + m[v.Id].SanitizeForNotLoggedIn() + } } w.Write([]byte(model.TeamMapToJson(m))) @@ -473,74 +471,6 @@ func FindTeamByName(c *Context, name string, all string) bool { return false } -func findTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - m := make(map[string]*model.Team) - for _, v := range teams { - v.Sanitize() - m[v.Id] = v - } - - w.Write([]byte(model.TeamMapToJson(m))) - } -} - -func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { - - m := model.MapFromJson(r.Body) - - email := strings.ToLower(strings.TrimSpace(m["email"])) - - if email == "" { - c.SetInvalidParam("findTeam", "email") - return - } - - siteURL := c.GetSiteURL() - subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale) - subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject", - map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) - - bodyPage := NewServerTemplatePage("find_teams_body", c.Locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title") - bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found") - bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found") - - if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { - c.Err = result.Err - } else { - teams := result.Data.([]*model.Team) - - // the template expects Props to be a map with team names as the keys and the team url as the value - props := make(map[string]string) - for _, team := range teams { - props[team.Name] = c.GetTeamURLFromTeam(team) - } - bodyPage.Extra = props - - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { - l4g.Error(utils.T("api.team.email_teams.sending.error"), err) - } - - w.Write([]byte(model.MapToJson(m))) - } -} - func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { invites := model.InvitesFromJson(r.Body) if len(invites.Invites) == 0 { @@ -600,11 +530,11 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str senderRole = c.T("api.team.invite_members.member") } - subjectPage := NewServerTemplatePage("invite_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("invite_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.invite_subject", map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("invite_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("invite_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.invite_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info", @@ -813,3 +743,25 @@ func exportTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(result))) } } + +func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { + m := model.MapFromJson(r.Body) + inviteId := m["invite_id"] + + if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil { + c.Err = result.Err + return + } else { + team := result.Data.(*model.Team) + if !(team.Type == model.TEAM_OPEN) { + c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId) + return + } + + result := map[string]string{} + result["display_name"] = team.DisplayName + result["name"] = team.Name + result["id"] = team.Id + w.Write([]byte(model.MapToJson(result))) + } +} diff --git a/api/team_test.go b/api/team_test.go index c942e2e1f6..bbbc8385df 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -108,37 +108,10 @@ func TestCreateTeam(t *testing.T) { } } -func TestFindTeamByEmail(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - if r1, err := Client.FindTeams(user.Email); err != nil { - t.Fatal(err) - } else { - teams := r1.Data.(map[string]*model.Team) - if teams[team.Id].Name != team.Name { - t.Fatal() - } - if teams[team.Id].DisplayName != team.DisplayName { - t.Fatal() - } - } - - if _, err := Client.FindTeams("missing"); err != nil { - t.Fatal(err) - } -} - func TestGetAllTeams(t *testing.T) { Setup() - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowTeamListing: true} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} @@ -147,8 +120,22 @@ func TestGetAllTeams(t *testing.T) { Client.LoginByEmail(team.Name, user.Email, "pwd") - if _, err := Client.GetAllTeams(); err == nil { - t.Fatal("you shouldn't have permissions") + enableIncomingHooks := *utils.Cfg.TeamSettings.EnableTeamListing + defer func() { + *utils.Cfg.TeamSettings.EnableTeamListing = enableIncomingHooks + }() + *utils.Cfg.TeamSettings.EnableTeamListing = true + + if r1, err := Client.GetAllTeams(); err != nil { + t.Fatal(err) + } else { + teams := r1.Data.(map[string]*model.Team) + if teams[team.Id].Name != team.Name { + t.Fatal() + } + if teams[team.Id].Email != "" { + t.Fatal("Non admin users shoudn't get full listings") + } } c := &Context{} @@ -165,6 +152,9 @@ func TestGetAllTeams(t *testing.T) { if teams[team.Id].Name != team.Name { t.Fatal() } + if teams[team.Id].Email != team.Email { + t.Fatal() + } } } @@ -207,75 +197,6 @@ func TestTeamPermDelete(t *testing.T) { Client.ClearOAuthToken() } -/* - -XXXXXX investigate and fix failing test - -func TestFindTeamByDomain(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - - if r1, err := Client.FindTeamByDomain(team.Name, false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain(team.Name, true); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if !val { - t.Fatal("should be a valid domain") - } - } - - if r1, err := Client.FindTeamByDomain("a"+model.NewId()+"a", false); err != nil { - t.Fatal(err) - } else { - val := r1.Data.(bool) - if val { - t.Fatal("shouldn't be a valid domain") - } - } -} - -*/ - -func TestFindTeamByEmailSend(t *testing.T) { - Setup() - - team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} - team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) - - user := &model.User{TeamId: team.Id, Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} - user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) - store.Must(Srv.Store.User().VerifyEmail(user.Id)) - Client.LoginByEmail(team.Name, user.Email, "pwd") - - if _, err := Client.FindTeamsSendEmail(user.Email); err != nil { - t.Fatal(err) - } else { - } - - if _, err := Client.FindTeamsSendEmail("missing"); err != nil { - - // It should actually succeed at sending the email since it doesn't exist - if !strings.Contains(err.DetailedError, "Failed to add to email address") { - t.Fatal(err) - } - } -} - func TestInviteMembers(t *testing.T) { Setup() diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html deleted file mode 100644 index afabc21915..0000000000 --- a/api/templates/email_change_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html deleted file mode 100644 index 4fc4f48467..0000000000 --- a/api/templates/email_change_verify_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/error.html b/api/templates/error.html deleted file mode 100644 index 2f588aead5..0000000000 --- a/api/templates/error.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - <span class='fa fa-chevron-left'></span>Back - Error - - - - - - - - - - - - - - - - - -
-
-
-

{{.Props.Title}}

-

{{ .Props.Message }}

- {{.Props.Link}} -
-
- - - diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html deleted file mode 100644 index 1324091aa0..0000000000 --- a/api/templates/find_teams_body.html +++ /dev/null @@ -1,52 +0,0 @@ -{{define "find_teams_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.Title}}

-

{{ if .Extra }} - {{.Props.Found}}
- {{range $index, $element := .Extra}} - {{ $index }}
- {{ end }} - {{ else }} - {{.Props.NotFound}} - {{ end }} -

-
-
-
-
- -{{end}} - - - diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html deleted file mode 100644 index ebc339562a..0000000000 --- a/api/templates/find_teams_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "find_teams_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html deleted file mode 100644 index 60daaa432a..0000000000 --- a/api/templates/post_subject.html +++ /dev/null @@ -1 +0,0 @@ -{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/user.go b/api/user.go index b7e6220d82..0841c38aa5 100644 --- a/api/user.go +++ b/api/user.go @@ -53,10 +53,13 @@ func InitUser(r *mux.Router) { sr.Handle("/attach_device", ApiUserRequired(attachDeviceId)).Methods("POST") sr.Handle("/switch_to_sso", ApiAppHandler(switchToSSO)).Methods("POST") sr.Handle("/switch_to_email", ApiUserRequired(switchToEmail)).Methods("POST") + sr.Handle("/verify_email", ApiAppHandler(verifyEmail)).Methods("POST") + sr.Handle("/resend_verification", ApiAppHandler(resendVerification)).Methods("POST") sr.Handle("/newimage", ApiUserRequired(uploadProfileImage)).Methods("POST") sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET") + sr.Handle("/me_logged_in", ApiAppHandler(getMeLoggedIn)).Methods("GET") sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST") sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET") sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET") @@ -315,10 +318,10 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { - subjectPage := NewServerTemplatePage("welcome_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("welcome_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) - bodyPage := NewServerTemplatePage("welcome_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info") @@ -328,7 +331,7 @@ func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayN bodyPage.Props["TeamURL"] = teamURL if !verified { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, email) bodyPage.Props["VerifyUrl"] = link } @@ -380,13 +383,13 @@ func addDirectChannelsAndForget(user *model.User) { func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) + link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) - subjectPage := NewServerTemplatePage("verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["Info"] = c.T("api.templates.verify_body.info") @@ -621,31 +624,17 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, w.Header().Set(model.HEADER_TOKEN, session.Token) - tokens := GetMultiSessionCookieTokens(r) - multiToken := "" - 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(multiToken + " " + session.Token) expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0) - - multiSessionCookie := &http.Cookie{ + sessionCookie := &http.Cookie{ Name: model.SESSION_COOKIE_TOKEN, - Value: multiToken, + Value: session.Token, Path: "/", MaxAge: maxAge, Expires: expiresAt, HttpOnly: true, } - http.SetCookie(w, multiSessionCookie) + http.SetCookie(w, sessionCookie) c.Session = *session c.LogAuditWithUserId(user.Id, "success") @@ -902,6 +891,26 @@ func getMe(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getMeLoggedIn(c *Context, w http.ResponseWriter, r *http.Request) { + data := make(map[string]string) + data["logged_in"] = "false" + data["team_name"] = "" + + if len(c.Session.UserId) != 0 { + teamChan := Srv.Store.Team().Get(c.Session.TeamId) + var team *model.Team + if tr := <-teamChan; tr.Err != nil { + c.Err = tr.Err + return + } else { + team = tr.Data.(*model.Team) + } + data["logged_in"] = "true" + data["team_name"] = team.Name + } + w.Write([]byte(model.MapToJson(data))) +} + func getUser(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id := params["id"] @@ -1622,12 +1631,12 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { data := model.MapToJson(newProps) hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) - link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) + link := fmt.Sprintf("%s/reset_password_complete?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) - subjectPage := NewServerTemplatePage("reset_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("reset_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.reset_subject") - bodyPage := NewServerTemplatePage("reset_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() bodyPage.Props["Title"] = c.T("api.templates.reset_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info")) @@ -1743,11 +1752,11 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("password_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("password_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("password_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info", @@ -1763,11 +1772,12 @@ func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamUR func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("email_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info", @@ -1785,11 +1795,12 @@ func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamN link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) - subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("email_change_verify_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + subjectPage.Props["SiteName"] = utils.Cfg.TeamSettings.SiteName - bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title") bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info", @@ -1918,7 +1929,7 @@ func GetAuthorizationCode(c *Context, service, teamName string, props map[string props["team"] = teamName state := b64.StdEncoding.EncodeToString([]byte(model.MapToJson(props))) - redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) + redirectUri := c.GetSiteURL() + "/api/v1/oauth/" + service + "/complete" authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri) + "&state=" + url.QueryEscape(state) @@ -2216,11 +2227,11 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale) + subjectPage := utils.NewHTMLTemplate("signin_change_subject", c.Locale) subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject", map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("signin_change_body", c.Locale) + bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title") bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info", @@ -2232,3 +2243,68 @@ func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, }() } + +func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + userId := props["uid"] + if len(userId) != 26 { + c.SetInvalidParam("verifyEmail", "uid") + return + } + + hashedId := props["hid"] + if len(hashedId) == 0 { + c.SetInvalidParam("verifyEmail", "hid") + return + } + + if model.ComparePassword(hashedId, userId) { + if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { + return + } else { + c.LogAudit("Email Verified") + return + } + } + + c.Err = model.NewLocAppError("verifyEmail", "api.user.verify_email.bad_link.app_error", nil, "") + c.Err.StatusCode = http.StatusForbidden +} + +func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + + teamName := props["team_name"] + if len(teamName) == 0 { + c.SetInvalidParam("resendVerification", "team_name") + return + } + + email := props["email"] + if len(email) == 0 { + c.SetInvalidParam("resendVerification", "email") + return + } + + var team *model.Team + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = result.Err + return + } else { + user := result.Data.(*model.User) + + if user.LastActivityAt > 0 { + SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } else { + SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + } + } +} diff --git a/api/user_test.go b/api/user_test.go index 1a1cf9634a..27f00829fd 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1263,3 +1263,38 @@ func TestSwitchToEmail(t *testing.T) { t.Fatal("should have failed - wrong user") } } + +func TestMeLoggedIn(t *testing.T) { + Setup() + + team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"} + ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(ruser.Id)) + + Client.AuthToken = "invalid" + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "false" { + t.Fatal("Got: " + val) + } + } + + Client.LoginByEmail(team.Name, user.Email, user.Password) + + if result, err := Client.GetMeLoggedIn(); err != nil { + t.Fatal(err) + } else { + meLoggedIn := result.Data.(map[string]string) + + if val, ok := meLoggedIn["logged_in"]; !ok || val != "true" { + t.Fatal("Got: " + val) + } + } +} diff --git a/docker/1.4/Dockerrun.aws.zip b/docker/1.4/Dockerrun.aws.zip deleted file mode 100644 index 55bc98eea2..0000000000 Binary files a/docker/1.4/Dockerrun.aws.zip and /dev/null differ diff --git a/docker/2.0/Dockerfile b/docker/2.0/Dockerfile index 0f7a13e450..38cb1e1978 100644 --- a/docker/2.0/Dockerfile +++ b/docker/2.0/Dockerfile @@ -34,7 +34,7 @@ VOLUME /var/lib/mysql WORKDIR /mattermost # Copy over files -ADD https://github.com/mattermost/platform/releases/download/v2.0.0-rc2/mattermost.tar.gz / +ADD https://github.com/mattermost/platform/releases/download/v2.0.0/mattermost.tar.gz / RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz ADD config_docker.json / ADD docker-entry.sh / diff --git a/docker/1.4/Dockerfile b/docker/2.1/Dockerfile similarity index 84% rename from docker/1.4/Dockerfile rename to docker/2.1/Dockerfile index 8dcea3927a..7ecf641162 100644 --- a/docker/1.4/Dockerfile +++ b/docker/2.1/Dockerfile @@ -34,8 +34,8 @@ VOLUME /var/lib/mysql WORKDIR /mattermost # Copy over files -ADD https://github.com/mattermost/platform/releases/download/v1.4.0/mattermost.tar.gz / -RUN tar -zxvf /mattermost.tar.gz --strip-components=1 && rm /mattermost.tar.gz +ADD https://releases.mattermost.com/2.1.0-rc1/mattermost-team-2.1.0-rc1-linux-amd64.tar.gz / +RUN tar -zxvf /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz --strip-components=1 && rm /mattermost-team-2.1.0-rc1-linux-amd64.tar.gz ADD config_docker.json / ADD docker-entry.sh / diff --git a/docker/2.1/Dockerrun.aws.zip b/docker/2.1/Dockerrun.aws.zip new file mode 100644 index 0000000000..19bbc74032 Binary files /dev/null and b/docker/2.1/Dockerrun.aws.zip differ diff --git a/docker/1.4/Dockerrun.aws/.ebextensions/01_files.config b/docker/2.1/Dockerrun.aws/.ebextensions/01_files.config similarity index 100% rename from docker/1.4/Dockerrun.aws/.ebextensions/01_files.config rename to docker/2.1/Dockerrun.aws/.ebextensions/01_files.config diff --git a/docker/1.4/Dockerrun.aws/Dockerrun.aws.json b/docker/2.1/Dockerrun.aws/Dockerrun.aws.json similarity index 75% rename from docker/1.4/Dockerrun.aws/Dockerrun.aws.json rename to docker/2.1/Dockerrun.aws/Dockerrun.aws.json index 6549615894..a52309aad4 100755 --- a/docker/1.4/Dockerrun.aws/Dockerrun.aws.json +++ b/docker/2.1/Dockerrun.aws/Dockerrun.aws.json @@ -1,7 +1,7 @@ { "AWSEBDockerrunVersion": "1", "Image": { - "Name": "mattermost/platform:1.4", + "Name": "mattermost/platform:2.1", "Update": "true" }, "Ports": [ diff --git a/docker/1.4/README.md b/docker/2.1/README.md similarity index 100% rename from docker/1.4/README.md rename to docker/2.1/README.md diff --git a/docker/1.4/config_docker.json b/docker/2.1/config_docker.json similarity index 94% rename from docker/1.4/config_docker.json rename to docker/2.1/config_docker.json index 1aa2ee843f..6a12901893 100644 --- a/docker/1.4/config_docker.json +++ b/docker/2.1/config_docker.json @@ -7,11 +7,14 @@ "EnableOAuthServiceProvider": false, "EnableIncomingWebhooks": false, "EnableOutgoingWebhooks": false, + "EnableCommands": false, + "EnableOnlyAdminIntegrations": true, "EnablePostUsernameOverride": false, "EnablePostIconOverride": false, "EnableTesting": false, "EnableDeveloper": false, "EnableSecurityFixAlert": true, + "EnableInsecureOutgoingConnections": false, "SessionLengthWebInDays" : 30, "SessionLengthMobileInDays" : 30, "SessionLengthSSOInDays" : 30, @@ -66,6 +69,8 @@ }, "EmailSettings": { "EnableSignUpWithEmail": true, + "EnableSignInWithEmail": true, + "EnableSignInWithUsername": false, "SendEmailNotifications": false, "RequireEmailVerification": false, "FeedbackName": "", diff --git a/docker/1.4/docker-entry.sh b/docker/2.1/docker-entry.sh similarity index 100% rename from docker/1.4/docker-entry.sh rename to docker/2.1/docker-entry.sh diff --git a/i18n/en.json b/i18n/en.json index 57d5179b64..e42ade162c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -456,8 +456,8 @@ "translation": "S3 is not supported for local storage export." }, { - "id": "api.export.write_file.app_error", - "translation": "Unable to write to export file" + "id": "api.file.file_upload.exceeds", + "translation": "File exceeds max image size." }, { "id": "api.file.file_upload.exceeds", @@ -507,6 +507,22 @@ "id": "api.file.init.debug", "translation": "Initializing file api routes" }, + { + "id": "api.file.move_file.configured.app_error", + "translation": "File storage not configured properly. Please configure for either S3 or local server file storage." + }, + { + "id": "api.file.move_file.delete_from_s3.app_error", + "translation": "Unable to delete file from S3." + }, + { + "id": "api.file.move_file.get_from_s3.app_error", + "translation": "Unable to get file from S3." + }, + { + "id": "api.file.move_file.rename.app_error", + "translation": "Unable to move file locally." + }, { "id": "api.file.open_file_write_stream.configured.app_error", "translation": "File storage not configured properly. Please configure for either S3 or local server file storage." @@ -1607,6 +1623,10 @@ "id": "api.user.upload_profile_user.upload_profile.app_error", "translation": "Couldn't upload profile image" }, + { + "id": "api.user.verify_email.bad_link.app_error", + "translation": "Bad verify email link." + }, { "id": "api.web_conn.new_web_conn.last_activity.error", "translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v" @@ -3404,22 +3424,6 @@ "id": "web.find_team.title", "translation": "Find Team" }, - { - "id": "web.footer.about", - "translation": "About" - }, - { - "id": "web.footer.help", - "translation": "Help" - }, - { - "id": "web.footer.privacy", - "translation": "Privacy" - }, - { - "id": "web.footer.terms", - "translation": "Terms" - }, { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: Bad client_id" @@ -3552,10 +3556,6 @@ "id": "web.root.home_title", "translation": "Home" }, - { - "id": "web.root.singup_info", - "translation": "All team communication in one place, searchable and accessible anywhere" - }, { "id": "web.root.singup_title", "translation": "Signup" @@ -3611,5 +3611,9 @@ { "id": "web.watcher_fail.error", "translation": "Failed to add directory to watcher %v" + }, + { + "id": "api.team.get_invite_info.not_open_team", + "translation": "Invite is invalid because this is not an open team." } ] diff --git a/i18n/es.json b/i18n/es.json index 6443d18dc0..93ffb23418 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -456,8 +456,8 @@ "translation": "S3 no está soportado para exportar al almacenamiento local." }, { - "id": "api.export.write_file.app_error", - "translation": "No se puede escribir al archivo a exportar" + "id": "api.file.file_upload.exceeds", + "translation": "El archivo excede el tamaño máximo para una imagen." }, { "id": "api.file.file_upload.exceeds", @@ -507,6 +507,22 @@ "id": "api.file.init.debug", "translation": "Inicializando rutas del API para los archivos" }, + { + "id": "api.file.move_file.configured.app_error", + "translation": "No ha sido configurado apropiadamente el almacenamiento. Por favor configuralo para utilizar ya sea S3 o almacenamiento local." + }, + { + "id": "api.file.move_file.delete_from_s3.app_error", + "translation": "No se pudo eliminar el archivo del S3." + }, + { + "id": "api.file.move_file.get_from_s3.app_error", + "translation": "No se pudo obtener el archivo desde el S3." + }, + { + "id": "api.file.move_file.rename.app_error", + "translation": "No se pudo mover el archivo localmente." + }, { "id": "api.file.open_file_write_stream.configured.app_error", "translation": "El almacenamiento de archivos no ha sido configurado apropiadamente. Por favor configuralo ya sea para S3 o para almacenamiento en el servidor local." @@ -3383,22 +3399,6 @@ "id": "web.find_team.title", "translation": "Encontrar Equipo" }, - { - "id": "web.footer.about", - "translation": "Acerca" - }, - { - "id": "web.footer.help", - "translation": "Ayuda" - }, - { - "id": "web.footer.privacy", - "translation": "Privacidad" - }, - { - "id": "web.footer.terms", - "translation": "Términos" - }, { "id": "web.get_access_token.bad_client_id.app_error", "translation": "invalid_request: client_id malo" @@ -3531,10 +3531,6 @@ "id": "web.root.home_title", "translation": "Inicio" }, - { - "id": "web.root.singup_info", - "translation": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" - }, { "id": "web.root.singup_title", "translation": "Registrar" diff --git a/i18n/pt.json b/i18n/pt.json index e01db6b22f..29c8c58c70 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -456,8 +456,8 @@ "translation": "S3 não é suportado para o armazenamento local de exportação." }, { - "id": "api.export.write_file.app_error", - "translation": "Não é possível gravar para exportar arquivo" + "id": "api.file.file_upload.exceeds", + "translation": "Arquivo excede o máximo de tamanho de imagem." }, { "id": "api.file.file_upload.exceeds", @@ -507,6 +507,22 @@ "id": "api.file.init.debug", "translation": "Inicializando file api routes" }, + { + "id": "api.file.move_file.configured.app_error", + "translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local." + }, + { + "id": "api.file.move_file.delete_from_s3.app_error", + "translation": "Não é possível deletar o arquivo a partir do S3." + }, + { + "id": "api.file.move_file.get_from_s3.app_error", + "translation": "Não é possível obter o arquivo a partir do S3." + }, + { + "id": "api.file.move_file.rename.app_error", + "translation": "Não foi possível mover o arquivo localmente." + }, { "id": "api.file.open_file_write_stream.configured.app_error", "translation": "Armazenamento de arquivos não está configurado corretamente, Por favor configure S3 ou armazenamento de arquivos no servidor local." @@ -1205,7 +1221,7 @@ }, { "id": "api.templates.signin_change_email.body.title", - "translation": "Você atualizou seu método de acesso" + "translation": "Você atualizou seu método de login" }, { "id": "api.templates.signup_team_body.button", @@ -1225,7 +1241,7 @@ }, { "id": "api.templates.singin_change_email.body.info", - "translation": "Você atualizou seu método de inscrição para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.
Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema." + "translation": "Você atualizou seu método de login para {{.TeamDisplayName}} no {{ .TeamURL }} para {{.Method}}.
Se esta mudança não foi iniciada por você, por favor entre em contato com o administrador do sistema." }, { "id": "api.templates.singin_change_email.subject", @@ -1257,7 +1273,7 @@ }, { "id": "api.templates.welcome_body.info2", - "translation": "Você pode acessar sua nova equipe pelo endereço web:" + "translation": "Você pode fazer login sua nova equipe pelo endereço web:" }, { "id": "api.templates.welcome_body.info3", @@ -1333,7 +1349,7 @@ }, { "id": "api.user.create_oauth_user.already_used.app_error", - "translation": "Está conta {{.Service}} já foi utilizada para logar na equipe {{.DisplayName}}" + "translation": "Está conta {{.Service}} já foi utilizada para se inscrever na equipe {{.DisplayName}}" }, { "id": "api.user.create_oauth_user.create.app_error", @@ -1357,7 +1373,7 @@ }, { "id": "api.user.create_user.accepted_domain.app_error", - "translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou entre com um email diferente." + "translation": "O email que você forneceu não pertence a um domínio aceito. Por favor contacte o seu administrador ou se inscreve com um email diferente." }, { "id": "api.user.create_user.joining.error", @@ -1373,11 +1389,11 @@ }, { "id": "api.user.create_user.signup_link_expired.app_error", - "translation": "O link de acesso expirou" + "translation": "O link de inscrição expirou" }, { "id": "api.user.create_user.signup_link_invalid.app_error", - "translation": "O link de acesso não parece ser válido" + "translation": "O link de inscrição não parece ser válido" }, { "id": "api.user.create_user.team_name.app_error", @@ -2407,6 +2423,10 @@ "id": "store.sql.pinging.info", "translation": "Pingando banco de dados sql %v" }, + { + "id": "store.sql.read_replicas_not_licensed.critical", + "translation": "A funcionalidade de mais de uma replica está desabilitada pela licença atual. Entre em contato com o administrador do sistema sobre como atualizar sua licença de empresa." + }, { "id": "store.sql.remove_index.critical", "translation": "Falha ao remover o índice %v" @@ -2647,6 +2667,10 @@ "id": "store.sql_channel.update_member.app_error", "translation": "Encontramos um erro ao atualizar o membro do canal" }, + { + "id": "store.sql_command.analytics_command_count.app_error", + "translation": "Não foi possível contar os comandos" + }, { "id": "store.sql_command.save.delete.app_error", "translation": "Não foi possível deletar o comando" @@ -2675,10 +2699,6 @@ "id": "store.sql_command.save.update.app_error", "translation": "Não foi possível atualizar o comando" }, - { - "id": "store.sql_command.analytics_command_count.app_error", - "translation": "Não foi possível contar os comandos" - }, { "id": "store.sql_license.get.app_error", "translation": "Encontramos um erro ao obter a licença" @@ -2899,6 +2919,10 @@ "id": "store.sql_preference.update.app_error", "translation": "Não foi possível atualizar a preferência" }, + { + "id": "store.sql_session.analytics_session_count.app_error", + "translation": "Não foi possível contar a sessão" + }, { "id": "store.sql_session.cleanup_expired_sessions.app_error", "translation": "Encontramos um erro enquanto deletava a sessão expirada do usuário" @@ -2951,10 +2975,6 @@ "id": "store.sql_session.update_roles.app_error", "translation": "Não foi possível atualizar as funções" }, - { - "id": "store.sql_session.analytics_session_count.app_error", - "translation": "Não foi possível contar a sessão" - }, { "id": "store.sql_system.get.app_error", "translation": "Encontramos um erro ao procurar as propriedades de sistema" @@ -2967,6 +2987,10 @@ "id": "store.sql_system.update.app_error", "translation": "Encontramos um erro ao atualizar as propriedades de sistema" }, + { + "id": "store.sql_team.analytics_team_count.app_error", + "translation": "Não foi possível contar as equipes" + }, { "id": "store.sql_team.get.find.app_error", "translation": "Não foi possível encontrar a equipe existente" @@ -3035,10 +3059,6 @@ "id": "store.sql_team.update_display_name.app_error", "translation": "Não foi possível atualizar o nome da equipe" }, - { - "id": "store.sql_team.analytics_team_count.app_error", - "translation": "Não foi possível contar as equipes" - }, { "id": "store.sql_user.analytics_unique_user_count.app_error", "translation": "Não foi possível obter o número de usuários únicos" @@ -3587,4 +3607,4 @@ "id": "web.watcher_fail.error", "translation": "Falha ao adicionar diretório observador %v" } -] +] \ No newline at end of file diff --git a/model/client.go b/model/client.go index 560e47b76c..3adcb980d7 100644 --- a/model/client.go +++ b/model/client.go @@ -16,19 +16,17 @@ 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" - HEADER_MM_SESSION_TOKEN_INDEX = "X-MM-TokenIndex" - SESSION_TOKEN_INDEX = "session_token_index" - 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" + API_URL_SUFFIX = "/api/v1" ) type Result struct { @@ -179,29 +177,6 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro } } -func (c *Client) FindTeams(email string) (*Result, *AppError) { - m := make(map[string]string) - m["email"] = email - if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil { - return nil, err - } else { - - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil - } -} - -func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) { - m := make(map[string]string) - m["email"] = email - if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil { - return nil, err - } else { - return &Result{r.Header.Get(HEADER_REQUEST_ID), - r.Header.Get(HEADER_ETAG_SERVER), ArrayFromJson(r.Body)}, nil - } -} - func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil { return nil, err @@ -938,7 +913,7 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (* } func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) { - if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil { + if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -1057,3 +1032,21 @@ func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken c.AuthType = HEADER_BEARER } + +func (c *Client) GetClientLicenceConfig() (*Result, *AppError) { + if r, err := c.DoApiGet("/license/client_config", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + +func (c *Client) GetMeLoggedIn() (*Result, *AppError) { + if r, err := c.DoApiGet("/users/me_logged_in", "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} diff --git a/model/session.go b/model/session.go index 5d9424d641..bf0d9531e0 100644 --- a/model/session.go +++ b/model/session.go @@ -9,7 +9,7 @@ import ( ) const ( - SESSION_COOKIE_TOKEN = "MMTOKEN" + SESSION_COOKIE_TOKEN = "MMAUTHTOKEN" SESSION_CACHE_SIZE = 10000 SESSION_PROP_PLATFORM = "platform" SESSION_PROP_OS = "os" diff --git a/model/team.go b/model/team.go index 9e9eaa25f3..bed7bbd8da 100644 --- a/model/team.go +++ b/model/team.go @@ -232,3 +232,10 @@ func (o *Team) Sanitize() { o.Email = "" o.AllowedDomains = "" } + +func (o *Team) SanitizeForNotLoggedIn() { + o.Email = "" + o.AllowedDomains = "" + o.CompanyName = "" + o.InviteId = "" +} diff --git a/model/version.go b/model/version.go index 8fbd65d03a..6011a92459 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maitained in chronological order with most current // release at the front of the list. var versions = []string{ + "2.1.0", "2.0.0", "1.4.0", "1.3.0", diff --git a/web/templates/authorize.html b/templates/authorize.html similarity index 100% rename from web/templates/authorize.html rename to templates/authorize.html diff --git a/api/templates/email_change_body.html b/templates/email_change_body.html similarity index 100% rename from api/templates/email_change_body.html rename to templates/email_change_body.html diff --git a/templates/email_change_subject.html b/templates/email_change_subject.html new file mode 100644 index 0000000000..540bc6eabb --- /dev/null +++ b/templates/email_change_subject.html @@ -0,0 +1 @@ +{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_change_verify_body.html b/templates/email_change_verify_body.html similarity index 100% rename from api/templates/email_change_verify_body.html rename to templates/email_change_verify_body.html diff --git a/templates/email_change_verify_subject.html b/templates/email_change_verify_subject.html new file mode 100644 index 0000000000..04da7593c4 --- /dev/null +++ b/templates/email_change_verify_subject.html @@ -0,0 +1 @@ +{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_footer.html b/templates/email_footer.html similarity index 100% rename from api/templates/email_footer.html rename to templates/email_footer.html diff --git a/api/templates/email_info.html b/templates/email_info.html similarity index 100% rename from api/templates/email_info.html rename to templates/email_info.html diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000000..b86039ca38 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,24 @@ +{{define "error"}} + + +{{template "head" . }} + +
+
+
+ +
+

{{.Props.Title}}

+

{{ .Props.Message }}

+ {{.Props.Link}} +
+
+ + + +{{end}} diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 0000000000..a7eacc85ff --- /dev/null +++ b/templates/head.html @@ -0,0 +1,92 @@ +{{define "head"}} + + + + + + {{ .Props.Title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/api/templates/invite_body.html b/templates/invite_body.html similarity index 100% rename from api/templates/invite_body.html rename to templates/invite_body.html diff --git a/api/templates/invite_subject.html b/templates/invite_subject.html similarity index 100% rename from api/templates/invite_subject.html rename to templates/invite_subject.html diff --git a/api/templates/password_change_body.html b/templates/password_change_body.html similarity index 100% rename from api/templates/password_change_body.html rename to templates/password_change_body.html diff --git a/api/templates/password_change_subject.html b/templates/password_change_subject.html similarity index 100% rename from api/templates/password_change_subject.html rename to templates/password_change_subject.html diff --git a/api/templates/post_body.html b/templates/post_body.html similarity index 100% rename from api/templates/post_body.html rename to templates/post_body.html diff --git a/templates/post_subject.html b/templates/post_subject.html new file mode 100644 index 0000000000..9789d4142c --- /dev/null +++ b/templates/post_subject.html @@ -0,0 +1 @@ +{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/reset_body.html b/templates/reset_body.html similarity index 100% rename from api/templates/reset_body.html rename to templates/reset_body.html diff --git a/api/templates/reset_subject.html b/templates/reset_subject.html similarity index 100% rename from api/templates/reset_subject.html rename to templates/reset_subject.html diff --git a/templates/root.html b/templates/root.html new file mode 100644 index 0000000000..560c7a4b08 --- /dev/null +++ b/templates/root.html @@ -0,0 +1,12 @@ +{{define "root"}} + + +{{template "head" . }} + +
+ + + +{{end}} diff --git a/api/templates/signin_change_body.html b/templates/signin_change_body.html similarity index 100% rename from api/templates/signin_change_body.html rename to templates/signin_change_body.html diff --git a/api/templates/signin_change_subject.html b/templates/signin_change_subject.html similarity index 100% rename from api/templates/signin_change_subject.html rename to templates/signin_change_subject.html diff --git a/api/templates/signup_team_body.html b/templates/signup_team_body.html similarity index 100% rename from api/templates/signup_team_body.html rename to templates/signup_team_body.html diff --git a/api/templates/signup_team_subject.html b/templates/signup_team_subject.html similarity index 100% rename from api/templates/signup_team_subject.html rename to templates/signup_team_subject.html diff --git a/api/templates/verify_body.html b/templates/verify_body.html similarity index 100% rename from api/templates/verify_body.html rename to templates/verify_body.html diff --git a/api/templates/verify_subject.html b/templates/verify_subject.html similarity index 100% rename from api/templates/verify_subject.html rename to templates/verify_subject.html diff --git a/api/templates/welcome_body.html b/templates/welcome_body.html similarity index 100% rename from api/templates/welcome_body.html rename to templates/welcome_body.html diff --git a/api/templates/welcome_subject.html b/templates/welcome_subject.html similarity index 100% rename from api/templates/welcome_subject.html rename to templates/welcome_subject.html diff --git a/tests/test-search.md b/tests/test-search.md index 0f0ba11534..828828c252 100644 --- a/tests/test-search.md +++ b/tests/test-search.md @@ -40,4 +40,5 @@ Click on the linked hashtags below, and confirm that the search results match th #### Markdown surrounding a hashtag: -*#markdown-hashtag* +*#markdown* **#markdown** ~~#markdown~~ +##### #markdown diff --git a/utils/html.go b/utils/html.go new file mode 100644 index 0000000000..4203160d5d --- /dev/null +++ b/utils/html.go @@ -0,0 +1,97 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "bytes" + "html/template" + "net/http" + + l4g "github.com/alecthomas/log4go" + "gopkg.in/fsnotify.v1" +) + +// Global storage for templates +var htmlTemplates *template.Template + +type HTMLTemplate struct { + TemplateName string + Props map[string]string + Html map[string]template.HTML + Locale string +} + +func InitHTML() { + templatesDir := FindDir("templates") + l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir) + var err error + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("api.api.init.parsing_templates.error"), err) + } + + // Watch the templates folder for changes. + watcher, err := fsnotify.NewWatcher() + if err != nil { + l4g.Error(T("web.create_dir.error"), err) + } + + go func() { + for { + select { + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + l4g.Info(T("web.reparse_templates.info"), event.Name) + if htmlTemplates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { + l4g.Error(T("web.parsing_templates.error"), err) + } + } + case err := <-watcher.Errors: + l4g.Error(T("web.dir_fail.error"), err) + } + } + }() + + err = watcher.Add(templatesDir) + if err != nil { + l4g.Error(T("web.watcher_fail.error"), err) + } +} + +func NewHTMLTemplate(templateName string, locale string) *HTMLTemplate { + return &HTMLTemplate{ + TemplateName: templateName, + Props: make(map[string]string), + Html: make(map[string]template.HTML), + Locale: locale, + } +} + +func (t *HTMLTemplate) addDefaultProps() { + T := GetUserTranslations(t.Locale) + t.Props["Footer"] = T("api.templates.email_footer") + t.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", + map[string]interface{}{"SupportEmail": Cfg.SupportSettings.SupportEmail, "SiteName": Cfg.TeamSettings.SiteName})) +} + +func (t *HTMLTemplate) Render() string { + t.addDefaultProps() + + var text bytes.Buffer + + if err := htmlTemplates.ExecuteTemplate(&text, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + } + + return text.String() +} + +func (t *HTMLTemplate) RenderToWriter(w http.ResponseWriter) error { + t.addDefaultProps() + + if err := htmlTemplates.ExecuteTemplate(w, t.TemplateName, t); err != nil { + l4g.Error(T("api.api.render.error"), t.TemplateName, err) + return err + } + return nil +} diff --git a/utils/license.go b/utils/license.go index b773a163ec..b1f15ad928 100644 --- a/utils/license.go +++ b/utils/license.go @@ -20,17 +20,16 @@ import ( var IsLicensed bool = false var License *model.License = &model.License{} -var ClientLicense map[string]string = make(map[string]string) +var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"} -// test public key var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3/k3Al9q1Xe+xngQ/yGn -0suaJopea3Cpf6NjIHdO8sYTwLlxqt0Mdb+qBR9LbCjZfcNmqc5mZONvsyCEoN/5 -VoLdlv1m9ao2BSAWphUxE2CPdUWdLOsDbQWliSc5//UhiYeR+67Xxon0Hg0LKXF6 -PumRIWQenRHJWqlUQZ147e7/1v9ySVRZksKpvlmMDzgq+kCH/uyM1uVP3z7YXhlN -K7vSSQYbt4cghvWQxDZFwpLlsChoY+mmzClgq+Yv6FLhj4/lk94twdOZau/AeZFJ -NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR -1wIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r +tSyzyxrXkJjsFUf0Ke7bm/TLtIggRdqOcUF3XEWqQk5RGD5vuq7Rlg1zZqMEBk8N +EZeRhkxyaZW8pLjxwuBUOnXfJew31+gsTNdKZzRjrvPumKr3EtkleuoxNdoatu4E +HrKmR/4Yi71EqAvkhk7ZjQFuF0osSWJMEEGGCSUYQnTEqUzcZSh1BhVpkIkeu8Kk +1wCtptODixvEujgqVe+SrE3UlZjBmPjC/CL+3cYmufpSNgcEJm2mwsdaXp2OPpfn +a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt +hwIDAQAB -----END PUBLIC KEY-----`) func LoadLicense(licenseBytes []byte) { diff --git a/web/react/dispatcher/event_helpers.jsx b/web/react/action_creators/global_actions.jsx similarity index 89% rename from web/react/dispatcher/event_helpers.jsx rename to web/react/action_creators/global_actions.jsx index 367347d4ba..4375d6c878 100644 --- a/web/react/dispatcher/event_helpers.jsx +++ b/web/react/action_creators/global_actions.jsx @@ -220,3 +220,33 @@ export function sendEphemeralPost(message, channelId) { emitPostRecievedEvent(post); } + +export function loadTeamRequiredPage() { + AsyncClient.getAllTeams(); +} + +export function newLocalizationSelected(locale) { + Client.getTranslations( + locale, + (data) => { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECEIVED_LOCALE, + locale, + translations: data + }); + }, + (err) => { + AsyncClient.dispatchError(err, 'getTranslations'); + } + ); +} + +export function viewLoggedIn() { + AsyncClient.getChannels(); + AsyncClient.getChannelExtraInfo(); + AsyncClient.getMyTeam(); + AsyncClient.getMe(); + + // Clear pending posts (shouldn't have pending posts if we are loading) + PostStore.clearPendingPosts(); +} diff --git a/web/react/components/about_build_modal.jsx b/web/react/components/about_build_modal.jsx index fe48bb48ef..34b1fdccfb 100644 --- a/web/react/components/about_build_modal.jsx +++ b/web/react/components/about_build_modal.jsx @@ -21,29 +21,38 @@ export default class AboutBuildModal extends React.Component { let title = ( ); + let licensee; - if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') { + if (config.BuildEnterpriseReady === 'true') { title = ( ); - licensee = ( -
-
- + if (license.IsLicensed === 'true') { + title = ( + + ); + licensee = ( +
+
+ +
+
{license.Company}
-
{license.Company}
-
- ); + ); + } } return ( diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 95b4caa127..db366f8ed1 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -8,7 +8,7 @@ const Modal = ReactBootstrap.Modal; import LoadingScreen from './loading_screen.jsx'; import * as Utils from '../utils/utils.jsx'; -import {FormattedMessage} from 'mm-intl'; +import {FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; export default class ActivityLogModal extends React.Component { constructor(props) { @@ -144,8 +144,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.firstTime' defaultMessage='First time active: {date}, {time}' values={{ - date: firstAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }} />
@@ -206,8 +219,21 @@ export default class ActivityLogModal extends React.Component { id='activity_log.lastActivity' defaultMessage='Last activity: {date}, {time}' values={{ - date: lastAccessTime.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: lastAccessTime.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }} />
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 32ed70a995..4c4f21f08b 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -6,7 +6,6 @@ import AdminStore from '../../stores/admin_store.jsx'; import TeamStore from '../../stores/team_store.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import LoadingScreen from '../loading_screen.jsx'; -import * as Utils from '../../utils/utils.jsx'; import EmailSettingsTab from './email_settings.jsx'; import LogSettingsTab from './log_settings.jsx'; @@ -50,11 +49,6 @@ export default class AdminController extends React.Component { selected: props.tab || 'system_analytics', selectedTeam: props.teamId || null }; - - if (!props.tab) { - var tokenIndex = Utils.getUrlParameter('session_token_index'); - history.replaceState(null, null, `/admin_console/${this.state.selected}?session_token_index=${tokenIndex}`); - } } componentDidMount() { @@ -63,6 +57,9 @@ export default class AdminController extends React.Component { AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange); AsyncClient.getAllTeams(); + + $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="popover"]').popover(); } componentWillUnmount() { @@ -175,7 +172,7 @@ export default class AdminController extends React.Component { } return ( -
+
@@ -134,17 +120,10 @@ class PasswordResetForm extends React.Component { } PasswordResetForm.defaultProps = { - teamName: '', - teamDisplayName: '', - hash: '', - data: '' }; PasswordResetForm.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string, - hash: React.PropTypes.string, - data: React.PropTypes.string + params: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetForm); \ No newline at end of file +export default PasswordResetForm; diff --git a/web/react/components/password_reset_send_link.jsx b/web/react/components/password_reset_send_link.jsx index 8cc8a050db..ce6253e16f 100644 --- a/web/react/components/password_reset_send_link.jsx +++ b/web/react/components/password_reset_send_link.jsx @@ -4,26 +4,7 @@ import * as Utils from '../utils/utils.jsx'; import * as client from '../utils/client.jsx'; -import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl'; - -const holders = defineMessages({ - error: { - id: 'password_send.error', - defaultMessage: 'Please enter a valid email address.' - }, - link: { - id: 'password_send.link', - defaultMessage: '

A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

' - }, - checkInbox: { - id: 'password_send.checkInbox', - defaultMessage: 'Please check your inbox.' - }, - email: { - id: 'password_send.email', - defaultMessage: 'Email' - } -}); +import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl'; class PasswordResetSendLink extends React.Component { constructor(props) { @@ -31,48 +12,64 @@ class PasswordResetSendLink extends React.Component { this.handleSendLink = this.handleSendLink.bind(this); - this.state = {}; + this.state = { + error: '', + updateText: '' + }; } handleSendLink(e) { e.preventDefault(); - var state = {}; - const {formatMessage} = this.props.intl; var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase(); if (!email || !Utils.isEmail(email)) { - state.error = formatMessage(holders.error); - this.setState(state); + this.setState({ + error: ( + + ) + }); return; } - state.error = null; - this.setState(state); + // End of error checking clear error + this.setState({ + error: '' + }); var data = {}; data.email = email; - data.name = this.props.teamName; - + data.name = this.props.params.team; client.sendPasswordReset(data, - function passwordResetSent() { - this.setState({error: null, updateText: formatMessage(holders.link, {email: email, teamDisplayName: this.props.teamDisplayName, hostname: window.location.hostname}), moreUpdateText: formatMessage(holders.checkInbox)}); - $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); - }.bind(this), - function passwordResetFailedToSend(err) { - this.setState({error: err.message, update_text: null, moreUpdateText: null}); - }.bind(this) - ); + () => { + this.setState({ + error: null, + updateText: ( +
+ + +
+ ) + }); + $(ReactDOM.findDOMNode(this.refs.reset_form)).hide(); + }, + (err) => { + this.setState({ + error: err.message, + update_text: null + }); + } + ); } render() { - var updateText = null; - if (this.state.updateText) { - updateText = ( -
-
- ); - } - var error = null; if (this.state.error) { error =
; @@ -83,51 +80,60 @@ class PasswordResetSendLink extends React.Component { formClass += ' has-error'; } - const {formatMessage} = this.props.intl; return ( -
- +
+
+

-

-
- -
- {error} - - +

+ +

+
+ +
+ {error} + + +

); @@ -135,13 +141,9 @@ class PasswordResetSendLink extends React.Component { } PasswordResetSendLink.defaultProps = { - teamName: '', - teamDisplayName: '' }; PasswordResetSendLink.propTypes = { - intl: intlShape.isRequired, - teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + params: React.PropTypes.object.isRequired }; -export default injectIntl(PasswordResetSendLink); \ No newline at end of file +export default PasswordResetSendLink; diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index afff78bae4..1943fb4097 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -118,7 +118,7 @@ export default class PopoverListMembers extends React.Component { className='profile-img rounded pull-left' width='26px' height='26px' - src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`} + src={`/api/v1/users/${m.id}/image?time=${m.update_at}`} />
@@ -247,5 +247,6 @@ Post.propTypes = { isLastComment: React.PropTypes.bool, shouldHighlight: React.PropTypes.bool, displayNameType: React.PropTypes.string, - hasProfiles: React.PropTypes.bool + hasProfiles: React.PropTypes.bool, + currentUser: React.PropTypes.object.isRequired }; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 70cf86748a..2fa4cebfe1 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -80,12 +80,10 @@ class PostBody extends React.Component { username = parentPost.props.override_username; } - if (global.window.mm_locale === 'en') { - if (username.slice(-1) === 's') { - apostrophe = '\''; - } else { - apostrophe = '\'s'; - } + if (username.slice(-1) === 's') { + apostrophe = '\''; + } else { + apostrophe = '\'s'; } name = ( + let toggle; + if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) { + toggle = ( - + /> + ); + } + + return ( +
+ {toggle}
- ); + ); } + return null; } } diff --git a/web/react/components/post_focus_view.jsx b/web/react/components/post_focus_view.jsx index 44a0bae092..fd654f502e 100644 --- a/web/react/components/post_focus_view.jsx +++ b/web/react/components/post_focus_view.jsx @@ -5,7 +5,8 @@ import PostsView from './posts_view.jsx'; import PostStore from '../stores/post_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import UserStore from '../stores/user_store.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {FormattedMessage} from 'mm-intl'; @@ -15,6 +16,7 @@ export default class PostFocusView extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this); @@ -26,18 +28,21 @@ export default class PostFocusView extends React.Component { scrollPostId: focusedPostId, postList: PostStore.getVisiblePosts(focusedPostId), atTop: PostStore.getVisibilityAtTop(focusedPostId), - atBottom: PostStore.getVisibilityAtBottom(focusedPostId) + atBottom: PostStore.getVisibilityAtBottom(focusedPostId), + currentUser: UserStore.getCurrentUser() }; } componentDidMount() { ChannelStore.addChangeListener(this.onChannelChange); PostStore.addChangeListener(this.onPostsChange); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); PostStore.removeChangeListener(this.onPostsChange); + UserStore.removeChangeListener(this.onUserChange); } onChannelChange() { @@ -46,6 +51,10 @@ export default class PostFocusView extends React.Component { }); } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser()}); + } + onPostsChange() { const focusedPostId = PostStore.getFocusedPostId(); if (focusedPostId == null) { @@ -65,11 +74,11 @@ export default class PostFocusView extends React.Component { } loadMorePostsTop() { - EventHelpers.emitLoadMorePostsFocusedTopEvent(); + GlobalActions.emitLoadMorePostsFocusedTopEvent(); } loadMorePostsBottom() { - EventHelpers.emitLoadMorePostsFocusedBottomEvent(); + GlobalActions.emitLoadMorePostsFocusedBottomEvent(); } getIntroMessage() { @@ -89,6 +98,10 @@ export default class PostFocusView extends React.Component { const postsToHighlight = {}; postsToHighlight[this.state.scrollPostId] = true; + if (!this.state.currentUser || !this.state.postList) { + return null; + } + return (
); diff --git a/web/react/components/post_header.jsx b/web/react/components/post_header.jsx index 2803fe3873..966775dad0 100644 --- a/web/react/components/post_header.jsx +++ b/web/react/components/post_header.jsx @@ -14,16 +14,15 @@ export default class PostHeader extends React.Component { } render() { const post = this.props.post; - const user = this.props.user; - let userProfile = ; + let userProfile = ; let botIndicator; if (post.props && post.props.from_webhook) { if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') { userProfile = ( @@ -54,6 +53,7 @@ export default class PostHeader extends React.Component { allowReply='true' isLastComment={this.props.isLastComment} sameUser={this.props.sameUser} + currentUser={this.props.currentUser} /> @@ -68,10 +68,11 @@ PostHeader.defaultProps = { sameUser: false }; PostHeader.propTypes = { - post: React.PropTypes.object, + post: React.PropTypes.object.isRequired, user: React.PropTypes.object, - commentCount: React.PropTypes.number, - isLastComment: React.PropTypes.bool, - handleCommentClick: React.PropTypes.func, - sameUser: React.PropTypes.bool + currentUser: React.PropTypes.object.isRequired, + commentCount: React.PropTypes.number.isRequired, + isLastComment: React.PropTypes.bool.isRequired, + handleCommentClick: React.PropTypes.func.isRequired, + sameUser: React.PropTypes.bool.isRequired }; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index ffac6eaefd..d0a4c828ec 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -1,10 +1,9 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; import TimeSince from './time_since.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -27,8 +26,8 @@ export default class PostInfo extends React.Component { } createDropdown() { var post = this.props.post; - var isOwner = UserStore.getCurrentId() === post.user_id; - var isAdmin = Utils.isAdmin(UserStore.getCurrentUser().roles); + var isOwner = this.props.currentUser.id === post.user_id; + var isAdmin = Utils.isAdmin(this.props.currentUser.roles); if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) { return ''; @@ -47,21 +46,21 @@ export default class PostInfo extends React.Component { if (this.props.allowReply === 'true') { dropdownContents.push( -
  • - - - -
  • +
  • + + + +
  • ); } @@ -93,7 +92,7 @@ export default class PostInfo extends React.Component { EventHelpers.showDeletePostModal(post, dataComments)} + onClick={() => GlobalActions.showDeletePostModal(post, dataComments)} > EventHelpers.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func + onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func displayNameType={this.state.displayNameType} - hasProfiles={profiles && Object.keys(profiles).length > 1} user={profile} + currentUser={this.props.currentUser} /> ); @@ -526,7 +525,7 @@ PostsView.defaultProps = { PostsView.propTypes = { isActive: React.PropTypes.bool, postList: React.PropTypes.object, - profiles: React.PropTypes.object, + profiles: React.PropTypes.object.isRequired, scrollPostId: React.PropTypes.string, scrollType: React.PropTypes.number, postViewScrolled: React.PropTypes.func.isRequired, @@ -536,7 +535,8 @@ PostsView.propTypes = { showMoreMessagesBottom: React.PropTypes.bool, introText: React.PropTypes.element, messageSeparatorTime: React.PropTypes.number, - postsToHighlight: React.PropTypes.object + postsToHighlight: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired }; function FloatingTimestamp({isScrolling, post}) { diff --git a/web/react/components/posts_view_container.jsx b/web/react/components/posts_view_container.jsx index 92d658b55a..b361779d2b 100644 --- a/web/react/components/posts_view_container.jsx +++ b/web/react/components/posts_view_container.jsx @@ -6,9 +6,10 @@ import LoadingScreen from './loading_screen.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import PostStore from '../stores/post_store.jsx'; +import UserStore from '../stores/user_store.jsx'; import * as Utils from '../utils/utils.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -21,6 +22,7 @@ export default class PostsViewContainer extends React.Component { this.onChannelChange = this.onChannelChange.bind(this); this.onChannelLeave = this.onChannelLeave.bind(this); this.onPostsChange = this.onPostsChange.bind(this); + this.onUserChange = this.onUserChange.bind(this); this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this); this.loadMorePostsTop = this.loadMorePostsTop.bind(this); this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this); @@ -28,7 +30,8 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const state = { scrollType: PostsView.SCROLL_TYPE_BOTTOM, - scrollPost: null + scrollPost: null, + currentUser: UserStore.getCurrentUser() }; if (currentChannelId) { Object.assign(state, { @@ -54,12 +57,17 @@ export default class PostsViewContainer extends React.Component { ChannelStore.addLeaveListener(this.onChannelLeave); PostStore.addChangeListener(this.onPostsChange); PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.addChangeListener(this.onUserChange); } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChannelChange); ChannelStore.removeLeaveListener(this.onChannelLeave); PostStore.removeChangeListener(this.onPostsChange); PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest); + UserStore.removeChangeListener(this.onUserChange); + } + onUserChange() { + this.setState({currentUser: UserStore.getCurrentUser()}); } handlePostsViewJumpRequest(type, post) { switch (type) { @@ -139,7 +147,7 @@ export default class PostsViewContainer extends React.Component { return PostStore.getVisiblePosts(id); } loadMorePostsTop() { - EventHelpers.emitLoadMorePostsEvent(); + GlobalActions.emitLoadMorePostsEvent(); } handlePostsViewScroll(atBottom) { if (atBottom) { @@ -149,11 +157,15 @@ export default class PostsViewContainer extends React.Component { } } shouldComponentUpdate(nextProps, nextState) { - if (Utils.areObjectsEqual(this.state, nextState)) { - return false; + if (!Utils.areObjectsEqual(this.state, nextState)) { + return true; } - return true; + if (!Utils.areObjectsEqual(this.props, nextProps)) { + return true; + } + + return false; } render() { const postLists = this.state.postLists; @@ -161,6 +173,10 @@ export default class PostsViewContainer extends React.Component { const currentChannelId = channels[this.state.currentChannelIndex]; const channel = ChannelStore.get(currentChannelId); + if (!this.state.currentUser || !channel) { + return null; + } + const postListCtls = []; for (let i = 0; i < channels.length; i++) { const isActive = (channels[i] === currentChannelId); @@ -181,6 +197,7 @@ export default class PostsViewContainer extends React.Component { introText={channel ? createChannelIntroMessage(channel) : null} messageSeparatorTime={this.state.currentLastViewed} profiles={this.props.profiles} + currentUser={this.state.currentUser} /> ); if (!postLists[i] && isActive) { diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx index 9588809ebf..9183b761f4 100644 --- a/web/react/components/rhs_comment.jsx +++ b/web/react/components/rhs_comment.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../utils/async_client.jsx'; var ActionTypes = Constants.ActionTypes; import * as TextFormatting from '../utils/text_formatting.jsx'; import twemoji from 'twemoji'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; @@ -70,7 +70,7 @@ class RhsComment extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -151,7 +151,7 @@ class RhsComment extends React.Component { EventHelpers.showDeletePostModal(post, 0)} + onClick={() => GlobalActions.showDeletePostModal(post, 0)} >
    diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx index 023f3dd2d9..fc1cd0b417 100644 --- a/web/react/components/rhs_root_post.jsx +++ b/web/react/components/rhs_root_post.jsx @@ -10,7 +10,7 @@ import * as Emoji from '../utils/emoticons.jsx'; import FileAttachmentList from './file_attachment_list.jsx'; import twemoji from 'twemoji'; import PostBodyAdditionalContent from './post_body_additional_content.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; @@ -34,7 +34,7 @@ export default class RhsRootPost extends React.Component { } handlePermalink(e) { e.preventDefault(); - EventHelpers.showGetPostLinkModal(this.props.post); + GlobalActions.showGetPostLinkModal(this.props.post); } componentDidMount() { this.parseEmojis(); @@ -142,7 +142,7 @@ export default class RhsRootPost extends React.Component { EventHelpers.showDeletePostModal(post, this.props.commentCount)} + onClick={() => GlobalActions.showDeletePostModal(post, this.props.commentCount)} > { + var l = {}; + l.level = 'ERROR'; + l.message = 'msg: ' + msg + ' row: ' + line + ' col: ' + column + ' stack: ' + stack + ' url: ' + url; + + $.ajax({ + url: '/api/v1/admin/log_client', + dataType: 'json', + contentType: 'application/json', + type: 'POST', + data: JSON.stringify(l) + }); + + if (window.mm_config.EnableDeveloper === 'true') { + window.ErrorStore.storeLastError({message: 'DEVELOPER MODE: A javascript error has occured. Please use the javascript console to capture and report the error (row: ' + line + ' col: ' + column + ').'}); + window.ErrorStore.emitChange(); + } + }; + + // Ya.... + /*eslint-disable */ + if (window.mm_config.SegmentDeveloperKey != null && window.mm_config.SegmentDeveloperKey !== "") { + !function(){var analytics=global.window.analytics=global.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; + } + + return ( + + {this.props.children} + + ); + } +} +Root.defaultProps = { +}; + +Root.propTypes = { + children: React.PropTypes.object +}; diff --git a/web/react/components/search_results_item.jsx b/web/react/components/search_results_item.jsx index 05292b7b3d..3a091bdd1b 100644 --- a/web/react/components/search_results_item.jsx +++ b/web/react/components/search_results_item.jsx @@ -3,8 +3,7 @@ import UserStore from '../stores/user_store.jsx'; import UserProfile from './user_profile.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; -import * as utils from '../utils/utils.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import * as TextFormatting from '../utils/text_formatting.jsx'; import Constants from '../utils/constants.jsx'; @@ -22,7 +21,7 @@ export default class SearchResultsItem extends React.Component { handleClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusEvent(this.props.post.id); + GlobalActions.emitPostFocusEvent(this.props.post.id); if ($(window).width() < 768) { $('.sidebar--right').removeClass('move--left'); @@ -32,7 +31,7 @@ export default class SearchResultsItem extends React.Component { handleFocusRHSClick(e) { e.preventDefault(); - EventHelpers.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); + GlobalActions.emitPostFocusRightHandSideFromSearch(this.props.post, this.props.isMentionSearch); } render() { @@ -78,7 +77,7 @@ export default class SearchResultsItem extends React.Component {
    @@ -123,6 +122,7 @@ export default class SearchResultsItem extends React.Component {
    diff --git a/web/react/components/should_verify_email.jsx b/web/react/components/should_verify_email.jsx new file mode 100644 index 0000000000..c473fe3669 --- /dev/null +++ b/web/react/components/should_verify_email.jsx @@ -0,0 +1,111 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {FormattedMessage} from 'mm-intl'; +import * as Client from '../utils/client.jsx'; + +export default class ShouldVerifyEmail extends React.Component { + constructor(props) { + super(props); + + this.handleResend = this.handleResend.bind(this); + + this.state = { + resendStatus: 'none' + }; + } + handleResend() { + const teamName = this.props.location.query.teamname; + const email = this.props.location.query.email; + + this.setState({resendStatus: 'sending'}); + + Client.resendVerification(() => { + this.setState({resendStatus: 'success'}); + }, + () => { + this.setState({resendStatus: 'failure'}); + }, + teamName, + email); + } + render() { + let resendConfirm = ''; + if (this.state.resendStatus === 'success') { + resendConfirm = ( +
    +
    +

    + + +

    +
    + ); + } + + if (this.state.resendStatus === 'failure') { + resendConfirm = ( +
    +
    +

    + + +

    +
    + ); + } + + return ( +
    + +
    +
    +

    + +

    +
    +

    + +

    + + {resendConfirm} +
    +
    +
    +
    + ); + } +} + +ShouldVerifyEmail.defaultProps = { +}; +ShouldVerifyEmail.propTypes = { + location: React.PropTypes.object.isRequired +}; diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index c7dba306b9..5c682d64b9 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -129,7 +129,9 @@ export default class Sidebar extends React.Component { directChannels, hiddenDirectChannelCount, unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())), - showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER + showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER, + currentTeam: TeamStore.getCurrent(), + currentUser: UserStore.getCurrentUser() }; } @@ -179,7 +181,7 @@ export default class Sidebar extends React.Component { } updateTitle() { const channel = ChannelStore.getCurrent(); - if (channel) { + if (channel && this.state.currentTeam) { let currentSiteName = ''; if (global.window.mm_config.SiteName != null) { currentSiteName = global.window.mm_config.SiteName; @@ -196,7 +198,7 @@ export default class Sidebar extends React.Component { const unread = this.getTotalUnreadCount(); const mentionTitle = unread.mentions > 0 ? '(' + unread.mentions + ') ' : ''; const unreadTitle = unread.msgs > 0 ? '* ' : ''; - document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + TeamStore.getCurrent().display_name + ' ' + currentSiteName; + document.title = mentionTitle + unreadTitle + currentChannelName + ' - ' + this.state.currentTeam.display_name + ' ' + currentSiteName; } } onScroll() { @@ -401,7 +403,6 @@ export default class Sidebar extends React.Component { // set up click handler to switch channels (or create a new channel for non-existant ones) var handleClick = null; var href = '#'; - var teamURL = TeamStore.getCurrentTeamUrl(); if (!channel.fake) { handleClick = function clickHandler(e) { @@ -413,7 +414,7 @@ export default class Sidebar extends React.Component { e.preventDefault(); }; - } else if (channel.fake && teamURL) { + } else if (channel.fake) { // It's a direct message channel that doesn't exist yet so let's create it now var otherUserId = Utils.getUserIdFromChannelName(channel); @@ -434,7 +435,7 @@ export default class Sidebar extends React.Component { }, () => { this.setState({loadingDMChannel: -1}); - window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name; + window.location.href = '/' + this.state.currentTeam.name; } ); } @@ -497,6 +498,11 @@ export default class Sidebar extends React.Component { ); } render() { + // Check if we have all info needed to render + if (this.state.currentTeam == null || this.state.currentUser == null) { + return (
    ); + } + this.badgesActive = false; // keep track of the first and last unread channels so we can use them to set the unread indicators @@ -586,7 +592,10 @@ export default class Sidebar extends React.Component { ); return ( -
    + ); @@ -230,16 +229,19 @@ export default class Textbox extends React.Component { >
    {helpText} - - - +
    + {previewLink} + + + +
    ); } diff --git a/web/react/components/user_list_row.jsx b/web/react/components/user_list_row.jsx index d8442e7708..1ca40687f3 100644 --- a/web/react/components/user_list_row.jsx +++ b/web/react/components/user_list_row.jsx @@ -32,7 +32,7 @@ export default function UserListRow({user, actions}) { >
    {name}
    ; } - var profileImg = '/api/v1/users/' + this.props.user.id + '/image?time=' + this.props.user.update_at + '&' + Utils.getSessionIndex(); - if (this.props.overwriteImage) { - profileImg = this.props.overwriteImage; - } - var dataContent = []; dataContent.push( - {this.props.user.email} + {email}
    ); @@ -114,7 +119,7 @@ UserProfile.defaultProps = { disablePopover: false }; UserProfile.propTypes = { - user: React.PropTypes.object.isRequired, + user: React.PropTypes.object, overwriteName: React.PropTypes.string, overwriteImage: React.PropTypes.string, disablePopover: React.PropTypes.bool diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 1e724bb6ef..4ee9fd0e24 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -253,6 +253,8 @@ class CustomThemeChooser extends React.Component {
    ); + + colors += theme[element.id] + ','; } else if (element.group === 'sidebarElements') { sidebarElements.push(
    { - window.location.reload(true); + GlobalActions.newLocalizationSelected(user.locale); }, (err) => { let serverError; diff --git a/web/react/components/user_settings/user_settings_developer.jsx b/web/react/components/user_settings/user_settings_developer.jsx index 0acfd4a16a..1dd564c8d4 100644 --- a/web/react/components/user_settings/user_settings_developer.jsx +++ b/web/react/components/user_settings/user_settings_developer.jsx @@ -3,7 +3,7 @@ import SettingItemMin from '../setting_item_min.jsx'; import SettingItemMax from '../setting_item_max.jsx'; -import * as EventHelpers from '../../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../../action_creators/global_actions.jsx'; import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; @@ -28,7 +28,7 @@ class DeveloperTab extends React.Component { } register() { this.props.closeModal(); - EventHelpers.showRegisterAppModal(); + GlobalActions.showRegisterAppModal(); } render() { var appSection; diff --git a/web/react/components/user_settings/user_settings_general.jsx b/web/react/components/user_settings/user_settings_general.jsx index b0b1c414ec..2358928192 100644 --- a/web/react/components/user_settings/user_settings_general.jsx +++ b/web/react/components/user_settings/user_settings_general.jsx @@ -13,7 +13,7 @@ import Constants from '../../utils/constants.jsx'; import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate} from 'mm-intl'; const holders = defineMessages({ usernameReserved: { @@ -712,7 +712,7 @@ class UserSettingsGeneralTab extends React.Component { + ) }); } pictureSection = ( @@ -805,4 +812,4 @@ UserSettingsGeneralTab.propTypes = { collapseModal: React.PropTypes.func.isRequired }; -export default injectIntl(UserSettingsGeneralTab); \ No newline at end of file +export default injectIntl(UserSettingsGeneralTab); diff --git a/web/react/components/user_settings/user_settings_modal.jsx b/web/react/components/user_settings/user_settings_modal.jsx index fa3415988f..0c4a3d526b 100644 --- a/web/react/components/user_settings/user_settings_modal.jsx +++ b/web/react/components/user_settings/user_settings_modal.jsx @@ -73,27 +73,35 @@ class UserSettingsModal extends React.Component { this.updateTab = this.updateTab.bind(this); this.updateSection = this.updateSection.bind(this); + this.onUserChanged = this.onUserChanged.bind(this); this.state = { active_tab: 'general', active_section: '', showConfirmModal: false, - enforceFocus: true + enforceFocus: true, + currentUser: UserStore.getCurrentUser() }; this.requireConfirm = false; } + onUserChanged() { + this.setState({currentUser: UserStore.getCurrentUser()}); + } + componentDidMount() { if (this.props.show) { this.handleShow(); } + UserStore.addChangeListener(this.onUserChanged); } componentDidUpdate(prevProps) { if (this.props.show && !prevProps.show) { this.handleShow(); } + UserStore.removeChangeListener(this.onUserChanged); } handleShow() { @@ -235,8 +243,10 @@ class UserSettingsModal extends React.Component { render() { const {formatMessage} = this.props.intl; - var currentUser = UserStore.getCurrentUser(); - var isAdmin = Utils.isAdmin(currentUser.roles); + if (this.state.currentUser == null) { + return (
    ); + } + var isAdmin = Utils.isAdmin(this.state.currentUser.roles); var tabs = []; tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'glyphicon glyphicon-cog'}); diff --git a/web/react/components/user_settings/user_settings_security.jsx b/web/react/components/user_settings/user_settings_security.jsx index cba7ffdea7..0b6b6c3986 100644 --- a/web/react/components/user_settings/user_settings_security.jsx +++ b/web/react/components/user_settings/user_settings_security.jsx @@ -14,7 +14,7 @@ import * as AsyncClient from '../../utils/async_client.jsx'; import * as Utils from '../../utils/utils.jsx'; import Constants from '../../utils/constants.jsx'; -import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl'; +import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedTime, FormattedDate} from 'mm-intl'; const holders = defineMessages({ currentPasswordError: { @@ -218,11 +218,24 @@ class SecurityTab extends React.Component { var describe; var d = new Date(this.props.user.last_password_update); - const locale = global.window.mm_locale; const hours12 = !Utils.isMilitaryTime(); describe = formatMessage(holders.lastUpdated, { - date: d.toLocaleDateString(locale, {month: 'short', day: '2-digit', year: 'numeric'}), - time: d.toLocaleTimeString(locale, {hour12: hours12, hour: '2-digit', minute: '2-digit'}) + date: ( + + ), + time: ( + + ) }); updateSectionStatus = function updateSection() { @@ -251,7 +264,7 @@ class SecurityTab extends React.Component {
    { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - -
    - - - -
    -
    - ); - } -} - -global.window.setup_admin_console_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('admin_controller') - ); -}; diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx deleted file mode 100644 index bc78c049cb..0000000000 --- a/web/react/pages/channel.jsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ChannelView from '../components/channel_view.jsx'; -import ChannelLoader from '../components/channel_loader.jsx'; -import ErrorBar from '../components/error_bar.jsx'; -import * as Client from '../utils/client.jsx'; - -import GetPostLinkModal from '../components/get_post_link_modal.jsx'; -import GetTeamInviteLinkModal from '../components/get_team_invite_link_modal.jsx'; -import EditPostModal from '../components/edit_post_modal.jsx'; -import DeletePostModal from '../components/delete_post_modal.jsx'; -import MoreChannelsModal from '../components/more_channels.jsx'; -import TeamSettingsModal from '../components/team_settings_modal.jsx'; -import RemovedFromChannelModal from '../components/removed_from_channel_modal.jsx'; -import RegisterAppModal from '../components/register_app_modal.jsx'; -import ImportThemeModal from '../components/user_settings/import_theme_modal.jsx'; -import InviteMemberModal from '../components/invite_member_modal.jsx'; - -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - -
    - - - - - - - - - - - - - -
    -
    - ); - } -} - -global.window.setup_channel_page = function setup(props, team, channel) { - if (props.PostId === '') { - EventHelpers.emitChannelClickEvent(channel); - } else { - EventHelpers.emitPostFocusEvent(props.PostId); - } - - ReactDOM.render( - , - document.getElementById('channel_view') - ); -}; diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx deleted file mode 100644 index abbf72ea34..0000000000 --- a/web/react/pages/claim_account.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import ClaimAccount from '../components/claim/claim_account.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_claim_account_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('claim') - ); -}; \ No newline at end of file diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx deleted file mode 100644 index 2e47e3e6a2..0000000000 --- a/web/react/pages/docs.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import Docs from '../components/docs.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.mm_user = global.window.mm_user || {}; - -global.window.setup_documentation_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('docs') - ); -}; diff --git a/web/react/pages/find_team.jsx b/web/react/pages/find_team.jsx deleted file mode 100644 index 93394fcde4..0000000000 --- a/web/react/pages/find_team.jsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import FindTeam from '../components/find_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_find_team_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('find-team') - ); -}; \ No newline at end of file diff --git a/web/react/pages/home.jsx b/web/react/pages/home.jsx deleted file mode 100644 index ff81c49945..0000000000 --- a/web/react/pages/home.jsx +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import TeamStore from '../stores/team_store.jsx'; -import Constants from '../utils/constants.jsx'; - -function setupHomePage() { - var last = null; - if (last == null || last.length === 0) { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + Constants.DEFAULT_CHANNEL; - } else { - window.location = TeamStore.getCurrentTeamUrl() + '/channels/' + last; - } -} - -global.window.setup_home_page = setupHomePage; diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx deleted file mode 100644 index ec90809453..0000000000 --- a/web/react/pages/login.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import * as Client from '../utils/client.jsx'; -import Login from '../components/login.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_login_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('login') - ); -}; \ No newline at end of file diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx deleted file mode 100644 index 7caff5034f..0000000000 --- a/web/react/pages/password_reset.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import PasswordReset from '../components/password_reset.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_password_reset_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('reset') - ); -}; diff --git a/web/react/pages/root.jsx b/web/react/pages/root.jsx new file mode 100644 index 0000000000..d0b06e32ef --- /dev/null +++ b/web/react/pages/root.jsx @@ -0,0 +1,290 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router'; +import Root from '../components/root.jsx'; +import Login from '../components/login.jsx'; +import LoggedIn from '../components/logged_in.jsx'; +import NotLoggedIn from '../components/not_logged_in.jsx'; +import NeedsTeam from '../components/needs_team.jsx'; +import PasswordResetSendLink from '../components/password_reset_send_link.jsx'; +import PasswordResetForm from '../components/password_reset_form.jsx'; +import ChannelView from '../components/channel_view.jsx'; +import Sidebar from '../components/sidebar.jsx'; +import * as AsyncClient from '../utils/async_client.jsx'; +import PreferenceStore from '../stores/preference_store.jsx'; +import ChannelStore from '../stores/channel_store.jsx'; +import ErrorStore from '../stores/error_store.jsx'; +import BrowserStore from '../stores/browser_store.jsx'; +import SignupTeam from '../components/signup_team.jsx'; +import * as Client from '../utils/client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; +import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; +import SignupUserComplete from '../components/signup_user_complete.jsx'; +import ShouldVerifyEmail from '../components/should_verify_email.jsx'; +import DoVerifyEmail from '../components/do_verify_email.jsx'; +import AdminConsole from '../components/admin_console/admin_controller.jsx'; +import ClaimAccount from '../components/claim/claim_account.jsx'; + +import SignupTeamComplete from '../components/signup_team_complete/components/signup_team_complete.jsx'; +import WelcomePage from '../components/signup_team_complete/components/team_signup_welcome_page.jsx'; +import TeamDisplayNamePage from '../components/signup_team_complete/components/team_signup_display_name_page.jsx'; +import TeamURLPage from '../components/signup_team_complete/components/team_signup_url_page.jsx'; +import SendInivtesPage from '../components/signup_team_complete/components/team_signup_send_invites_page.jsx'; +import UsernamePage from '../components/signup_team_complete/components/team_signup_username_page.jsx'; +import PasswordPage from '../components/signup_team_complete/components/team_signup_password_page.jsx'; +import FinishedPage from '../components/signup_team_complete/components/team_signup_finished.jsx'; + +// This is for anything that needs to be done for ALL react components. +// This runs before we start to render anything. +function preRenderSetup(callwhendone) { + const d1 = Client.getClientConfig( + (data, textStatus, xhr) => { + if (!data) { + return; + } + + global.window.mm_config = data; + + var serverVersion = xhr.getResponseHeader('X-Version-ID'); + + if (serverVersion !== BrowserStore.getLastServerVersion()) { + if (!BrowserStore.getLastServerVersion() || BrowserStore.getLastServerVersion() === '') { + BrowserStore.setLastServerVersion(serverVersion); + } else { + BrowserStore.setLastServerVersion(serverVersion); + window.location.reload(true); + console.log('Detected version update refreshing the page'); //eslint-disable-line no-console + } + } + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientConfig'); + } + ); + + const d2 = Client.getClientLicenceConfig( + (data) => { + if (!data) { + return; + } + + global.window.mm_license = data; + }, + (err) => { + AsyncClient.dispatchError(err, 'getClientLicenceConfig'); + } + ); + + // Set these here so they don't fail in client.jsx track + global.window.analytics = {}; + global.window.analytics.page = () => { + // Do Nothing + }; + global.window.analytics.track = () => { + // Do Nothing + }; + + $.when(d1, d2).done(callwhendone); +} + +function preLoggedIn(nextState, replace, callback) { + const d1 = Client.getAllPreferences( + (data) => { + if (!data) { + return; + } + + PreferenceStore.setPreferences(data); + }, + (err) => { + AsyncClient.dispatchError(err, 'getAllPreferences'); + } + ); + + const d2 = AsyncClient.getChannels(); + + $.when(d1, d2).done(() => callback()); +} + +function onChannelChange(nextState) { + const channelName = nextState.params.channel; + + // Make sure we have all the channels + AsyncClient.getChannels(true); + + // Get our channel's ID + const channel = ChannelStore.getByName(channelName); + + // User clicked channel + GlobalActions.emitChannelClickEvent(channel); +} + +function onRootEnter(nextState, replace, callback) { + if (nextState.location.pathname === '/') { + Client.getMeLoggedIn((data) => { + if (!data || data.logged_in === 'false') { + replace({pathname: '/signup_team'}); + callback(); + } else { + replace({pathname: '/' + data.team_name + '/channels/town-square'}); + callback(); + } + }); + return; + } + + callback(); +} + +function onPermalinkEnter(nextState) { + const postId = nextState.params.postid; + + GlobalActions.emitPostFocusEvent(postId); +} + +function onLoggedOut(nextState) { + const teamName = nextState.params.team; + Client.logout( + () => { + browserHistory.push('/' + teamName + '/login'); + BrowserStore.signalLogout(); + BrowserStore.clear(); + ErrorStore.clearLastError(); + }, + () => { + browserHistory.push('/' + teamName + '/login'); + } + ); +} + +function renderRootComponent() { + ReactDOM.render(( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + document.getElementById('root')); +} + +global.window.setup_root = () => { + // Do the pre-render setup and call renderRootComponent when done + preRenderSetup(renderRootComponent); +}; diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx deleted file mode 100644 index f276c3ff7d..0000000000 --- a/web/react/pages/signup_team.jsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeam from '../components/signup_team.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired, - teams: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_page = function setup(props) { - var teams = []; - - for (var prop in props) { - if (props.hasOwnProperty(prop)) { - if (prop !== 'Title' && prop !== 'Locale' && prop !== 'Info') { - teams.push({name: prop, display_name: props[prop]}); - } - } - } - - ReactDOM.render( - , - document.getElementById('signup-team') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx deleted file mode 100644 index 8c237f6989..0000000000 --- a/web/react/pages/signup_team_complete.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamComplete from '../components/signup_team_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_complete_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-team-complete') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_team_confirm.jsx b/web/react/pages/signup_team_confirm.jsx deleted file mode 100644 index 13c8f3fd00..0000000000 --- a/web/react/pages/signup_team_confirm.jsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupTeamConfirm from '../components/signup_team_confirm.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_team_confirm_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-team-confirm') - ); -}; \ No newline at end of file diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx deleted file mode 100644 index a14f2140bc..0000000000 --- a/web/react/pages/signup_user_complete.jsx +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import SignupUserComplete from '../components/signup_user_complete.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setup_signup_user_complete_page = function setup(props) { - ReactDOM.render( - , - document.getElementById('signup-user-complete') - ); -}; \ No newline at end of file diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx deleted file mode 100644 index 6b336daa1e..0000000000 --- a/web/react/pages/verify.jsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import EmailVerify from '../components/email_verify.jsx'; -import * as Client from '../utils/client.jsx'; - -var IntlProvider = ReactIntl.IntlProvider; - -class Root extends React.Component { - constructor() { - super(); - this.state = { - translations: null, - loaded: false - }; - } - - static propTypes() { - return { - map: React.PropTypes.object.isRequired - }; - } - - componentWillMount() { - Client.getTranslations( - this.props.map.Locale, - (data) => { - this.setState({ - translations: data, - loaded: true - }); - }, - () => { - this.setState({ - loaded: true - }); - } - ); - } - - render() { - if (!this.state.loaded) { - return
    ; - } - - return ( - - - - ); - } -} - -global.window.setupVerifyPage = function setup(props) { - ReactDOM.render( - , - document.getElementById('verify') - ); -}; diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx index eb3254cfef..9f7f6e7ffc 100644 --- a/web/react/stores/admin_store.jsx +++ b/web/react/stores/admin_store.jsx @@ -121,7 +121,11 @@ class AdminStoreClass extends EventEmitter { } getSelectedTeams() { - return BrowserStore.getItem('seleted_teams'); + const result = BrowserStore.getItem('seleted_teams'); + if (!result) { + return {}; + } + return result; } saveSelectedTeams(teams) { diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 3417faaafa..3b35916b3b 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -4,8 +4,8 @@ import {generateId} from '../utils/utils.jsx'; function getPrefix() { - if (global.window.mm_user) { - return global.window.mm_user.id + '_'; + if (global.window.mm_current_user_id) { + return global.window.mm_current_user_id + '_'; } return 'unknown_'; @@ -31,7 +31,9 @@ class BrowserStoreClass { this.isSignallingLogout = this.isSignallingLogout.bind(this); this.signalLogin = this.signalLogin.bind(this); this.isSignallingLogin = this.isSignallingLogin.bind(this); + } + checkVersion() { var currentVersion = sessionStorage.getItem('storage_version'); if (currentVersion !== global.window.mm_config.Version) { sessionStorage.clear(); diff --git a/web/react/stores/localization_store.jsx b/web/react/stores/localization_store.jsx new file mode 100644 index 0000000000..0e3a63724c --- /dev/null +++ b/web/react/stores/localization_store.jsx @@ -0,0 +1,60 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; +import EventEmitter from 'events'; +import Constants from '../utils/constants.jsx'; +const ActionTypes = Constants.ActionTypes; + +const CHANGE_EVENT = 'change'; + +class LocalizationStoreClass extends EventEmitter { + constructor() { + super(); + + this.currentLocale = 'en'; + this.currentTranslations = null; + } + + emitChange() { + this.emit(CHANGE_EVENT); + } + + addChangeListener(callback) { + this.on(CHANGE_EVENT, callback); + } + + removeChangeListener(callback) { + this.removeListener(CHANGE_EVENT, callback); + } + + setCurrentLocale(locale, translations) { + this.currentLocale = locale; + this.currentTranslations = translations; + } + + getLocale() { + return this.currentLocale; + } + + getTranslations() { + return this.currentTranslations; + } +} + +var LocalizationStore = new LocalizationStoreClass(); +LocalizationStore.setMaxListeners(0); + +LocalizationStore.dispatchToken = AppDispatcher.register((payload) => { + var action = payload.action; + + switch (action.type) { + case ActionTypes.RECEIVED_LOCALE: + LocalizationStore.setCurrentLocale(action.locale, action.translations); + LocalizationStore.emitChange(); + break; + default: + } +}); + +export default LocalizationStore; diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 9b2b049b74..181de53d7b 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -10,7 +10,7 @@ import EventEmitter from 'events'; import * as Utils from '../utils/utils.jsx'; import * as AsyncClient from '../utils/async_client.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import Constants from '../utils/constants.jsx'; const SocketEvents = Constants.SocketEvents; @@ -31,6 +31,7 @@ class SocketStoreClass extends EventEmitter { this.close = this.close.bind(this); this.failCount = 0; + this.isInitialize = false; this.translations = this.getDefaultTranslations(); @@ -42,10 +43,6 @@ class SocketStoreClass extends EventEmitter { return; } - if (!global.window.hasOwnProperty('mm_session_token_index')) { - return; - } - this.setMaxListeners(0); if (window.WebSocket && !conn) { @@ -54,28 +51,27 @@ class SocketStoreClass extends EventEmitter { protocol = 'wss://'; } - var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket?' + Utils.getSessionIndex(); + var connUrl = protocol + location.host + ((/:\d+/).test(location.host) ? '' : Utils.getWebsocketPort(protocol)) + '/api/v1/websocket'; if (this.failCount === 0) { console.log('websocket connecting to ' + connUrl); //eslint-disable-line no-console - if (ErrorStore.getConnectionErrorCount() > 0) { - ErrorStore.setConnectionErrorCount(0); - ErrorStore.emitChange(); - } } + conn = new WebSocket(connUrl); conn.onopen = () => { if (this.failCount > 0) { console.log('websocket re-established connection'); //eslint-disable-line no-console - - ErrorStore.clearLastError(); - ErrorStore.emitChange(); - AsyncClient.getChannels(); AsyncClient.getPosts(ChannelStore.getCurrentId()); } + if (this.isInitialize) { + ErrorStore.clearLastError(); + ErrorStore.emitChange(); + } + + this.isInitialize = true; this.failCount = 0; }; @@ -204,7 +200,7 @@ class SocketStoreClass extends EventEmitter { function handleNewPostEvent(msg, translations) { // Store post const post = JSON.parse(msg.props.post); - EventHelpers.emitPostRecievedEvent(post); + GlobalActions.emitPostRecievedEvent(post); // Update channel state if (ChannelStore.getCurrentId() === msg.channel_id) { @@ -291,7 +287,7 @@ function handlePostEditEvent(msg) { function handlePostDeleteEvent(msg) { const post = JSON.parse(msg.props.post); - EventHelpers.emitPostDeletedEvent(post); + GlobalActions.emitPostDeletedEvent(post); } function handleNewUserEvent() { @@ -337,7 +333,7 @@ function handleChannelViewedEvent(msg) { function handlePreferenceChangedEvent(msg) { const preference = JSON.parse(msg.props.preference); - EventHelpers.emitPreferenceChangedEvent(preference); + GlobalActions.emitPreferenceChangedEvent(preference); } var SocketStore = new SocketStoreClass(); diff --git a/web/react/stores/team_store.jsx b/web/react/stores/team_store.jsx index 7a1a2ef420..354a07b726 100644 --- a/web/react/stores/team_store.jsx +++ b/web/react/stores/team_store.jsx @@ -6,7 +6,6 @@ import EventEmitter from 'events'; import Constants from '../utils/constants.jsx'; const ActionTypes = Constants.ActionTypes; -import BrowserStore from '../stores/browser_store.jsx'; const CHANGE_EVENT = 'change'; @@ -33,6 +32,9 @@ class TeamStoreClass extends EventEmitter { this.getCurrentTeamUrl = this.getCurrentTeamUrl.bind(this); this.getCurrentInviteLink = this.getCurrentInviteLink.bind(this); this.saveTeam = this.saveTeam.bind(this); + + this.teams = {}; + this.currentTeamId = ''; } emitChange() { @@ -65,11 +67,11 @@ class TeamStoreClass extends EventEmitter { } getAll() { - return BrowserStore.getItem('user_teams', {}); + return this.teams; } getCurrentId() { - var team = global.window.mm_team; + var team = this.get(this.currentTeamId); if (team) { return team.id; @@ -79,11 +81,13 @@ class TeamStoreClass extends EventEmitter { } getCurrent() { - if (global.window.mm_team != null && this.get(global.window.mm_team.id) == null) { - this.saveTeam(global.window.mm_team); + const team = this.teams[this.currentTeamId]; + + if (team) { + return team; } - return global.window.mm_team; + return null; } getCurrentTeamUrl() { @@ -104,9 +108,16 @@ class TeamStoreClass extends EventEmitter { } saveTeam(team) { - var teams = this.getAll(); - teams[team.id] = team; - BrowserStore.setItem('user_teams', teams); + this.teams[team.id] = team; + } + + saveTeams(teams) { + this.teams = teams; + } + + saveMyTeam(team) { + this.saveTeam(team); + this.currentTeamId = team.id; } } @@ -116,11 +127,14 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => { var action = payload.action; switch (action.type) { - case ActionTypes.RECEIVED_TEAM: - TeamStore.saveTeam(action.team); + case ActionTypes.RECEIVED_MY_TEAM: + TeamStore.saveMyTeam(action.team); + TeamStore.emitChange(); + break; + case ActionTypes.RECEIVED_ALL_TEAMS: + TeamStore.saveTeams(action.teams); TeamStore.emitChange(); break; - default: } }); diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 75a87d424d..c1e5c75dc4 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -11,13 +11,13 @@ import BrowserStore from './browser_store.jsx'; const CHANGE_EVENT = 'change'; const CHANGE_EVENT_SESSIONS = 'change_sessions'; const CHANGE_EVENT_AUDITS = 'change_audits'; -const CHANGE_EVENT_TEAMS = 'change_teams'; const CHANGE_EVENT_STATUSES = 'change_statuses'; class UserStoreClass extends EventEmitter { constructor() { super(); this.profileCache = null; + this.currentUserId = ''; } emitChange(userId) { @@ -56,18 +56,6 @@ class UserStoreClass extends EventEmitter { 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); } @@ -81,26 +69,17 @@ class UserStoreClass extends EventEmitter { } getCurrentUser() { - if (this.getProfiles()[global.window.mm_user.id] == null) { - this.saveProfile(global.window.mm_user); - } - - return global.window.mm_user; + return this.getProfiles()[this.currentUserId]; } setCurrentUser(user) { - var oldUser = global.window.mm_user; - - 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); - } + this.saveProfile(user); + this.currentUserId = user.id; + global.window.mm_current_user_id = this.currentUserId; } getCurrentId() { - var user = global.window.mm_user; + var user = this.getCurrentUser(); if (user) { return user.id; @@ -200,11 +179,22 @@ class UserStoreClass extends EventEmitter { saveProfiles(profiles) { const currentId = this.getCurrentId(); - if (currentId in profiles) { - delete profiles[currentId]; + if (this.profileCache) { + const currentUser = this.profileCache[currentId]; + if (currentUser) { + if (currentId in profiles) { + delete profiles[currentId]; + } + + this.profileCache = profiles; + this.profileCache[currentId] = currentUser; + } else { + this.profileCache = profiles; + } + } else { + this.profileCache = profiles; } - this.profileCache = profiles; BrowserStore.setItem('profiles', profiles); } @@ -224,14 +214,6 @@ class UserStoreClass extends EventEmitter { return BrowserStore.getItem('audits', {loading: true}); } - setTeams(teams) { - BrowserStore.setItem('teams', teams); - } - - getTeams() { - return BrowserStore.getItem('teams', []); - } - getCurrentMentionKeys() { return this.getMentionKeys(this.getCurrentId()); } @@ -312,10 +294,6 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => { UserStore.setAudits(action.audits); UserStore.emitAuditsChange(); break; - case ActionTypes.RECEIVED_TEAMS: - UserStore.setTeams(action.teams); - UserStore.emitTeamsChange(); - break; case ActionTypes.RECEIVED_STATUSES: UserStore.pSetStatuses(action.statuses); UserStore.emitStatusesChange(); diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 7d5e1bd0f4..b9770a6e99 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. import * as client from './client.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; import BrowserStore from '../stores/browser_store.jsx'; import ChannelStore from '../stores/channel_store.jsx'; @@ -44,15 +45,19 @@ function isCallInProgress(callName) { export function getChannels(checkVersion) { if (isCallInProgress('getChannels')) { - return; + return null; } callTracker.getChannels = utils.getTimestamp(); - client.getChannels( + return client.getChannels( (data, textStatus, xhr) => { callTracker.getChannels = 0; + if (xhr.status === 304 || !data) { + return; + } + if (checkVersion) { var serverVersion = xhr.getResponseHeader('X-Version-ID'); @@ -67,10 +72,6 @@ export function getChannels(checkVersion) { } } - if (xhr.status === 304 || !data) { - return; - } - AppDispatcher.handleServerAction({ type: ActionTypes.RECEIVED_CHANNELS, channels: data.channels, @@ -392,36 +393,6 @@ export function getAllTeams() { ); } -export function findTeams(email) { - if (isCallInProgress('findTeams_' + email)) { - return; - } - - var user = UserStore.getCurrentUser(); - if (user) { - callTracker['findTeams_' + email] = utils.getTimestamp(); - client.findTeams( - user.email, - function findTeamsSuccess(data, textStatus, xhr) { - callTracker['findTeams_' + email] = 0; - - if (xhr.status === 304 || !data) { - return; - } - - AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAMS, - teams: data - }); - }, - function findTeamsFailure(err) { - callTracker['findTeams_' + email] = 0; - dispatchError(err, 'findTeams'); - } - ); - } -} - export function search(terms) { if (isCallInProgress('search_' + String(terms))) { return; @@ -645,11 +616,11 @@ export function getPostsAfter(postId, offset, numPost) { export function getMe() { if (isCallInProgress('getMe')) { - return; + return null; } callTracker.getMe = utils.getTimestamp(); - client.getMe( + return client.getMe( (data, textStatus, xhr) => { callTracker.getMe = 0; @@ -661,6 +632,8 @@ export function getMe() { type: ActionTypes.RECEIVED_ME, me: data }); + + GlobalActions.newLocalizationSelected(data.locale); }, (err) => { callTracker.getMe = 0; @@ -706,11 +679,11 @@ export function getStatuses() { export function getMyTeam() { if (isCallInProgress('getMyTeam')) { - return; + return null; } callTracker.getMyTeam = utils.getTimestamp(); - client.getMyTeam( + return client.getMyTeam( function getMyTeamSuccess(data, textStatus, xhr) { callTracker.getMyTeam = 0; @@ -719,7 +692,7 @@ export function getMyTeam() { } AppDispatcher.handleServerAction({ - type: ActionTypes.RECEIVED_TEAM, + type: ActionTypes.RECEIVED_MY_TEAM, team: data }); }, diff --git a/web/react/utils/channel_intro_messages.jsx b/web/react/utils/channel_intro_messages.jsx index ed94f94b81..94f3f0ce0f 100644 --- a/web/react/utils/channel_intro_messages.jsx +++ b/web/react/utils/channel_intro_messages.jsx @@ -8,8 +8,7 @@ import ToggleModalButton from '../components/toggle_modal_button.jsx'; import UserProfile from '../components/user_profile.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import Constants from '../utils/constants.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'mm-intl'; @@ -40,7 +39,7 @@ export function createDMIntroMessage(channel) {
    @@ -93,37 +92,19 @@ export function createOffTopicIntroMessage(channel) { } export function createDefaultIntroMessage(channel) { - const team = TeamStore.getCurrent(); - let inviteModalLink; - if (team.type === Constants.INVITE_TEAM) { - inviteModalLink = ( - - - - - ); - } else { - inviteModalLink = ( - - - - - ); - } + const inviteModalLink = ( + + + + + ); return (
    diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 76d42137a1..e00f28a14a 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -1,8 +1,8 @@ // See License.txt for license information. import BrowserStore from '../stores/browser_store.jsx'; -import TeamStore from '../stores/team_store.jsx'; -import ErrorStore from '../stores/error_store.jsx'; + +import {browserHistory} from 'react-router'; let translations = { connectionError: 'There appears to be a problem with your internet connection.', @@ -50,10 +50,10 @@ function handleError(methodName, xhr, status, err) { if (xhr.status === 401) { if (window.location.href.indexOf('/channels') === 0) { - window.location.pathname = '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + browserHistory.push('/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } else { - var teamURL = window.location.href.split('/channels')[0]; - window.location.href = teamURL + '/login?redirect=' + encodeURIComponent(window.location.pathname + window.location.search); + var teamURL = window.location.pathname.split('/channels')[0]; + browserHistory.push(teamURL + '/login?extra=expired&redirect=' + encodeURIComponent(window.location.pathname + window.location.search)); } } @@ -289,13 +289,17 @@ export function switchToEmail(data, success, error) { track('api', 'api_users_switch_to_email'); } -export function logout() { +export function logout(success, error) { track('api', 'api_users_logout'); - var currentTeamUrl = TeamStore.getCurrentTeamUrl(); - BrowserStore.signalLogout(); - BrowserStore.clear(); - ErrorStore.clearLastError(); - window.location.href = currentTeamUrl + '/logout'; + $.ajax({ + url: '/api/v1/users/logout', + type: 'POST', + success, + error: function onError(xhr, status, err) { + var e = handleError('logout', xhr, status, err); + error(e); + } + }); } export function loginByEmail(name, email, password, success, error) { @@ -437,7 +441,7 @@ export function getServerAudits(success, error) { } export function getConfig(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/admin/config', dataType: 'json', contentType: 'application/json', @@ -457,7 +461,6 @@ export function getAnalytics(name, teamId, success, error) { } else { url += teamId + '/' + name; } - $.ajax({ url, dataType: 'json', @@ -471,6 +474,34 @@ export function getAnalytics(name, teamId, success, error) { }); } +export function getClientConfig(success, error) { + return $.ajax({ + url: '/api/v1/admin/client_props', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientConfig', xhr, status, err); + error(e); + } + }); +} + +export function getTeamAnalytics(teamId, name, success, error) { + $.ajax({ + url: '/api/v1/admin/analytics/' + teamId + '/' + name, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getTeamAnalytics', xhr, status, err); + error(e); + } + }); +} + export function saveConfig(config, success, error) { $.ajax({ url: '/api/v1/admin/save_config', @@ -529,6 +560,21 @@ export function getAllTeams(success, error) { }); } +export function getMeLoggedIn(success, error) { + return $.ajax({ + cache: false, + url: '/api/v1/users/me_logged_in', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getMeLoggedIn', xhr, status, err); + error(e); + } + }); +} + export function getMe(success, error) { var currentUser = null; $.ajax({ @@ -635,38 +681,6 @@ export function findTeamByName(teamName, success, error) { }); } -export function findTeamsSendEmail(email, success, error) { - $.ajax({ - url: '/api/v1/teams/email_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeamsSendEmail', xhr, status, err); - error(e); - } - }); - - track('api', 'api_teams_email_teams'); -} - -export function findTeams(email, success, error) { - $.ajax({ - url: '/api/v1/teams/find_teams', - dataType: 'json', - contentType: 'application/json', - type: 'POST', - data: JSON.stringify({email: email}), - success, - error: function onError(xhr, status, err) { - var e = handleError('findTeams', xhr, status, err); - error(e); - } - }); -} - export function createChannel(channel, success, error) { $.ajax({ url: '/api/v1/channels/create', @@ -835,7 +849,7 @@ export function updateLastViewedAt(channelId, success, error) { } export function getChannels(success, error) { - $.ajax({ + return $.ajax({ cache: false, url: '/api/v1/channels/', dataType: 'json', @@ -901,7 +915,7 @@ export function getChannelExtraInfo(id, memberLimit, success, error) { url += '/' + memberLimit; } - $.ajax({ + return $.ajax({ url, dataType: 'json', contentType: 'application/json', @@ -1018,7 +1032,7 @@ export function getPostsPage(channelId, offset, limit, success, error, complete) } export function getPosts(channelId, since, success, error, complete) { - $.ajax({ + return $.ajax({ url: '/api/v1/channels/' + channelId + '/posts/' + since, dataType: 'json', type: 'GET', @@ -1347,7 +1361,7 @@ export function getStatuses(ids, success, error) { } export function getMyTeam(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/teams/me', dataType: 'json', type: 'GET', @@ -1437,7 +1451,7 @@ export function listIncomingHooks(success, error) { } export function getAllPreferences(success, error) { - $.ajax({ + return $.ajax({ url: '/api/v1/preferences/', dataType: 'json', type: 'GET', @@ -1569,3 +1583,68 @@ export function removeLicenseFile(success, error) { track('api', 'api_license_upload'); } + +export function getClientLicenceConfig(success, error) { + return $.ajax({ + url: '/api/v1/license/client_config', + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: function onError(xhr, status, err) { + var e = handleError('getClientLicenceConfig', xhr, status, err); + error(e); + } + }); +} + +export function getInviteInfo(success, error, id) { + $.ajax({ + url: '/api/v1/teams/get_invite_info', + type: 'POST', + dataType: 'json', + contentType: 'application/json', + data: JSON.stringify({invite_id: id}), + success, + error: function onError(xhr, status, err) { + var e = handleError('getInviteInfo', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function verifyEmail(success, error, uid, hid) { + $.ajax({ + url: '/api/v1/users/verify_email', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({uid, hid}), + success, + error: function onError(xhr, status, err) { + var e = handleError('verifyEmail', xhr, status, err); + if (error) { + error(e); + } + } + }); +} + +export function resendVerification(success, error, teamName, email) { + $.ajax({ + url: '/api/v1/users/resend_verification', + type: 'POST', + contentType: 'application/json', + dataType: 'text', + data: JSON.stringify({team_name: teamName, email}), + success, + error: function onError(xhr, status, err) { + var e = handleError('resendVerification', xhr, status, err); + if (error) { + error(e); + } + } + }); +} diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index daea9f43e7..3de562b7b2 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -42,13 +42,15 @@ export default { RECEIVED_MSG: null, - RECEIVED_TEAM: null, + RECEIVED_MY_TEAM: null, RECEIVED_CONFIG: null, RECEIVED_LOGS: null, RECEIVED_SERVER_AUDITS: null, RECEIVED_ALL_TEAMS: null, + RECEIVED_LOCALE: null, + SHOW_SEARCH: null, TOGGLE_IMPORT_THEME_MODAL: null, @@ -143,6 +145,7 @@ export default { EMAIL_SERVICE: 'email', SIGNIN_CHANGE: 'signin_change', SIGNIN_VERIFIED: 'verified', + SESSION_EXPIRED: 'expired', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, POST_FOCUS_CONTEXT_RADIUS: 10, @@ -267,7 +270,7 @@ export default { buttonColor: '#FFFFFF', mentionHighlightBg: '#984063', mentionHighlightLink: '#A4FFEB', - codeTheme: 'solarized_dark' + codeTheme: 'solarized-dark' }, windows10: { type: 'Windows Dark', @@ -370,21 +373,6 @@ export default { id: 'newMessageSeparator', uiName: 'New Message Separator' }, - { - group: 'linkAndButtonElements', - id: 'linkColor', - uiName: 'Link Color' - }, - { - group: 'linkAndButtonElements', - id: 'buttonBg', - uiName: 'Button BG' - }, - { - group: 'linkAndButtonElements', - id: 'buttonColor', - uiName: 'Button Text' - }, { group: 'centerChannelElements', id: 'mentionHighlightBg', @@ -401,11 +389,11 @@ export default { uiName: 'Code Theme', themes: [ { - id: 'solarized_dark', + id: 'solarized-dark', uiName: 'Solarized Dark' }, { - id: 'solarized_light', + id: 'solarized-light', uiName: 'Solarized Light' }, { @@ -417,6 +405,21 @@ export default { uiName: 'Monokai' } ] + }, + { + group: 'linkAndButtonElements', + id: 'linkColor', + uiName: 'Link Color' + }, + { + group: 'linkAndButtonElements', + id: 'buttonBg', + uiName: 'Button BG' + }, + { + group: 'linkAndButtonElements', + id: 'buttonColor', + uiName: 'Button Text' } ], DEFAULT_CODE_THEME: 'github', diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx index 4939160588..2b1aed9c06 100644 --- a/web/react/utils/markdown.jsx +++ b/web/react/utils/markdown.jsx @@ -193,6 +193,16 @@ class MattermostMarkdownRenderer extends marked.Renderer { outHref = outHref.substring(1, outHref.length - 1); } + try { + const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase(); + + if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0) { // eslint-disable-line no-script-url + return ''; + } + } catch (e) { + return ''; + } + if (!(/[a-z+.-]+:/i).test(outHref)) { outHref = `http://${outHref}`; } @@ -548,3 +558,18 @@ export function format(text, options) { return new MattermostParser(markdownOptions).parse(tokens); } +// Marked helper functions that should probably just be exported + +function unescape(html) { + return html.replace(/&([#\w]+);/g, (_, m) => { + const n = m.toLowerCase(); + if (n === 'colon') { + return ':'; + } else if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' ? + String.fromCharCode(parseInt(n.substring(2), 16)) : + String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 6942a8e08c..88777164b8 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -2,9 +2,10 @@ // See License.txt for license information. import AppDispatcher from '../dispatcher/app_dispatcher.jsx'; -import * as EventHelpers from '../dispatcher/event_helpers.jsx'; +import * as GlobalActions from '../action_creators/global_actions.jsx'; import ChannelStore from '../stores/channel_store.jsx'; import UserStore from '../stores/user_store.jsx'; +import LocalizationStore from '../stores/localization_store.jsx'; import PreferenceStore from '../stores/preference_store.jsx'; import TeamStore from '../stores/team_store.jsx'; import Constants from '../utils/constants.jsx'; @@ -941,7 +942,7 @@ export function updateAddressBar(channelName) { } export function switchChannel(channel) { - EventHelpers.emitChannelClickEvent(channel); + GlobalActions.emitChannelClickEvent(channel); updateAddressBar(channel.name); @@ -1130,8 +1131,8 @@ export function fileSizeToString(bytes) { // Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server. export function getFileUrl(filename, isDownload) { - const downloadParam = isDownload ? '&download=1' : ''; - return getWindowLocationOrigin() + '/api/v1/files/get' + filename + '?' + getSessionIndex() + downloadParam; + const downloadParam = isDownload ? '?download=1' : ''; + return getWindowLocationOrigin() + '/api/v1/files/get' + filename + downloadParam; } // Gets the name of a file (including extension) from a given url or file path. @@ -1151,14 +1152,6 @@ export function getWebsocketPort(protocol) { return ''; } -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 @@ -1405,3 +1398,19 @@ export function isPostEphemeral(post) { export function getRootId(post) { return post.root_id === '' ? post.id : post.root_id; } + +export function localizeMessage(id, defaultMessage) { + const translations = LocalizationStore.getTranslations(); + if (translations) { + const value = translations[id]; + if (value) { + return value; + } + } + + if (defaultMessage) { + return defaultMessage; + } + + return id; +} diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss index f782da36bb..76081710fd 100644 --- a/web/sass-files/sass/partials/_admin-console.scss +++ b/web/sass-files/sass/partials/_admin-console.scss @@ -145,12 +145,27 @@ .form-group { margin-bottom: 25px; } - .help-text { - ul, ol { - padding-left: 23px; + .file__upload { + position: relative; + margin: 0 10px 10px 0; + display: inline-block; + input { + position: absolute; + @include opacity(0); + width: 100%; + height: 100%; + z-index: 5; + top: 0; + left: 0; } } .help-text { + &.no-margin { + margin: 0; + } + ul, ol { + padding-left: 23px; + } margin: 10px 0 0 15px; color: #777; .help-link { diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 39cebe8569..209f8e27f5 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -58,23 +58,27 @@ body.ios { box-shadow: none; white-space: normal; } - .textbox-help-link { + .help__text { + right: 0; position: absolute; z-index: 3; bottom: -23px; font-size: 13px; cursor: pointer; } - .textbox-help-link { - right: 0; + .textbox-preview-link { + margin-right: 8px; } min-height:36px; } .help_format_text { + display: none !important; position: absolute; bottom: -23px; - right: 40px; + left: 0px; + overflow: hidden; + text-overflow: ellipsis; font-size: 0.85em; @include opacity(0); @include single-transition(all 0.2s ease); @@ -391,15 +395,16 @@ body.ios { top: -5px; position: relative; } - .msg-typing { - min-height: 25px; - display: block; - @include opacity(0.7); - white-space: nowrap; - width: 80%; - overflow: hidden; - text-overflow: ellipsis; - } + } + .msg-typing { + display: block; + @include opacity(0.7); + white-space: nowrap; + margin-bottom: 5px; + overflow: hidden; + font-size: 0.95em; + text-overflow: ellipsis; + height: 20px; } } diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index a9a5727686..06ce17041f 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -573,8 +573,7 @@ .glyphicon-refresh-animate { right: 33px; top: 15px; - color: #fff; - color: rgba(255,255,255,0.5); + color: #aaa; } .form-control { border: none; diff --git a/web/sass-files/sass/partials/_sidebar--left.scss b/web/sass-files/sass/partials/_sidebar--left.scss index 5e7f04724a..44681291c4 100644 --- a/web/sass-files/sass/partials/_sidebar--left.scss +++ b/web/sass-files/sass/partials/_sidebar--left.scss @@ -24,12 +24,6 @@ padding: 1em 1em 0; display: none; } - > div { - height: 100%; - position: absolute; - padding-bottom: 70px; - width: 100%; - } .badge { background-color: $primary-color; position: absolute; diff --git a/web/static/help/Messaging_en.md b/web/static/help/Messaging_en.md deleted file mode 100644 index 2063ad41cc..0000000000 --- a/web/static/help/Messaging_en.md +++ /dev/null @@ -1,47 +0,0 @@ -# Messaging - -### Writing Messages - -You can write messages using the input box with the text "Write a message..." at the bottom of Mattermost. - -Press **ENTER** to send a message. Use **Shift+ENTER** to create a new line without sending a message. - -### Formatting Messages - -Mattermost messages are formatted using a standard called "markdown". Here are examples: - -| Text Entered | How it appears | -|:---------------|:---------------| -|`**bold**`| **bold** | -| `_italic_`|_italic_| -|`[hyperlink](http://mattermost.org)`|[hyperlink](http://mattermost.org)| -|`![embedded image](https://travis-ci.org/mattermost/platform.svg)`|![embedded image](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Emojis provided free from [Emoji One](http://emojione.com/). Check out a full list of Emojis [here](http://emoji.codes/). - - -### Mentioning Teammates - -You can mention a teammate by using the `@` symbol plus their username to send them a special notification to draw their attention. - -For example, you might write: - -``` -@alice how did your interview go with the new candidate? -``` - -Which sends a special mention notification to **alice** to check your message. - -To mention a teammate, press `@` and you should see a list of team members who can be messaged. You can either type their username or use the **Up** and **Down** arrow keys and then **ENTER** to select them to be mentioned. - -You can configure how you'd like to be alerted about mentions of your username, your first name, your nickname, or other keywords from **Account Settings** > **Notifications** and you can set channel-specific preferences from **[Channel Name]** > **Notification Preferences** - -### Messages Dropdown Menu - -To get to the Messages Dropdown Menu, hover over a message and click on the [...] menu. This shows a dropdown list containing additional actions you can perform on a message: - -- **Reply:** Opens up the sidebar so you can reply to a message in a comment thread. -- **Permalink:** Creates a link to the message. Sharing this link with other users in the channel lets them view the linked message in the Message Archives. -- **Delete:** Deletes the message so it is no longer visible. Team Administrators and System Administrators can also delete another user's message. -- **Edit:** Lets you edit your own message. diff --git a/web/static/help/Messaging_es.md b/web/static/help/Messaging_es.md deleted file mode 100644 index d3947f36a1..0000000000 --- a/web/static/help/Messaging_es.md +++ /dev/null @@ -1,37 +0,0 @@ -# Mensajes - -## Escribiendo Mensajes - -Puedes escribir mensajes utilizando el cuadro de texto que dice "Escribe un mensaje..." al final de Mattermost. - -Presiona **RETORNO** para enviar un mensaje. Utiliza **Shift+RETORNO** para crear una nueva linea sin enviar el mensaje. - -## Darle formato a los Mensajes - -Los mensajes de Mattermost se les asigna formato utilizando un estándard que se llama "markdown". Aquí algunos ejemplos: - -| Texto escrito | Como aparece | -|:--------------|:-------------| -|`**negrita**`| **negrita** | -| `_italica_`|_italica_| -|`[hipervinculo](http://mattermost.org)`|[hipervinculo](http://mattermost.org)| -|`![imagen embebida](https://travis-ci.org/mattermost/platform.svg)`|![imagen embebida](https://travis-ci.org/mattermost/platform.svg)| -|`:smile:` `:sheep:` `:alien:`|:smile: :sheep: :alien:| - -Revisa la lista completa de Emojis [aquí](http://www.emoji-cheat-sheet.com/). - -## Mencionando a compañeros - -Puedes mencionar a un compañero al utilizar el simbolo `@` más el nombre de usuario para enviarles una notificación especial que llame su atención. - -Por ejemplo, podrías escribir: - -``` -@alicia como te fue con la entrevista del nuevo candidato? -``` - -Lo cual enviará una notificación especial de mención a **alicia** para que lea tu mensaje. - -Para mencionar un compañero, presiona `@` y podrás ver una lista de los miembros de equipo a quienes puedes mandarles un mensaje. Puedes escribir su nombre de usuario o utilizar las flechas de **Arriba** y **Abajo** y presionar **RETORNO** para seleccionarlos. - -Puedes configurar como te gustaría ser notificado cuando alguien te menciona por nombre de usuario, tu primer nombre, sobrenombre o cualquier otra palabra clave en **Configurar Cuenta** > **Notificaciones** y puedes asignar preferencias especificas para un canal en **[Nombre del Canal]** > **Preferencias de Notificación** diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json index 4c6fa0eae2..2a536925cf 100644 --- a/web/static/i18n/en.json +++ b/web/static/i18n/en.json @@ -1,11 +1,12 @@ { "about.close": "Close", "about.date": "Build Date:", - "about.enterpriseEdition": "Enterprise Edition", + "about.enterpriseEditione1": "Enterprise Edition E1", "about.hash": "Build Hash:", "about.licensed": "Licensed by:", "about.number": "Build Number:", - "about.teamEdtion": "Team Edition", + "about.teamEditiont0": "Team Edition T0", + "about.teamEditiont1": "Team Edition T1", "about.title": "About Mattermost", "about.version": "Version:", "access_history.title": "Access History", @@ -152,14 +153,14 @@ "admin.ldap.bannerDesc": "If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.", "admin.ldap.bannerHeading": "Note:", "admin.ldap.baseDesc": "The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.", - "admin.ldap.baseEx": "Ex \"dc=mydomain,dc=com\"", + "admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "BaseDN:", "admin.ldap.bindPwdDesc": "Password of the user given in \"Bind Username\".", "admin.ldap.bindPwdTitle": "Bind Password:", "admin.ldap.bindUserDesc": "The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.", "admin.ldap.bindUserTitle": "Bind Username:", "admin.ldap.emailAttrDesc": "The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.", - "admin.ldap.emailAttrEx": "Ex \"mail\"", + "admin.ldap.emailAttrEx": "Ex \"mail\" or \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Email Attribute:", "admin.ldap.enableDesc": "When true, Mattermost allows login using LDAP", "admin.ldap.enableTitle": "Enable Login With LDAP:", @@ -191,11 +192,13 @@ "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Username Attribute:", "admin.licence.keyMigration": "If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, disable all Enterprise Edition features on this server. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.", + "admin.license.chooseFile": "Choose File", "admin.license.edition": "Edition: ", "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Designed for enterprise-scale communication.", "admin.license.enterpriseType": "

    This compiled release of Mattermost platform is provided under a commercial license from Mattermost, Inc. based on your subscription level and is subject to the Terms of Service.

    Your subscription details are as follows:

    Name: {name}
    Company or organization name: {company}
    Number of users: {users}
    License issued: {issued}
    Start date of license: {start}
    Expiry date of license: {expires}
    LDAP: {ldap}
    ", "admin.license.key": "License Key: ", "admin.license.keyRemove": "Remove Enterprise License and Downgrade Server", + "admin.license.noFile": "No file uploaded", "admin.license.removing": "Removing License...", "admin.license.teamEdition": "Mattermost Team Edition. Designed for teams from 5 to 50 users.", "admin.license.teamType": "

    This compiled release of Mattermost platform is offered under an MIT license.

    See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.

    ", @@ -658,8 +661,10 @@ "email_signup.find": "Find my teams", "email_verify.almost": "{siteName}: You are almost done", "email_verify.notVerifiedBody": "Please verify your email address. Check your inbox for an email.", + "email_verify.verifyFailed": "Failed to verify your email.", "email_verify.resend": "Resend Email", "email_verify.sent": " Verification email sent.", + "email_verify.failed": " Failed to send verification email.", "email_verify.verified": "{siteName} Email Verified", "email_verify.verifiedBody": "

    Your email has been verified! Click here to log in.

    ", "error_bar.preview_mode": "Preview Mode: Email notifications have not been configured", @@ -755,6 +760,7 @@ "login.or": "or", "login.signTo": "Sign in to:", "login.verified": " Email Verified", + "login.session_expired": " Your session has expired. Please login again.", "login_email.badTeam": "Bad team name", "login_email.email": "Email", "login_email.emailReq": "An email is required", @@ -819,16 +825,16 @@ "navbar_dropdown.teamSettings": "Team Settings", "password_form.change": "Change my password", "password_form.click": "Click here to log in.", - "password_form.enter": "Enter a new password for your {teamDisplayName} {siteName} account.", + "password_form.enter": "Enter a new password for your {siteName} account.", "password_form.error": "Please enter at least {chars} characters.", "password_form.pwd": "Password", "password_form.title": "Password Reset", "password_form.update": "Your password has been updated successfully.", "password_send.checkInbox": "Please check your inbox.", - "password_send.description": "To reset your password, enter the email address you used to sign up for {teamName}.", + "password_send.description": "To reset your password, enter the email address you used to sign up.", "password_send.email": "Email", "password_send.error": "Please enter a valid email address.", - "password_send.link": "

    A password reset link has been sent to {email} for your {teamDisplayName} team on {hostname}.

    ", + "password_send.link": "

    A password reset link has been sent to {email}

    ", "password_send.reset": "Reset my password", "password_send.title": "Password Reset", "post_attachment.collapse": "▲ collapse text", @@ -1300,5 +1306,11 @@ "view_image.loading": "Loading ", "view_image_popover.download": "Download", "view_image_popover.file": "File {count} of {total}", - "view_image_popover.publicLink": "Get Public Link" + "view_image_popover.publicLink": "Get Public Link", + "web.footer.about": "About", + "web.footer.help": "Help", + "web.footer.privacy": "Privacy", + "web.footer.terms": "Terms", + "web.header.back": "Back", + "web.root.singup_info": "All team communication in one place, searchable and accessible anywhere" } diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json index b144708722..f42dc879a6 100644 --- a/web/static/i18n/es.json +++ b/web/static/i18n/es.json @@ -1,11 +1,12 @@ { "about.close": "Cerrar", "about.date": "Fecha de compilación:", - "about.enterpriseEdition": "Edición Enterprise", + "about.enterpriseEditione1": "Edición Enterprise E1", "about.hash": "Hash de compilación:", "about.licensed": "Licenciado por:", "about.number": "Número de compilación:", - "about.teamEdtion": "Edición Team", + "about.teamEditiont0": "Edición Team T0", + "about.teamEditiont1": "Edición Team T1", "about.title": "Acerca de Mattermost", "about.version": "Versión:", "access_history.title": "Historial de Acceso", @@ -152,14 +153,14 @@ "admin.ldap.bannerDesc": "Si el atributo de un usuario cambia en el servidor LDAP será actualizado la próxima vez que el usuario ingrese sus credenciales para iniciar sesión en Mattermost. Esto incluye si un usuario se inactiva o se remueve en el servidor LDAP. Sincronización con servidores LDAP está planificado para futuras versiones.", "admin.ldap.bannerHeading": "Nota:", "admin.ldap.baseDesc": "El DN Base es el Nombre Distinguido de la ubicación donde Mattermost debe comenzar a buscar a los usuarios en el árbol del LDAP.", - "admin.ldap.baseEx": "Ex \"dc=midominio,dc=com\"", + "admin.ldap.baseEx": "Ej \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "DN Base:", "admin.ldap.bindPwdDesc": "Contraseña del usuario asignado en \"Usuario de Enlace\".", "admin.ldap.bindPwdTitle": "Contraseña de Enlace:", "admin.ldap.bindUserDesc": "El usuario que realizará las busquedas LDAP. Normalmente este debería ser una cuenta específicamente creada para ser utilizada por Mattermost. Debería contat con acceso limitado para leer la porción del árbol LDAP especificada en el campo DN Base.", "admin.ldap.bindUserTitle": "Usuario de Enlace:", "admin.ldap.emailAttrDesc": "El atributo en el servidor LDAP que será utilizado para poblar la dirección de correo electrónico de los usuarios en Mattermost.", - "admin.ldap.emailAttrEx": "Ej \"mail\"", + "admin.ldap.emailAttrEx": "Ej \"mail\" o \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Atributo de Correo Electrónico:", "admin.ldap.enableDesc": "Cuando es verdadero, Mattermost permite realizar inicio de sesión utilizando LDAP", "admin.ldap.enableTitle": "Habilitar inicio de sesión con LDAP:", @@ -191,11 +192,13 @@ "admin.ldap.usernameAttrEx": "Ej \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Atributo Usuario:", "admin.licence.keyMigration": "Si estás migrando servidores es posible que necesites remover tu licencia de este servidor para poder instalarlo en un servidor nuevo. Para empezar, deshabilita todas las características de la Edición Enterprise de este servidor. Esta operación habilitará la opción para remover la licencia y degradar este servidor de la Edición Enterprise a la Edición Team.", + "admin.license.chooseFile": "Escoger Archivo", "admin.license.edition": "Edición: ", "admin.license.enterpriseEdition": "Mattermost Edición Enterprise. Diseñada para comunicación de escala empresarial.", "admin.license.enterpriseType": "

    Esta versión compilada de la plataforma de Mattermost es provista bajo una licencia comercial de Mattermost, Inc. en función en su nivel de subscripción y bajo los Términos del Servicio.

    Los detalles de tu subscripción son los siguientes:

    Nombre: {name}
    Nombre compañía u organización: {company}
    Cantidad de usuarios: {users}
    Licencia emitida: {issued}
    Fecha de inicio: {start}
    Fecha de expiración: {expires}
    LDAP: {ldap}
    ", "admin.license.key": "Llave de la Licencia: ", "admin.license.keyRemove": "Remover la Licencia Enterprise y Degradar el Servidor", + "admin.license.noFile": "No se subió ningún archivo", "admin.license.removing": "Removiendo Licencia...", "admin.license.teamEdition": "Mattermost Edición Team. Diseñado para equipos desde 5 hasta 50 usuarios.", "admin.license.teamType": "

    Esta versión compilada de la plataforma de Mattermost es proporcionada bajo la licencia MIT.

    Lea MIT-COMPILED-LICENSE.txt en el directorio raíz de la instalación para más detalles. Lea NOTICES.txt para información sobre software libre utilizado en este sistema.

    ", @@ -819,16 +822,16 @@ "navbar_dropdown.teamSettings": "Configurar Equipo", "password_form.change": "Cambiar mi contraseña", "password_form.click": " Pincha aquí para iniciar sesión.", - "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {teamDisplayName} {siteName}.", + "password_form.enter": "Ingresa una nueva contraseña para tu cuenta en {siteName}.", "password_form.error": "Por favor ingresa al menos {chars} caracteres.", "password_form.pwd": "Contraseña", "password_form.title": "Restablecer Contraseña", "password_form.update": "Tu contraseña ha sido actualizada satisfactoriamente.", "password_send.checkInbox": "Por favor revisa tu bandeja de entrada.", - "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte en {teamName}.", + "password_send.description": "Para restablecer tu contraseña, ingresa la dirección de correo electrónico que utilizaste para registrarte.", "password_send.email": "Correo electrónico", "password_send.error": "Por favor ingresa una dirección correo electrónico válida.", - "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email} para tu equipo {teamDisplayName} en {hostname}.

    ", + "password_send.link": "

    Se ha enviado un enlace para restablecer la contraseña a {email}

    ", "password_send.reset": "Restablecer mi contraseña", "password_send.title": "Restablecer Contraseña", "post_attachment.collapse": "▲ colapsar texto", @@ -1300,5 +1303,11 @@ "view_image.loading": "Cargando ", "view_image_popover.download": "Descargar", "view_image_popover.file": "Archivo {count} de {total}", - "view_image_popover.publicLink": "Obtener Enlace Público" + "view_image_popover.publicLink": "Obtener Enlace Público", + "web.footer.about": "Acerca", + "web.footer.help": "Ayuda", + "web.footer.privacy": "Privacidad", + "web.footer.terms": "Términos", + "web.header.back": "Atrás", + "web.root.singup_info": "Todas las comunicaciones del equipo en un sólo lugar, con búsquedas y accesible desde cualquier parte" } diff --git a/web/static/i18n/pt.json b/web/static/i18n/pt.json index f4b998ded9..f6210a6ae4 100644 --- a/web/static/i18n/pt.json +++ b/web/static/i18n/pt.json @@ -1,11 +1,12 @@ { "about.close": "Fechar", "about.date": "Data De Criação:", - "about.enterpriseEdition": "Enterprise Edition", + "about.enterpriseEditione1": "Enterprise Edition E1", "about.hash": "Hash de Compilação:", "about.licensed": "Licenciado pela:", "about.number": "O Número De Compilação:", - "about.teamEdtion": "Team Edition", + "about.teamEditiont0": "Team Edition T0", + "about.teamEditiont1": "Team Edition T1", "about.title": "Sobre o Mattermost", "about.version": "Versão:", "access_history.title": "Histórico de Acesso", @@ -26,7 +27,7 @@ "admin.email.allowEmailSignInDescription": "Quando verdadeiro, Mattermost permite aos usuários fazer login usando o e-mail e senha.", "admin.email.allowEmailSignInTitle": "Permitir Login Com E-mail: ", "admin.email.allowSignupDescription": "Quando verdadeiro, Mattermost permite a criação de equipe e conta de inscrição através de e-mail e senha. Este valor deve ser falso somente quando você deseja limitar a entrada para o single-sign-on service como OAuth ou LDAP.", - "admin.email.allowSignupTitle": "Permitir Login com E-Mail: ", + "admin.email.allowSignupTitle": "Permitir Inscrição com E-Mail: ", "admin.email.allowUsernameSignInDescription": "Quando verdadeiro, Mattermost permite os usuários fazer login usando seu nome de usuário e senha. Esta configuração é normalmente utilizado apenas quando a verificação de e-mail está desativada.", "admin.email.allowUsernameSignInTitle": "Permitir Login Com Usuário: ", "admin.email.connectionSecurityNone": "Nenhum", @@ -43,7 +44,7 @@ "admin.email.false": "falso", "admin.email.inviteSaltDescription": "32-caracteres salt adicionados a assinatura de convites por e-mail. Aleatoriamente gerados na instalação. Click \"Re-Gerar\" para criar um novo salt.", "admin.email.inviteSaltExample": "Ex \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"", - "admin.email.inviteSaltTitle": "Convidar Salt:", + "admin.email.inviteSaltTitle": "Salt Convite:", "admin.email.notificationDisplayDescription": "Mostra o nome da conta de e-mail usada quando a notificação de e-mail é enviado do Mattermost.", "admin.email.notificationDisplayExample": "Ex: \"Mattermost Notificação\", \"Sistema\", \"Não-Responda\"", "admin.email.notificationDisplayTitle": "Notificação Nome de Exibição:", @@ -152,14 +153,14 @@ "admin.ldap.bannerDesc": "Se um atributo de usuário de mudar no servidor LDAP, ele será atualizado na próxima vez que o usuário inserir suas credenciais para iniciar sessão no Mattermost. Isso inclui se um usuário estiver inativo ou removido de um servidor LDAP. Sincronização com servidores LDAP está prevista para um lançamento futuro.", "admin.ldap.bannerHeading": "Nota:", "admin.ldap.baseDesc": "Base DN é o nome distinto do local onde Mattermost deve começar sua busca para os usuários na árvore LDAP.", - "admin.ldap.baseEx": "Ex \"dc=mydomain,dc=com\"", + "admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"", "admin.ldap.baseTitle": "BaseDN:", "admin.ldap.bindPwdDesc": "Senha do usuário fornecido em \"Bind Username\".", "admin.ldap.bindPwdTitle": "Vincular Senha:", "admin.ldap.bindUserDesc": "O nome de usuário usado para realizar a pesquisa LDAP. Isso deve ser tipicamente uma conta criada especificamente para uso do Mattermost. Deve ter acesso limitado a ler a parte da árvore LDAP especificado no campo BaseDN.", "admin.ldap.bindUserTitle": "Bind Username:", "admin.ldap.emailAttrDesc": "O atributo no servidor LDAP que será usado para preencher os endereços de e-mail de usuários no Mattermost.", - "admin.ldap.emailAttrEx": "Ex \"mail\"", + "admin.ldap.emailAttrEx": "Ex \"mail\" ou \"userPrincipalName\"", "admin.ldap.emailAttrTitle": "Atributo de E-mail:", "admin.ldap.enableDesc": "Quando verdadeiro, Mattermost permite login utilizando LDAP", "admin.ldap.enableTitle": "Ativar Login With LDAP:", @@ -191,11 +192,13 @@ "admin.ldap.usernameAttrEx": "Ex \"sAMAccountName\"", "admin.ldap.usernameAttrTitle": "Atributo do Usuário:", "admin.licence.keyMigration": "Se você estiver migrando seu servidor você pode precisar remover sua chave da licença deste servidor a pedido para instala-la em um novo servidor. Para iniciar, desativar todos os recursos Enterprise Edition deste servidor. Isto irá habilitar para remover a chave da licença e fazer downgrade deste servidor Enterprise Edition para Team Edition.", + "admin.license.chooseFile": "Escolha um Arquivo", "admin.license.edition": "Edição: ", "admin.license.enterpriseEdition": "Mattermost Enterprise Edition. Desenvolvido para escala empresarial de comunicação.", "admin.license.enterpriseType": "

    Esta versão compilada da plataforma Mattermost é fornecida sob a licença comercial para Mattermost, Inc. com base em seu nível de subscrição e está sujeito a Termos de Serviço.

    Os detalhes de sua assinatura, são como segue:

    Nome: {name}
    Nome da Empresa ou organização: {company}
    Número de usuários: {users}
    Licença emitida: {issued}
    Data de Início da licença: {start}
    Data de expiração da licença: {expires}
    LDAP: {ldap}
    ", "admin.license.key": "Chave da Licença: ", "admin.license.keyRemove": "Remover a Licença Enterprise e fazer Downgrade do Servidor", + "admin.license.noFile": "Nenhum arquivo enviado", "admin.license.removing": "Removendo a Licença...", "admin.license.teamEdition": "Mattermost Team Edition. Desenvolvido para equipes de 5 a 50 usuários.", "admin.license.teamType": "

    Esta versão compilada da plataforma Mattermost é oferecido sob uma licença MIT.

    Ver MIT-COMPILED-LICENSE.txt no raiz do diretório de instalação para obter detalhes. Ver NOTICES.txt para obter informações sobre o software open source usados neste sistema.

    ", @@ -205,7 +208,7 @@ "admin.license.uploadDesc": "Enviar uma chave da licença para Mattermost Enterprise Edition para fazer upgrade deste servidor. Visite-nos online para saber mais sobre os beneficios da Enterprise Edition ou para comprar uma chave.", "admin.license.uploading": "Enviando Licença...", "admin.log.consoleDescription": "Normalmente definido como falso em produção. Os desenvolvedores podem definir este campo como verdadeiro para mensagens de log de saída no console baseado na opção de nível de console. Se verdadeiro, o servidor escreve mensagens para o fluxo de saída padrão (stdout).", - "admin.log.consoleTitle": "Log Do Console: ", + "admin.log.consoleTitle": "Log Para o Console: ", "admin.log.false": "falso", "admin.log.fileDescription": "Normalmente definido como verdadeiro em produção. Quando verdadeiro, arquivos de log são gravados no arquivo de log especificado no campo de localização abaixo.", "admin.log.fileLevelDescription": "Esta configuração determina o nível de detalhe que são gravados no log de eventos no console. ERROR: Saídas somente mensagens de erro. INFO: Saídas de mensagens de erro e informações em torno de inicialização. DEBUG: Impressões de alto detalhe para desenvolvedores que trabalham na depuração de problemas.", @@ -277,6 +280,9 @@ "admin.service.attemptTitle": "Máxima Tentativas de Login:", "admin.service.cmdsDesc": "Quando verdadeiro, comandos slash criados por usuários serão permitidos.", "admin.service.cmdsTitle": "Ativar Comandos Slash: ", + "admin.service.corsDescription": "Ativar requisição de origem HTTP Cross dos domínios especificados (separados por espaço). Usar \"*\" se você quiser permitir CORS de qualquer domínio ou deixe em branco para desativar.", + "admin.service.corsEx": "http://example.com https://example.com", + "admin.service.corsTitle": "Permitir Requisição Cross-origin de:", "admin.service.developerDesc": "(Opção dos desenvolvedores) Quando verdadeira, a informação extra em torno dos erros será exibida na UI.", "admin.service.developerTitle": "Ativar o Modo Desenvolvedor: ", "admin.service.false": "falso", @@ -558,6 +564,7 @@ "channel_loader.wrote": " escreveu: ", "channel_members_modal.addNew": " Adicionar Novos Membros", "channel_members_modal.close": "Fechar", + "channel_members_modal.remove": "Remover", "channel_memebers_modal.members": " Membros", "channel_modal.cancel": "Cancelar", "channel_modal.channel": "Canal", @@ -589,6 +596,7 @@ "choose_auth_page.find": "Encontrar minhas equipes", "choose_auth_page.gitlabCreate": "Criar uma equipe com uma conta GitLab", "choose_auth_page.googleCreate": "Criar nova equipe com a Conta do Google Apps", + "choose_auth_page.ldapCreate": "Criar uma nova equipe com uma conta LDAP", "choose_auth_page.noSignup": "Nenhum método de inscrição configurado, por favor contate seu administrador do sistema.", "claim.account.noEmail": "Nenhum email específicado", "claim.email_to_sso.enterPwd": "Entre a senha para o sua conta {team} {site}", @@ -665,8 +673,8 @@ "file_upload.filesAbove": "Arquivos acima {max}MB não podem ser enviados: {filenames}", "file_upload.limited": "Limite máximo de uploads de {count} arquivos. Por favor use um post adicional para mais arquivos.", "file_upload.pasted": "Imagem Colada em ", - "filtered_user_list.count": "{count, number} {count, plural, one {Membro} other {Membros}}", - "filtered_user_list.countTotal": "{count, number} {count, plural, one {Membro} other {Membros}} de {total} Total", + "filtered_user_list.count": "{count, number} {count, plural, one {membro} other {membros}}", + "filtered_user_list.countTotal": "{count, number} {count, plural, one {membro} other {membros}} de {total} Total", "filtered_user_list.search": "Procurar membros", "find_team.email": "E-mail", "find_team.findDescription": "Foi enviado um e-mail com links para todas as equipes do qual você é membro.", @@ -690,7 +698,7 @@ "general_tab.regenerate": "Re-Gerar", "general_tab.required": "Este campo é obrigatório", "general_tab.teamName": "Nome da Equipe", - "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela inicial e no topo na lateral esquerda.", + "general_tab.teamNameInfo": "Defina o nome da equipe como aparece na sua tela de login e no topo na lateral esquerda.", "general_tab.title": "Definições Gerais", "general_tab.yes": "Sim", "get_link.clipboard": " Link copiado para a área de transferência.", @@ -698,6 +706,7 @@ "get_link.copy": "Copiar Link", "get_post_link_modal.help": "O link abaixo permite que usuários autorizados possam ver seus posts.", "get_post_link_modal.title": "Copiar Permalink", + "get_team_invite_link_modal.help": "Enviar o link abaixo para sua equipe de trabalho para que eles se inscrevam no site da sua equipe. O Link de Convite de Equipe como ele não muda pode ser compartilhado com várias pessoas ao menos que seja re-gerado em Configurações de Equipe pelo Administrador de Equipe.", "get_team_invite_link_modal.helpDisabled": "Criação de usuários está desabilitada para sua equipe. Por favor peça ao administrador de equipe por detalhes.", "get_team_invite_link_modal.title": "Link para Convite de Equipe", "intro_messages.DM": "Este é o início do seu histórico de mensagens diretas com {teammate}.
    Mensagens diretas e arquivos compartilhados aqui não são mostrados para pessoas de fora desta área.", @@ -731,6 +740,11 @@ "invite_member.send2": "Enviar Convites", "invite_member.sending": " Enviando", "invite_member.teamInviteLink": "Você também pode convidar pessoas usando o {link}", + "ldap_signup.find": "Encontrar minhas equipes", + "ldap_signup.ldap": "Criar uma equipe com uma conta LDAP", + "ldap_signup.length_error": "O nome deve ser de 3 ou mais caracteres até um máximo de 15", + "ldap_signup.teamName": "Entre o nome da nova equipe", + "ldap_signup.team_error": "Por favor entre o nome da equipe", "loading_screen.loading": "Carregando", "login.changed": " Método de login alterada com sucesso", "login.create": "Crie um agora", @@ -742,7 +756,7 @@ "login.noAccount": "Não tem uma conta? ", "login.on": "no {siteName}", "login.or": "ou", - "login.signTo": "Entrar em:", + "login.signTo": "Login em:", "login.verified": " Email Verificado", "login_email.badTeam": "Nome ruim de equipe", "login_email.email": "E-mail", @@ -766,6 +780,7 @@ "login_username.verifyEmailError": "Por favor verifique seu endereço de email. Verifique por um email na sua caixa de entrada.", "member_item.makeAdmin": "Tornar Admin", "member_item.member": "Membro", + "member_list.noUsersAdd": "Nenhum usuário para adicionar.", "members_popover.msg": "Mensagem", "members_popover.title": "Membros", "more_channels.close": "Fechar", @@ -807,16 +822,15 @@ "navbar_dropdown.teamSettings": "Configurações da Equipe", "password_form.change": "Alterar minha senha", "password_form.click": "Clique aqui para logar.", - "password_form.enter": "Entre uma nova senha para sua conta {teamDisplayName} {siteName}.", + "password_form.enter": "Entre uma nova senha para sua conta {siteName}.", "password_form.error": "Por favor, insira pelo menos {chars} caracteres.", "password_form.pwd": "Senha", "password_form.title": "Resetar Senha", "password_form.update": "Sua senha foi atualizada com sucesso.", "password_send.checkInbox": "Por favor verifique sua caixa de entrada.", - "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever em {teamName}.", + "password_send.description": "Para resetar sua senha, entre o endereço de email que você usou para se inscrever.", "password_send.email": "E-mail", "password_send.error": "Por favor entre um endereço de e-mail válido.", - "password_send.link": "

    Um link para resetar a sua senha na equipe {teamDisplayName} em {hostname} foi enviado para {email}.

    ", "password_send.reset": "Resetar minha senha", "password_send.title": "Resetar Senha", "post_attachment.collapse": "▲ recolher texto", @@ -936,7 +950,7 @@ "signup_user_completed.chooseUser": "Escolha o seu nome de usuário", "signup_user_completed.create": "Criar Conta", "signup_user_completed.emailHelp": "Email valido necessário para inscrição", - "signup_user_completed.emailIs": "Seu endereço de e-mail é {email}. Você irá usar esse endereço para entrar em {siteName}.", + "signup_user_completed.emailIs": "Seu endereço de e-mail é {email}. Você irá usar esse endereço para logar no {siteName}.", "signup_user_completed.expired": "Você já concluiu o processo de inscrição para este convite ou este convite expirou.", "signup_user_completed.gitlab": "com GitLab", "signup_user_completed.google": "com Google", @@ -960,6 +974,7 @@ "sso_signup.team_error": "Por favor entre o nome da equipe", "suggestion.mention.all": "Notificar todo mundo na equipe", "suggestion.mention.channel": "Notifica todos no canal", + "suggestion.search.private": "Grupos Privados", "suggestion.search.public": "Canais Públicos", "team_export_tab.download": "download", "team_export_tab.export": "Exportar", @@ -999,7 +1014,7 @@ "team_signup_display_name.required": "Este campo é obrigatório", "team_signup_display_name.teamName": "Nome Da Equipe", "team_signup_email.address": "Endereço de E-mail", - "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado no login", + "team_signup_email.different": "Por favor, use um e-mail diferente do que o usado na inscrição", "team_signup_email.validEmail": "Por favor entre um endereço de e-mail válido", "team_signup_password.agreement": "Ao prosseguir para criar sua conta e usar {siteName}, você concorda com nosso Termo de Serviço e Politica de Privacidade. Se você não concorda, você não pode usar {siteName}.", "team_signup_password.back": "Voltar para o passo anterior", @@ -1052,9 +1067,9 @@ "textbox.help": "Ajuda", "textbox.inlinecode": "`código`", "textbox.italic": "_itálico_", - "textbox.preformatted": "```pre-formatado```", + "textbox.preformatted": "```pré-formatado```", "textbox.preview": "Pré-visualização", - "textbox.quote": ">citado", + "textbox.quote": ">citar", "textbox.strike": "tachado", "tutorial_intro.allSet": "Está tudo pronto", "tutorial_intro.end": "Clique em “Próximo” para entrar Town Square. Este é o primeiro canal que sua equipe de trabalho vê quando eles se inscrevem. Use para postar atualizações que todos precisam saber.", @@ -1272,7 +1287,7 @@ "user.settings.security.logoutActiveSessions": "Ver e fazer Logout das Sessões Ativas", "user.settings.security.method": "Método de Login", "user.settings.security.newPassword": "Nova Senha", - "user.settings.security.oneSignin": "Você pode ter somente um método de inscrição por vez. Trocando o método de inscrição será enviado um email de notificação se você alterar com sucesso.", + "user.settings.security.oneSignin": "Você pode ter somente um método de login por vez. Trocando o método de login será enviado um email de notificação se você alterar com sucesso.", "user.settings.security.password": "Senha", "user.settings.security.passwordLengthError": "Novas senhas precisam ter pelo menos {chars} characters", "user.settings.security.passwordMatchError": "As novas senhas que você inseriu não correspondem", diff --git a/web/static/images/themes/code_themes/solarized_dark.png b/web/static/images/themes/code_themes/solarized-dark.png similarity index 100% rename from web/static/images/themes/code_themes/solarized_dark.png rename to web/static/images/themes/code_themes/solarized-dark.png diff --git a/web/static/images/themes/code_themes/solarized_light.png b/web/static/images/themes/code_themes/solarized-light.png similarity index 100% rename from web/static/images/themes/code_themes/solarized_light.png rename to web/static/images/themes/code_themes/solarized-light.png diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html deleted file mode 100644 index 08c90493ee..0000000000 --- a/web/templates/admin_console.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "admin_console"}} - - -{{template "head" . }} - - - -
    - - - - -{{end}} diff --git a/web/templates/channel.html b/web/templates/channel.html deleted file mode 100644 index 94d79a0227..0000000000 --- a/web/templates/channel.html +++ /dev/null @@ -1,21 +0,0 @@ - -{{define "channel"}} - - -{{template "head" . }} - -
    - - - -{{end}} diff --git a/web/templates/claim_account.html b/web/templates/claim_account.html deleted file mode 100644 index 2a9126d1be..0000000000 --- a/web/templates/claim_account.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "claim_account"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/docs.html b/web/templates/docs.html deleted file mode 100644 index dc18e5cb69..0000000000 --- a/web/templates/docs.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "docs"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/find_team.html b/web/templates/find_team.html deleted file mode 100644 index b7e1d7eca0..0000000000 --- a/web/templates/find_team.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "find_team"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/footer.html b/web/templates/footer.html deleted file mode 100644 index 5b11328fba..0000000000 --- a/web/templates/footer.html +++ /dev/null @@ -1,39 +0,0 @@ -{{define "footer"}} - - -{{end}} diff --git a/web/templates/head.html b/web/templates/head.html deleted file mode 100644 index 61b1aa12bd..0000000000 --- a/web/templates/head.html +++ /dev/null @@ -1,191 +0,0 @@ -{{define "head"}} - - - - - - {{ .Props.Title }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{{end}} diff --git a/web/templates/home.html b/web/templates/home.html deleted file mode 100644 index 08876d41d0..0000000000 --- a/web/templates/home.html +++ /dev/null @@ -1,24 +0,0 @@ -{{define "home"}} - - -{{template "head" . }} - -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    - - - -{{end}} diff --git a/web/templates/login.html b/web/templates/login.html deleted file mode 100644 index 88540a9068..0000000000 --- a/web/templates/login.html +++ /dev/null @@ -1,27 +0,0 @@ -{{define "login"}} - - -{{template "head" . }} - -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/password_reset.html b/web/templates/password_reset.html deleted file mode 100644 index e68f8b693a..0000000000 --- a/web/templates/password_reset.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "password_reset"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team.html b/web/templates/signup_team.html deleted file mode 100644 index afba580662..0000000000 --- a/web/templates/signup_team.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team"}} - - -{{template "head" . }} - -
    -
    -
    -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_complete.html b/web/templates/signup_team_complete.html deleted file mode 100644 index 3873d8978d..0000000000 --- a/web/templates/signup_team_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_team_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_team_confirm.html b/web/templates/signup_team_confirm.html deleted file mode 100644 index 31f1ba95bb..0000000000 --- a/web/templates/signup_team_confirm.html +++ /dev/null @@ -1,26 +0,0 @@ -{{define "signup_team_confirm"}} - - -{{template "head" . }} - -
    -
    - - -
    -
    - - - -{{end}} diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html deleted file mode 100644 index 937a89dd2c..0000000000 --- a/web/templates/signup_user_complete.html +++ /dev/null @@ -1,29 +0,0 @@ -{{define "signup_user_complete"}} - - -{{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/templates/verify.html b/web/templates/verify.html deleted file mode 100644 index 2e5496d7a8..0000000000 --- a/web/templates/verify.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "verify"}} - - - {{template "head" . }} - -
    -
    -
    - -
    - -
    - -
    - -
    -
    - - - -{{end}} diff --git a/web/web.go b/web/web.go index 09450b9767..2a44ece00c 100644 --- a/web/web.go +++ b/web/web.go @@ -4,67 +4,16 @@ package web import ( - "fmt" + "net/http" + "strings" + l4g "github.com/alecthomas/log4go" - "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" - "gopkg.in/fsnotify.v1" - "html/template" - "net/http" - "net/url" - "strconv" - "strings" ) -var Templates *template.Template - -type HtmlTemplatePage api.Page - -func NewHtmlTemplatePage(templateName string, title string, locale string) *HtmlTemplatePage { - - if len(title) > 0 { - title = utils.Cfg.TeamSettings.SiteName + " - " + title - } - - props := make(map[string]string) - props["Title"] = title - return &HtmlTemplatePage{ - TemplateName: templateName, - Props: props, - ClientCfg: utils.ClientCfg, - ClientLicense: utils.ClientLicense, - Locale: locale, - } -} - -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.Locale = me.User.Locale - } - - me.Props["Locale"] = me.Locale - me.SessionTokenIndex = c.SessionTokenIndex - - me.ClientCfg["HeaderBack"] = c.T("web.header.back") - me.ClientCfg["FooterHelp"] = c.T("web.footer.help") - me.ClientCfg["FooterTerms"] = c.T("web.footer.terms") - me.ClientCfg["FooterPrivacy"] = c.T("web.footer.privacy") - me.ClientCfg["FooterAbout"] = c.T("web.footer.about") - - if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil { - c.SetUnknownError(me.TemplateName, err.Error()) - } -} - func InitWeb() { l4g.Debug(utils.T("web.init.debug")) @@ -74,81 +23,7 @@ func InitWeb() { l4g.Debug("Using static directory at %v", staticDir) mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) - mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") - mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET") - mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST") - - mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") - mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") - mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") - mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") - mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") // Remove after a few releases (~1.8) - mainrouter.Handle("/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(completeOAuth)).Methods("GET") - - mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}", api.UserRequired(adminConsole)).Methods("GET") - mainrouter.Handle("/admin_console/{tab:[A-Za-z0-9-_]+}/{team:[A-Za-z0-9-]*}", api.UserRequired(adminConsole)).Methods("GET") - - mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST") - - mainrouter.Handle("/docs/{doc:[A-Za-z0-9]+}", api.AppHandlerIndependent(docs)).Methods("GET") - - // ---------------------------------------------------------------------------------------------- - // *ANYTHING* team specific should go below this line - // ---------------------------------------------------------------------------------------------- - - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") - 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:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/claim", api.AppHandler(claimAccount)).Methods("GET") - mainrouter.Handle("/{team}/pl/{postid}", api.AppHandler(postPermalink)).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() -} - -func watchAndParseTemplates() { - - templatesDir := utils.FindDir("web/templates") - l4g.Debug(utils.T("web.parsing_templates.debug"), templatesDir) - var err error - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - l4g.Error(utils.T("web.create_dir.error"), err) - } - - go func() { - for { - select { - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write { - l4g.Info(utils.T("web.reparse_templates.info"), event.Name) - if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { - l4g.Error(utils.T("web.parsing_templates.error"), err) - } - } - case err := <-watcher.Errors: - l4g.Error(utils.T("web.dir_fail.error"), err) - } - } - }() - - err = watcher.Add(templatesDir) - if err != nil { - l4g.Error(utils.T("web.watcher_fail.error"), err) - } + mainrouter.Handle("/{anything:.*}", api.AppHandlerIndependent(root)).Methods("GET") } var browsersNotSupported string = "MSIE/8;MSIE/9;MSIE/10;Internet Explorer/8;Internet Explorer/9;Internet Explorer/10;Safari/7;Safari/8" @@ -177,1026 +52,9 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { return } - if len(c.Session.UserId) == 0 { - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Props["Info"] = c.T("web.root.singup_info") - - if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil { - c.Err = result.Err - return - } else { - teams := result.Data.([]*model.Team) - for _, team := range teams { - page.Props[team.Name] = team.DisplayName - } - - if len(teams) == 1 && *utils.Cfg.TeamSettings.EnableTeamListing && !utils.Cfg.TeamSettings.EnableTeamCreation { - http.Redirect(w, r, c.GetSiteURL()+"/"+teams[0].Name, http.StatusTemporaryRedirect) - return - } - } - - 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", c.T("web.root.home_title"), c.Locale) - page.Team = team - page.User = user - page.Render(c, w) + page := utils.NewHTMLTemplate("root", c.Locale) + page.Props["Title"] = c.T("web.root.home_title") + if err := page.RenderToWriter(w); err != nil { + c.SetUnknownError(page.TemplateName, err.Error()) } } - -func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !CheckBrowserCompatability(c, r) { - return - } - - page := NewHtmlTemplatePage("signup_team", c.T("web.root.singup_title"), c.Locale) - page.Render(c, w) -} - -func login(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - params := mux.Vars(r) - teamName := params["team"] - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.login.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - // We still might be able to switch to this team because we've logged in before - _, session := api.FindMultiSessionForTeamId(r, team.Id) - if session != nil { - w.Header().Set(model.HEADER_TOKEN, session.Token) - lastViewChannelName := "town-square" - if lastViewResult := <-api.Srv.Store.Preference().Get(session.UserId, model.PREFERENCE_CATEGORY_LAST, model.PREFERENCE_NAME_LAST_CHANNEL); lastViewResult.Err == nil { - if lastViewChannelResult := <-api.Srv.Store.Channel().Get(lastViewResult.Data.(model.Preference).Value); lastViewChannelResult.Err == nil { - lastViewChannelName = lastViewChannelResult.Data.(*model.Channel).Name - } - } - - http.Redirect(w, r, c.GetSiteURL()+"/"+team.Name+"/channels/"+lastViewChannelName, http.StatusTemporaryRedirect) - return - } - - page := NewHtmlTemplatePage("login", c.T("web.login.login_title"), c.Locale) - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - if team.AllowOpenInvite { - page.Props["InviteId"] = team.InviteId - } - - page.Render(c, w) -} - -func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) { - email := r.FormValue("email") - - page := NewHtmlTemplatePage("signup_team_confirm", c.T("web.signup_team_confirm.title"), c.Locale) - page.Props["Email"] = email - page.Render(c, w) -} - -func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - data := r.FormValue("d") - hash := r.FormValue("h") - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*24*30 { // 30 days - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_team_complete.link_expired.app_error", nil, "") - return - } - - page := NewHtmlTemplatePage("signup_team_complete", c.T("web.signup_team_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { - - id := r.FormValue("id") - data := r.FormValue("d") - hash := r.FormValue("h") - var props map[string]string - - if len(id) > 0 { - props = make(map[string]string) - - if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil { - c.Err = result.Err - return - } else { - team := result.Data.(*model.Team) - if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) { - c.Err = model.NewLocAppError("signupUserComplete", "web.signup_user_complete.no_invites.app_error", nil, "id="+id) - return - } - - props["email"] = "" - props["display_name"] = team.DisplayName - props["name"] = team.Name - props["id"] = team.Id - data = model.MapToJson(props) - hash = "" - } - } else { - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_invalid.app_error", nil, "") - return - } - - props = model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour - c.Err = model.NewLocAppError("signupTeamComplete", "web.signup_user_complete.link_expired.app_error", nil, "") - return - } - } - - page := NewHtmlTemplatePage("signup_user_complete", c.T("web.signup_user_complete.title"), c.Locale) - page.Props["Email"] = props["email"] - page.Props["TeamDisplayName"] = props["display_name"] - page.Props["TeamName"] = props["name"] - page.Props["TeamId"] = props["id"] - page.Props["Data"] = data - page.Props["Hash"] = hash - page.Render(c, w) -} - -func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { - api.Logout(c, w, r) - http.Redirect(w, r, c.GetTeamURL(), http.StatusTemporaryRedirect) -} - -func postPermalink(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - teamName := params["team"] - postId := params["postid"] - - if len(postId) != 26 { - c.Err = model.NewLocAppError("postPermalink", "web.post_permalink.app_error", nil, "id="+postId) - return - } - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var post *model.Post - if result := <-api.Srv.Store.Post().Get(postId); result.Err != nil { - c.Err = result.Err - return - } else { - postlist := result.Data.(*model.PostList) - post = postlist.Posts[postlist.Order[0]] - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - if result.Data.(int64) == 0 { - if channel = autoJoinChannelId(c, w, r, post.ChannelId); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(post.ChannelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, post.Id) -} - -func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - name := params["channelname"] - teamName := params["team"] - - team := checkSessionSwitch(c, w, r, teamName) - if team == nil { - // Error already set by getTeam - return - } - - var channel *model.Channel - if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { - c.Err = result.Err - return - } else { - channelId := result.Data.(string) - if len(channelId) == 0 { - if channel = autoJoinChannelName(c, w, r, name); channel == nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return - } - } else { - if result := <-api.Srv.Store.Channel().Get(channelId); result.Err != nil { - c.Err = result.Err - return - } else { - channel = result.Data.(*model.Channel) - } - } - } - - doLoadChannel(c, w, r, team, channel, "") -} - -func autoJoinChannelName(c *api.Context, w http.ResponseWriter, r *http.Request, channelName string) *model.Channel { - if strings.Index(channelName, "__") > 0 { - // It's a direct message channel that doesn't exist yet so let's create it - ids := strings.Split(channelName, "__") - otherUserId := "" - if ids[0] == c.Session.UserId { - otherUserId = ids[1] - } else { - otherUserId = ids[0] - } - - if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil { - api.Handle404(w, r) - return nil - } else { - return sc - } - } else { - // We will attempt to auto-join open channels - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().GetByName(c.Session.TeamId, channelName)) - } - - return nil -} - -func autoJoinChannelId(c *api.Context, w http.ResponseWriter, r *http.Request, channelId string) *model.Channel { - return joinOpenChannel(c, w, r, api.Srv.Store.Channel().Get(channelId)) -} - -func joinOpenChannel(c *api.Context, w http.ResponseWriter, r *http.Request, channel store.StoreChannel) *model.Channel { - if cr := <-channel; cr.Err != nil { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } else { - channel := cr.Data.(*model.Channel) - if channel.Type == model.CHANNEL_OPEN { - api.JoinChannel(c, channel.Id, "") - if c.Err != nil { - return nil - } - } else { - http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) - return nil - } - return channel - } -} - -func checkSessionSwitch(c *api.Context, w http.ResponseWriter, r *http.Request, teamName string) *model.Team { - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return nil - } 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 - } - } - - return team -} - -func doLoadChannel(c *api.Context, w http.ResponseWriter, r *http.Request, team *model.Team, channel *model.Channel, postid string) { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - prefChan := api.Srv.Store.Preference().GetAll(c.Session.UserId) - - var user *model.User - if ur := <-userChan; ur.Err != nil { - c.Err = ur.Err - c.RemoveSessionCookie(w, r) - l4g.Error(utils.T("web.do_load_channel.error"), c.Session.UserId) - return - } else { - user = ur.Data.(*model.User) - } - - var preferences model.Preferences - if result := <-prefChan; result.Err != nil { - l4g.Error("Error in getting preferences for id=%v", c.Session.UserId) - } else { - preferences = result.Data.(model.Preferences) - } - - page := NewHtmlTemplatePage("channel", "", c.Locale) - page.Props["Title"] = channel.DisplayName + " - " + team.DisplayName + " " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["ChannelName"] = channel.Name - page.Props["ChannelId"] = channel.Id - page.Props["PostId"] = postid - page.Team = team - page.User = user - page.Channel = channel - page.Preferences = &preferences - page.Render(c, w) -} - -func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { - resend := r.URL.Query().Get("resend") - resendSuccess := r.URL.Query().Get("resend_success") - name := r.URL.Query().Get("teamname") - email := r.URL.Query().Get("email") - hashedId := r.URL.Query().Get("hid") - userId := r.URL.Query().Get("uid") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if resend == "true" { - if result := <-api.Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { - c.Err = result.Err - return - } else { - user := result.Data.(*model.User) - - if user.LastActivityAt > 0 { - api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } else { - api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - } - - newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) - http.Redirect(w, r, newAddress, http.StatusFound) - return - } - } - - if len(userId) == 26 && len(hashedId) != 0 && model.ComparePassword(hashedId, userId) { - if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { - return - } else { - c.LogAudit("Email Verified") - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+name+"/login?extra=verified&email="+url.QueryEscape(email), http.StatusTemporaryRedirect) - return - } - } - - page := NewHtmlTemplatePage("verify", c.T("web.email_verified.title"), c.Locale) - page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) - page.Props["UserEmail"] = email - page.Props["ResendSuccess"] = resendSuccess - page.Render(c, w) -} - -func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) { - page := NewHtmlTemplatePage("find_team", c.T("web.find_team.title"), c.Locale) - page.Render(c, w) -} - -func docs(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - doc := params["doc"] - - var user *model.User - if len(c.Session.UserId) != 0 { - userChan := api.Srv.Store.User().Get(c.Session.UserId) - if userChan := <-userChan; userChan.Err == nil { - user = userChan.Data.(*model.User) - } - } - - page := NewHtmlTemplatePage("docs", c.T("web.doc.title"), c.Locale) - page.Props["Site"] = doc - page.User = user - page.Render(c, w) -} - -func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { - isResetLink := true - hash := r.URL.Query().Get("h") - data := r.URL.Query().Get("d") - params := mux.Vars(r) - teamName := params["team"] - - if len(hash) == 0 || len(data) == 0 { - isResetLink = false - } else { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) { - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.invalid_link.app_error", nil, "") - return - } - - props := model.MapFromJson(strings.NewReader(data)) - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour - c.Err = model.NewLocAppError("resetPassword", "web.reset_password.expired_link.app_error", nil, "") - return - } - } - - teamDisplayName := "Developer/Beta" - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) - } - - if team != nil { - teamDisplayName = team.DisplayName - } - - page := NewHtmlTemplatePage("password_reset", "", c.Locale) - page.Props["Title"] = "Reset Password " + page.ClientCfg["SiteName"] - page.Props["TeamDisplayName"] = teamDisplayName - page.Props["TeamName"] = teamName - page.Props["Hash"] = hash - page.Props["Data"] = data - page.Props["TeamName"] = teamName - page.Props["IsReset"] = strconv.FormatBool(isResetLink) - page.Render(c, w) -} - -func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - - if !utils.Cfg.TeamSettings.EnableUserCreation { - c.Err = model.NewLocAppError("signupTeam", "web.singup_with_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - hash := r.URL.Query().Get("h") - - var team *model.Team - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - if api.IsVerifyHashRequired(nil, team, hash) { - data := r.URL.Query().Get("d") - props := model.MapFromJson(strings.NewReader(data)) - - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_link.app_error", nil, "") - return - } - - t, err := strconv.ParseInt(props["time"], 10, 64) - if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.expired_link.app_error", nil, "") - return - } - - if team.Id != props["id"] { - c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.invalid_team.app_error", nil, data) - return - } - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SIGNUP - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func completeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - - code := r.URL.Query().Get("code") - state := r.URL.Query().Get("state") - - uri := c.GetSiteURL() + "/signup/" + service + "/complete" // Remove /signup after a few releases (~1.8) - - if body, team, props, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { - c.Err = err - return - } else { - action := props["action"] - switch action { - case model.OAUTH_ACTION_SIGNUP: - api.CreateOAuthUser(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_LOGIN: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - case model.OAUTH_ACTION_EMAIL_TO_SSO: - api.CompleteSwitchWithOAuth(c, w, r, service, body, team, props["email"]) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/login?extra=signin_change", http.StatusTemporaryRedirect) - } - break - case model.OAUTH_ACTION_SSO_TO_EMAIL: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host+"/"+team.Name+"/"+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect) - } - break - default: - api.LoginByOAuth(c, w, r, service, body, team) - if c.Err == nil { - root(c, w, r) - } - break - } - } -} - -func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - params := mux.Vars(r) - service := params["service"] - teamName := params["team"] - loginHint := r.URL.Query().Get("login_hint") - - if len(teamName) == 0 { - c.Err = model.NewLocAppError("loginWithOAuth", "web.login_with_oauth.invalid_team.app_error", nil, "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - // Make sure team exists - if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - return - } - - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_LOGIN - - if authUrl, err := api.GetAuthorizationCode(c, service, teamName, stateProps, loginHint); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } -} - -func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { - - if !c.HasSystemAdminPermissions("adminConsole") { - 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", c.T("web.admin_console.title"), c.Locale) - page.User = user - page.Team = team - page.Props["ActiveTab"] = activeTab - page.Props["TeamId"] = teamId - page.Render(c, w) -} - -func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - if !CheckBrowserCompatability(c, r) { - return - } - - responseType := r.URL.Query().Get("response_type") - clientId := r.URL.Query().Get("client_id") - redirect := r.URL.Query().Get("redirect_uri") - scope := r.URL.Query().Get("scope") - state := r.URL.Query().Get("state") - - if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { - c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { - c.Err = result.Err - return - } else { - app = result.Data.(*model.OAuthApp) - } - - var team *model.Team - if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { - c.Err = result.Err - return - } else { - team = result.Data.(*model.Team) - } - - page := NewHtmlTemplatePage("authorize", c.T("web.authorize_oauth.title"), c.Locale) - page.Props["TeamName"] = team.Name - page.Props["AppName"] = app.Name - page.Props["ResponseType"] = responseType - page.Props["ClientId"] = clientId - page.Props["RedirectUri"] = redirect - page.Props["Scope"] = scope - page.Props["State"] = state - page.Render(c, w) -} - -func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - c.LogAudit("attempt") - - r.ParseForm() - - grantType := r.FormValue("grant_type") - if grantType != model.ACCESS_TOKEN_GRANT_TYPE { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "") - return - } - - clientId := r.FormValue("client_id") - if len(clientId) != 26 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "") - return - } - - secret := r.FormValue("client_secret") - if len(secret) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "") - return - } - - code := r.FormValue("code") - if len(code) == 0 { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "") - return - } - - redirectUri := r.FormValue("redirect_uri") - - achan := api.Srv.Store.OAuth().GetApp(clientId) - tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code) - - authData := api.GetAuthData(code) - - if authData == nil { - c.LogAudit("fail - invalid auth code") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - uchan := api.Srv.Store.User().Get(authData.UserId) - - if authData.IsExpired() { - c.LogAudit("fail - auth code expired") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - if authData.RedirectUri != redirectUri { - c.LogAudit("fail - redirect uri provided did not match previous redirect uri") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "") - return - } - - if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { - c.LogAudit("fail - auth code is invalid") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "") - return - } - - var app *model.OAuthApp - if result := <-achan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } else { - app = result.Data.(*model.OAuthApp) - } - - if !model.ComparePassword(app.ClientSecret, secret) { - c.LogAudit("fail - invalid client credentials") - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "") - return - } - - callback := redirectUri - if len(callback) == 0 { - callback = app.CallbackUrls[0] - } - - if result := <-tchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "") - return - } else if result.Data != nil { - c.LogAudit("fail - auth code has been used previously") - accessData := result.Data.(*model.AccessData) - - // Revoke access token, related auth code, and session from DB as well as from cache - if err := api.RevokeAccessToken(accessData.Token); err != nil { - l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message) - } - - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "") - return - } - - var user *model.User - if result := <-uchan; result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "") - return - } else { - user = result.Data.(*model.User) - } - - session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} - - if result := <-api.Srv.Store.Session().Save(session); result.Err != nil { - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "") - return - } else { - session = result.Data.(*model.Session) - api.AddSessionToCache(session) - } - - accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} - - if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { - l4g.Error(result.Err) - c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "") - return - } - - accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)} - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-store") - w.Header().Set("Pragma", "no-cache") - - c.LogAuditWithUserId(user.Id, "success") - - w.Write([]byte(accessRsp.ToJson())) -} - -func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.disabled.app_error", nil, "") - c.Err.StatusCode = http.StatusNotImplemented - return - } - - params := mux.Vars(r) - id := params["id"] - - hchan := api.Srv.Store.Webhook().GetIncoming(id) - - r.ParseForm() - - var parsedRequest *model.IncomingWebhookRequest - contentType := r.Header.Get("Content-Type") - if strings.Split(contentType, "; ")[0] == "application/json" { - parsedRequest = model.IncomingWebhookRequestFromJson(r.Body) - } else { - parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload"))) - } - - if parsedRequest == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "") - return - } - - text := parsedRequest.Text - if len(text) == 0 && parsedRequest.Attachments == nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.text.app_error", nil, "") - return - } - - channelName := parsedRequest.ChannelName - webhookType := parsedRequest.Type - - //attachments is in here for slack compatibility - if parsedRequest.Attachments != nil { - if len(parsedRequest.Props) == 0 { - parsedRequest.Props = make(model.StringInterface) - } - parsedRequest.Props["attachments"] = parsedRequest.Attachments - webhookType = model.POST_SLACK_ATTACHMENT - } - - var hook *model.IncomingWebhook - if result := <-hchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.invalid.app_error", nil, "err="+result.Err.Message) - return - } else { - hook = result.Data.(*model.IncomingWebhook) - } - - var channel *model.Channel - var cchan store.StoreChannel - - if len(channelName) != 0 { - if channelName[0] == '@' { - if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message) - return - } else { - channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId) - } - } else if channelName[0] == '#' { - channelName = channelName[1:] - } - - cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName) - } else { - cchan = api.Srv.Store.Channel().Get(hook.ChannelId) - } - - overrideUsername := parsedRequest.Username - overrideIconUrl := parsedRequest.IconURL - - if result := <-cchan; result.Err != nil { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message) - return - } else { - channel = result.Data.(*model.Channel) - } - - pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) - - // create a mock session - c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} - - if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { - c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "") - return - } - - if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil { - c.Err = err - return - } - - w.Header().Set("Content-Type", "text/plain") - w.Write([]byte("ok")) -} - -func claimAccount(c *api.Context, w http.ResponseWriter, r *http.Request) { - if !CheckBrowserCompatability(c, r) { - return - } - - params := mux.Vars(r) - teamName := params["team"] - email := r.URL.Query().Get("email") - newType := r.URL.Query().Get("new_type") - - var team *model.Team - if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { - l4g.Error(utils.T("web.claim_account.team.error"), teamName, tResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - team = tResult.Data.(*model.Team) - } - - authType := "" - if len(email) != 0 { - if uResult := <-api.Srv.Store.User().GetByEmail(team.Id, email); uResult.Err != nil { - l4g.Error(utils.T("web.claim_account.user.error"), team.Id, email, uResult.Err.Message) - http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) - return - } else { - user := uResult.Data.(*model.User) - authType = user.AuthService - - // if user is not logged in to their SSO account, ask them to log in - if len(authType) != 0 && user.Id != c.Session.UserId { - stateProps := map[string]string{} - stateProps["action"] = model.OAUTH_ACTION_SSO_TO_EMAIL - stateProps["email"] = email - - if authUrl, err := api.GetAuthorizationCode(c, authType, team.Name, stateProps, ""); err != nil { - c.Err = err - return - } else { - http.Redirect(w, r, authUrl, http.StatusFound) - } - } - } - } - - page := NewHtmlTemplatePage("claim_account", c.T("web.claim_account.title"), c.Locale) - page.Props["Email"] = email - page.Props["CurrentType"] = authType - page.Props["NewType"] = newType - page.Props["TeamDisplayName"] = team.DisplayName - page.Props["TeamName"] = team.Name - - page.Render(c, w) -}