// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. package web import ( l4g "code.google.com/p/log4go" "fmt" "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" "gopkg.in/fsnotify.v1" "html/template" "net/http" "strconv" "strings" ) var Templates *template.Template type HtmlTemplatePage api.Page func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage { if len(title) > 0 { title = utils.Cfg.ServiceSettings.SiteName + " - " + title } props := make(map[string]string) props["AnalyticsUrl"] = utils.Cfg.ServiceSettings.AnalyticsUrl return &HtmlTemplatePage{TemplateName: templateName, Title: title, SiteName: utils.Cfg.ServiceSettings.SiteName, Props: props} } func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { if err := Templates.ExecuteTemplate(w, me.TemplateName, me); err != nil { c.SetUnknownError(me.TemplateName, err.Error()) } } func InitWeb() { l4g.Debug("Initializing web routes") mainrouter := api.Srv.Router staticDir := utils.FindDir("web/static") l4g.Debug("Using static directory at %v", staticDir) mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") // Bug in gorilla.mux pervents us from using regex here. mainrouter.Handle("/{team}/channels/{channelname}", api.UserRequired(getChannel)).Methods("GET") // Anything added here must have an _ in it so it does not conflict with team names mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") watchAndParseTemplates() } func watchAndParseTemplates() { templatesDir := utils.FindDir("web/templates") l4g.Debug("Parsing templates at %v", templatesDir) var err error if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { l4g.Error("Failed to parse templates %v", err) } watcher, err := fsnotify.NewWatcher() if err != nil { l4g.Error("Failed to create directory watcher %v", err) } go func() { for { select { case event := <-watcher.Events: if event.Op&fsnotify.Write == fsnotify.Write { l4g.Info("Re-parsing templates because of modified file %v", event.Name) if Templates, err = template.ParseGlob(templatesDir + "*.html"); err != nil { l4g.Error("Failed to parse templates %v", err) } } case err := <-watcher.Errors: l4g.Error("Failed in directory watcher %v", err) } } }() err = watcher.Add(templatesDir) if err != nil { l4g.Error("Failed to add directory to watcher %v", err) } } var browsersNotSupported string = "MSIE/8;MSIE/9;Internet Explorer/8;Internet Explorer/9" func CheckBrowserCompatability(c *api.Context, r *http.Request) bool { ua := user_agent.New(r.UserAgent()) bname, bversion := ua.Browser() browsers := strings.Split(browsersNotSupported, ";") for _, browser := range browsers { version := strings.Split(browser, "/") if strings.HasPrefix(bname, version[0]) && strings.HasPrefix(bversion, version[1]) { c.Err = model.NewAppError("CheckBrowserCompatability", "Your current browser is not supported, please upgrade to one of the following browsers: Google Chrome 21 or higher, Internet Explorer 10 or higher, FireFox 14 or higher", "") return false } } return true } func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if !CheckBrowserCompatability(c, r) { return } if len(c.Session.UserId) == 0 { page := NewHtmlTemplatePage("signup_team", "Signup") page.Render(c, w) } else { page := NewHtmlTemplatePage("home", "Home") page.Props["TeamURL"] = c.GetTeamURL() page.Render(c, w) } } func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { if !CheckBrowserCompatability(c, r) { return } page := NewHtmlTemplatePage("signup_team", "Signup") page.Render(c, w) } func login(c *api.Context, w http.ResponseWriter, r *http.Request) { if !CheckBrowserCompatability(c, r) { return } params := mux.Vars(r) teamName := params["team"] var team *model.Team if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { l4g.Error("Couldn't find team name=%v, teamURL=%v, err=%v", teamName, c.GetTeamURL(), tResult.Err.Message) // This should probably do somthing nicer http.Redirect(w, r, "http://"+r.Host, http.StatusTemporaryRedirect) return } else { team = tResult.Data.(*model.Team) } // If we are already logged into this team then go to home if len(c.Session.UserId) != 0 && c.Session.TeamId == team.Id { page := NewHtmlTemplatePage("home", "Home") page.Props["TeamURL"] = c.GetTeamURL() page.Render(c, w) return } page := NewHtmlTemplatePage("login", "Login") page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamName"] = teamName page.Render(c, w) } func signupTeamConfirm(c *api.Context, w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") page := NewHtmlTemplatePage("signup_team_confirm", "Signup Email Sent") page.Props["Email"] = email page.Render(c, w) } func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { data := r.FormValue("d") hash := r.FormValue("h") if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "") return } props := model.MapFromJson(strings.NewReader(data)) t, err := strconv.ParseInt(props["time"], 10, 64) if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour c.Err = model.NewAppError("signupTeamComplete", "The signup link has expired", "") return } page := NewHtmlTemplatePage("signup_team_complete", "Complete Team Sign Up") page.Props["Email"] = props["email"] page.Props["DisplayName"] = props["display_name"] page.Props["Data"] = data page.Props["Hash"] = hash page.Render(c, w) } func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) { id := r.FormValue("id") data := r.FormValue("d") hash := r.FormValue("h") var props map[string]string if len(id) > 0 { props = make(map[string]string) if result := <-api.Srv.Store.Team().Get(id); result.Err != nil { c.Err = result.Err return } else { team := result.Data.(*model.Team) if !(team.Type == model.TEAM_OPEN || (team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0)) { c.Err = model.NewAppError("signupUserComplete", "The team type doesn't allow open invites", "id="+id) return } props["email"] = "" props["display_name"] = team.DisplayName props["name"] = team.Name props["id"] = team.Id data = model.MapToJson(props) hash = "" } } else { if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "") return } props = model.MapFromJson(strings.NewReader(data)) t, err := strconv.ParseInt(props["time"], 10, 64) if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hour c.Err = model.NewAppError("signupTeamComplete", "The signup link has expired", "") return } } page := NewHtmlTemplatePage("signup_user_complete", "Complete User Sign Up") page.Props["Email"] = props["email"] page.Props["TeamDisplayName"] = props["display_name"] page.Props["TeamName"] = props["name"] page.Props["TeamId"] = props["id"] page.Props["Data"] = data page.Props["Hash"] = hash page.Render(c, w) } func logout(c *api.Context, w http.ResponseWriter, r *http.Request) { api.Logout(c, w, r) http.Redirect(w, r, c.GetTeamURL(), http.StatusFound) } func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) name := params["channelname"] var channelId string if result := <-api.Srv.Store.Channel().CheckPermissionsToByName(c.Session.TeamId, name, c.Session.UserId); result.Err != nil { c.Err = result.Err return } else { channelId = result.Data.(string) } if len(channelId) == 0 { if strings.Index(name, "__") > 0 { // It's a direct message channel that doesn't exist yet so let's create it ids := strings.Split(name, "__") otherUserId := "" if ids[0] == c.Session.UserId { otherUserId = ids[1] } else { otherUserId = ids[0] } if sc, err := api.CreateDirectChannel(c, otherUserId); err != nil { api.Handle404(w, r) return } else { channelId = sc.Id } } else { // lets make sure the user is valid if result := <-api.Srv.Store.User().Get(c.Session.UserId); result.Err != nil { c.Err = result.Err c.RemoveSessionCookie(w) l4g.Error("Error in getting users profile for id=%v forcing logout", c.Session.UserId) return } //api.Handle404(w, r) //Bad channel urls just redirect to the town-square for now http.Redirect(w, r, c.GetTeamURL()+"/channels/town-square", http.StatusFound) return } } var team *model.Team if tResult := <-api.Srv.Store.Team().Get(c.Session.TeamId); tResult.Err != nil { c.Err = tResult.Err return } else { team = tResult.Data.(*model.Team) } page := NewHtmlTemplatePage("channel", "") page.Title = name + " - " + team.DisplayName + " " + page.SiteName page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamType"] = team.Type page.Props["TeamId"] = team.Id page.Props["ChannelName"] = name page.Props["ChannelId"] = channelId page.Props["UserId"] = c.Session.UserId page.Render(c, w) } func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { resend := r.URL.Query().Get("resend") name := r.URL.Query().Get("name") email := r.URL.Query().Get("email") hashedId := r.URL.Query().Get("hid") userId := r.URL.Query().Get("uid") if resend == "true" { teamId := "" if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { c.Err = result.Err return } else { teamId = result.Data.(*model.Team).Id } if result := <-api.Srv.Store.User().GetByEmail(teamId, email); result.Err != nil { c.Err = result.Err return } else { user := result.Data.(*model.User) api.FireAndForgetVerifyEmail(user.Id, strings.Split(user.Nickname, " ")[0], user.Email, name, c.GetTeamURL()) http.Redirect(w, r, "/", http.StatusFound) return } } var isVerified string if len(userId) != 26 { isVerified = "false" } else if len(hashedId) == 0 { isVerified = "false" } else if model.ComparePassword(hashedId, userId) { isVerified = "true" if c.Err = (<-api.Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil { return } else { c.LogAudit("") } } else { isVerified = "false" } page := NewHtmlTemplatePage("verify", "Email Verified") page.Props["IsVerified"] = isVerified page.Render(c, w) } func findTeam(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("find_team", "Find Team") page.Render(c, w) } func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { isResetLink := true hash := r.URL.Query().Get("h") data := r.URL.Query().Get("d") params := mux.Vars(r) teamName := params["team"] if len(hash) == 0 || len(data) == 0 { isResetLink = false } else { if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.ResetSalt)) { c.Err = model.NewAppError("resetPassword", "The reset link does not appear to be valid", "") return } props := model.MapFromJson(strings.NewReader(data)) t, err := strconv.ParseInt(props["time"], 10, 64) if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour c.Err = model.NewAppError("resetPassword", "The signup link has expired", "") return } } teamDisplayName := "Developer/Beta" var team *model.Team if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { c.Err = tResult.Err return } else { team = tResult.Data.(*model.Team) } if team != nil { teamDisplayName = team.DisplayName } page := NewHtmlTemplatePage("password_reset", "") page.Title = "Reset Password - " + page.SiteName page.Props["TeamDisplayName"] = teamDisplayName page.Props["Hash"] = hash page.Props["Data"] = data page.Props["TeamName"] = teamName page.Props["IsReset"] = strconv.FormatBool(isResetLink) page.Render(c, w) }