diff options
Diffstat (limited to 'api')
-rw-r--r-- | api/channel.go | 2 | ||||
-rw-r--r-- | api/command.go | 112 | ||||
-rw-r--r-- | api/command_test.go | 37 | ||||
-rw-r--r-- | api/context.go | 21 | ||||
-rw-r--r-- | api/file.go | 75 | ||||
-rw-r--r-- | api/file_test.go | 9 | ||||
-rw-r--r-- | api/post.go | 38 | ||||
-rw-r--r-- | api/post_test.go | 74 | ||||
-rw-r--r-- | api/slackimport.go | 28 | ||||
-rw-r--r-- | api/team.go | 122 | ||||
-rw-r--r-- | api/templates/error.html | 1 | ||||
-rw-r--r-- | api/templates/find_teams_body.html | 2 | ||||
-rw-r--r-- | api/user.go | 22 | ||||
-rw-r--r-- | api/web_conn.go | 5 | ||||
-rw-r--r-- | api/web_hub.go | 8 | ||||
-rw-r--r-- | api/web_team_hub.go | 9 |
16 files changed, 471 insertions, 94 deletions
diff --git a/api/channel.go b/api/channel.go index 5f3282072..b40366719 100644 --- a/api/channel.go +++ b/api/channel.go @@ -391,6 +391,8 @@ func JoinChannel(c *Context, channelId string, role string) { c.Err = model.NewAppError("joinChannel", "Failed to send join request", "") return } + + UpdateChannelAccessCacheAndForget(c.Session.TeamId, c.Session.UserId, channel.Id) } else { c.Err = model.NewAppError("joinChannel", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden diff --git a/api/command.go b/api/command.go index f051bd42e..749cbf790 100644 --- a/api/command.go +++ b/api/command.go @@ -4,15 +4,15 @@ package api import ( + "net/http" + "strconv" + "strings" + "time" + l4g "code.google.com/p/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" - "net/http" - "reflect" - "runtime" - "strconv" - "strings" ) type commandHandler func(c *Context, command *model.Command) bool @@ -24,6 +24,8 @@ var commands = []commandHandler{ echoCommand, } +var echoSem chan bool + func InitCommand(r *mux.Router) { l4g.Debug("Initializing command api routes") r.Handle("/command", ApiUserRequired(command)).Methods("POST") @@ -41,7 +43,6 @@ func command(c *Context, w http.ResponseWriter, r *http.Request) { } checkCommand(c, command) - if c.Err != nil { return } else { @@ -56,8 +57,6 @@ func checkCommand(c *Context, command *model.Command) bool { return false } - tchan := Srv.Store.Team().Get(c.Session.TeamId) - if len(command.ChannelId) > 0 { cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId) @@ -66,24 +65,9 @@ func checkCommand(c *Context, command *model.Command) bool { } } - allowValet := false - if tResult := <-tchan; tResult.Err != nil { - c.Err = model.NewAppError("checkCommand", "Could not find the team for this session, team_id="+c.Session.TeamId, "") - return false - } else { - allowValet = tResult.Data.(*model.Team).AllowValet - } - - ec := runtime.FuncForPC(reflect.ValueOf(echoCommand).Pointer()).Name() - for _, v := range commands { - if !allowValet && ec == runtime.FuncForPC(reflect.ValueOf(v).Pointer()).Name() { - continue - } - if v(c, command) { - return true - } else if c.Err != nil { + if v(c, command) || c.Err != nil { return true } } @@ -112,55 +96,65 @@ func logoutCommand(c *Context, command *model.Command) bool { } func echoCommand(c *Context, command *model.Command) bool { - cmd := "/echo" + maxThreads := 100 - if strings.Index(command.Command, cmd) == 0 { - parts := strings.SplitN(command.Command, " ", 3) - - channelName := "" - if len(parts) >= 2 { - channelName = parts[1] + if !command.Suggest && strings.Index(command.Command, cmd) == 0 { + parameters := strings.SplitN(command.Command, " ", 2) + if len(parameters) != 2 || len(parameters[1]) == 0 { + return false } - - message := "" - if len(parts) >= 3 { - message = parts[2] + message := strings.Trim(parameters[1], " ") + delay := 0 + if endMsg := strings.LastIndex(message, "\""); string(message[0]) == "\"" && endMsg > 1 { + if checkDelay, err := strconv.Atoi(strings.Trim(message[endMsg:], " \"")); err == nil { + delay = checkDelay + } + message = message[1:endMsg] + } else if strings.Index(message, " ") > -1 { + delayIdx := strings.LastIndex(message, " ") + delayStr := strings.Trim(message[delayIdx:], " ") + + if checkDelay, err := strconv.Atoi(delayStr); err == nil { + delay = checkDelay + message = message[:delayIdx] + } } - if result := <-Srv.Store.Channel().GetChannels(c.Session.TeamId, c.Session.UserId); result.Err != nil { - c.Err = result.Err + if delay > 10000 { + c.Err = model.NewAppError("echoCommand", "Delays must be under 10000 seconds", "") return false - } else { - channels := result.Data.(*model.ChannelList) + } - for _, v := range channels.Channels { - if v.Type == model.CHANNEL_DIRECT { - continue - } + if echoSem == nil { + // We want one additional thread allowed so we never reach channel lockup + echoSem = make(chan bool, maxThreads+1) + } - if v.Name == channelName && !command.Suggest { - post := &model.Post{} - post.ChannelId = v.Id - post.Message = message + if len(echoSem) >= maxThreads { + c.Err = model.NewAppError("echoCommand", "High volume of echo request, cannot process request", "") + return false + } - if _, err := CreateValetPost(c, post); err != nil { - c.Err = err - return false - } + echoSem <- true + go func() { + defer func() { <-echoSem }() + post := &model.Post{} + post.ChannelId = command.ChannelId + post.Message = message - command.Response = model.RESP_EXECUTED - return true - } + time.Sleep(time.Duration(delay) * time.Second) - if len(channelName) == 0 || (strings.Index(v.Name, channelName) == 0 && len(parts) < 3) { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd + " " + v.Name, Description: "Echo a message using Valet in a channel"}) - } + if _, err := CreatePost(c, post, false); err != nil { + l4g.Error("Unable to create /echo post, err=%v", err) } - } + }() + + command.Response = model.RESP_EXECUTED + return true } else if strings.Index(cmd, command.Command) == 0 { - command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo a message using Valet in a channel"}) + command.AddSuggestion(&model.SuggestCommand{Suggestion: cmd, Description: "Echo back text from your account, /echo \"message\" [delay in seoncds]"}) } return false diff --git a/api/command_test.go b/api/command_test.go index a58ef9be5..fe52dd41b 100644 --- a/api/command_test.go +++ b/api/command_test.go @@ -4,9 +4,10 @@ package api import ( + "testing" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" - "testing" ) func TestSuggestRootCommands(t *testing.T) { @@ -50,6 +51,12 @@ func TestSuggestRootCommands(t *testing.T) { if rs3.Suggestions[0].Suggestion != "/join" { t.Fatal("should have join cmd") } + + rs4 := Client.Must(Client.Command("", "/ech", true)).Data.(*model.Command) + + if rs4.Suggestions[0].Suggestion != "/echo" { + t.Fatal("should have echo cmd") + } } func TestLogoutCommands(t *testing.T) { @@ -145,3 +152,31 @@ func TestJoinCommands(t *testing.T) { t.Fatal("didn't join channel") } } + +func TestEchoCommand(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) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "AA", Name: "aa" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + echoTestString := "/echo test" + + r1 := Client.Must(Client.Command(channel1.Id, echoTestString, false)).Data.(*model.Command) + if r1.Response != model.RESP_EXECUTED { + t.Fatal("Echo command failed to execute") + } + + p1 := Client.Must(Client.GetPosts(channel1.Id, 0, 2, "")).Data.(*model.PostList) + if len(p1.Order) != 1 { + t.Fatal("Echo command failed to send") + } +} diff --git a/api/context.go b/api/context.go index 8babf85f2..aaf304e2c 100644 --- a/api/context.go +++ b/api/context.go @@ -4,14 +4,15 @@ package api import ( - l4g "code.google.com/p/log4go" - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/store" - "github.com/mattermost/platform/utils" "net" "net/http" "net/url" "strings" + + l4g "code.google.com/p/log4go" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" ) var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) @@ -431,10 +432,22 @@ func IsPrivateIpAddress(ipAddress string) bool { } func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) { + + protocol := "http" + if utils.Cfg.ServiceSettings.UseSSL { + forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO) + if forwardProto != "http" { + protocol = "https" + } + } + + SiteURL := protocol + "://" + r.Host + m := make(map[string]string) m["Message"] = err.Message m["Details"] = err.DetailedError m["SiteName"] = utils.Cfg.ServiceSettings.SiteName + m["SiteURL"] = SiteURL w.WriteHeader(err.StatusCode) ServerTemplates.ExecuteTemplate(w, "error.html", m) diff --git a/api/file.go b/api/file.go index 558f9357e..800c512c5 100644 --- a/api/file.go +++ b/api/file.go @@ -30,12 +30,15 @@ import ( "time" ) +var fileInfoCache *utils.Cache = utils.NewLru(1000) + func InitFile(r *mux.Router) { l4g.Debug("Initializing file api routes") sr := r.PathPrefix("/files").Subrouter() sr.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST") - sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET", "HEAD") + sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET") + sr.Handle("/get_info/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFileInfo)).Methods("GET") sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST") } @@ -213,9 +216,72 @@ type ImageGetResult struct { ImageData []byte } +func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { + c.Err = model.NewAppError("getFileInfo", "Unable to get file info. Amazon S3 not configured and local server storage turned off. ", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + params := mux.Vars(r) + + channelId := params["channel_id"] + if len(channelId) != 26 { + c.SetInvalidParam("getFileInfo", "channel_id") + return + } + + userId := params["user_id"] + if len(userId) != 26 { + c.SetInvalidParam("getFileInfo", "user_id") + return + } + + filename := params["filename"] + if len(filename) == 0 { + c.SetInvalidParam("getFileInfo", "filename") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) + + path := "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + userId + "/" + filename + size := "" + + if s, ok := fileInfoCache.Get(path); ok { + size = s.(string) + } else { + + fileData := make(chan []byte) + asyncGetFile(path, fileData) + + f := <-fileData + + if f == nil { + c.Err = model.NewAppError("getFileInfo", "Could not find file.", "path="+path) + c.Err.StatusCode = http.StatusNotFound + return + } + + size = strconv.Itoa(len(f)) + fileInfoCache.Add(path, size) + } + + if !c.HasPermissionsToChannel(cchan, "getFileInfo") { + return + } + + w.Header().Set("Cache-Control", "max-age=2592000, public") + + result := make(map[string]string) + result["filename"] = filename + result["size"] = size + w.Write([]byte(model.MapToJson(result))) +} + func getFile(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage { - c.Err = model.NewAppError("getFile", "Unable to upload file. Amazon S3 not configured and local server storage turned off. ", "") + c.Err = model.NewAppError("getFile", "Unable to get file. Amazon S3 not configured and local server storage turned off. ", "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -282,10 +348,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=2592000, public") w.Header().Set("Content-Length", strconv.Itoa(len(f))) - - if r.Method != "HEAD" { - w.Write(f) - } + w.Write(f) } func asyncGetFile(path string, fileData chan []byte) { diff --git a/api/file_test.go b/api/file_test.go index 566fd69d0..eb1fcf2ce 100644 --- a/api/file_test.go +++ b/api/file_test.go @@ -201,6 +201,15 @@ func TestGetFile(t *testing.T) { t.Fatal(downErr) } + if resp, downErr := Client.GetFileInfo(filenames[0]); downErr != nil { + t.Fatal(downErr) + } else { + m := resp.Data.(map[string]string) + if len(m["size"]) == 0 { + t.Fail() + } + } + team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) diff --git a/api/post.go b/api/post.go index c013df87f..5363fdf79 100644 --- a/api/post.go +++ b/api/post.go @@ -28,6 +28,7 @@ func InitPost(r *mux.Router) { sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST") sr.Handle("/update", ApiUserRequired(updatePost)).Methods("POST") sr.Handle("/posts/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET") + sr.Handle("/posts/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET") sr.Handle("/post/{post_id:[A-Za-z0-9]+}", ApiUserRequired(getPost)).Methods("GET") sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST") } @@ -545,9 +546,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { rpost := result.Data.(*model.Post) message := model.NewMessage(c.Session.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED) - message.Add("post_id", rpost.Id) - message.Add("channel_id", rpost.ChannelId) - message.Add("message", rpost.Message) + message.Add("post", rpost.ToJson()) PublishAndForget(message) @@ -603,6 +602,39 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) { } +func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + + id := params["id"] + if len(id) != 26 { + c.SetInvalidParam("getPostsSince", "channelId") + return + } + + time, err := strconv.ParseInt(params["time"], 10, 64) + if err != nil { + c.SetInvalidParam("getPostsSince", "time") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + pchan := Srv.Store.Post().GetPostsSince(id, time) + + if !c.HasPermissionsToChannel(cchan, "getPostsSince") { + return + } + + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else { + list := result.Data.(*model.PostList) + + w.Write([]byte(list.ToJson())) + } + +} + func getPost(c *Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) diff --git a/api/post_test.go b/api/post_test.go index cbba83af6..85d92de3a 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -331,7 +331,7 @@ func TestGetPosts(t *testing.T) { t.Fatal("wrong order") } - if len(r1.Posts) != 4 { + if len(r1.Posts) != 2 { // 3a1 and 3; 3a1's parent already there t.Fatal("wrong size") } @@ -345,12 +345,82 @@ func TestGetPosts(t *testing.T) { t.Fatal("wrong order") } - if len(r2.Posts) != 4 { + if len(r2.Posts) != 3 { // 2 and 1a1; + 1a1's parent t.Log(r2.Posts) t.Fatal("wrong size") } } +func TestGetPostsSince(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) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + time.Sleep(10 * time.Millisecond) + post0 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post0 = Client.Must(Client.CreatePost(post0)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post1a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post1.Id} + post1a1 = Client.Must(Client.CreatePost(post1a1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post3.Id} + post3a1 = Client.Must(Client.CreatePost(post3a1)).Data.(*model.Post) + + r1 := Client.Must(Client.GetPostsSince(channel1.Id, post1.CreateAt)).Data.(*model.PostList) + + if r1.Order[0] != post3a1.Id { + t.Fatal("wrong order") + } + + if r1.Order[1] != post3.Id { + t.Fatal("wrong order") + } + + if len(r1.Posts) != 5 { + t.Fatal("wrong size") + } + + now := model.GetMillis() + r2 := Client.Must(Client.GetPostsSince(channel1.Id, now)).Data.(*model.PostList) + + if len(r2.Posts) != 0 { + t.Fatal("should have been empty") + } + + post2.Message = "new message" + Client.Must(Client.UpdatePost(post2)) + + r3 := Client.Must(Client.GetPostsSince(channel1.Id, now)).Data.(*model.PostList) + + if len(r3.Order) != 2 { // 2 because deleted post is returned as well + t.Fatal("missing post update") + } +} + func TestSearchPosts(t *testing.T) { Setup() diff --git a/api/slackimport.go b/api/slackimport.go index ca3fdf3d1..1d037a934 100644 --- a/api/slackimport.go +++ b/api/slackimport.go @@ -82,8 +82,8 @@ func SlackParsePosts(data io.Reader) []SlackPost { func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map[string]*model.User { // Log header - log.WriteString("\n Users Created\n") - log.WriteString("===============\n\n") + log.WriteString("\r\n Users Created\r\n") + log.WriteString("===============\r\n\r\n") addedUsers := make(map[string]*model.User) for _, sUser := range slackusers { @@ -109,9 +109,9 @@ func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map if mUser := ImportUser(&newUser); mUser != nil { addedUsers[sUser.Id] = mUser - log.WriteString("Email, Password: " + newUser.Email + ", " + password + "\n") + log.WriteString("Email, Password: " + newUser.Email + ", " + password + "\r\n") } else { - log.WriteString("Unable to import user: " + sUser.Username) + log.WriteString("Unable to import user: " + sUser.Username + "\r\n") } } @@ -163,8 +163,8 @@ func SlackAddPosts(channel *model.Channel, posts []SlackPost, users map[string]* func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[string][]SlackPost, users map[string]*model.User, log *bytes.Buffer) map[string]*model.Channel { // Write Header - log.WriteString("\n Channels Added \n") - log.WriteString("=================\n\n") + log.WriteString("\r\n Channels Added \r\n") + log.WriteString("=================\r\n\r\n") addedChannels := make(map[string]*model.Channel) for _, sChannel := range slackchannels { @@ -180,14 +180,14 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str // Maybe it already exists? if result := <-Srv.Store.Channel().GetByName(teamId, sChannel.Name); result.Err != nil { l4g.Debug("Failed to import: %s", newChannel.DisplayName) - log.WriteString("Failed to import: " + newChannel.DisplayName + "\n") + log.WriteString("Failed to import: " + newChannel.DisplayName + "\r\n") continue } else { mChannel = result.Data.(*model.Channel) - log.WriteString("Merged with existing channel: " + newChannel.DisplayName + "\n") + log.WriteString("Merged with existing channel: " + newChannel.DisplayName + "\r\n") } } - log.WriteString(newChannel.DisplayName + "\n") + log.WriteString(newChannel.DisplayName + "\r\n") addedChannels[sChannel.Id] = mChannel SlackAddPosts(mChannel, posts[sChannel.Name], users) } @@ -202,7 +202,7 @@ func SlackImport(fileData multipart.File, fileSize int64, teamID string) (*model } // Create log file - log := bytes.NewBufferString("Mattermost Slack Import Log\n") + log := bytes.NewBufferString("Mattermost Slack Import Log\r\n") var channels []SlackChannel var users []SlackUser @@ -234,11 +234,11 @@ func SlackImport(fileData multipart.File, fileSize int64, teamID string) (*model addedUsers := SlackAddUsers(teamID, users, log) SlackAddChannels(teamID, channels, posts, addedUsers, log) - log.WriteString("\n Notes \n") - log.WriteString("=======\n\n") + log.WriteString("\r\n Notes \r\n") + log.WriteString("=======\r\n\r\n") - log.WriteString("- Some posts may not have been imported because they where not supported by this importer.\n") - log.WriteString("- Slack bot posts are currently not supported.\n") + log.WriteString("- Some posts may not have been imported because they where not supported by this importer.\r\n") + log.WriteString("- Slack bot posts are currently not supported.\r\n") return nil, log } diff --git a/api/team.go b/api/team.go index a331e9e34..e34b3a610 100644 --- a/api/team.go +++ b/api/team.go @@ -23,6 +23,7 @@ func InitTeam(r *mux.Router) { sr := r.PathPrefix("/teams").Subrouter() sr.Handle("/create", ApiAppHandler(createTeam)).Methods("POST") sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST") + sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST") sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST") sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST") sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") @@ -35,6 +36,11 @@ func InitTeam(r *mux.Router) { } func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("signupTeam", "Team sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } m := model.MapFromJson(r.Body) email := strings.ToLower(strings.TrimSpace(m["email"])) @@ -44,6 +50,10 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } + if !isTreamCreationAllowed(c, email) { + return + } + subjectPage := NewServerTemplatePage("signup_team_subject", c.GetSiteURL()) bodyPage := NewServerTemplatePage("signup_team_body", c.GetSiteURL()) bodyPage.Props["TourUrl"] = utils.Cfg.TeamSettings.TourLink @@ -70,7 +80,70 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(m))) } +func createTeamFromSSO(c *Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + if !utils.IsServiceAllowed(service) { + c.SetInvalidParam("createTeamFromSSO", "service") + return + } + + team := model.TeamFromJson(r.Body) + + if team == nil { + c.SetInvalidParam("createTeamFromSSO", "team") + return + } + + team.PreSave() + + team.Name = model.CleanTeamName(team.Name) + + if err := team.IsValid(); err != nil { + c.Err = err + return + } + + team.Id = "" + + found := true + count := 0 + for found { + if found = FindTeamByName(c, team.Name, "true"); c.Err != nil { + return + } else if found { + team.Name = team.Name + strconv.Itoa(count) + count += 1 + } + } + + team.AllowValet = utils.Cfg.TeamSettings.AllowValetDefault + + if result := <-Srv.Store.Team().Save(team); result.Err != nil { + c.Err = result.Err + return + } else { + rteam := result.Data.(*model.Team) + + if _, err := CreateDefaultChannels(c, rteam.Id); err != nil { + c.Err = nil + return + } + + data := map[string]string{"follow_link": c.GetSiteURL() + "/" + rteam.Name + "/signup/" + service} + w.Write([]byte(model.MapToJson(data))) + + } + +} + func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("createTeamFromSignup", "Team sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } teamSignup := model.TeamSignupFromJson(r.Body) @@ -89,6 +162,11 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = err return } + + if !isTreamCreationAllowed(c, teamSignup.Team.Email) { + return + } + teamSignup.Team.Id = "" password := teamSignup.User.Password @@ -161,6 +239,11 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { } func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("createTeam", "Team sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } team := model.TeamFromJson(r.Body) @@ -169,6 +252,10 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } + if !isTreamCreationAllowed(c, team.Email) { + return + } + if utils.Cfg.ServiceSettings.Mode != utils.MODE_DEV { c.Err = model.NewAppError("createTeam", "The mode does not allow network creation without a valid invite", "") return @@ -181,7 +268,7 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { rteam := result.Data.(*model.Team) if _, err := CreateDefaultChannels(c, rteam.Id); err != nil { - c.Err = nil + c.Err = err return } @@ -196,6 +283,35 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { } } +func isTreamCreationAllowed(c *Context, email string) bool { + + email = strings.ToLower(email) + + if utils.Cfg.TeamSettings.DisableTeamCreation { + c.Err = model.NewAppError("isTreamCreationAllowed", "Team creation has been disabled. Please ask your systems administrator for details.", "") + return false + } + + // commas and @ signs are optional + // can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org + domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(utils.Cfg.TeamSettings.RestrictCreationToDomains, "@", " ", -1), ",", " ", -1)))) + + matched := false + for _, d := range domains { + if strings.HasSuffix(email, "@"+d) { + matched = true + break + } + } + + if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched { + c.Err = model.NewAppError("isTreamCreationAllowed", "Email must be from a specific domain (e.g. @example.com). Please ask your systems administrator for details.", "") + return false + } + + return true +} + func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) { m := model.MapFromJson(r.Body) @@ -283,10 +399,10 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { } else { teams := result.Data.([]*model.Team) - // the template expects Props to be a map with team names as the keys + // 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] = team.Name + props[team.Name] = c.GetTeamURLFromTeam(team) } bodyPage.Props = props diff --git a/api/templates/error.html b/api/templates/error.html index f38bb81a1..3474c9e1e 100644 --- a/api/templates/error.html +++ b/api/templates/error.html @@ -14,6 +14,7 @@ <div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div> <h2>{{ .SiteName }} needs your help:</h2> <p>{{.Message}}</p> + <a href="{{.SiteURL}}">Go back to team site</a> </div> </div> </body> diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html index bd151a819..64bff8126 100644 --- a/api/templates/find_teams_body.html +++ b/api/templates/find_teams_body.html @@ -21,7 +21,7 @@ <p>{{ if .Props }} The following teams were found:<br> {{range $index, $element := .Props}} - {{ $index }}<br> + <a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br> {{ end }} {{ else }} We could not find any teams for the given email. diff --git a/api/user.go b/api/user.go index 05ccd03e8..3796dde2a 100644 --- a/api/user.go +++ b/api/user.go @@ -58,6 +58,11 @@ func InitUser(r *mux.Router) { } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.AllowEmailSignUp { + c.Err = model.NewAppError("signupTeam", "User sign-up with email is disabled.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } user := model.UserFromJson(r.Body) @@ -181,7 +186,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { if result := <-Srv.Store.User().Save(user); result.Err != nil { c.Err = result.Err - l4g.Error("Filae err=%v", result.Err) + l4g.Error("Couldn't save the user err=%v", result.Err) return nil } else { ruser := result.Data.(*model.User) @@ -1426,3 +1431,18 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser } } + +func IsUsernameTaken(name string, teamId string) bool { + + if !model.IsValidUsername(name) { + return false + } + + if result := <-Srv.Store.User().GetByUsername(teamId, name); result.Err != nil { + return false + } else { + return true + } + + return false +} diff --git a/api/web_conn.go b/api/web_conn.go index 0990de8ef..4315f5650 100644 --- a/api/web_conn.go +++ b/api/web_conn.go @@ -121,6 +121,11 @@ func (c *WebConn) writePump() { } } +func (c *WebConn) updateChannelAccessCache(channelId string) { + allowed := hasPermissionsToChannel(Srv.Store.Channel().CheckPermissionsTo(c.TeamId, channelId, c.UserId)) + c.ChannelAccessCache[channelId] = allowed +} + func hasPermissionsToChannel(sc store.StoreChannel) bool { if cresult := <-sc; cresult.Err != nil { return false diff --git a/api/web_hub.go b/api/web_hub.go index c7be19cac..44d405283 100644 --- a/api/web_hub.go +++ b/api/web_hub.go @@ -30,6 +30,14 @@ func PublishAndForget(message *model.Message) { }() } +func UpdateChannelAccessCacheAndForget(teamId, userId, channelId string) { + go func() { + if nh, ok := hub.teamHubs[teamId]; ok { + nh.UpdateChannelAccessCache(userId, channelId) + } + }() +} + func (h *Hub) Register(webConn *WebConn) { h.register <- webConn } diff --git a/api/web_team_hub.go b/api/web_team_hub.go index 7a63b84d1..31c8dfedf 100644 --- a/api/web_team_hub.go +++ b/api/web_team_hub.go @@ -77,3 +77,12 @@ func (h *TeamHub) Start() { } }() } + +func (h *TeamHub) UpdateChannelAccessCache(userId string, channelId string) { + for webCon := range h.connections { + if webCon.UserId == userId { + webCon.updateChannelAccessCache(channelId) + break + } + } +} |