Merge branch 'master' into PLT-2115

This commit is contained in:
=Corey Hulen
2016-03-14 10:23:51 -07:00
203 changed files with 3976 additions and 5210 deletions

View File

@@ -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 dont 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 senders 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

View File

@@ -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'

View File

@@ -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 {

View File

@@ -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))
}

View File

@@ -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())

View File

@@ -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)))
}

22
api/license_test.go Normal file
View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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)))
}
}

View File

@@ -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()

View File

@@ -1 +0,0 @@
{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}

View File

@@ -1 +0,0 @@
{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}

View File

@@ -1,37 +0,0 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title><span class='fa fa-chevron-left'></span>Back - Error</title>
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
<script src="/static/js/react-with-addons-0.13.3.min.js"></script>
<script src="/static/js/jquery-1.11.1.min.js"></script>
<script src="/static/js/bootstrap-3.3.5.min.js"></script>
<script src="/static/js/react-bootstrap-0.25.1.min.js"></script>
<link id="favicon" rel="icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
<link rel="shortcut icon" href="/static/images/favicon/favicon-16x16.png" type="image/x-icon">
<link href='/static/css/google-fonts.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body class="white error">
<div class="container-fluid">
<div class="error__container">
<div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div>
<h2>{{.Props.Title}}</h2>
<p>{{ .Props.Message }}</p>
<a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
</div>
</div>
</body>
<script>
var details = "{{ .Details }}";
if (details.length > 0) {
console.log("error details: " + details);
}
</script>
</html>

View File

@@ -1,52 +0,0 @@
{{define "find_teams_body"}}
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-top: 20px; line-height: 1.7; color: #555;">
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
<tr>
<td style="border: 1px solid #ddd;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="padding: 20px 20px 10px; text-align:left;">
<img src="{{.ClientCfg.SiteURL}}/static/images/logo-email.png" width="130px" style="opacity: 0.5" alt="">
</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
<p>{{ if .Extra }}
{{.Props.Found}}<br>
{{range $index, $element := .Extra}}
<a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br>
{{ end }}
{{ else }}
{{.Props.NotFound}}
{{ end }}
</p>
</td>
</tr>
<tr>
{{template "email_info" . }}
</tr>
</table>
</td>
</tr>
<tr>
{{template "email_footer" . }}
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
{{end}}

View File

@@ -1 +0,0 @@
{{define "find_teams_subject"}}{{.Props.Subject}}{{end}}

View File

@@ -1 +0,0 @@
{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}}

View File

@@ -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))
}
}
}

View File

@@ -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)
}
}
}

Binary file not shown.

View File

@@ -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 /

View File

@@ -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 /

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{
"AWSEBDockerrunVersion": "1",
"Image": {
"Name": "mattermost/platform:1.4",
"Name": "mattermost/platform:2.1",
"Update": "true"
},
"Ports": [

View File

@@ -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": "",

View File

@@ -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."
}
]

View File

@@ -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"

View File

@@ -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}}.<br>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}}.<br>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"
}
]
]

View File

@@ -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
}
}

View File

@@ -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"

View File

@@ -232,3 +232,10 @@ func (o *Team) Sanitize() {
o.Email = ""
o.AllowedDomains = ""
}
func (o *Team) SanitizeForNotLoggedIn() {
o.Email = ""
o.AllowedDomains = ""
o.CompanyName = ""
o.InviteId = ""
}

View File

@@ -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",

View File

@@ -0,0 +1 @@
{{define "email_change_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}

View File

@@ -0,0 +1 @@
{{define "email_change_verify_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}

24
templates/error.html Normal file
View File

@@ -0,0 +1,24 @@
{{define "error"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white error">
<div class="container-fluid">
<div class="error__container">
<div class="error__icon">
<i class="fa fa-exclamation-triangle"/>
</div>
<h2>{{.Props.Title}}</h2>
<p>{{ .Props.Message }}</p>
<a href="{{.Props.SiteURL}}">{{.Props.Link}}</a>
</div>
</div>
</body>
<script>
var details = {{ .Props.Details }};
if (details.length > 0) {
console.log("error details: " + details);
}
</script>
</html>
{{end}}

92
templates/head.html Normal file
View File

@@ -0,0 +1,92 @@
{{define "head"}}
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<title>{{ .Props.Title }}</title>
<!-- iOS add to homescreen -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="{{ .Props.Title }}">
<meta name="application-name" content="{{ .Props.Title }}">
<meta name="format-detection" content="telephone=no">
<!-- iOS add to homescreen -->
<!-- Android add to homescreen -->
<link rel="apple-touch-icon" sizes="57x57" href="/static/images/favicon/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/static/images/favicon/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/static/images/favicon/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/static/images/favicon/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/static/images/favicon/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/static/images/favicon/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/static/images/favicon/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/images/favicon/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/favicon/apple-touch-icon-180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="192x192" href="/static/images/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="96x96" href="/static/images/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon/favicon-16x16.png">
<link rel="manifest" href="/static/config/manifest.json">
<!-- Android add to homescreen -->
<!-- CSS Should always go first -->
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-colorpicker.min.css">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/google-fonts.css">
<link rel="stylesheet" href="/static/css/katex.min.css">
<link rel="stylesheet" class="code_theme" href="">
<script src="/static/js/intl-1.0.0/Intl.js"></script>
<script src="/static/js/intl-1.0.0/locale-data/jsonp/en.js"></script>
<script src="/static/js/intl-1.0.0/locale-data/jsonp/es.js"></script>
<script src="/static/js/intl-1.0.0/locale-data/jsonp/pt.js"></script>
<script src="/static/js/react-0.14.3.js"></script>
<script src="/static/js/react-dom-0.14.3.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/react-intl.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/en.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/es.js"></script>
<script src="/static/js/react-intl-2.0.0-beta-2/locale-data/pt.js"></script>
<script src="/static/js/jquery-2.1.4.js"></script>
<script src="/static/js/bootstrap-3.3.5.js"></script>
<script src="/static/js/bootstrap-colorpicker.min.js"></script>
<script src="/static/js/react-bootstrap-0.28.1.js"></script>
<script src="/static/js/velocity.min.js"></script>
<script src="/static/js/perfect-scrollbar-0.6.7.jquery.min.js"></script>
<script src="/static/js/jquery-dragster/jquery.dragster.js"></script>
<script src="/static/js/babel-polyfill-6.1.18.min.js"></script>
<script src="/static/js/katex.min.js"></script>
<script src="/static/js/Chart.min.js"></script>
<style id="antiClickjack">body{display:none !important;}</style>
<script>
if ('ReactIntl' in window && 'ReactIntlLocaleData' in window) {
Object.keys(ReactIntlLocaleData).forEach(function(lang) {
ReactIntl.addLocaleData(ReactIntlLocaleData[lang]);
});
}
$(window).on('beforeunload', function(){
if (window.SocketStore) {
SocketStore.close();
}
});
</script>
<script src="/static/js/libs.min.js"></script>
<script src="/static/js/bundle.js"></script>
<script type="text/javascript">
if (self === top) {
var blocker = document.getElementById("antiClickjack");
blocker.parentNode.removeChild(blocker);
}
</script>
</head>
{{end}}

View File

@@ -0,0 +1 @@
{{define "post_subject"}}[{{.Props.SiteName}}] {{.Props.Subject}}{{end}}

12
templates/root.html Normal file
View File

@@ -0,0 +1,12 @@
{{define "root"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body>
<div id='root'/>
<script>
window.setup_root();
</script>
</body>
</html>
{{end}}

View File

@@ -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

97
utils/html.go Normal file
View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -21,29 +21,38 @@ export default class AboutBuildModal extends React.Component {
let title = (
<FormattedMessage
id='about.teamEdtion'
defaultMessage='Team Edition'
id='about.teamEditiont0'
defaultMessage='Team Edition T0'
/>
);
let licensee;
if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') {
if (config.BuildEnterpriseReady === 'true') {
title = (
<FormattedMessage
id='about.enterpriseEdition'
defaultMessage='Enterprise Edition'
id='about.teamEditiont1'
defaultMessage='Team Edition T1'
/>
);
licensee = (
<div className='row form-group'>
<div className='col-sm-3 info__label'>
<FormattedMessage
id='about.licensed'
defaultMessage='Licensed by:'
/>
if (license.IsLicensed === 'true') {
title = (
<FormattedMessage
id='about.enterpriseEditione1'
defaultMessage='Enterprise Edition E1'
/>
);
licensee = (
<div className='row form-group'>
<div className='col-sm-3 info__label'>
<FormattedMessage
id='about.licensed'
defaultMessage='Licensed by:'
/>
</div>
<div className='col-sm-9'>{license.Company}</div>
</div>
<div className='col-sm-9'>{license.Company}</div>
</div>
);
);
}
}
return (

View File

@@ -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: (
<FormattedDate
value={firstAccessTime}
day='2-digit'
month='long'
year='numeric'
/>
),
time: (
<FormattedTime
value={firstAccessTime}
hour='2-digit'
minute='2-digit'
/>
)
}}
/>
</div>
@@ -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: (
<FormattedDate
value={lastAccessTime}
day='2-digit'
month='long'
year='numeric'
/>
),
time: (
<FormattedTime
value={lastAccessTime}
hour='2-digit'
minute='2-digit'
/>
)
}}
/>
</div>

View File

@@ -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 (
<div>
<div id='admin_controller'>
<div
className='sidebar--menu'
id='sidebar-menu'

View File

@@ -2,13 +2,14 @@
// See License.txt for license information.
import * as Utils from '../../utils/utils.jsx';
import * as Client from '../../utils/client.jsx';
import TeamStore from '../../stores/team_store.jsx';
import Constants from '../../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
import {Link} from 'react-router';
function getStateFromStores() {
return {currentTeam: TeamStore.getCurrent()};
}
@@ -18,16 +19,9 @@ export default class AdminNavbarDropdown extends React.Component {
super(props);
this.blockToggle = false;
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = getStateFromStores();
}
handleLogoutClick(e) {
e.preventDefault();
Client.logout();
}
componentDidMount() {
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
@@ -78,15 +72,12 @@ export default class AdminNavbarDropdown extends React.Component {
</a>
</li>
<li>
<a
href='#'
onClick={this.handleLogoutClick}
>
<Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
<FormattedMessage
id='admin.nav.logout'
defaultMessage='Logout'
/>
</a>
</Link>
</li>
<li className='divider'></li>
<li>
@@ -116,4 +107,4 @@ export default class AdminNavbarDropdown extends React.Component {
</ul>
);
}
}
}

View File

@@ -3,7 +3,6 @@
import AdminSidebarHeader from './admin_sidebar_header.jsx';
import SelectTeamModal from './select_team_modal.jsx';
import * as Utils from '../../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -30,8 +29,6 @@ export default class AdminSidebar extends React.Component {
handleClick(name, teamId, e) {
e.preventDefault();
this.props.selectTab(name, teamId);
var tokenIndex = Utils.getUrlParameter('session_token_index');
history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}?session_token_index=${tokenIndex}`);
}
isSelected(name, teamId) {
@@ -73,7 +70,6 @@ export default class AdminSidebar extends React.Component {
}
teamSelectedModal(teamId) {
this.props.selectedTeams[teamId] = 'true';
this.setState({showSelectModal: false});
this.props.addSelectedTeam(teamId);
this.forceUpdate();

View File

@@ -3,7 +3,6 @@
import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
import UserStore from '../../stores/user_store.jsx';
import * as Utils from '../../utils/utils.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -39,7 +38,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at + '&' + Utils.getSessionIndex()}
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
/>
);
}
@@ -65,4 +64,4 @@ export default class SidebarHeader extends React.Component {
</div>
);
}
}
}

View File

@@ -20,7 +20,7 @@ var holders = defineMessages({
},
baseEx: {
id: 'admin.ldap.baseEx',
defaultMessage: 'Ex "dc=mydomain,dc=com"'
defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"'
},
firstnameAttrEx: {
id: 'admin.ldap.firstnameAttrEx',
@@ -32,7 +32,7 @@ var holders = defineMessages({
},
emailAttrEx: {
id: 'admin.ldap.emailAttrEx',
defaultMessage: 'Ex "mail"'
defaultMessage: 'Ex "mail" or "userPrincipalName"'
},
usernameAttrEx: {
id: 'admin.ldap.usernameAttrEx',
@@ -581,4 +581,4 @@ LdapSettings.propTypes = {
config: React.PropTypes.object
};
export default injectIntl(LdapSettings);
export default injectIntl(LdapSettings);

View File

@@ -27,6 +27,7 @@ class LicenseSettings extends React.Component {
this.state = {
fileSelected: false,
fileName: null,
serverError: null
};
}
@@ -34,7 +35,7 @@ class LicenseSettings extends React.Component {
handleChange() {
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
if (element.prop('files').length > 0) {
this.setState({fileSelected: true});
this.setState({fileSelected: true, fileName: element.prop('files')[0].name});
}
}
@@ -56,13 +57,13 @@ class LicenseSettings extends React.Component {
() => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
this.setState({serverError: null});
this.setState({fileSelected: false, fileName: null, serverError: null});
window.location.reload(true);
},
(error) => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
this.setState({serverError: error.message});
this.setState({fileSelected: false, fileName: null, serverError: error.message});
}
);
}
@@ -75,12 +76,12 @@ class LicenseSettings extends React.Component {
Client.removeLicenseFile(
() => {
$('#remove-button').button('reset');
this.setState({serverError: null});
this.setState({fileSelected: false, fileName: null, serverError: null});
window.location.reload(true);
},
(error) => {
$('#remove-button').button('reset');
this.setState({serverError: error.message});
this.setState({fileSelected: false, fileName: null, serverError: error.message});
}
);
}
@@ -172,17 +173,36 @@ class LicenseSettings extends React.Component {
/>
);
let fileName;
if (this.state.fileName) {
fileName = this.state.fileName;
} else {
fileName = (
<FormattedMessage
id='admin.license.noFile'
defaultMessage='No file uploaded'
/>
);
}
licenseKey = (
<div className='col-sm-8'>
<input
className='pull-left'
ref='fileInput'
type='file'
accept='.mattermost-license'
onChange={this.handleChange}
/>
<div className='file__upload'>
<button className='btn btn-default'>
<FormattedMessage
id='admin.license.choose'
defaultMessage='Choose File'
/>
</button>
<input
ref='fileInput'
type='file'
accept='.mattermost-license'
onChange={this.handleChange}
/>
</div>
<button
className={btnClass + ' pull-left'}
className={btnClass}
disabled={!this.state.fileSelected}
onClick={this.handleSubmit}
id='upload-button'
@@ -193,11 +213,12 @@ class LicenseSettings extends React.Component {
defaultMessage='Upload'
/>
</button>
<br/>
<br/>
<div className='help-text no-margin'>
{fileName}
</div>
<br/>
{serverError}
<p className='help-text'>
<p className='help-text no-margin'>
<FormattedHTMLMessage
id='admin.license.uploadDesc'
defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.'

View File

@@ -366,7 +366,7 @@ export default class UserItem extends React.Component {
<td className='row member-div padding--equal'>
<img
className='post-profile-img pull-left'
src={`/api/v1/users/${user.id}/image?time=${user.update_at}&${Utils.getSessionIndex()}`}
src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
height='36'
width='36'
/>

View File

@@ -5,7 +5,7 @@ import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import * as Utils from '../utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate, FormattedTime} from 'mm-intl';
const holders = defineMessages({
sessionRevoked: {
@@ -598,8 +598,23 @@ export function formatAuditInfo(audit, formatMessage) {
}
const date = new Date(audit.create_at);
let auditInfo = {};
auditInfo.timestamp = date.toLocaleDateString(global.window.mm_locale, {month: 'short', day: '2-digit', year: 'numeric'}) + ' - ' + date.toLocaleTimeString(global.window.mm_locale, {hour: '2-digit', minute: '2-digit'});
const auditInfo = {};
auditInfo.timestamp = (
<div>
<FormattedDate
value={date}
day='2-digit'
month='short'
year='numeric'
/>
{' - '}
<FormattedTime
value={date}
hour='2-digit'
minute='2-digit'
/>
</div>
);
auditInfo.userId = audit.user_id;
auditInfo.desc = auditDesc;
auditInfo.ip = audit.ip_address;

View File

@@ -25,40 +25,43 @@ export default class CenterPanel extends React.Component {
constructor(props) {
super(props);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.onChannelChange = this.onChannelChange.bind(this);
this.onUserChange = this.onUserChange.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.validState = this.validState.bind(this);
this.onStoresChange = this.onStoresChange.bind(this);
this.state = this.getStateFromStores();
}
getStateFromStores() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
this.state = {
showTutorialScreens: tutorialStep === TutorialSteps.INTRO_SCREENS,
return {
showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS,
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
user: UserStore.getCurrentUser(),
channel: ChannelStore.getCurrent(),
profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
};
}
validState() {
return this.state.user && this.state.channel && this.state.profiles;
}
onStoresChange() {
this.setState(this.getStateFromStores());
}
componentDidMount() {
PreferenceStore.addChangeListener(this.onPreferenceChange);
ChannelStore.addChangeListener(this.onChannelChange);
UserStore.addChangeListener(this.onUserChange);
PreferenceStore.addChangeListener(this.onStoresChange);
ChannelStore.addChangeListener(this.onStoresChange);
UserStore.addChangeListener(this.onStoresChange);
}
componentWillUnmount() {
PreferenceStore.removeChangeListener(this.onPreferenceChange);
ChannelStore.removeChangeListener(this.onChannelChange);
UserStore.removeChangeListener(this.onUserChange);
}
onPreferenceChange() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
this.setState({showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS});
}
onChannelChange() {
this.setState({showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS});
}
onUserChange() {
this.setState({user: UserStore.getCurrentUser(), profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))});
PreferenceStore.removeChangeListener(this.onStoresChange);
ChannelStore.removeChangeListener(this.onStoresChange);
UserStore.removeChangeListener(this.onStoresChange);
}
render() {
const channel = ChannelStore.getCurrent();
if (!this.validState()) {
return null;
}
const channel = this.state.channel;
var handleClick = null;
let postsContainer;
let createPost;

View File

@@ -57,20 +57,33 @@ export default class ChannelHeader extends React.Component {
memberChannel: ChannelStore.getCurrentMember(),
users: extraInfo.members,
userCount: extraInfo.member_count,
searchVisible: SearchStore.getSearchResults() !== null
searchVisible: SearchStore.getSearchResults() !== null,
currentUser: UserStore.getCurrentUser()
};
}
validState() {
if (!this.state.channel ||
!this.state.memberChannel ||
!this.state.users ||
!this.state.userCount ||
!this.state.currentUser) {
return false;
}
return true;
}
componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange);
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
SearchStore.addSearchChangeListener(this.onListenerChange);
PreferenceStore.addChangeListener(this.onListenerChange);
UserStore.addChangeListener(this.onListenerChange);
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange);
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
SearchStore.removeSearchChangeListener(this.onListenerChange);
PreferenceStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
const newState = this.getStateFromStores();
@@ -98,7 +111,7 @@ export default class ChannelHeader extends React.Component {
searchMentions(e) {
e.preventDefault();
const user = this.props.user;
const user = this.state.currentUser;
let terms = '';
if (user.notify_props && user.notify_props.mention_keys) {
@@ -134,7 +147,7 @@ export default class ChannelHeader extends React.Component {
});
}
render() {
if (this.state.channel === null) {
if (!this.validState()) {
return null;
}
@@ -163,8 +176,8 @@ export default class ChannelHeader extends React.Component {
</Popover>
);
let channelTitle = channel.display_name;
const currentId = this.props.user.id;
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.props.user.roles);
const currentId = this.state.currentUser.id;
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles);
const isDirect = (this.state.channel.type === 'D');
if (isDirect) {
@@ -252,7 +265,7 @@ export default class ChannelHeader extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelInviteModal}
dialogProps={{channel}}
dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
id='chanel_header.addMembers'
@@ -331,7 +344,11 @@ export default class ChannelHeader extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
dialogProps={{
channel,
channelMember: this.state.memberChannel,
currentUser: this.state.currentUser
}}
>
<FormattedMessage
id='channel_header.notificationPreferences'
@@ -497,5 +514,4 @@ export default class ChannelHeader extends React.Component {
}
ChannelHeader.propTypes = {
user: React.PropTypes.object.isRequired
};

View File

@@ -4,8 +4,8 @@
import FilteredUserList from './filtered_user_list.jsx';
import LoadingScreen from './loading_screen.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import UserStore from '../stores/user_store.jsx';
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
@@ -16,18 +16,15 @@ import {FormattedMessage} from 'mm-intl';
const Modal = ReactBootstrap.Modal;
export default class ChannelInviteModal extends React.Component {
constructor() {
super();
constructor(props) {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
this.handleInvite = this.handleInvite.bind(this);
this.getStateFromStores = this.getStateFromStores.bind(this);
this.createInviteButton = this.createInviteButton.bind(this);
// the state gets populated when the modal is shown
this.state = {
loading: true
};
this.state = this.getStateFromStores();
}
shouldComponentUpdate(nextProps, nextState) {
if (!this.props.show && !nextProps.show) {
@@ -63,6 +60,20 @@ export default class ChannelInviteModal extends React.Component {
};
}
const currentUser = UserStore.getCurrentUser();
if (!currentUser) {
return {
loading: true
};
}
const currentMember = ChannelStore.getCurrentMember();
if (!currentMember) {
return {
loading: true
};
}
const memberIds = extraInfo.members.map((user) => user.id);
var nonmembers = [];
@@ -78,7 +89,9 @@ export default class ChannelInviteModal extends React.Component {
return {
nonmembers,
loading: false
loading: false,
currentUser,
currentMember
};
}
componentWillReceiveProps(nextProps) {
@@ -93,6 +106,11 @@ export default class ChannelInviteModal extends React.Component {
UserStore.removeChangeListener(this.onListenerChange);
}
}
componentWillUnmount() {
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
ChannelStore.removeChangeListener(this.onListenerChange);
UserStore.removeChangeListener(this.onListenerChange);
}
onListenerChange() {
var newState = this.getStateFromStores();
if (!Utils.areObjectsEqual(this.state, newState)) {
@@ -144,7 +162,6 @@ export default class ChannelInviteModal extends React.Component {
if (Utils.windowHeight() <= 1200) {
maxHeight = Utils.windowHeight() - 300;
}
content = (
<FilteredUserList
style={{maxHeight}}

View File

@@ -1,204 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
/* This is a special React control with the sole purpose of making all the AsyncClient calls
to the server on page load. This is to prevent other React controls from spamming
AsyncClient with requests. */
import * as AsyncClient from '../utils/async_client.jsx';
import * as Client from '../utils/client.jsx';
import SocketStore from '../stores/socket_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PostStore from '../stores/post_store.jsx';
import UserStore from '../stores/user_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import {intlShape, injectIntl, defineMessages} from 'mm-intl';
const holders = defineMessages({
socketError: {
id: 'channel_loader.socketError',
defaultMessage: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'
},
someone: {
id: 'channel_loader.someone',
defaultMessage: 'Someone'
},
posted: {
id: 'channel_loader.posted',
defaultMessage: 'Posted'
},
uploadedImage: {
id: 'channel_loader.uploadedImage',
defaultMessage: ' uploaded an image'
},
uploadedFile: {
id: 'channel_loader.uploadedFile',
defaultMessage: ' uploaded a file'
},
something: {
id: 'channel_loader.something',
defaultMessage: ' did something new'
},
wrote: {
id: 'channel_loader.wrote',
defaultMessage: ' wrote: '
},
connectionError: {
id: 'channel_loader.connection_error',
defaultMessage: 'There appears to be a problem with your internet connection.'
},
unknownError: {
id: 'channel_loader.unknown_error',
defaultMessage: 'We received an unexpected status code from the server.'
}
});
class ChannelLoader extends React.Component {
constructor(props) {
super(props);
this.intervalId = null;
this.onSocketChange = this.onSocketChange.bind(this);
const {formatMessage} = this.props.intl;
SocketStore.setTranslations({
socketError: formatMessage(holders.socketError),
someone: formatMessage(holders.someone),
posted: formatMessage(holders.posted),
uploadedImage: formatMessage(holders.uploadedImage),
uploadedFile: formatMessage(holders.uploadedFile),
something: formatMessage(holders.something),
wrote: formatMessage(holders.wrote)
});
Client.setTranslations({
connectionError: formatMessage(holders.connectionError),
unknownError: formatMessage(holders.unknownError)
});
this.state = {};
}
componentDidMount() {
/* Initial aysnc loads */
AsyncClient.getPosts(ChannelStore.getCurrentId());
AsyncClient.getChannels();
AsyncClient.getChannelExtraInfo();
AsyncClient.findTeams();
AsyncClient.getMyTeam();
setTimeout(() => AsyncClient.getStatuses(), 3000); // temporary until statuses are reworked a bit
/* Perform pending post clean-up */
PostStore.clearPendingPosts();
/* Set up interval functions */
this.intervalId = setInterval(() => AsyncClient.getStatuses(), 30000);
/* Device tracking setup */
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
if (iOS) {
$('body').addClass('ios');
}
/* Set up tracking for whether the window is active */
window.isActive = true;
$(window).on('focus', function windowFocus() {
AsyncClient.updateLastViewedAt();
ChannelStore.resetCounts(ChannelStore.getCurrentId());
ChannelStore.emitChange();
window.isActive = true;
});
$(window).on('blur', function windowBlur() {
window.isActive = false;
});
/* Start global change listeners setup */
SocketStore.addChangeListener(this.onSocketChange);
/* Update CSS classes to match user theme */
var user = UserStore.getCurrentUser();
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
Utils.applyTheme(user.theme_props);
} else {
Utils.applyTheme(Constants.THEMES.default);
}
// if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
Utils.applyFont(selectedFont);
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
} else {
$(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
}
});
/* Prevent backspace from navigating back a page */
$(window).on('keydown.preventBackspace', (e) => {
if (e.which === 8 && !$(e.target).is('input, textarea')) {
e.preventDefault();
}
});
}
componentWillUnmount() {
clearInterval(this.intervalId);
$(window).off('focus');
$(window).off('blur');
SocketStore.removeChangeListener(this.onSocketChange);
$('body').off('click.userpopover');
$('body').off('mouseenter mouseleave', '.post');
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
$(window).off('keydown.preventBackspace');
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
UserStore.setStatus(msg.user_id, 'online');
}
}
render() {
return <div/>;
}
}
ChannelLoader.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(ChannelLoader);

View File

@@ -6,7 +6,6 @@ import SettingItemMin from './setting_item_min.jsx';
import SettingItemMax from './setting_item_max.jsx';
import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -15,7 +14,6 @@ export default class ChannelNotificationsModal extends React.Component {
constructor(props) {
super(props);
this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.bind(this);
this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
@@ -26,58 +24,41 @@ export default class ChannelNotificationsModal extends React.Component {
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
const member = ChannelStore.getMember(props.channel.id);
this.state = {
notifyLevel: member.notify_props.desktop,
markUnreadLevel: member.notify_props.mark_unread,
channelId: ChannelStore.getCurrentId(),
activeSection: ''
activeSection: '',
notifyLevel: '',
unreadLevel: ''
};
}
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
this.onListenerChange();
ChannelStore.addChangeListener(this.onListenerChange);
} else {
ChannelStore.removeChangeListener(this.onListenerChange);
}
}
onListenerChange() {
const curChannelId = ChannelStore.getCurrentId();
if (!curChannelId) {
return;
}
const newState = {channelId: curChannelId};
const member = ChannelStore.getMember(curChannelId);
if (member.notify_props.desktop !== this.state.notifyLevel || member.notify_props.mark_unread !== this.state.mark_unread) {
newState.notifyLevel = member.notify_props.desktop;
newState.markUnreadLevel = member.notify_props.mark_unread;
}
this.setState(newState);
}
updateSection(section) {
this.setState({activeSection: section});
}
componentWillReceiveProps(nextProps) {
if (!this.props.show && nextProps.show) {
this.setState({
notifyLevel: nextProps.channelMember.notify_props.desktop,
unreadLevel: nextProps.channelMember.notify_props.mark_unread
});
}
}
handleSubmitNotifyLevel() {
var channelId = this.state.channelId;
var channelId = this.props.channel.id;
var notifyLevel = this.state.notifyLevel;
if (ChannelStore.getMember(channelId).notify_props.desktop === notifyLevel) {
if (this.props.channelMember.notify_props.desktop === notifyLevel) {
this.updateSection('');
return;
}
var data = {};
data.channel_id = channelId;
data.user_id = UserStore.getCurrentId();
data.user_id = this.props.currentUser.id;
data.desktop = notifyLevel;
//TODO: This should be moved to event_helpers
Client.updateNotifyProps(data,
() => {
// YUCK
var member = ChannelStore.getMember(channelId);
member.notify_props.desktop = notifyLevel;
ChannelStore.setChannelMember(member);
@@ -92,11 +73,8 @@ export default class ChannelNotificationsModal extends React.Component {
this.setState({notifyLevel});
}
createNotifyLevelSection(serverError) {
var handleUpdateSection;
const user = UserStore.getCurrentUser();
const globalNotifyLevel = user.notify_props.desktop;
// Get glabal user setting for notifications
const globalNotifyLevel = this.props.currentUser.notify_props.desktop;
let globalNotifyLevelName;
if (globalNotifyLevel === 'all') {
globalNotifyLevelName = (
@@ -128,13 +106,15 @@ export default class ChannelNotificationsModal extends React.Component {
/>
);
const notificationLevel = this.state.notifyLevel;
if (this.state.activeSection === 'desktop') {
var notifyActive = [false, false, false, false];
if (this.state.notifyLevel === 'default') {
const notifyActive = [false, false, false, false];
if (notificationLevel === 'default') {
notifyActive[0] = true;
} else if (this.state.notifyLevel === 'all') {
} else if (notificationLevel === 'all') {
notifyActive[1] = true;
} else if (this.state.notifyLevel === 'mention') {
} else if (notificationLevel === 'mention') {
notifyActive[2] = true;
} else {
notifyActive[3] = true;
@@ -196,7 +176,7 @@ export default class ChannelNotificationsModal extends React.Component {
</div>
);
handleUpdateSection = function updateSection(e) {
const handleUpdateSection = function updateSection(e) {
this.updateSection('');
this.onListenerChange();
e.preventDefault();
@@ -224,7 +204,7 @@ export default class ChannelNotificationsModal extends React.Component {
}
var describe;
if (this.state.notifyLevel === 'default') {
if (notificationLevel === 'default') {
describe = (
<FormattedMessage
id='channel_notifications.globalDefault'
@@ -233,45 +213,44 @@ export default class ChannelNotificationsModal extends React.Component {
}}
/>
);
} else if (this.state.notifyLevel === 'mention') {
} else if (notificationLevel === 'mention') {
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
} else if (this.state.notifyLevel === 'all') {
} else if (notificationLevel === 'all') {
describe = (<FormattedMessage id='channel_notifications.allActivity'/>);
} else {
describe = (<FormattedMessage id='channel_notifications.never'/>);
}
handleUpdateSection = function updateSection(e) {
this.updateSection('desktop');
e.preventDefault();
}.bind(this);
return (
<SettingItemMin
title={sendDesktop}
describe={describe}
updateSection={handleUpdateSection}
updateSection={() => {
this.updateSection('desktop');
}}
/>
);
}
handleSubmitMarkUnreadLevel() {
const channelId = this.state.channelId;
const markUnreadLevel = this.state.markUnreadLevel;
const channelId = this.props.channel.id;
const markUnreadLevel = this.state.unreadLevel;
if (ChannelStore.getMember(channelId).notify_props.mark_unread === markUnreadLevel) {
if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) {
this.updateSection('');
return;
}
const data = {
channel_id: channelId,
user_id: UserStore.getCurrentId(),
user_id: this.props.currentUser.id,
mark_unread: markUnreadLevel
};
//TODO: This should be fixed, moved to event_helpers
Client.updateNotifyProps(data,
() => {
// Yuck...
var member = ChannelStore.getMember(channelId);
member.notify_props.mark_unread = markUnreadLevel;
ChannelStore.setChannelMember(member);
@@ -283,8 +262,8 @@ export default class ChannelNotificationsModal extends React.Component {
);
}
handleUpdateMarkUnreadLevel(markUnreadLevel) {
this.setState({markUnreadLevel});
handleUpdateMarkUnreadLevel(unreadLevel) {
this.setState({unreadLevel});
}
createMarkUnreadLevelSection(serverError) {
@@ -303,7 +282,7 @@ export default class ChannelNotificationsModal extends React.Component {
<label>
<input
type='radio'
checked={this.state.markUnreadLevel === 'all'}
checked={this.state.unreadLevel === 'all'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
/>
<FormattedMessage
@@ -317,7 +296,7 @@ export default class ChannelNotificationsModal extends React.Component {
<label>
<input
type='radio'
checked={this.state.markUnreadLevel === 'mention'}
checked={this.state.unreadLevel === 'mention'}
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
/>
<FormattedMessage id='channel_notifications.onlyMentions'/>
@@ -355,7 +334,7 @@ export default class ChannelNotificationsModal extends React.Component {
} else {
let describe;
if (!this.state.markUnreadLevel || this.state.markUnreadLevel === 'all') {
if (!this.state.unreadLevel || this.state.unreadLevel === 'all') {
describe = (
<FormattedMessage
id='channel_notifications.allUnread'
@@ -430,5 +409,7 @@ export default class ChannelNotificationsModal extends React.Component {
ChannelNotificationsModal.propTypes = {
show: React.PropTypes.bool.isRequired,
onHide: React.PropTypes.func.isRequired,
channel: React.PropTypes.object.isRequired
channel: React.PropTypes.object.isRequired,
channelMember: React.PropTypes.object.isRequired,
currentUser: React.PropTypes.object.isRequired
};

View File

@@ -2,34 +2,11 @@
// See License.txt for license information.
import CenterPanel from '../components/center_panel.jsx';
import Sidebar from '../components/sidebar.jsx';
import SidebarRight from '../components/sidebar_right.jsx';
import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
export default class ChannelView extends React.Component {
render() {
return (
<div className='container-fluid'>
<div
className='sidebar--right'
id='sidebar-right'
>
<SidebarRight/>
</div>
<div
className='sidebar--menu'
id='sidebar-menu'
>
<SidebarRightMenu/>
</div>
<div
className='sidebar--left'
id='sidebar-left'
>
<Sidebar/>
</div>
<CenterPanel/>
</div>
<CenterPanel/>
);
}
}
@@ -37,4 +14,5 @@ ChannelView.defaultProps = {
};
ChannelView.propTypes = {
params: React.PropTypes.object
};

View File

@@ -3,6 +3,7 @@
import EmailToSSO from './email_to_sso.jsx';
import SSOToEmail from './sso_to_email.jsx';
import TeamStore from '../../stores/team_store.jsx';
import {FormattedMessage} from 'mm-intl';
@@ -10,11 +11,46 @@ export default class ClaimAccount extends React.Component {
constructor(props) {
super(props);
this.onTeamChange = this.onTeamChange.bind(this);
this.updateStateFromStores = this.updateStateFromStores.bind(this);
this.state = {};
}
componentWillMount() {
this.setState({
email: this.props.location.query.email,
newType: this.props.location.query.new_type,
oldType: this.props.location.query.old_type,
teamName: this.props.params.team,
teamDisplayName: ''
});
this.updateStateFromStores();
}
componentDidMount() {
TeamStore.addChangeListener(this.onTeamChange);
}
componentWillUnmount() {
TeamStore.removeChangeListener(this.onTeamChange);
}
updateStateFromStores() {
const team = TeamStore.getByName(this.state.teamName);
let displayName = '';
if (team) {
displayName = team.displayName;
}
this.setState({
teamDisplayName: displayName
});
}
onTeamChange() {
this.updateStateFromStores();
}
render() {
if (this.state.teamDisplayName === '') {
return (<div/>);
}
let content;
if (this.props.email === '') {
if (this.state.email === '') {
content = (
<p>
<FormattedMessage
@@ -23,36 +59,55 @@ export default class ClaimAccount extends React.Component {
/>
</p>
);
} else if (this.props.currentType === '' && this.props.newType !== '') {
} else if (this.state.oldType === '' && this.state.newType !== '') {
content = (
<EmailToSSO
email={this.props.email}
type={this.props.newType}
teamName={this.props.teamName}
teamDisplayName={this.props.teamDisplayName}
email={this.state.email}
type={this.state.newType}
teamName={this.state.teamName}
teamDisplayName={this.state.teamDisplayName}
/>
);
} else {
content = (
<SSOToEmail
email={this.props.email}
currentType={this.props.currentType}
teamName={this.props.teamName}
teamDisplayName={this.props.teamDisplayName}
email={this.state.email}
currentType={this.state.oldType}
teamName={this.state.teamName}
teamDisplayName={this.state.teamDisplayName}
/>
);
}
return content;
return (
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<div id='claim'>
{content}
</div>
</div>
</div>
</div>
);
}
}
ClaimAccount.defaultProps = {
};
ClaimAccount.propTypes = {
currentType: React.PropTypes.string.isRequired,
newType: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
params: React.PropTypes.object.isRequired,
location: React.PropTypes.object.isRequired
};

View File

@@ -159,7 +159,7 @@ SSOToEmail.propTypes = {
currentType: React.PropTypes.string.isRequired,
email: React.PropTypes.string.isRequired,
teamName: React.PropTypes.string.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
teamDisplayName: React.PropTypes.string
};
export default injectIntl(SSOToEmail);

View File

@@ -9,7 +9,7 @@ import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
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 * as Client from '../utils/client.jsx';
import * as AsyncClient from '../utils/async_client.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -165,7 +165,7 @@ class CreatePost extends React.Component {
const channel = ChannelStore.get(this.state.channelId);
EventHelpers.emitUserPostedEvent(post);
GlobalActions.emitUserPostedEvent(post);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post, channel,
@@ -177,7 +177,7 @@ class CreatePost extends React.Component {
member.last_viewed_at = Date.now();
ChannelStore.setChannelMember(member);
EventHelpers.emitPostRecievedEvent(data);
GlobalActions.emitPostRecievedEvent(data);
},
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {

View File

@@ -9,6 +9,8 @@ import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
import {browserHistory} from 'react-router';
export default class DeleteChannelModal extends React.Component {
constructor(props) {
super(props);
@@ -21,11 +23,11 @@ export default class DeleteChannelModal extends React.Component {
return;
}
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
Client.deleteChannel(
this.props.channel.id,
() => {
AsyncClient.getChannels(true);
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
},
(err) => {
AsyncClient.dispatchError(err, 'handleDelete');

View File

@@ -0,0 +1,82 @@
// 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';
import LoadingScreen from './loading_screen.jsx';
import {browserHistory} from 'react-router';
export default class DoVerifyEmail extends React.Component {
constructor(props) {
super(props);
this.state = {
verifyStatus: 'pending',
serverError: ''
};
}
componentWillMount() {
const uid = this.props.location.query.uid;
const hid = this.props.location.query.hid;
const teamName = this.props.location.query.teamname;
const email = this.props.location.query.email;
Client.verifyEmail(
() => {
browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email);
},
(err) => {
this.setState({verifyStatus: 'failure', serverError: err.message});
},
uid,
hid
);
}
render() {
if (this.state.verifyStatus !== 'failure') {
return (<LoadingScreen/>);
}
return (
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>
<FormattedMessage
id='email_verify.almost'
defaultMessage='{siteName}: You are almost done'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h3>
<div>
<p>
<FormattedMessage id='email_verify.verifyFailed'/>
</p>
<p className='alert alert-danger'>
<i className='fa fa-times'/>
{this.state.serverError}
</p>
</div>
</div>
</div>
</div>
);
}
}
DoVerifyEmail.defaultProps = {
};
DoVerifyEmail.propTypes = {
location: React.PropTypes.object.isRequired
};

View File

@@ -1,41 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as TextFormatting from '../utils/text_formatting.jsx';
import UserStore from '../stores/user_store.jsx';
export default class Docs extends React.Component {
constructor(props) {
super(props);
UserStore.setCurrentUser(global.window.mm_user || {});
this.state = {text: ''};
const errorState = {text: '## 404'};
if (props.site) {
$.get(`/static/help/${props.site}_${global.window.mm_locale}.md`).then((response) => {
this.setState({text: response});
}, () => {
this.setState(errorState);
});
} else {
this.setState(errorState);
}
}
render() {
return (
<div
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.text)}}
>
</div>
);
}
}
Docs.defaultProps = {
site: ''
};
Docs.propTypes = {
site: React.PropTypes.string
};

View File

@@ -3,7 +3,7 @@
import * as Client from '../utils/client.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 Textbox from './textbox.jsx';
import BrowserStore from '../stores/browser_store.jsx';
import PostStore from '../stores/post_store.jsx';
@@ -45,7 +45,7 @@ class EditPostModal extends React.Component {
delete tempState.editText;
BrowserStore.setItem('edit_state_transfer', tempState);
$('#edit_post').modal('hide');
EventHelpers.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
return;
}

View File

@@ -1,108 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
export default class EmailVerify extends React.Component {
constructor(props) {
super(props);
this.handleResend = this.handleResend.bind(this);
this.state = {};
}
handleResend() {
const newAddress = window.location.href.replace('&resend_success=true', '');
window.location.href = newAddress + '&resend=true';
}
render() {
var title = '';
var body = '';
var resend = '';
var resendConfirm = '';
if (this.props.isVerified === 'true') {
title = (
<FormattedMessage
id='email_verify.verified'
defaultMessage='{siteName} Email Verified'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
);
body = (
<FormattedHTMLMessage
id='email_verify.verifiedBody'
defaultMessage='<p>Your email has been verified! Click <a href={url}>here</a> to log in.</p>'
values={{
url: this.props.teamURL + '?email=' + this.props.userEmail
}}
/>
);
} else {
title = (
<FormattedMessage
id='email_verify.almost'
defaultMessage='{siteName}: You are almost done'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
);
body = (
<p>
<FormattedMessage
id='email_verify.notVerifiedBody'
defaultMessage='Please verify your email address. Check your inbox for an email.'
/>
</p>
);
resend = (
<button
onClick={this.handleResend}
className='btn btn-primary'
>
<FormattedMessage
id='email_verify.resend'
defaultMessage='Resend Email'
/>
</button>
);
if (this.props.resendSuccess) {
resendConfirm = (
<div><br/><p className='alert alert-success'><i className='fa fa-check'></i>
<FormattedMessage
id='email_verify.sent'
defaultMessage=' Verification email sent.'
/>
</p></div>);
}
}
return (
<div className='col-sm-12'>
<div className='signup-team__container'>
<h3>{title}</h3>
<div>
{body}
{resend}
{resendConfirm}
</div>
</div>
</div>
);
}
}
EmailVerify.defaultProps = {
isVerified: 'false',
teamURL: '',
userEmail: '',
resendSuccess: 'false'
};
EmailVerify.propTypes = {
isVerified: React.PropTypes.string,
teamURL: React.PropTypes.string,
userEmail: React.PropTypes.string,
resendSuccess: React.PropTypes.string
};

View File

@@ -43,7 +43,7 @@ class FileAttachment extends React.Component {
if (type === 'image') {
var self = this; // Need this reference since we use the given "this"
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg?' + utils.getSessionIndex()).load(function loadWrapper(path, name) {
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
return function loader() {
$(this).remove();
if (name in self.refs) {
@@ -114,7 +114,7 @@ class FileAttachment extends React.Component {
var re3 = new RegExp('\\)', 'g');
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg?' + utils.getSessionIndex() + ')');
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
}
}
removeBackgroundImage(name) {
@@ -185,6 +185,7 @@ class FileAttachment extends React.Component {
data-toggle='tooltip'
title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
className='post-image__name'
target='_blank'
>
{trimmedFilename}
</a>
@@ -193,6 +194,7 @@ class FileAttachment extends React.Component {
href={fileUrl}
download={filenameString}
className='post-image__download'
target='_blank'
>
<span
className='fa fa-download'

View File

@@ -1,135 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
var holders = defineMessages({
submitError: {
id: 'find_team.submitError',
defaultMessage: 'Please enter a valid email address'
},
placeholder: {
id: 'find_team.placeholder',
defaultMessage: 'you@domain.com'
}
});
class FindTeam extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
var state = { };
var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
if (!email || !utils.isEmail(email)) {
state.email_error = this.props.intl.formatMessage(holders.submitError);
this.setState(state);
return;
}
state.email_error = '';
client.findTeamsSendEmail(email,
function success() {
state.sent = true;
this.setState(state);
}.bind(this),
function fail(err) {
state.email_error = err.message;
this.setState(state);
}.bind(this)
);
}
render() {
var emailError = null;
var emailErrorClass = 'form-group';
if (this.state.email_error) {
emailError = <label className='control-label'>{this.state.email_error}</label>;
emailErrorClass = 'form-group has-error';
}
if (this.state.sent) {
return (
<div>
<h4>
<FormattedMessage
id='find_team.findTitle'
defaultMessage='Find Your Team'
/>
</h4>
<p>
<FormattedMessage
id='find_team.findDescription'
defaultMessage='An email was sent with links to any teams to which you are a member.'
/>
</p>
</div>
);
}
return (
<div>
<h4>
<FormattedMessage
id='find_team.findTitle'
defaultMessage='Find Your Team'
/>
</h4>
<form onSubmit={this.handleSubmit}>
<p>
<FormattedMessage
id='find_team.getLinks'
defaultMessage='Get an email with links to any teams to which you are a member.'
/>
</p>
<div className='form-group'>
<label className='control-label'>
<FormattedMessage
id='find_team.email'
defaultMessage='Email'
/>
</label>
<div className={emailErrorClass}>
<input
type='text'
ref='email'
className='form-control'
placeholder={this.props.intl.formatMessage(holders.placeholder)}
maxLength='128'
spellCheck='false'
/>
{emailError}
</div>
</div>
<button
className='btn btn-md btn-primary'
type='submit'
>
<FormattedMessage
id='find_team.send'
defaultMessage='Send'
/>
</button>
</form>
</div>
);
}
}
FindTeam.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(FindTeam);

View File

@@ -5,7 +5,7 @@ import * as utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
import * as Client from '../utils/client.jsx';
import * as EventHelpers from '../dispatcher/event_helpers.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import ModalStore from '../stores/modal_store.jsx';
import UserStore from '../stores/user_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
@@ -223,7 +223,7 @@ class InviteMemberModal extends React.Component {
showGetTeamInviteLinkModal() {
this.handleHide(false);
EventHelpers.showGetTeamInviteLinkModal();
GlobalActions.showGetTeamInviteLinkModal();
}
render() {

View File

@@ -0,0 +1,224 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as AsyncClient from '../utils/async_client.jsx';
import * as GlobalActions from '../action_creators/global_actions.jsx';
import UserStore from '../stores/user_store.jsx';
import SocketStore from '../stores/socket_store.jsx';
import ChannelStore from '../stores/channel_store.jsx';
import PreferenceStore from '../stores/preference_store.jsx';
import * as Utils from '../utils/utils.jsx';
import Constants from '../utils/constants.jsx';
import ErrorBar from '../components/error_bar.jsx';
import {browserHistory} from 'react-router';
import SidebarRight from '../components/sidebar_right.jsx';
import SidebarRightMenu from '../components/sidebar_right_menu.jsx';
// Modals
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 SelectTeamModal from '../components/admin_console/select_team_modal.jsx';
const CLIENT_STATUS_INTERVAL = 30000;
const BACKSPACE_CHAR = 8;
export default class LoggedIn extends React.Component {
constructor(params) {
super(params);
this.onUserChanged = this.onUserChanged.bind(this);
}
onUserChanged() {
// Grab the current user
const user = UserStore.getCurrentUser();
// Update segment indentify
if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') {
global.window.analytics.identify(user.id, {
name: user.nickname,
email: user.email,
createdAt: user.create_at,
username: user.username,
team_id: user.team_id,
id: user.id
});
}
// Update CSS classes to match user theme
if (user) {
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
Utils.applyTheme(user.theme_props);
} else {
Utils.applyTheme(Constants.THEMES.default);
}
}
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
UserStore.setStatus(msg.user_id, 'online');
}
}
componentWillMount() {
// Emit view action
GlobalActions.viewLoggedIn();
// Listen for user
UserStore.addChangeListener(this.onUserChanged);
// Add listner for socker store
SocketStore.addChangeListener(this.onSocketChange);
// Get all statuses regularally. (Soon to be switched to websocket)
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
// Force logout of all tabs if one tab is logged out
$(window).bind('storage', (e) => {
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
return;
}
console.log('detected logout from a different tab'); //eslint-disable-line no-console
browserHistory.push('/' + this.props.params.team);
}
if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
return;
}
console.log('detected login from a different tab'); //eslint-disable-line no-console
location.reload();
}
});
// Because current CSS requires the root tag to have specific stuff
$('#root').attr('class', 'channel-view');
// ???
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
} else {
$(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
$(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
}
});
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
if (ev.type === 'mouseenter') {
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
} else {
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
}
});
// Device tracking setup
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
if (iOS) {
$('body').addClass('ios');
}
// Set up tracking for whether the window is active
window.isActive = true;
$(window).on('focus', () => {
AsyncClient.updateLastViewedAt();
ChannelStore.resetCounts(ChannelStore.getCurrentId());
ChannelStore.emitChange();
window.isActive = true;
});
$(window).on('blur', () => {
window.isActive = false;
});
// if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
Utils.applyFont(selectedFont);
// Pervent backspace from navigating back a page
$(window).on('keydown.preventBackspace', (e) => {
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
e.preventDefault();
}
});
}
componentWillUnmount() {
$('#root').attr('class', '');
clearInterval(this.intervalId);
$(window).off('focus');
$(window).off('blur');
SocketStore.removeChangeListener(this.onSocketChange);
UserStore.removeChangeListener(this.onUserChanged);
$('body').off('click.userpopover');
$('body').off('mouseenter mouseleave', '.post');
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
$(window).off('keydown.preventBackspace');
}
render() {
return (
<div className='channel-view'>
<ErrorBar/>
<div className='container-fluid'>
<SidebarRight/>
<SidebarRightMenu/>
{this.props.sidebar}
{this.props.center}
<GetPostLinkModal/>
<GetTeamInviteLinkModal/>
<InviteMemberModal/>
<ImportThemeModal/>
<TeamSettingsModal/>
<MoreChannelsModal/>
<EditPostModal/>
<DeletePostModal/>
<RemovedFromChannelModal/>
<RegisterAppModal/>
<SelectTeamModal/>
</div>
</div>
);
}
}
LoggedIn.defaultProps = {
};
LoggedIn.propTypes = {
children: React.PropTypes.object,
sidebar: React.PropTypes.object,
center: React.PropTypes.object,
params: React.PropTypes.object
};

View File

@@ -6,82 +6,120 @@ import LoginUsername from './login_username.jsx';
import LoginLdap from './login_ldap.jsx';
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx';
import TeamStore from '../stores/team_store.jsx';
import {FormattedMessage} from 'mm-intl';
import {browserHistory} from 'react-router';
export default class Login extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.getStateFromStores = this.getStateFromStores.bind(this);
this.onTeamChange = this.onTeamChange.bind(this);
this.state = this.getStateFromStores();
}
componentDidMount() {
TeamStore.addChangeListener(this.onTeamChange);
Client.getMeLoggedIn((data) => {
if (data && data.logged_in !== 'false') {
browserHistory.push('/' + this.props.params.team + '/channels/town-square');
}
});
}
componentWillUnmount() {
TeamStore.removeChangeListener(this.onTeamChange);
}
getStateFromStores() {
return {
currentTeam: TeamStore.getByName(this.props.params.team)
};
}
onTeamChange() {
this.setState(this.getStateFromStores());
}
render() {
const teamDisplayName = this.props.teamDisplayName;
const teamName = this.props.teamName;
const currentTeam = this.state.currentTeam;
if (currentTeam == null) {
return <div/>;
}
const teamDisplayName = currentTeam.display_name;
const teamName = currentTeam.name;
const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
let loginMessage = [];
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login gitlab'
key='gitlab'
href={'/' + teamName + '/login/gitlab'}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.gitlab'
defaultMessage='with GitLab'
/>
</span>
</a>
<a
className='btn btn-custom-login gitlab'
key='gitlab'
href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.gitlab'
defaultMessage='with GitLab'
/>
</span>
</a>
);
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
loginMessage.push(
<a
className='btn btn-custom-login google'
key='google'
href={'/' + teamName + '/login/google'}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.google'
defaultMessage='with Google Apps'
/>
</span>
</a>
);
<a
className='btn btn-custom-login google'
key='google'
href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
>
<span className='icon'/>
<span>
<FormattedMessage
id='login.google'
defaultMessage='with Google Apps'
/>
</span>
</a>
);
}
const extraParam = Utils.getUrlParameter('extra');
let extraBox = '';
if (extraParam) {
let msg;
if (extraParam === Constants.SIGNIN_CHANGE) {
msg = (
<FormattedMessage
id='login.changed'
defaultMessage=' Sign-in method changed successfully'
/>
);
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
msg = (
<FormattedMessage
id='login.verified'
defaultMessage=' Email Verified'
/>
);
}
if (msg != null) {
extraBox = (
<div className='alert alert-success'>
<i className='fa fa-check'/>
{msg}
<FormattedMessage
id='login.changed'
defaultMessage=' Sign-in method changed successfully'
/>
</div>
);
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
extraBox = (
<div className='alert alert-success'>
<i className='fa fa-check'/>
<FormattedMessage
id='login.verified'
defaultMessage=' Email Verified'
/>
</div>
);
} else if (extraParam === Constants.SESSION_EXPIRED) {
extraBox = (
<div className='alert alert-warning'>
<i className='fa fa-exclamation-triangle'/>
<FormattedMessage
id='login.session_expired'
defaultMessage=' Your session has expired. Please login again.'
/>
</div>
);
}
@@ -91,7 +129,7 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
emailSignup = (
<LoginEmail
teamName={this.props.teamName}
teamName={teamName}
/>
);
}
@@ -125,7 +163,7 @@ export default class Login extends React.Component {
}
let userSignUp = null;
if (this.props.inviteId) {
if (currentTeam.allow_open_invite) {
userSignUp = (
<div>
<span>
@@ -134,7 +172,7 @@ export default class Login extends React.Component {
defaultMessage="Don't have an account? "
/>
<a
href={'/signup_user_complete/?id=' + this.props.inviteId}
href={'/signup_user_complete/?id=' + currentTeam.invite_id}
className='signup-team-login'
>
<FormattedMessage
@@ -168,22 +206,23 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableLdap === 'true') {
ldapLogin = (
<LoginLdap
teamName={this.props.teamName}
teamName={teamName}
/>
);
}
let findTeams = null;
if (!Utils.isMobileApp()) {
findTeams = (
<div className='form-group margin--extra form-group--small'>
<span>
<a href='/find_team'>
<FormattedMessage
id='login.find'
defaultMessage='Find your other teams'
/>
</a></span>
if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
ldapLogin = (
<div>
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
</div>
<LoginLdap
teamName={teamName}
/>
</div>
);
}
@@ -192,49 +231,72 @@ export default class Login extends React.Component {
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
usernameLogin = (
<LoginUsername
teamName={this.props.teamName}
teamName={teamName}
/>
);
}
if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) {
usernameLogin = (
<div>
<div className='or__container'>
<FormattedMessage
id='login.or'
defaultMessage='or'
/>
</div>
<LoginUsername
teamName={teamName}
/>
</div>
);
}
return (
<div className='signup-team__container'>
<h5 className='margin--less'>
<FormattedMessage
id='login.signTo'
defaultMessage='Sign in to:'
/>
</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
defaultMessage='on {siteName}'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h2>
{extraBox}
{loginMessage}
{emailSignup}
{usernameLogin}
{ldapLogin}
{userSignUp}
{findTeams}
{forgotPassword}
{teamSignUp}
<div>
<div className='signup-header'>
<a href='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
/>
</a>
</div>
<div className='col-sm-12'>
<div className='signup-team__container'>
<h5 className='margin--less'>
<FormattedMessage
id='login.signTo'
defaultMessage='Sign in to:'
/>
</h5>
<h2 className='signup-team__name'>{teamDisplayName}</h2>
<h2 className='signup-team__subdomain'>
<FormattedMessage
id='login.on'
defaultMessage='on {siteName}'
values={{
siteName: global.window.mm_config.SiteName
}}
/>
</h2>
{extraBox}
{loginMessage}
{emailSignup}
{usernameLogin}
{ldapLogin}
{userSignUp}
{forgotPassword}
{teamSignUp}
</div>
</div>
</div>
);
}
}
Login.defaultProps = {
teamName: '',
teamDisplayName: ''
};
Login.propTypes = {
teamName: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
inviteId: React.PropTypes.string
params: React.PropTypes.object.isRequired
};

View File

@@ -4,6 +4,7 @@
import * as Utils from '../utils/utils.jsx';
import * as Client from '../utils/client.jsx';
import UserStore from '../stores/user_store.jsx';
import {browserHistory} from 'react-router';
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
@@ -72,13 +73,7 @@ class LoginEmail extends React.Component {
Client.loginByEmail(name, email, password,
() => {
UserStore.setLastEmail(email);
const redirect = Utils.getUrlParameter('redirect');
if (redirect) {
window.location.href = decodeURIComponent(redirect);
} else {
window.location.href = '/' + name + '/channels/town-square';
}
browserHistory.push('/' + name + '/channels/town-square');
},
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
@@ -167,4 +162,4 @@ LoginEmail.propTypes = {
teamName: React.PropTypes.string.isRequired
};
export default injectIntl(LoginEmail);
export default injectIntl(LoginEmail);

View File

@@ -56,9 +56,13 @@ export default class Navbar extends React.Component {
return {
channel: ChannelStore.getCurrent(),
member: ChannelStore.getCurrentMember(),
users: ChannelStore.getCurrentExtraInfo().members
users: ChannelStore.getCurrentExtraInfo().members,
currentUser: UserStore.getCurrentUser()
};
}
stateValid() {
return this.state.channel && this.state.member && this.state.users && this.state.currentUser;
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
@@ -201,7 +205,7 @@ export default class Navbar extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelInviteModal}
dialogProps={{channel}}
dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
id='navbar.addMembers'
@@ -286,7 +290,11 @@ export default class Navbar extends React.Component {
<ToggleModalButton
role='menuitem'
dialogType={ChannelNotificationsModal}
dialogProps={{channel}}
dialogProps={{
channel,
channelMember: this.state.member,
currentUser: this.state.currentUser
}}
>
<FormattedMessage
id='navbar.preferences'
@@ -412,7 +420,11 @@ export default class Navbar extends React.Component {
return buttons;
}
render() {
var currentId = UserStore.getCurrentId();
if (!this.stateValid()) {
return null;
}
var currentId = this.state.currentUser.id;
var channel = this.state.channel;
var channelTitle = this.props.teamDisplayName;
var popoverContent;

View File

@@ -2,10 +2,7 @@
// See License.txt for license information.
import * as Utils from '../utils/utils.jsx';
import * as client from '../utils/client.jsx';
import UserStore from '../stores/user_store.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 AboutBuildModal from './about_build_modal.jsx';
import TeamMembersModal from './team_members_modal.jsx';
@@ -15,38 +12,20 @@ import UserSettingsModal from './user_settings/user_settings_modal.jsx';
import Constants from '../utils/constants.jsx';
import {FormattedMessage} from 'mm-intl';
function getStateFromStores() {
const teams = [];
const teamsObject = UserStore.getTeams();
for (const teamId in teamsObject) {
if (teamsObject.hasOwnProperty(teamId)) {
teams.push(teamsObject[teamId]);
}
}
teams.sort(Utils.sortByDisplayName);
return {teams};
}
import {Link} from 'react-router';
export default class NavbarDropdown extends React.Component {
constructor(props) {
super(props);
this.blockToggle = false;
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.handleAboutModal = this.handleAboutModal.bind(this);
this.onListenerChange = this.onListenerChange.bind(this);
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
const state = getStateFromStores();
state.showUserSettingsModal = false;
state.showAboutModal = false;
this.state = state;
}
handleLogoutClick(e) {
e.preventDefault();
client.logout();
this.state = {
showUserSettingsModal: false,
showAboutModal: false
};
}
handleAboutModal() {
this.setState({showAboutModal: true});
@@ -55,9 +34,6 @@ export default class NavbarDropdown extends React.Component {
this.setState({showAboutModal: false});
}
componentDidMount() {
UserStore.addTeamsChangeListener(this.onListenerChange);
TeamStore.addChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
$('.sidebar--left .dropdown-menu').scrollTop(0);
this.blockToggle = true;
@@ -67,24 +43,15 @@ export default class NavbarDropdown extends React.Component {
});
}
componentWillUnmount() {
UserStore.removeTeamsChangeListener(this.onListenerChange);
TeamStore.removeChangeListener(this.onListenerChange);
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
}
onListenerChange() {
var newState = getStateFromStores();
if (!Utils.areObjectsEqual(newState, this.state)) {
this.setState(newState);
}
}
render() {
var teamLink = '';
var inviteLink = '';
var manageLink = '';
var sysAdminLink = '';
var adminDivider = '';
var currentUser = UserStore.getCurrentUser();
var currentUser = this.props.currentUser;
var isAdmin = false;
var isSystemAdmin = false;
var teamSettings = null;
@@ -97,7 +64,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
onClick={EventHelpers.showInviteMemberModal}
onClick={GlobalActions.showInviteMemberModal}
>
<FormattedMessage
id='navbar_dropdown.inviteMember'
@@ -112,7 +79,7 @@ export default class NavbarDropdown extends React.Component {
<li>
<a
href='#'
onClick={EventHelpers.showGetTeamInviteLinkModal}
onClick={GlobalActions.showGetTeamInviteLinkModal}
>
<FormattedMessage
id='navbar_dropdown.teamLink'
@@ -158,7 +125,7 @@ export default class NavbarDropdown extends React.Component {
sysAdminLink = (
<li>
<a
href={'/admin_console?' + Utils.getSessionIndex()}
href={'/admin_console'}
>
<FormattedMessage
id='navbar_dropdown.console'
@@ -171,31 +138,6 @@ export default class NavbarDropdown extends React.Component {
var teams = [];
if (this.state.teams.length > 1) {
teams.push(
<li
className='divider'
key='div'
>
</li>
);
this.state.teams.forEach((team) => {
if (team.name !== this.props.teamName) {
teams.push(
<li key={team.name}><a href={Utils.getWindowLocationOrigin() + '/' + team.name}>
<FormattedMessage
id='navbar_dropdown.switchTeam'
defaultMessage='Switch to {team}'
values={{
team: team.display_name
}}
/>
</a></li>);
}
});
}
if (global.window.mm_config.EnableTeamCreation === 'true') {
teams.push(
<li key='newTeam_li'>
@@ -283,15 +225,12 @@ export default class NavbarDropdown extends React.Component {
{inviteLink}
{teamLink}
<li>
<a
href='#'
onClick={this.handleLogoutClick}
>
<Link to={'/' + this.props.teamName + '/logout'}>
<FormattedMessage
id='navbar_dropdown.logout'
defaultMessage='Logout'
/>
</a>
</Link>
</li>
{adminDivider}
{teamSettings}
@@ -333,5 +272,6 @@ NavbarDropdown.defaultProps = {
NavbarDropdown.propTypes = {
teamType: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
teamName: React.PropTypes.string
teamName: React.PropTypes.string,
currentUser: React.PropTypes.object
};

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as GlobalActions from '../action_creators/global_actions.jsx';
export default class NeedsTeam extends React.Component {
componentWillMount() {
GlobalActions.loadTeamRequiredPage();
}
render() {
return this.props.children;
}
}
NeedsTeam.defaultProps = {
};
NeedsTeam.propTypes = {
children: React.PropTypes.object
};

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FormattedMessage} from 'mm-intl';
export default class NotLoggedIn extends React.Component {
componentDidMount() {
$('body').attr('class', 'white');
$('#root').attr('class', 'container-fluid');
}
componentWillUnmount() {
$('body').attr('class', '');
$('#root').attr('class', '');
}
render() {
return (
<div className='inner__wrap'>
<div className='row content'>
{this.props.children}
<div className='footer-push'></div>
</div>
<div className='row footer'>
<div className='footer-pane col-xs-12'>
<div className='col-xs-12'>
<span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span>
</div>
<div className='col-xs-12'>
<span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span>
<a
id='help_link'
className='pull-right footer-link'
href={global.window.mm_config.HelpLink}
>
<FormattedMessage id='web.footer.help'/>
</a>
<a
id='terms_link'
className='pull-right footer-link'
href={global.window.mm_config.TermsOfServiceLink}
>
<FormattedMessage id='web.footer.terms'/>
</a>
<a
id='privacy_link'
className='pull-right footer-link'
href={global.window.mm_config.PrivacyPolicyLink}
>
<FormattedMessage id='web.footer.privacy'/>
</a>
<a
id='about_link'
className='pull-right footer-link'
href={global.window.mm_config.AboutLink}
>
<FormattedMessage id='web.footer.about'/>
</a>
</div>
</div>
</div>
</div>
);
}
}
NotLoggedIn.defaultProps = {
};
NotLoggedIn.propTypes = {
children: React.PropTypes.object
};

View File

@@ -1,47 +0,0 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PasswordResetSendLink from './password_reset_send_link.jsx';
import PasswordResetForm from './password_reset_form.jsx';
export default class PasswordReset extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
if (this.props.isReset === 'false') {
return (
<PasswordResetSendLink
teamDisplayName={this.props.teamDisplayName}
teamName={this.props.teamName}
/>
);
}
return (
<PasswordResetForm
teamDisplayName={this.props.teamDisplayName}
teamName={this.props.teamName}
hash={this.props.hash}
data={this.props.data}
/>
);
}
}
PasswordReset.defaultProps = {
isReset: '',
teamName: '',
teamDisplayName: '',
hash: '',
data: ''
};
PasswordReset.propTypes = {
isReset: React.PropTypes.string,
teamName: React.PropTypes.string,
teamDisplayName: React.PropTypes.string,
hash: React.PropTypes.string,
data: React.PropTypes.string
};

Some files were not shown because too many files have changed in this diff Show More